Los poderes maléficos de los mutantes

Mutar es evolucionar. Lo propuso Sir Charles Darwin y lo usamos en la industria del software. Pero algo no esta saliendo bien.

Desde la aparición del concepto de programa almacenado aprendimos que el software es Programas + Datos. Esta claro que sin datos no hay software.

En programación orientada a objetos construimos modelos que evolucionan con el paso del tiempo emulando los conocimientos que aprendemos al observar la realidad que estamos representando.

Sin embargo, manipulamos y abusamos de esos cambios sin control violando el único principio de diseño importante al generar representaciones incompletas (y por lo tanto inválidas) y propagando el efecto de onda con nuestros cambios.

En el paradigma funcional, esto se resuelve de manera elegante prohibiendo directamente las mutaciones. Podemos ser (un poco) menos drásticos.

Volviendo a lo esencial

El gran Fred Brooks nos regaló unas cuantos pensamientos. Entre otras nos habló del mito de las embarazadas, nos enseñó a desconfiar de las balas de plata y nos educó en la separación de complejidad accidental vs esencial.

La esencia de un ente de la realidad es aquello que lo hace ser él mismo y no otro.

La accidentalidad de un ente se da por situaciones temporales que a pesar de cambiar el comportamiento no impiden observar que estemos ante la misma entidad por más que evolucionemos y no nos bañemos dos veces en el mismo rio.

Image for post
Image for post
Foto por Alexander McFeron en Unsplash

Siendo fieles a nuestra biyección, en nuestro modelo computable deberíamos poder distinguir cuando un objeto cambia en cuanto a lo accidental y prohibir todo cambio esencial (porque violarían dicha biyección que es nuestro único principio en MAPPER).

Los objetos deberían saber defenderse de representaciones inválidas. Son los poderes contra los mutantes.

Image for post
Image for post
Foto por Joey Nicotra en Unsplash

Is data ok ?

En la mayoría de los países, una factura es un documento escrito e inmodificable. Alterarlo tiene consecuencias penales de manera similar a lo que ocurre con una transacción en una cadena blockchain o un asiento contable.
En este dominio de problema la inmutabilidad es una regla por lo tanto nuestras simulaciones deberán respetarlo.

Sin embargo, los que hemos trabajado en dominios económicos, financieros o bancarios hemos construido sistemas violando estas reglas de manera sistemática con alguna excusa relacionada a la vagancia o la performance (nuestras favoritas).

Como anécdota personal, en uno mis primeros trabajos para un gran banco internacional, modelábamos las transacciones financieras con un atributo isDataOK que teníamos como un flag booleano hasta asegurarnos que la transacción era efectivamente una transacción válida y procesable. Esto nos trajo múltiples problemas de acoplamiento en múltiples ocasiones.

Ademas muchos de esos campos permanecían con valores nulos (en vez de modelar la incompletitud o indefinición del dato) por lo cual debíamos esparcir el código con múltiples controles por ifs para validar que alguno de los datos no fueran nulls.

Al pensar en cómo construir una solución al problema que estábamos resolviendo por aquel entonces encuentro que la guía debería ser nuestro único axioma. La biyección uno a uno con la realidad.

Si en nuestra visión de la realidad algo no muta no tiene porque hacerlo en el modelo computable.

Veamos un poco de código versión años 90:

Una clase hueca con un montón de atributos y nada de encapsulamiento pero con un flag (isDataOK) indicando cuando podía ser utilizada de manera segura.

Empecemos por esconder la decisión de saber cuando es un movimiento procesable.

Luego continuemos por encapsular los atributos del movimiento:

Este movimiento es mutable (a pesar de no serlo en el mundo real). Debemos asegurarnos que se comporte como nuestro ente observado.

Simple, elegante, inmutable, sin dataOk, siempre válido, sin setters ni getters.

El movimiento es válido desde su nacimiento, tal como sucede en el mundo real.

Supongamos ahora que una regla de negocio nos impide realizar movimientos entre la misma parte y contraparte (esto sucede en el mundo real).

En nuestra primera versión, este control seria imposible. En la versión inmutable solo representamos situaciones reales, bastará con impedir la construcción de dichos objetos.

Los tiempos están cambiando

Vamos a continuar el ejemplo anterior haciendo foco en la fecha en que se realizó dicha transacción.

En el mundo real una fecha representa un día en un calendario arbitrario:

Image for post
Image for post

Si creamos un movimiento en bitcoins para el halving del día 12 de mayo de 2020 y lo modelamos en nuestro modelo computable tendremos algo de esta forma:

Pero esto viola nuestro único principio de diseño de mantener una biyección con el mundo real.

Seamos fieles a nuestra única regla.

Modelamos entes de la realidad como un día de un mes, un año calendario y una fecha olvidándonos de implementaciones arbitrarias con enteros porque la biyección y la declaratividad son más importantes que la performance y la pereza.

Detengámonos por un minuto en la mutabilidad de una fecha. Uno espera que una fecha no mute jamás porque no lo hace en el mundo real. A ninguna persona no informática se le ocurriría jamás modificar una fecha.

Analicemos por el método de reducción al absurdo qué ocurriría si permitimos que una fecha cambie:

Nuestra transacción acreditada el día del halving conoce su fecha de imputación . Si esta cambia internamente todos los blockchains consecutivos deberían recalcularse y esto está expresamente prohibido por el dominio financiero. Está claro que la fecha no debe mutar jamás.

¿Está claro que una fecha no debe mutar?

Repasemos la clase fecha en los lenguajes más utilizados en la industria actual.

Go: Date es una struct en Go.

Java: Mutable (deprecada).

PHP: Mutable con abuso de setters.

Python: Mutable (Los atributos son públicos en Python).

Swift: Mutable.

El dominio del problema de las fechas es, probablemente uno de los mas antiguos y conocidos por la humanidad. La excusa de que se están deprecando estos getters habla de un mal diseño inicial en la mayoría de los lenguajes modernos.

Posibles Soluciones

Un posible ataque es invertir la carga de la prueba. Los objetos son completamente inmutables salvo que se indique lo contrario.
Si necesitan evolucionar deben hacerlo siempre en sus aspectos accidentales. Nunca en su esencia. Dicho cambio no debe estar acoplado a todos los demás objetos que lo utilizan.

Conclusiones

Si un objeto está completo desde su creación, siempre responderá los mensajes que definió de manera válida.

Un objeto debe representar correctamente el ente desde su inicio.

Si trabajamos en un ambiente concurrente es esencial que los objetos sean siempre válidos.

Un objeto debe ser inmutable si el ente que representa es inmutable.

La mayoría de los entes son inmutables.

Estas reglas mantienen el modelo consistente constantemente como invariante de representación.

Como corolario de la demostración por el absurdo podemos derivar una serie de reglas:

Corolario 1

Los objetos deben estar completos desde su creación

Corolario 2

No deben existir los setters.

Corolario 3

No deben existir los getters (mutan y exponen los atributos)

Corolario 4

No deben existir los getters (salvo que existan en el mundo real y entonces la biyección es válida). No es responsabilidad de ningún ente de la realidad responder a un mensaje getXXX().

Parte del objetivo de esta serie de artículos es generar debates y espacios de discusión sobre la problemática del diseño de software.

Esperamos ansiosamente los observaciones y comentarios sobre esta nota.

Este artículo fue publicado al mismo tiempo en inglés aquí.

Written by

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

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store