Modelos Desnudos — Parte II: Getters

Las viejas y confiables estructuras de datos y su polémico acceso (de lectura)

Maximiliano Contieri
Diseño de Software
7 min readJul 22, 2020

--

Utilizar a los objetos como estructuras de datos es una práctica arraigada que genera muchos problemas asociados a la mantenibilidad y evolución del software y desaprovecha brillantes conceptos enunciados hace 5 décadas. En esta segunda parte reflexionaremos sobre los accesos de lectura de dichos objetos.

En la primera parte de este artículo enunciamos la evolución del concepto de information hiding hacia los objetos vivos con responsabilidades definidas (el qué esencial) escondiendo la implementación (el cómo accidental).

En esta segunda parte mostraremos argumentos opuestos al uso de getters.

Foto por Dominik Vanyi en Unsplash

El nombre que no existe en la realidad (Nuevamente)

Los programadores utilizamos por convención los nombres de la forma getAlgo..() para exponer (y perder el control de) un atributo antes privado. Por los mismos argumentos enunciados en el artículo acerca de los setters, este nombre no podrá ser contrastado con un equivalente en el mundo real mediante la biyección usando MAPPER.

La conclusión final sobre estos nombres es:

No deberían existir nunca metodos de la forma setAlgo…() o getAlgo..()

No exponer las colecciones

Muchos objetos administran colecciones. La modificación de dichas colecciones, los invariantes o la forma de recorrerlas deben ser únicamente responsabilidad de dichos objetos.

Supongamos que queremos dibujar nuestro polígono presentado en la parte I, en un canvas. Lo haremos con el siguiente código:

Dibujando un Polígono

Al exponer la colección de vértices (y como las colecciones se pasan por referencia en la mayoría de los lenguajes) perdemos el control sobre dicha colección. Nada impide que se ejecute este otro código:

array_shift() Quita el primer valor del array

Esto hace que el triángulo mute, generando una inconsistencia en la biyección con el mundo real. Los polígonos de dos lados violarían el principio de ser una figura cerrada.

Este defecto será notado mucho tiempo después por no haber sido detectado a tiempo violando el principio de fallar rápido.

En ningún caso dichos objetos deberán exponer sus colecciones preservando la ley de Demeter

En caso de necesitar retornar los elementos coleccionados, deberá devolverse una copia (shallow) para evitar perder el control. Con el estado del arte actual, copiar colecciones es sumamente rápido. En caso de ser colecciones muy grandes existen soluciones de diseño con iteradores, proxies y cursores para evitar realizar la operación de copia completa.

Iterando colecciones

¿Cómo solucionamos el dibujo del polígono?

Iterar una colección es un tema bien conocido cuando trabajamos con patrones de diseño:

Si queremos recorrer nuestro polígono, podemos retornar un iterador (que indica lo qué necesitamos hacer) sin revelar nuestra estructura de datos subyacente (el cómo lo guardamos).

Retornar un iterador le da flexibilidad al objeto para cambiar su representación

En caso de trabajar con un lenguaje que soporte funciones anónimas o clausuras, podríamos tomar la responsabilidad de iterar elementos sin exponer un iterador hacia afuera:

Mutando colecciones

Los polígonos no deben mutar porque los vértices son parte de su esencia minimal: si le sacamos alguno de sus vértices, dejan de ser ese polígono que los hace únicos.

Existen muchos objetos en nuestro negocio que pueden mutar en sus colecciones accidentales y existen mecanismos para administrar dichas mutaciones.

Si quisiéramos modelar una cuenta de Twitter y mantener sus seguidores, conociendo el negocio, la cuenta se crea sin seguidores (ignoremos por un momento las sugerencias que nos ofrece al crear la cuenta).

Utilizando setters y getters, un programador novato estaría tentado de agregar una seguidor de la siguiente manera:

Un correcto seteo de responsabilidades y reglas de negocio nos sugiere que es responsabilidad de la cuenta agregar a un nuevo seguidor, realizar validaciones (por ejemplo que no lo esté siguiendo previamente) y mantener la integridad de la colección.

Por lo tanto, una mejor solución sería:

Doble encapsulamiento

En la década de los 90s existía la tendencia a crear un doble encapsulamiento a los atributos como un extremo sobre la privacidad. Esto quiere decir que, aun desde los métodos privados de un objeto, se evitaría el acceso directo a las variables.

Esta práctica no genera ningún beneficio. Agrega una indirección innecesaria, y expone setters y getters en los lenguajes que no tienen diferenciación entre métodos públicos y privados. Además, esconde el acoplamiento entre un atributo y los métodos directos que lo referencian, evitando posibles refactorizaciones.

Diga, no pregunte

Existe un principio que establece:

No solicite la información que necesita para hacer el trabajo; pide al objeto que tiene la información que haga el trabajo por ti

Este principio se conoce en inglés como: Tell, don’t ask.

Esto que nos recuerda que, en lugar de pedir datos a un objeto y actuar sobre estos datos, deberíamos decirle a un objeto qué hacer. Esto alienta a mover el comportamiento en un objeto junto con el conocimiento que es responsable de administrar.

Demasiada información puede matarnos

Parafraseando la Ley de Demeter, del mínimo acoplamiento y la máxima cohesion:

  • Cada unidad debe tener un limitado conocimiento sobre otras unidades y solo conocer aquellas unidades estrechamente relacionadas a la unidad actual.
  • Cada unidad debe hablar sólo a sus amigos y no hablar con extraños.
  • Sólo hablar con sus amigos inmediatos.

Agregar complejidad accidental con setters y getters implica generar acoplamiento violando estas reglas y generando un mayor efecto de onda ante posibles cambios.

Distinguir complejidad accidental de complejidad esencial es el mayor reto para un desarrollador de software.

Foto by Macau Photo Agency en Unsplash

Los setters y getters violan el antropomorfismo

Volvamos a nuestra única regla de diseño que nos pide una biyección entre el modelo que estamos construyendo y el mundo real y respetando el principio de Antropomorfismo (darle una entidad viva a cada objeto).

Ela hacerlo descubriremos que las responsabilidades que les damos a los objetos luego de haber sido obtenidos con un getter no se mapean con el mundo real violando la biyección.

En esta página hay un excelente ejemplo de antropomorfismo no respetado al utilizar getters.

Cambiando la forma de pensar

Cuando empezamos a modelar nuestros objetos olvidándonos de su representación accidental, lograremos evitar las clases anémicas (que únicamente cumplen la función de guardar datos resultando en un anti-patrón bien conocido).

Al igual que ocurre con las estructuras de datos, no hay forma que una clase anémica garantice la integridad de sus datos y relaciones.

Como las operaciones sobre las clases anémicas están fuera de dichas clases no hay un único punto de control. Por lo tanto generaremos tanto código repetido como puntos de acceso a dichos atributos existan en nuestro modelo.

Siempre buscaremos emular el comportamiento de los objetos como cajas negras, obteniendo biyecciones mucho más realistas y declarativas.

Recomendaciones

  • No usar setters. No existen razones bien argumentadas para hacerlo.
  • No usar getters. En caso de que alguna de las responsabilidades de un objeto esté relacionada con responder un mensaje coincidente con un atributo, hacerlo pensando antes si no estamos rompiendo el encapsulamiento.
  • Jamás prefijar el nombre de la función con la palabra get. Si un polígono en el mundo real puede contestar cuáles son sus vértices, sea con el nombre del mundo real (vertices()).
  • En caso de retornar colecciones, devolver una copia o un proxy para no perder el control y favorecer el uso de iteradores.
  • No tener atributos públicos. A fines prácticos es como tener setters y getters. Es un code smell de uso de clases anémicas.
  • No tener atributos estáticos públicos. Además de lo enumerado más arriba las clases deberían ser stateless y esto es un code smell indicando que se está usando una clase como una variable global.

Transición desde un sistema de código heredado

No todas son malas noticias. Convertir un mal modelo en uno bueno es posible mediante una correcta re-asignación de responsabilidades y con ayuda de los refactors adecuados.

Si tenemos la ventaja de estar mejorando un sistema con buena cobertura podremos encapsular gradualmente los objetos restringiendo su acceso de manera iterativa incremental.

En caso de no contar con cobertura suficiente estaremos frente a un sistema de código heredado según la excelente definición de Michael Feathers:

Un sistema de código heredado es aquel que no tiene cobertura.

En estos casos deberemos primero cubrir la funcionalidad existente para luego poder realizar las transformaciones necesarias.

Foto por Greg Nunes en Unsplash

Conclusiones

La práctica de utilizar setters y getters genera acoplamiento e imposibilita la evolución incremental de nuestros sistemas informáticos.

Por los argumentos enunciados en este artículo, deberíamos restringir su uso lo máximo posible.

Parte del objetivo de esta serie de artículos es generar espacios de debate y discusión sobre diseño de software.

Esperamos comentarios y sugerencias sobre este artículo.

Este artículo también está disponible en inglés aquí.

--

--

Maximiliano Contieri
Diseño de Software

I’m a senior software engineer specialized in declarative designs. S.O.L.I.D. and agile methodologies fan. Maximilianocontieri.com