miércoles, 18 de febrero de 2009

Tip 3: Objects must be valid since its creation time

Este tip de diseño del que hablé en mi presentación de Smalltalks 2008 es muy importante de mi punto de vista. La idea que trata de transmitir este tip es que cuando se crea un objeto, el mismo debe ser válido. Hay motivos teóricos y prácticos para demostrar por qué es un tip importante. Voy a empezar por el teórico.
Si están de acuerdo con la visión de que un objeto es una representación de un ente de la realidad, es muy importante preguntarse a partir de qué momento ese objeto representa el ente en cuestión. Y la respuesta sería (rápidamente) ¡cuanto antes mejor!, sino mientras tanto tendremos un objeto que no representa nada, un objeto que no puede cumplir con sus responsabilidad, un objeto que puede traer problemas. Este tip se relaciona bastante con el anterior en el cual comentaba la importancia de identificar claramente cuál es la identidad del ente representada en el objeto puesto que el objeto puede representar el ente una vez que la identidad del mismo es modelada por el objeto. Esto ya parece un trabalenguas. Voy a dar un ejemplo para tratar de transmitir la idea más claramente por lo que ahora nos iremos metiendo en los motivos prácticos. 
Supongamos que tenemos que modelar una tasa de interés. En cualquier libro financiero verán que una tasa de interés es representada por un porcentaje durante cierto tiempo. Es común por lo tanto encontrarse con tasas de interés del 10% anual, o del 5% mensual. Una tasa de interés del 10% anual significa que si invertimos nuestro capital por un año, al finalizar el año recibiremos un 10% de nuestro capital como "premio" por haber hecho la inversión (estoy suponiendo que no hay pago intermedios, etc). La tasa de interés es presentada algebraicamente como la división del porcentaje por el tiempo. Esto se puede ver claramente cuando escribimos la fórmula de interés:

interés = capitalInvertido * tasaDeInterés * tiempoDeInversión  donde tasaDeInterés = porcentaje / tiempo 
por lo tanto:
interés = capitalInvertido * porcentaje / tiempo * tiempoDeInversión  

Por lo tanto si capitalInvertido es 100 $, la tasa es 10% anual e invertimos por 2 años, tendríamos:

interés = 100 $ * 0.1 / 1 año * 2 años --> años con años se simplifican y tenemos = 100 $ * 0.1 * 2 = 20 $

Volviendo a nuestro modelo, podemos usar la clase "InterestRate" para modelar tasas de interés, la cual sabrá responder mensajes de creación de instancia como #of: aPercentage every: aTimeMeasure, o mejor aún #yearlyOf: aPercentage. Veamos un ejemplo en código:

InterestRate of: 10 % every: 1 year --> Devuelve una tasa de interés del 10% anual
InterestRate yearlyOf: 10% --> Hace lo mismo que la colaboración anterior

Ahora bien, este tip dice que el objeto que los objetos deben ser válidos desde el momento que se crean, esto significa para este ejemplo que cuando se crea una tasa de interés la misma debe ser válida. ¿Qué tasa de interés no sería válida? (una pregunta medio retorcida puesto que en realidad si no es válida no existe como tasa de interés, pero creo que se entiende no?). Claramente una tasa de interés no válida es aquella cuyo lapso de tiempo sea 0 (0 años, 0 meses, 0 días, etc. ¿recuerdan mi comentario sobre la igualdad del 0 con las medidas?) o cuyo porcentaje o tiempo sea negativo. El primer caso se puede deducir fácilmente puesto que de lo contrario el valor representado por una tasa de interés (que es la división del porcentaje por el lapso de tiempo como vimos más arriba) no se podría calcular por estar dividiendo por 0. El segundo tiene que ver más que nada con reglas de negocio, no existen tasas de interés negativas o con tiempos de inversión negativo! no tiene sentido. 
Esto significa que en el mensaje de construcción de instancia de "InterestRate" tenemos que asegurar estas tres pre-condiciones. La manera en que nosotros lo implementamos es la siguiente:

InteresRate class>>of: aPercentage every: aTimeMeasure

self assertPercentageIsPositive: aPercentage.
self assertTermNotZeroOrNegative: aTimeMeasure.

^self new initializeOf: aPercentage every: aTimeMeasurement
Se puede ver claramente que lo primero que se hace es validar que las pre-condiciones de existencia (y fíjense que ya no dije validez) de una tasa de interés se cumplan. Si la validación no es exitósa se generará una excepción, que en nuestro caso será una subclase de InstanceCreationException, indicando que pre-condición no se cumplió.
Fíjense que por haber hecho esto al momento de crear una tasa de interés, es imposible tener tasas de interés inválidas. El concepto de tener objetos inválidos desaparece y de repente solo tengo o no tengo dichos objetos, lo cual podríamos decir que se condice con la realidad puesto que en la misma no existen tasas de interés inválidas, nunca un banco daría una tasa de interés con tiempo 0 o negativo, es impensable. Uno podría decir que -10% anual es una tasa de interés, lo podría escribir en un pizarrón o aún en este blog, pero llamarla tasa de interés es un abuso y hacerlo es simplemente una ilusión que podemos tener puesto que realmente no es una tasa de interés y un experto del dominio nos lo diría de entrada.
El problema de tener objetos válidos o inválidos tiene que ver más que nada con un problema computacional debido a que alguien (persona o computadora) en algún momento debe ingresar los "datos" para crear la tasa de interés. Esto implica que existe un tiempo en el cual se ingresan "datos" y que por lo tanto deben estar en "algún lado" en nuestro modelo durante ese tiempo y también implica que esos datos pueden estar incompletos. Pero entonces ese problema no corresponde ser resuelto por el objeto del modelo, en este caso "InteresRate", sino por aquel que se encarga del ingreso de datos. 
Al haber hecho que una tasa de interés no se pueda crear si no es válida, ya no solo podemos asegurar que el usuario nunca creará tasas inválidas sino también que ningún programador lo hará!, si lo hace enseguida nuestro modelo le indicará que se equivocó, lo cual es buenísimo y tiene que ver con otro tip que veremos más adelante que se llama "Fail fast". Otra ventaja es que tenemos las famosas validaciones de negocio, o mal llamadas reglas de negocio en un solo lugar y no dispersas por cualquier lado (la UI, una interfaz batch, etc). Siempre que se cree una tasa de interés, venga de donde venga, la UI, un programador, otra computadora, etc, se realizará este chequeo que nos asegura que nuestros objetos siempre serán.... válidos (pensé en terminar la frase en serán, puesto que si no lo son, no existen que es lo mismo según mi punto de vista a que no son válidos; es como pensar que la palabra "válido" desaparece de mi idioma, deja de tener sentido).
Ustedes podrán preguntar cómo representamos entonces aquellos casos para los cuales la información ingresada es incompleta o no correcta, bueno, por medio de un objeto para tal fin! Por ejemplo, si aplicamos el mismo concepto para un "Deal" podríamos tener una intención de deal (DealIntention) para representar aquel que aún no se sabe si es válido. Esto puede parecer como que generará una explosión de clases pero la realidad no es así. En nuestro caso en lo que respecta a la UI, son los mismos widget o controllers los que se encargan de contener esa información inválida (Este blog ya es muy largo, pero cuando pueda voy a comentar como funciona la UI que hicimos para XTrade porque me parece muy interesante)
Esta característica de tener objetos solo válidos se potencia aún más cuando dichos objetos son inmutables, como el caso de la tasa de interés, o fechas, o medidas, etc. puesto que no habrá manera de que dicho objeto pase a ser inválido con el transcurso del tiempo. Si el objeto es mutable, podemos aplicar lo que mencione y detallaré más adelante como sincronización por copias. 
Analizando aún más este tip, veremos que para objetos inmutables estas pre-condiciones se fusionan con el concepto de invariante. Hemos logrado poner en un solo lugar las pre-condiciones de varios métodos y la invariante de un objeto, lo cual me parece muy interesante por haber simplificado esta representación.
Algunos detalles de implementación adcionales:
1) Todos los mensajes de creación de instancia deben en definitiva terminar cayendo en la evaluación de un único método de creación de instancia que es además es que realiza todas las aserciones. Para nuestro ejemplo, #yearlyOf: debería estar implementado así:

InterestRate class>>yearlyOf: aPercentage

   ^self of: aPercentage every: 1 year

2) Debería haber un solo mensaje de inicialización que es enviado desde un único método de creación de instancias que es el que realiza las aserciones. Esto implica que el mensaje #new enviado a self se realiza en un solo lugar.
3) Se puede reificar aún mejor el concepto de pre-condiciones para luego hacer meta-programación con las mismas y por lo tanto generar documentación automática (para interfaces por ejemplo) o test automáticos como presentó Meyer la semana pasada
Bueno, suficiente por ahora, no tengo ni tiempo de releer lo que escribí así que espero que se pueda leer bien! 

1 comentario:

Hernan Wilkinson dijo...

Maxi Tabacman me hizo un comentario importante. Pueden existir tasas de interés negativas, ese es un cambio que hemos hecho recientemente con el desarrollo de curvas y valuaciones que Maxi está haciendo, lo que sucede es que utilicé una versión vieja del sistema para el ejemplo de código.
Más allá de este detalle funcional, el resto sigue siendo válido
Gracias Maxi!