Profile Image

Carlos Vigueras

Senior Backend Developer

Object Calisthenics – Evitar getters/setters o atributos públicos

En esta entrada voy a explicar la última regla, la número 9 para ser mas exactos de Object Calisthenics, la cual, para mi es muy importante.

Para aplicar bien esta regla, hay tener en cuenta, que nuestros objetos deben siempre estar encapsulados y no permitir el acceso a los mismos de una forma directa, ya que si no se encapsulan de una forma correcta podremos acceder a ellos desde diferentes partes del código y esto va a generar un código poco limpio, poco entendible, y sobre todo poco mantenible.

Y no solo eso, si no que estaremos creando objetos o entidades no inmutables, y en cualquier momento se podrán modificar de forma deliberada sin control ninguno por nuestra parte.

Estaremos permitiendo modificar el valor de una variable sin una posible validación añadida en la creación del objeto, y como he dicho anteriormente, no estaremos respetando la inmutabilidad.

Cualquier programador que desarrolle en el mismo código, podrá tener acceso a esa variable y alterar su valor, y esto nos podrá generar una serie de errores incontrolados, que seguro ninguno de nosotros deseamos.

Esta regla, básicamente, se refiere a que debemos encapsular nuestros objetos, y de esta forma que sean inmutables, que como siempre digo, no hay que tomarse las cosas al pie de la letra, pero en mi opinión es que nos ciñamos lo máximo posible y la cumplamos, aplicando lo que se denomina “Tell, don’t ask”.

Voy a comenzar una breve explicación, comenzando por los métodos Setters y posteriormente iré a los Getters.

Setters

Si en cualquier parte del código de nuestra aplicación permitimos establecer el valor de una variable, estamos permitiendo que el acceso no esté centralizado en un solo lugar, por lo tanto estaremos ante la situación de un Code Smell que se denomina Shotgun Surgery, y aunque no sea el propósito del post, voy a explicar que es el Shotgun Surgery:

Cuando el valor de una variable se puede establecer desde diferentes sitios, o acceder a su valor, o cuando una función o método no están centralizados, hemos de replicar código repetido por toda nuestra aplicación, y el código repetido es un serio problema a la hora de refactorizar, ya que en el mejor de los casos, aunque tengamos unas buenas Suites de Tests no vamos a llegar a cubrir todos los casos que han sido modificados.

Si en este caso que comento se ha de realizar algún tipo de refactorización, bien sea el cambio de un tipo de una variable, o bien sea la modificación de un proceso, si estos no están centralizados, tendremos que ir por todo el código buscando donde está esa lógica aplicada y aplicar este cambio en todas las partes donde lo encontremos, por lo que seguramente se nos olvidará algún sitio donde modificarlo y estaremos dejamos un trozo de código sin modificar, generando futuros errores.

Y aunque en el caso de .NET, en el cambio de un tipado de variable, el compilador nos va a indicar un error, no en todos los lenguajes de programación, ni en todos los compiladores se comporta igual.

Y esto es Shotgun Surgery, sobre el que escribiré en una futura entrada, pero sigamos a lo nuestro, que no quiero desviarme del tema.

Getters

Las variables no deberían siempre por defecto estar expuestas para acceder a su valor directamente, si no que deberíamos crear métodos en los objetos que ofrezcan comportamientos del objeto con el que estamos trabajando, aunque para mi gusto, personalmente en muchas ocasiones prefiero utilizar un método Get o dejar la propiedad libre para ser leída, ya que si no vamos a generar una complejidad y código innecesario en la clase, pero esto lo veremos en un ejemplo, ya que es mas fácil de transmitir la idea.

Vamos a ver un ejemplo de implementación con Getters y Setters y posteriormente su refactorización, y para ello nos vamos a centrar sólo en la clase Robot() modificándola y obviando todas las demás clases, así podremos ver el ejemplo mas simplificado:

public class Robot
{
    public string Name { get; set; }
    public Position Position { get; set; }

    public Robot()
    {
        Position = new Position(0, 0);
    }

    public void SetPosition(Position position)
    {
        if (position is { X: > 0, Y: > 0 })
        {
            Position = position;
        }
    }

    public Position GetPosition()
    {
        return Position;
    }
}Lenguaje del código: JavaScript (javascript)

En esta clase Robot(), vemos que tenemos dos métodos, SetPosition(), que recibe una posición y se la establece a Robot(), y por otro lado un método GetPosition(), el cual devuelve la posición del Robot(), y como variable del objecto tenemos Position establecido con su get y su set correspondiente.

No estamos encapsulando nada, si que es cierto que ponemos un método GetPosition() a disposición de quien quiera usarlo, pero tambien podemos hacer un Robot.Position y acceder a su posición, por lo que de este modo no tenemos nada encapsulado. Además no es responsabilidad de Robot() establecer la posición, aunque para este ejemplo lo haya puesto así.

Si os fijáis en el método SetPosition(), establecemos una posición y posteriormente deberíamos acceder a Robot.Position para saber cual es su posición actual, por lo que estamos exponiendo nuestro objeto, ya que se podría hacer un Robot.Position = new Position(1,2) y modificaríamos su posición sin hacer alusión alguna al método Set que hemos establecido, y por lo tanto no se aplicaría la validación de que las posiciones no fuesen negativas.

Lo mismo aplicaría para Name, ya que para un Robot() que hemos creado le podríamos cambiar el nombre a nuestro antojo en cualquier momento.

Aquí rompemos totalmente la encapsulación y dejamos a merced de cualquiera modificar nuestro objeto como se le antoje creando errores en nuestro flujo, ya que podríamos mover a Robot() a unas posiciones negativas que no existen.

Vamos a ver como se debería quedar la clase Robot() bajo mi punto de vista, ya que esto puede ser llevado a miles de interpretaciones diferentes:

    public class Robot(string name)
    {
        public string Name { get; } = name;
        public Position Position { get; private set; } = new(0, 0);

        public Position Move(Position position)
        {
            if (position is { X: > 0, Y: > 0 })
            {
                Position = position;
            }
            return Position;
        }
    }

var robot = new Robot("Pepito Grillo");
var position = robot.Position;
var name = Robot.Name;
var positionMoved = robot.Move(new Position(5, 9));Lenguaje del código: JavaScript (javascript)

Comenzando a analizar la refactorización, voy a empezar por Position, la cual ahora es controlada por un método de Robot().Move(), y lo que hace es mover al Robot() a una posición dada y devolver la nueva Position, por lo que aquí ya tenemos un método de comportamiento y no un Set, que lo que solo hace es devolver la posición.

¿ Pero necesariamente para conocer la posición del Robot() he de moverlo ?

¿ No puedo conocer la posición de Robot() en un momento dado sin mas ?

Pues aquí es donde yo quería llegar, y dar mi opinión en base a esto. Bajo mi punto de vista, no siempre creo que sea necesario crear un método de comportamiento, si en mayor medida para un método Set, pero no tanto para un método Get, es más, soy partidario de simplemente, tal y como he hecho en el ejemplo de refactorización, dejar que la variable que se desee consultar sea accesible para su lectura, ya que por ejemplo con la variable Name sucede lo mismo.

¿Acaso podemos crear un método de comportamiento para una variable de Nombre ?

Yo no le veo sentido alguno, ni para conseguir el Name en un momento dado ni para conseguir la Position, y añadir un método Get para conseguirlos me parece innecesario. Pero esta es mi aproximación, no digo que sea la mas correcta.

¿ Cómo lo harías vosotros ? Os leo en comentarios!

2 thoughts on “Object Calisthenics – Evitar getters/setters o atributos públicos

  1. Enhorabuena por la entrada. Muy de acuerdo en todo. ¡Viva el sentido común!

    Justo hoy, en mi equipo, nos hemos encontrado un bug por no utilizar este principio. Un value object, que durante la ejecución se modificaban algunas de sus propiedades para un fin que no era el de este objeto. Al tratatarse de una clase y pasarse por referencia, el objeto origen terminaba teniendo un comportamiento no deseado.

    ¿Considerarías una buena práctica utilizar un Record para estos casos?

    1. Muy buenas Delineante, muchas gracias por leerme. Me alegro mucho te haya gustado la entrada. Con respecto a tu pregunta, desde mi punto de vista, si usaría un Record para un Value Object, ya que este ha de ser inmutable. Justo tenía pensado hacer una entrada sobre cuando es recomendable usar Records. Gracias por tu comentario!!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *