El objetivo de este blog post es mostrar cómo lograr que un diseño nos enseñe, esto va a permitir una serie de beneficios importantes en nuestro día a día como programadores 😀
El diseño es la definición que usamos para construir software, en dicha definición construimos un modelo que representa un dominio de la realidad, ese modelo se considera “bueno” si es consistente y es capaz de reflejar la realidad de lo que estamos modelando.
Es importante anotar que el diseño está expresado en el código fuente, los diagramas de clases son solo una vista de alto nivel que sirven para comunicar el diseño.
Un objeto es la representación de un ente del dominio del problema, dicha representación tiene un estado (valores concretos de sus atributos) y mensajes (métodos que es capaz de ejecutar).
En este blog post vamos a responder a estas dos preguntas:
1. ¿Desde cuándo el objeto debe representar el ente del dominio?
2. ¿Cómo debe el diseño enseñarnos?
Una característica fundamental en un diseño es el paso del tiempo, no olvidemos que el desarrollo de software es iterativo e incremental y por ende nuestros diseños iniciales están en constante evolución, una de las particularidades que tiene nuestra profesión es que los diseños no son estáticos, como si lo son en otro tipo de ingenierías o disciplinas.
Para este ejemplo vamos a usar el siguiente modelo de dominio:
‘Persona’ en esta primera iteración es una clase anémica (Clase con solo getters y setters que se puede crear de cualquier manera sin validar la consistencia del objeto o reglas de negocio).
Problemas de Diseño
Iniciemos identificando oportunidades de mejora en nuestro código, para esto vamos a crear un pequeño set de pruebas unitarias
Primer problema: Constructores vacíos
En el modelo actual, cuando se crea un objeto ‘Persona’ no se exige en ningún momento crearlo con los atributos mínimos que lo componen y que definen la clase (‘identificación’ y ‘nombreCompleto’), debemos recurrir a la memoria o detalles de implementación para así podernos dar cuenta que me hace falta hacer set de los atributos restantes.
Al realizar las asignaciones manuales obtenemos el siguiente resultado.
Notamos también cómo en la parte del assert estamos violando el encapsulamiento debido a que estamos accediendo directamente a estados de objetos internos para hacer cómputos sobre los mismos, este tema lo vamos a resolver más adelante con el concepto de “TellDontAsk”.
Segundo problema: Objetos creados que no son consistentes respecto al dominio
¿Desde cuándo nuestro objeto creado representa una Persona?
De ninguna manera podemos pensar que la respuesta es “desde el momento que se creó haciendo uso de new Persona()”
El objeto es consistente solo en este momento.
Pasaron 6 líneas de código para que el objeto fuera consistente y tuviera sentido en nuestro dominio, debemos tener presente que “una persona no es persona sin nombre y sin identificación”. Esto quiere decir que hacer únicamente Persona fulanoUno = new Persona() nos da origen a algo que realmente no representa nada y es un error en todo sentido.
Tercer problema: Modelo propenso a errores
¿El modelo anterior es propenso a errores?
Observemos la siguiente imagen:
Por accidente u omisión podríamos olvidar la asignación de la identificación y nuestro modelo nuevamente lo permitiría, lo cual significa que este error puede suceder no solamente en un ambiente de pruebas sino también en producción.
Nota: Lógicamente también debemos tener un set de pruebas unitarias que evite que eso suceda, pero si no contamos con dichas pruebas es casi seguro que vamos a tener errores en ambientes no deseados; el compilador debería ayudarnos en tiempo de desarrollo.
Cuarto problema: Al evolucionar el modelo no me enseña
¿Cómo se comporta el modelo respecto al paso del tiempo? ¿Qué sucede si nos piden un nuevo requerimiento, acaso el modelo nos enseña que existe el cambio para hacer los ajustes respectivos?
Vamos a suponer que ahora nuestro negocio evolucionó y necesitamos la fecha de nacimiento de la persona, vamos a hacer el cambio.
Por supuesto la solución fue agregar el atributo y el respectivo get y set 😀.
Pero observemos que nuestro set de pruebas se mantuvo intacto sin problemas de compilación o notificaciones.
La evolución del modelo no enseña a quienes lo usan, si el modelo no enseña de manera explícita vamos a tener que ir a buscar, y lo que es peor, nunca vamos a saber que el objeto se debe usar de otra manera.
Pensemos en equipos donde trabajan múltiples personas, al adicionar nuevos atributos existirá un riesgo de que se olviden hacer los ajustes respectivos en todas las clases que se usaban este modelo.
Soluciones
Vamos a comenzar a hacer una serie de cambios para lograr el estado ideal que nos permita responder las 2 preguntas iniciales.
Primer ajuste: Consistencia
Un objeto debe representar el ente del dominio desde el momento en que
se crea, un objeto debe enseñar de manera explícita qué necesita para ser una fiel representación de la realidad.
Ajustamos nuestra clase creando un constructor que solamente nos permita crear el objeto de una única manera es decir enviado los atributos que necesitamos.
Nota: También debemos de analizar si es coherente tener o no los métodos set; muchas cosas son inmutables y no tiene sentido cambiarlas en el dominio, por ejemplo: la fecha de nacimiento de una persona, un número de factura, etc.
Observemos qué efecto causa en nuestras pruebas:
Inmediatamente en nuestro set de pruebas el compilador nos obligó a realizar el cambio, el modelo nos enseñó que la forma correcta de crear una persona.
Segundo ajuste: No romper encapsulamiento
Aplicando TellDontAsk, trasladamos la lógica al objeto que es capaz de responder.
Lo cual genera el siguiente impacto en nuestro set de pruebas:
Como podemos observar, con este ajuste el código queda más simple y legible, en definitiva el encapsulamiento no es solo poner get y set en las clases.
Tercer ajuste: Eliminar Clases Anémicas
Todas las clases que componen nuestro objeto modelo de dominio ahora tiene un constructor que nos muestra cuál es la manera indicada de crearse.
Debemos decir que esto no es suficiente; aún podemos crear instancias enviando los atributos en null y nada nos lo impide. Continuamos teniendo un modelo de dominio inconsistente, la solución completa debería ser validar campos obligatorios y reglas como por ejemplo rangos, validaciones de fecha, etc., garantizando esto tenemos un modelo 100% consistente respecto al dominio.
Pensemos en las bondades que tenemos con esto, la posibilidad de tener excepciones en nuestro modelo de dominio en tiempo de ejecución sería prácticamente nula, dado que desde el momento en el que hacemos new Persona(…) estamos garantizando la consistencia del objeto.
Conclusiones finales
1. Debemos tener en cuenta el paso del tiempo en los diseños, nuestros diseños nunca van a ser estáticos.
2. Los diseños deben enseñar lo que representan.
3. Los objetos deben representar el ente del dominio desde el momento en que se crean, no respetar esto nos lleva a modelos inconsistentes que son propensos a errores y adicionalmente vamos a tener mayor dificultad en el mantenimiento del proyecto.
“Este blog post fue construido con base en uno de los episodios del webinar “Diseño a la gorra” de Hernan Wilkinson, el cual presenta una forma fresca y muy interesante de lograr mejores diseños basados en herramientas como TDD, POO, entre otras. En lo personal me encantó esta iniciativa y se las recomiendo.”
Referencias
- https://www.martinfowler.com/bliki/AnemicDomainModel.html
- https://martinfowler.com/bliki/TellDontAsk.html
- https://academia.10pines.com/disenio_a_la_gorra
- https://wiki.c2.com/?WhatIsSoftwareDesign
Gracias por este post, aclara muchos detalles que me ayudan como programador.
En verdad que articulo tan enriquecedor, muestra que el desarrollo debe estar en constante evolución y como las pruebas pueden ser pilar fundamental del desarrollo. Lo que más me gusto es que retrata en pequeñas imágenes como tradicionalmente codificamos y además como se deberían abordar las mismas situaciones con un enfoque sin tantos vacíos, consistente y con código que habla por si mismo .