Mostrando las entradas con la etiqueta oop. Mostrar todas las entradas
Mostrando las entradas con la etiqueta oop. Mostrar todas las entradas

09 diciembre 2011

Mapeos Objeto/Relacional

Llegué a la conclusión de que los frameworks de mapeo Objeto/Relacional no sirven.

- Ehh! ¿Por que? Si yo uso Hibernate y esta bueno!

Aca van mis razones...

De Objetos...
Aprendí con el tiempo que la principal ventaja de la programación orientada a objetos es la posibilidad de tratar con distintas implementaciones a traves de su interfaz. 

Si estas pensando que me refiero al polimorfismo, tenes razón, pero di esta vuelta para evitar la tipica asociación de polimorfismo con jerarquias de clases.

Veamos un ejemplo que suele darse en los cursos introductorios: la cuenta bancaria.


Supongamos que tenemos en nuestro sistema un Cajero que puede hacer depositos en estas cuentas:



La instancia cuenta se pudo haber definido mediante una sub-clase, clase anonima, proxy dinamico o usando AspectJ y manipulación de bytecode. No importa. Mientras cumpla con la interfaz CuentaBancaria nuestro Cajero va a funcionar.

... y Relacional
Ahora supongamos que definimos una Cuenta de la siguiente manera:

CuentaBancaria cuenta = new Decorator(cuentaOriginal);

Nuestro Cajero sigue funcionando pero...

motorDeMapeoOR.guardar(cuenta);

explota.

El motor de mapeo O/R necesita saber como tratar la instancia para poder mapear su clase y sus variables de instancia a la base de datos. En otras palabras: encapsulamiento y polimorfismo las pelotas.

Quizas pienses que me fui a un ejemplo extremo y que uno nunca tiene casos como estos. 

Sin embargo en todos los sistemas JEE que veo siempre hay un "modelo" que termina estando sumamente acoplado a la base de datos:
  • No es conveniente jugar con las jerarquias de clases por que no se pueden mapear de una forma buena.
  • No se puede refactorizar algo para cambiar su composición por que es engorroso modificar los mapeos.
  • No se pueden usar objetos más "dinamicos", como objetos que tengan closures (en un lenguaje que lo permita), implemetaciones de interfaces definidas inline o simplemente un decorator como el del ejemplo.
Entonces la solución es: hacemos objetos pobres casi sin comportamiento solo para transferir datos (DTOs). Luego tenemos un modelo mas "rico" que se crea a partir de estos DTOs. 

Suena simple, pero en la práctica termina siendo una engorrosa y aburridisima repetición de código que los programadores tienden a solucionar de forma sencilla: en lugar de separar estos DTOs del modelo de dominio, terminan juntandolo en un modelo de dominio que solo guarda datos y muchas "clases utilitarias". Es decir aprovechan muy poco las posibilidades de polimorfimo que brinda el lenguaje. 

Pero si iba a estar todo tan acoplado y dependiente de la base de datos, ¿Para que nos preocupamos en hacer el mapeo? ¿No era mejor usar algo tipo Active Record? Al fin de cuentas son lo mismo que los DTOs pero con menos código.

Esto es lo que hace Rails y la primera vez que lo vi me parecio una cagada. Huele mal, pero los mapeos O/R no huelen mucho mejor.

...a las conclusiones
Los frameworks de mapeo O/R no cumplen con su promesa de un modelo de dominio libre de la base de datos, y me pregunto cuales son las ventajas en usarlos. Es decir si los usamos solo para facilitar la interacción con JDBC, quizás hay otras maneras que no requieran ningún tipo de mapeo y mantenimiento de "beans" intermedios.

En el caso de Java, creo que si Sun en lugar de definir una interfaz tan fea como la de RowSet que mezcla un montón de responsabilidades, hubiese hecho algo mas sencillo que se pueda usar como un Active Record, no habria razones para utilizar mapeos O/R como JPA.

09 noviembre 2011

Microwiki: Primeros pasos de diseño

No importa que hable sobre principios y patrones de diseño, sobre polimorfismo, o si el ejemplo que di fue bueno o malo; al momento de la practica quien recién comienza aprendiendo diseño orientado a objetos se siente perdido y quiere una especie de guía que le diga que esta bien o mal.

El secreto es que incluso con más experiencia, uno tambien se encuentra algo perdido cuando se empieza con un problema nuevo.

Creo que un buen consejo para encarar un diseño es tener una "mente de principiante" y no dejar de preguntarse "¿Por qué?".
Claro que para hacerse las preguntas y poder responderlas se necesita un conocimiento previo.
Ambas cosas -hacerse las preguntas y construir ese conocimiento- van de la mano.

Esta introducción, viene a cuento que me dieron ganas de contarles las preguntas que me fui haciendo en la construcción de microwiki.

Para quienes no hayan visto mi post anterior, microwiki, es un pequeño servidor wiki que comencé a programar en mis ratos libres. Como objetivos de diseño, microwiki tiene las siguientes características:
  • Se utiliza localmente: no hay usuarios, ni permisos, ni historial de versiones en las paginas. 
  • Iniciar el servidor debe ser tan simple como ejecutar un comando.
  • Las paginas se guardan en el file system, y pueden editarse tanto dentro como fuera de la aplicación web.
  • La búsqueda de contenido debe ser rápida.


Las cuestiones técnicas

En un mundo teórico ideal, uno debería evaluar la funcionalidad, los atributos de calidad, ver las herramientas disponibles, un largo etcéra y luego elegir la solución técnica más adecuada. La realidad es mucho más simple, venia jugando un poco con Groovy y Gradle así que esas fueron las herramientas que elegí para trabajar.

Al principio pensé en hacerlo con Scala para practicar un poco este lenguaje, pero la comodidad de IntelliJ IDEA para usar Groovy me compro -me estoy volviendo viejo, ya no tengo ganas de ponerme a configurar plugins en versión beta.

El resto de las opciones fueron más simples. Conocía la sintaxis Markdown de usar GitHub y Stackoverflow, y PegDown fue el primer parser que encontré para Java.

Y si voy a hacer un pequeño web server tampoco iba a empezar de cero, Jetty es muy conocido por proveer un API simple para crear web servers en Java sin meterse con todo el lío de JEE (otra opción en Grizzly, pero es mucho más nuevo y no tiene tanta documentación).


Primeros pasos

Partiendo de que microwiki es una aplicación web, y que voy a utilizar servlets con Jetty, el primer paso fue pensar en un servlet que mostrara una pagina.
Consejo: Empezar a diseñar siempre por un caso particular y simple.
Entonces tenemos nuestro servlet que muestra la pagina. ¿Implementamos en el servlet la funcionalidad de abrir el archivo e invocar al parser? Respuesta rápida: no.


¿Por qué? El servlet se encarga de manejar el request y response de http, si ponemos todo junto no hay forma de testear la funcionalidad de obtener y parsear una pagina por separado.

Probablemente en alguna clase sobre diseño orientado a objetos escuchaste que las clases deberían tener alta cohesión, se referían justamente a este tipo de casos: hacer que la clase servlet implemente dos funcionalidades distintas es contraproducente a la hora de introducir cambios.

Los tests de unidad son útiles para detectar este tipo de problemas: si dejamos todo junto para testear la responsabilidad de brindar una pagina vamos a tener que crear un mock HttpServletRequest y un mock HttpServletResponse.
Consejo: Los mock objects son útiles, pero si tus tests necesitan muchos, probablemente le estés pifiando en la separación de responsabilidades.
Entonces separando responsabilidades termine con algo así:


Para los "Templates" no hubo mucho análisis de mi parte: implementar la generación de HTML dentro del servlet es engorroso e inmantenible (en este caso la necesidad de separar responsabilidades es bien clara). Para implementar los templates use los GStrings de Groovy. Me pareció bueno mantener las cosas bien concretas: el template por ahora se utiliza para visualizar una pagina.
Nota: En el código actual en GitHub van a ver que el uso de los templates evoluciono hacia algo más generalizado, en otros posts les cuento el por que.
¿Por que Writable?
Writable es una interfaz de Groovy que simplemente describe el método "writeTo(Writer)". Podría haber usado String, pero usar Writable permite expresar solo lo que necesito y optimizar las cosas si fuese necesario.
Si te estas preguntando a que me refiero con "optimizar": si uno tiene una pagina grande es preferible hacer un "streaming" que guardar toda la pagina en un gran String. Lamentablemente el parser de Markdown que estoy usando no permite hacerlo, pero como están planteadas las cosas podría usar otro parser sin afectar al resto de la aplicación.

¿Por que un objeto Page y no retornar directamente el String con el contenido?
Esta claro que para mi sistema una pagina no es simplemente un String.
Un fanático de TDD y del "paso a paso" me diría que para el test de mostrar la página un String alcanza. Sin embargo sé que voy a querer modelar más cosas de una pagina: una pagina tiene un titulo, una representación HTML y una representación en formato wiki.

¿Por qué una interface PageProvider?
Bueno yo tambien tengo la misma duda :)
Por ahora solo tengo una implementación de PageProvider y tampoco tengo intenciones de tener una distinta a futuro. En este punto hay dos cuestiones basadas en la experiencia que me llevaron a esto:
  • Una interfaz PageProvider me facilitaría la creación de mocks en el caso que quisiera testear otros componentes que dependan de un PageProvider (si esta es una de las "malas" costumbres adquiridas de la experiencia en Java).
  • Si quisiera agregar un cache podría usar un decorator que implemente esta interfaz.
    Otra vez me estoy adelantando -son las manias que uno adquiere de la experiencia previa- en estos casos es importante tomar nota mental de que uno se esta adelantando. A veces por adelantarse, uno le puede errar fiero (de hecho me paso con la búsqueda, pero eso se los voy a contar en otro post). En este caso decidí seguir adelante: le veo mas ventajas que contras, pero si les pasa algo asi en un diseño y les queda la duda... paso a paso como diría mostaza.

Espero no haberlos aburrido mucho, la próxima les cuento algunos pasos más.

14 septiembre 2011

Herencia en JavaScript

JavaScript es en esencia un lenguaje basado en prototipos, que sufre de personalidades múltiples: tiene algunos operadores y comportamientos de un lenguaje con clasificación.

Para entender esto olvidemos por un momento a JavaScript.

Un lenguaje orientado a prototipos -como por ejemplo Self- se basa en que uno obtiene nuevas instancias a partir de la clonación de otras, por ejemplo en un "pseudo-JavaScript":

var auto = {
     acelerarACienEnUnSegundo: function() { return 'No puedo'; },
     arrancar: function() { return 'brummm... :)' }
};

var ferrari = clonar(auto);
ferrari.acelerarACienEnUnSegundo = function() { return 'fiuuummm'; };
ferrari.arrancar(); // retorna brummm....

¿Donde esta la esquizofrenia de JavaScript? Esta en que contamos con un operador new que simula comportarse como si estuviésemos trabajando con un lenguaje con clasificación, por ejemplo:

var auto = new Auto();
function Auto() {
     this.arrancar = function() { return 'brummm....'; };
}

¿Qué hace new?
Cada función que uno define en JavaScript es un objeto que tiene asociada una propiedad prototype.
Es como si cuando uno escribe function Auto() {} escribiera:

var Auto = {
    prototype: {}, 
    constructor: function() { /* cuerpo de la función Auto */ } };

Cuando se usa el operador new JavaScript lo que hace es:

  • Clonar el objeto prototype
  • Después evaluar la función en constructor usando la nueva instancia como contexto (es decir this es el clon de prototype)

Es por eso que uno podría escribir la función Auto como:

function Auto() { console.debug(this.arrancar); }
Auto.prototype.arrancarfunction() { return 'brummm....'; }; 

Esto es muy confuso, por que pareciera que una función se puede comportar como si fuese una clase. De hecho genera más confusión aún cuando uno quiere compartir implementación usando algo similar a la herencia.

Sin embargo el problema es más simple de resolver: no hay clases ni tipos, solo tenemos que agregar los métodos que necesitamos compartir, por ejemplo:

var agregarComportamientoDeAuto = function(obj) {
      obj.arrancar =  function() { return 'brummm... :)' };
}

¿Como creamos un objeto que se comporte como un auto?

var ferrari = function() {
      var nuevaInstancia = {};
      agregarComportamientoDeAuto(nuevaInstancia);
      nuevaInstancia.acelerarACienEnUnSegundo = function() { return 'fiuuummm'; };
};

Quizás no luzca muy elegante, pero en mi opinión es mejor que simular un lenguaje con clasificación donde no lo hay.
La ventaja de este método es que es muy fácil crear mixins o tener variables privadas (en el ejemplo podríamos pasar argumentos adicionales a agregarComportamientoDeAuto y aprovechar las clausuras).

También es posible simular delegación estilo "super", la próxima voy a dar un par de ejemplos de eso, pero adelanto que es una solución muy poco elegante.

18 abril 2011

Java coding best practices

For a software project in which I worked as a external code/design reviewer, a contractor asked me to do a document with Java coding best practices.

Instead of giving them a big document that nobody is going to read, I preferred to make a small list of good practices - and try to fit them in a A4 page.

I'm not a big fan of "best practices" documents. I know that they are good to standardize the work and share knowledge. But there is a "dark side": a "best practice" depends on a particular context. So is good to understand why a practice was good and distinguish when there are better options; in the end you can't get blood out of a stone: the best way to produce good quality systems is to work with good professionals.

So this is the list of best practices that I made (in no particular order):

Choose Meaningful Names
- Classes:
     If a class represents a concept of the problem domain, use a well known name for that domain (the technical analyst/product owner is our friend here).
     Forget the name, look at the class responsibilities and them choose a name based on that (CRC helps).
     Using suffixes like "Data", "Info", "Object"; doesn't add information and can be confusing ie. What is the difference between Customer and CustomerInfo? 
- If you still don't known how to name something and you invent a new name: communicate that to your team, and be sure that everybody agrees to name "the thing" in the same way. 
- When you start to feel uncomfortable with a method or class name and you have a better name, don't heistate: rename it.
Write short methods
- Remember the "Extract method" refactoring shortcut keys, you are going to use them a lot. 
- Methods from comments: usualy in a long method programmers start to use comments to explain parts of the code, create a new method instead of using comments in that way. 
- If you can't do an extract method because you end with lots of  parameters or you have some obscure dependency; is a sign that something is wrong with the design. Sometimes using a "Method Object" to fix that helps to find what is missing.
Avoid undesired side-effects (use immutable objects as much as possible)
- Avoid mutable global state, ie: non-final static variables or "singleton" like globals (see "Avoid Singletons"). 
- Try to use constructors with arguments instead of a combination of "default constructor/setters" (this is related with "Avoid object instances in invalid state" and "Avoid nulls")- 
- Be careful when returning collections: most of the Java collections are mutable, returning those could expose your object internal state.
Avoid object instances in an "invalid state"
- Declare required instance variables as final, and initialize them in the constructor. 
Use good defaults. 
- If you end with a constructor with lots of parameters: refactor the code, maybe you can group of those parameters in an object.
- If you need to build a complex instance, you can create a builder (but never put the validations in the builder, put them in the constructor).
Avoid nulls
- You can use the Null Object pattern 
- Use good defaults instead of null
Use singletons ONLY to represent unique objects
- A lot of programmers use the singleton pattern to point to globally well known objects, this combined with mutable objects is a time bomb: lots of undesired side effects and code that could have concurrency problems. 
- If you need a "setInstance" method for testing purposes: is not a singleton! 
- Usually "Null Objects" can be singletons, but most of the time singletons are not needed. 
- Singletons should be immutable.
Classes should have one responsibility - one reason to change
- Using the CRC method sometimes helps to think in single responsibility classes. 
- Classes with many responsibilities tend to be difficult to test with unit tests; so a hard to write unit test could be a sign that you are dealing with too many responsibilities.
Write unit tests like specifications
- In TDD the most confusing part of unit test is the word "test". If you think in tests like in small specifications of that your code supposes to do, then is very easy to write tests first.
- Don't put to many assertions in one test method, is better to have only one or two assertions per unit test method. Why? You'll start working faster with the TDD cycle and if something fails is easy to see what's happening.
Use exceptions instead of return codes
- If you "Avoid Nulls" you will find this straight forward.
- Think in exceptions in a layered way, and handle them in the place that you could do something about it.
- Don't hide exceptions with empty catch blocks: if you have a bug it will be very difficult to find.
- Never catch Throwable: because you can catch sub-classes of Error too.
When creating polymorphic class hierarchies: remember the substitution principle
- If you have a method that is overridden by different subclasses: avoid coupling between the type of the method argument and specific subclasses.
- If you need to cast an argument, maybe you are not following the substitution principle, take account that even if the "subtypes" are ok to the compiler you will not be able to change implementations easily.  
Optimize only if it's necessary and with the help of a profiler
- Applying optimization tips blindly... tend to create wrong assumptions, and sometimes code that is difficult to understand.
- A linear search in a collection with a bounded number of elements is O(1) (even when the max number of elements is 100, 1000 or 10000)
- I/O is orders of magnitude more expensive than CPU/Memory tasks.
- Generational Garbage Collectors like the ones in Java are optimized for short lived objects, it means that if you need to create objects to write more understandable code: create them. Early optimizations in object instance creation could cause other problems in concurrency and impact in the GC time; optimize object creation ONLY if you conclude that is necessary after using a profiler.

13 abril 2010

Composición

Hace unos años atrás cuando dictaba cursos sobre POO y Java, di un ejercicio que consistía en diseñar el "checkout" de un Supermercado con soporte para ofertas del estilo "lleve 2 y pague 1".
La intención del ejercicio era que los alumnos piensen en como utilizar objetos para lidiar con la complejidad de aplicar las diferentes ofertas.
Esperaba también que se dieran cuenta que el precio de un producto depende de factores como: la fecha, el "mercado", ofertas u estrategias de negocio.
Imagine que comenzarían planteando la clase Producto con un "estilo base de datos":


Y que a lo largo del ejercicio iban a llegar a una especie de "lista de precios":


Sin embargo no fue como esperaba. Para uno de los alumnos la expresión "el producto tiene un precio" significaba que el un producto tenia que responder directamente su precio. Trate de justificar mi posición desde distintos puntos de vista, pero luego abandone la discusión.

Nota al margen:
Mi experiencia dando clases me enseño que es mejor no entrar en discusiones sin salida. Es preferible escuchar pacientemente y expresar cuales son los problemas del planteo desde distintos puntos de vista, con suerte la discusión evoluciona y se aprende mucho en el proceso. Pero si la discusión se estanca, es preferible abandonarla.

Escribo sobre este ejemplo después de tanto tiempo, por que veo que el problema de como expresar la relación de composición se repite en otros contextos.
Por ejemplo ¿Cómo modelarían que "un cliente que tiene contactos"? Una forma de hacerlo es:



Sin embargo, al hacer la traducción literal de este diagrama de clases, el modelo esta diciendo: "el cliente esta compuesto de contactos". La diferencia parece sutil, pero a medida que el sistema crece tiene consecuencias importantes:
  • Agregar nuevos contactos implica modificar la instancia de cliente y trae consecuencias en la forma de asegurar que se cumplan las reglas de negocio, por ejemplo: que un cliente cambie su numero CUIT es raro y puede afectar a los sistemas de facturación; pero es esperable que sus contactos cambien seguido sin mayores consecuencias para el resto del sistema, entonces hay que distinguir entre "modificaciones pesadas" y "modificaciones livianas".
  • Transmitir el cliente por "el cable" implica transmitir sus contactos, y como esta información puede ser pesada uno tiene que usar alguna estrategia como un objeto que represente una vista del cliente sin sus contactos (DTO), o bien hacer que la lista de contactos se cargue por demanda (lazy load).

Nota al margen: Creo que uno de los causantes de confusión es el uso del verbo tener. En nuestra manera de expresarnos es natural decir "el producto tiene un precio", sin embargo el "tiene" de nuestro lenguaje no expresa necesariamente una relación de composición. Erich Fromm, escribió un libro muy bueno llamado "Tener o Ser" que habla sobre la influencia del "tener" en la sociedad. Es curioso como el consumismo cambio nuestras formas de expresión, por ejemplo en lugar de decir "siento un dolor de cabeza" decimos "tengo un dolor de cabeza".

En cierto sentido el problema es similar al de Producto-Precio, una alternativa para expresar la relación del cliente con sus contactos es tener una "agenda de contactos" mediante la cual se pueden encontrar los contactos para un cliente:



¿Qué modelo expresa mejor la realidad?

Creo que pensar que un modelo es correcto por que expresa "la realidad" mejor que otro, sufre del problema de dar por sentado que nuestra percepción y traducción al sistema es absolutamente correcta: para uno de mis alumnos la realidad que "el producto tiene un precio", se traducía correctamente en "unProducto.getPrecio()".

Por eso es necesario tener en cuenta cuestiones que afectan a la percepción y traducción del problema:
  • El verbo "tener" tiene un uso extremadamente ambiguo en nuestra cultura, por ejemplo es más adecuado decir que a un producto se le asigna un precio y quizás de esa forma ya vemos el dominio con otra perspectiva.
  • Los "ritmos" de cambio pueden hacer que sea más conveniente pensar en objetos distintos. Por ejemplo el precio varia con el tiempo y el entorno, pero el producto no; algo similar ocurre con el cliente y sus contactos.
    Separar las cosas que cambian a distintas velocidades ayuda a evitar side effects innecesarios y simplifica la aplicación de reglas de negocio.
  • Ver si existe en el dominio un intermediario en la relación, como por ejemplo una "lista de precios". Es claro que el software es un medio distinto: la lista de precio existe físicamente por que es una herramienta para definir y recordar el precio de un producto, y ese rol puede cumplirse en software sin necesidad de tener una "lista de precios" propiamente dicha. Sin embargo explorar esas analogías puede servir para ver conceptos del dominio.

10 diciembre 2009

Test driven development... algunas lecciones aprendidas

En este post quería compartirles algunas cuestiones sobre Test Driven Development (TDD) que fui aprendiendo de los proyectos y personas con las que trabaje.

TDD != Testing en busca de bugs

Es muy común confundir TDD con testing: al menos comparten la palabra "test" en el nombre.

La diferencia esta en que uno generalmente usa la palabra "testing" para referirse a la búsqueda de bugs. Mientras que en TDD la intención es otra: uno busca agilizar el diseño, facilitando la incorporación de cambios.

A grandes rasgos TDD consiste en expresar con un programa las "expectativas" sobre lo que se va a desarrollar.


Supongamos, por ejemplo, que estamos aprendiendo C con libro de Kernighan y Ritchie y su ya famoso "Hello World", así que escribimos:

#include <stdio.h>
int main() {
    printf("Hello World\n")
    return 0;
}

Compilamos...

$ cc hello.c -o hello
hello.c: In function ‘main’:
hello.c:5: error: expected ‘;’ before ‘}’ token

Arreglamos código, compilamos y ejecutamos, hasta que vemos en la pantalla el resultado esperado:

Hello World

La diferencia usando TDD es que primero escribimos un programa para verificar el resultado esperado (algo que comúnmente hacemos de forma manual):

ejecutarPrograma();
verificar(salidaDePrograma, "Hello World");

Nuestro "test", no intenta buscar bugs probando diferentes casos, o casos "borde" que puedan ser problemáticos.
Si no que simplemente es una forma de establecer que es lo que queremos hacer.

Esta diferencia es sutil pero tiene muchas implicancias en la forma de trabajar:
  • No es necesario pensar casos de test complejos, solo basta con pensar que es lo que se quiere hacer.
  • Con TDD el ciclo de escribir código - compilar, se transforma en: escribir tests/código - compilar/ejecutar tests.
    Es decir, crear/ejecutar tests debe ser parte común del desarrollo. Si los tests tardan en ejecutarse se transforman en una carga.

En mi caso los "tests" usados de esta forma se convierten en una especie de "TODO list". En efecto muchas veces siento que si no tengo planteado un "test" (aunque sea en mi cabeza) no sé por donde empezar.

¿Vale la pena dedicar tiempo a escribir tests?


El ejemplo de "Hello World" muestra una cuestión evidente: programar el test puede ser mucho más difícil que hacer el programa a testear.
Cabe preguntarse si es siempre así, y si el trabajo adicional vale la pena.

En general no siempre es tan difícil escribir tests, y hay factores que influyen en la dificultad:
  • Componentes y frameworks "acoplados" a sistemas externos.
    Por ejemplo, si quisieran escribir el test del "Hello World" en C deberían capturar el STDOUT del programa. Suele ser fácil reemplazar la salida standard, aunque si el stream fuese parametrizable sería mucho más fácil.
  • Frameworks que no proveen interfaces separadas de a implementación.
    Siguiendo con el "Hello World", supongamos que podemos parametrizar el stream de salida, sería ideal poder capturar la salida en un String.
    Si el "Stream" que nos brinda el lenguaje no permite hacerlo, podemos hacer otra implementación que cumpla la misma interfaz y capture el String. Pero si "Stream" es una clase cerrada, estamos en problemas.
  • Diseños que no cumplen con el principio de Single Responsibility.
    Si la "responsabilidad" de lo que queremos testear es acotada, entonces el test también va a ser más corto y fácil de escribir.
  • Dependencias globales mutables (como Singletons que no son "singletons").
    En un post anterior ya hable sobre los problemas del estado global... así que voy a evitar la tentación de volver a comentarlos :)
  • Tests con grandes conjuntos de datos.
    Complican la mantención del test y por lo general también hacen que la ejecución sea más lenta.
  • El lenguaje/tecnología a usar influyen en la forma de trabajo.
    Por ejemplo en Smalltalk es muy común desarrollar mientras se ejecuta el test (completando lo que falta implementar en el debugger), ya que entorno permite modificar clases sin "parar el mundo". En Java este tipo de prácticas no es posible.
    En otro extremo el lenguaje C++ no cuenta con reflection out-of-the-box, y por lo tanto escribir tests es mucho más trabajoso (hay que indicarle al framework explicitamente que tests ejecutar).
    Es conveniente tener en cuenta la tecnología que se usa, por ejemplo en C++ intentería tener tests más grandes para evitar el "overhead" que implica crear tests nuevos. En Smalltalk, Java u otros lenguajes el entorno ayuda a que este problema no exista.

Desde un punto de vista de management, quizás la cuestión esta en cual es el "retorno de inversión" de lidiar con todo esto. Para mi el valor esta en que:

  • Al tener tests automatizados uno tiene más confianza a la hora de hacer refactorings o cambios en el diseño.
    Esto puedo afirmarlo con mi experiencia: trabaje en un sistema financiero donde teníamos una gran cantidad de tests (si no recuerdo mal, más de 10.000).
    Tuve que hacer un rediseño importante de una parte "core" del sistema, hice los cambios y emepecé a ver los tests que fallaban.
    Los tests que entendí, los arregle. Los que no, los consulte con sus autores y los arreglamos en conjunto. Todo este trabajo hubiese llevado muchísimo más tiempo sin tests automatizados.
  • Ayuda a tener un diseño más abierto a los cambios
    La razón es simple: cuanto más acoplado esta un objeto al uso de sistemas o frameworks externos, más difícil es testearlo. Por lo tanto con TDD uno tiende a diseñar de forma tal de disminuir este acoplamiento.
    Lo mismo ocurre cuando los objetos tienen dependencias globales mutables: se vuelven dificiles de testear por que un test cambia el estado global, haciendo que otro test falle. Por lo tanto uno tiende a evitar este tipo de globales.
    A la larga las consecuencias de usar TDD son beneficiosas para el diseño.

No todo es color verde...

Como mencione antes: la intención de TDD es facilitar cambios y ayudar en el diseño. No encontrar bugs.

Por lo tanto aunque la aplicación pase los tests de unidad, puede contener errores funcionales (casos de test mal planteados), bugs por casos "especiales" y problemas de UI.

Volviendo al ejemplo de este sistema en el que trabaje con más de 10.000 tests de unidad:
Al principio no teniamos un equipo de QA, los tests automáticos pasaban y algunos de nosotros probamos la aplicación durante el desarrollo.

Llego el día "D": empaquetamos todo para producción y llevamos el software al cliente.

A las horas recibimos un llamado: un botón de la UI permanecía deshabilitado y el cliente no podía acceder a una funcionalidad del programa (funcionalidad que estaba desarrollada, pero inaccesible). Asi que tuvimos que corregir el problema y hacer un nuevo deploy.

Los tests de unidad no reemplazan a un equipo de QA. No es la intención de TDD reemplazar las prácticas comunes de testing y verificación de calidad.

La diferencia esta en los detalles

Supongamos los convencí de las "bondades" de TDD. Asi que empiezan a practicarlo en un proyecto.

Al tiempo hay una alta probabilidad de que empiece a pasar lo siguiente. Algunos tests fallan pero arreglarlos es un problema: los tests se volvieron enormes e inmantenibles. Solución rápida: los tests que fallan se ignoran. Un par de meses despúes los tests que fallan se siguen ignorando, y se pierde la ventaja de TDD: si necesitamos hacer un cambio de diseño no tenemos forma de saber que rompimos.

¿Por que se llega a este punto?
Principalmente es una cuestión de la costumbre del equipo en hacer y mantener los tests.
Pero además hay un montón de detalles que con el tiempo se acumulan cual bola de nieve e influyen en que los tests se vuelvan inmantenibles. Algunos "detalles" a tener en cuenta:

No depender de bases de datos externas

Hacer que un test de unidad requiera de una base de datos es problemático por que:
  • El entorno de desarrollo es dificil de configurar: cada desarrollador tiene que configurar los drivers y conexiones para poder empezar a trabajar con los tests.
  • Si se usa una base compartida: un cambio hace que los tests fallen para el resto de los desarrolladores. Para hacer TDD los tests se deben correr todo el tiempo, asi que tener tests que fallen por cambios de otros es inadminsible.
  • Es más trabajoso para usar un servidor de integración continua: hay que asegurarse de que el servidor de integración use correctamente la base de datos de prueba.
  • La ejecución de los tests suele ser más lenta, asi que correr todos los tests no es algo que se haga muy seguido.

En el caso de que un test necesite una base de datos es necesario distinguir:
  • Que es lo que se esta testeando: ¿Quiero realmente testear el acceso a la base de datos?
  • ¿Puedo reemplazar el acceso a la base de datos, por una implementación que "simule" la respuesta y no acceda a una base de datos real?

Muchas veces uno quiere testear la implementación a la base de datos, por ejemplo se usa Hibernate para responder algunas consultas y cambiar la implementación por una que simule la base no aporta nada.
En esos casos lo mejor es usar una base en memoria (como HSQLDB) y levantarla durante el test.

HSQLDB es liviano, soporta Hibernate y toda la sintaxis de SQL. El unico punto en contra es que si uno quiere "ver" como se guardan las cosas en la base hay que frenar el test para no bajar el servidor.

El siguiente es un pequeño ejemplo de como usar HSQLDB en un test:

public class TestDatabase {
    public static final String DEFAULT_DATABASE_NAME = "test";
    public static final int DEFAULT_SERVER_PORT = 9001;

    private Server hsqldbServer;
    private HsqlProperties hsqldbProperties;
    private String jdbcConnectionUrl;
    
    public TestDatabase(String databaseName, int serverPort) {
        hsqldbProperties = new HsqlProperties();
        jdbcConnectionUrl = "jdbc:hsqldb:hsql://localhost:" + serverPort + "/" + databaseName;
        hsqldbProperties.setProperty("server.port", serverPort);
        hsqldbProperties.setProperty("server.database.0", databaseName);
        hsqldbProperties.setProperty("server.dbname.0", databaseName);
    }

    public static TestDatabase startWithDefaultConfiguration() {
        return new TestDatabase(DEFAULT_DATABASE_NAME, DEFAULT_SERVER_PORT).start();
    }

    private TestDatabase start() {
        hsqldbServer = new Server();
        hsqldbServer.setProperties(hsqldbProperties);
        hsqldbServer.start();
        return this;
    }

    public void shutdown() {
        if (hsqldbServer == null) return;
        hsqldbServer.shutdown();
    }
}

public class AccesoALaBaseTest {
    private static TestDatabase database;

    @BeforeClass
    public static void startTestDatabase() {
        database = TestDatabase.startWithDefaultConfiguration();
    }
    
    @AfterClass
    public static void shutdownHsqldbServer() {
        database.shutdown();
    }
}

No usar herencia para compartir instancias de prueba

Usar herencia para compartir instancias entre tests es un error muy común.

Supongamos que estamos testeando la implementación de CajaDeAhorro. Para crear una instancia de la caja de ahorros, necesitamos una instancia de Cliente, que a su vez necesita una instancia de Contrato, que a su vez necesita una instancia de... bueno se hacen a la idea: crear todas estas instancias es trabajoso, nos tomamos el trabajo :(

Despúes queremos testear CuentaCorriente, y queremos reusar las instancias que creamos para el test de CajaDeAhorro.
Muchas veces se suele crear una superclase para el test, supongamos AbstractTest, donde se colocan estas instancias que necesitamos compartir.
Los desarrolladores empiezan a heredar todos los tests de AbstractTest, y van "subiendo" instancias que quieren compartir entre tests.

¿Se ve el problema?
AbstractTest termina siendo una gran bolsa de gatos. Cuando se quiere refactorizar AbstractTest hay otro problema: es engorroso buscar las referencias a cada variable usada en las subclases. Por lo general este refactoring implica mucho trabajo y nadie lo hace. AbstractTest sobrevive a varias versiones transformandose en una inmensa bola de lodo.

La solución:
No usar herencia para compartir instancias entre los tests (usar herencia para re usar código es mala idea, en este caso es malisima).
Es preferible crear una clase, por ejemplo ClienteTestResource (suelo usar el sufijo "TestResource" para esas clases) que brinda instancias de Cliente para los tests.

Nada evita que ClienteTestResource no se convierta tambien en una bolsa de gatos, pero la cuestión es más controlada: uno puede crear clases distintas para agrupar recursos necesarios en los tests. Y a diferencia de AbstractTest, los desarrolladores solo usan ClienteTestResource cuando lo necesitan.

Evitar código redundante

Otro problema común es la actitud de: "hago copy & paste, total es un test!".

Esto es perjudicial: los tests tambien hay que mantenerlos. Si hay que hacer un "copy & paste" para crear instancias que se necesitan en el test... usar un "TestResource".

Si hay que implementar un método en común para facilitar el testing: ¿Entonces por que no implementarlo en el modelo? Por ejemplo, si estoy testeando el balance de una cuenta y el codigo del test es algo asi:

cuenta.balanceAl(crearFecha("01/01/2009"));
...

private function Date crearFecha(String s) {
      SimpleDateFormat sdf .....
      return sdf.parse(s);
}

Y empiezo a copiar "crearFecha" en varios tests, la alternativa es dar una interfaz amigable en Cuenta (la otra alternativa es tener una interfaz más amigable para construir instancias de Date en general, un buen ejemplo de esto en Smalltalk es el framework Chaltén, que pemite expresar fechas como: "Jaunary first, 2009" donde Jaunary es un objeto first y , son mensajes... asi que la expresión es basicamente da una fecha) que permita escribir:

cuenta.balanceAl("01/01/2009");

Eso no quita que el metodo balanceAl(Date) no tenga que existir. El formateo de fechas depende del Locale y no debería usarse con un Locale implicito en la aplicación. Sin embargo es conveniente pensar en la facilidad de uso de las interfaces que uno provee: las interfaces de los objetos son como las interfaces graficas.

Tener interfaces que permitan expresar las cosas de forma natural, usando defaults apropiados y simplificando ciertas cuestiones, ayuda muchisimo a la hora de escribir tests.

La clave es pensar que cuando uno programa esta construyendo un lenguaje, y esto abarca tambien a los tests. No es lo mismo escribir:

assertThat(coleccion, hasItem("hola"));

Que escribir:

boolean found = false;
for (String s : coleccion) {
    if (s.equals("hola")) found = true;
}
assertTrue(found);

(Nota: sé que existe coleccion.contains("hola") pero quería explicitar la fealdad de esta alternativa ;) )

Un test por "observación"

Supongamos que para una CajaDeAhorro queremos testear que:
  • Los depositos incrementan el balance.
  • Las extracciones decrementan el balance.
  • No se puede hacer una extracción si no hay fondos.

Uno puede estar tentado a testear todo esto junto:

@Test public void depositoYExtraccion() {
assertThat(cuenta.balance(), is(monto));
cuenta.depositar(monto);
assertThat(cuenta.balance(), is(monto * 2));
cuenta.extraer(cuenta.balance());
assertThat(cuenta.balance(), is(0));
try {
    cuenta.extraer(monto);
    fail();
} catch (NoHayFondosException e) {}
}

Pero tiene algunos problemas:
  • Si el test falla es necesario debuggear para saber si el problema esta en depositar, extraer o balance.
  • El ejemplo es chico, pero en tests más grandes los errores tienden ser más dificiles de ver.

En comparación esta solución es mejor:

@Test public void losDepositosIncrementanElBalance() {
    assertThat(cuenta.balance(), is(0));
    cuenta.depositar(monto);
    assertThat(cuenta.balance(), is(monto));
}
@Test public void lasExtraccionesDecrementanElBalance() {
    assertThat(cuenta.balance(), is(not(0)));
    cuenta.extraer(cuenta.balance());
    assertThat(cuenta.balance(), is(0));
}
@Test(expected=NoHayFondosException.class)
public void noSePuedeHacerUnaExtraccionSiNoHayFondos() {
    assertThat(cuenta.balance(), is(not(0)));
    cuenta.extraer(cuenta.balance() * 2);
}

Parece más largo pero tiene varias ventajas:
  • Es más facil ver que esta fallando.
  • Si un test falla es más facil de corregir.
  • Tiene (para mi) una implicación psicologica: ir haciendo pequeños tests que van pasando, en general me ayuda a tener un mejor ritmo de trabajo. Incluso entre pequeños test a veces se me ocurren rediseños que no habia pensado originalmente.

Evitar tests no deterministicos

Cuando el resultado esperado depende de un valor del "entorno", por ejemplo: la hora actual o un número random. Es común terminar con tests no deterministicos: a veces pasan y otras veces no.

Por ejemplo supongamos que estamos testeando una aplicación que genera registros de auditoria con la hora actual:

registroEsperado = new RegistroAuditoria(nuevoTimestamp, etc, etc, etc);
sistema.ejecutarMetodoQueGeneraAuditoria();
assertThat(sistema.logDeAuditoria(), hasItem(registroEsperado));

El problema es que si la comparación de RegistroAuditoria involucra comparar el "time stamp", entonces el test pasa o falla según la resolución de la hora y si hubo pausas en el medio de la ejecución (por ejemplo se ejecutó el GC).

Una forma de evitar este tipo de cosas es "fijar" estos valores. Por ejemplo podemos tener una interfaz Clock que provee la hora:

Clock fixedClock = new FixedClock(nuevoTimestamp);
sistema = new Sistema(fixedClock);
registroEsperado = new RegistroAuditoria(nuevoTimestamp, etc, etc, etc);
sistema.ejecutarMetodoQueGeneraAuditoria();
assertThat(sistema.logDeAuditoria(), hasItem(registroEsperado));

Ya no hay más problemas de indeterminismo :)

Evitar delays

Muchas veces por razones de concurrencia, se termina agregando al test un "delay" (por ejemplo se ejecuta un thread y quiero esperar a que el thread actualize un valor que despues voy a verificar).

Este tipo de test tiene dos problemas:
  • El delay hace que los tests corran más lento.
  • Por lo general resultan en tests no-deterministicos por que dependen de como la plataforma maneja la ejecución de los threads.

La alternativa es investigar un poco más lo que se esta testeando. Si es necesario tener el thread separado en el test conviene usar un semanforo (o monitor en Java) para hacer un wait hasta que se modifique el valor.

Lamentablemente no tengo ningun ejemplo a mano para mostrar en el blog.
Lo he hecho tanto en Smalltalk y en Java, y quizas lo unico que puedo agregar es que las cosas concurrentes son extremandamente dificiles de testear.
Algunas veces una solución intermedia es usar "yield" para que la VM ejecute otros procesos, esto salva el delay pero puede generar tambien tests no-deterministicos... la mejor opción sigue siendo usar algun tipo de semaforo.

De todas formas vale la pena hacer la "investigación" para evitar el delay: uno termina aprendiendo muchas cosas de concurrencia :), y tener un delay de un 1seg en varios tests es muy molesto.

Usar mock objects con cuidado

Un mock object es simplemente una implementación "de mentira" que se usa para reemplazar en un test a una implementación real (esta es al menos mi definición de mock object).

En el ejemplo anterior FixedClock es una implementación de mentira de Clock, que nos facilita el testing.

Existen frameworks de mock objects que por lo general:
  • Facilitan la implementación de mock objects usando meta-programación.
  • Permiten hacer tests de caja blanca, pudiendo validar si se ejecuto o no un método del mock object.

En mi experiecia estos frameworks terminan derivando en tests dificiles de mantener:
  • Los tests de "caja blanca" suelen ser una mala idea, al primer refactoring fallan y se convierten en un "dolor" de mantener. En esos casos es mejor re-plantearse el caso de test (y recordar cual es la intención de los tests en TDD).
  • En general es mucho más facil tener una interfaz y hacer la implementación de mentira para el test que usar un framework de mock objects.
  • Cuando no se generan dependencias adicionales (explicación a continuación) y no hay problemas como en el ejemplo de FixedClock, es más facil usar objetos reales que mock objects (usar un "TestResource" puede ayudar a escribir menos codigo en estos casos).
  • En lenguajes dinamicos como Smalltalk es muchisimo mas facil crear mock objects, pero tambien tienen problemas: si se usan muchos mock objects y se hace un refactoring, es probable que los test compilen y pasen... aunque deberían haber fallado.

Un pequeño hint: usar mock objects no es necesariamente malo, pero necesitar de mucha logica en un mock object es signo de que algo esta mal. A veces no se necesita un mock object si no una implementación más sencilla de la "interfaz" que queremos simular.

Ser cuidadoso con las dependencias de los tests

Ya estoy llegando al final de este enorme post.

El tema de las dependencias de tests requiere un poco más de explicación, asi que voy a ser breve:

Muchas veces uno expresa los tests a más alto nivel por ejemplo:

"El usuario agrega items al carrito de compras. Procede al checkout, donde obtiene una factura de los items que compró"

Este test abarca toda una historia de usuario y a veces es bueno automatizarlo.

Sin embargo hay que tener en cuenta que este tipo de test no solo es más largo, si no que posiblemente dependa de otras implementaciones: por ejemplo el carrito de compras se implementa en un proyecto, la generación de factura en otro y quien usa ambas implementaciones (llamemosle Cajero) usa interfaces asi que el proyecto donde esta el Cajero no depende de una implementacion particular de carrito de compra o el generador de factura.

¿Donde colocamos el test?
Si lo hacemos en el proyecto donde esta Cajero, y no usamos mock objects, generamos dependencias adicionales que no son necesarias (o quizas dependencias circulares que son problematicas). Y si usamos mock objects para todo, el test se vuelve más dificil de mantener.

Lo mejor en estos casos es ver el test a otro nivel. Si quieren pueden llamarle "test de integración" (siguiendo una convención que usabamos en Mercap, prefiero llamarles "user story test" por que abarcan una historia de usuario, y reservar el nombre "test de integración" para tests que verifican la integración con sistemas externos).

Es conveniente que este tipo de test este en un proyecto separado. De esta forma se evitan dependencias circulares, y es más facil compartir información entre dintintos "user story tests" (usando de "TestResources").

Además este tipo de test suele ser más lento, y por lo general no se ejecuta con la misma frecuencia que los tests de unidad.

El problema de este tipo de test es que son más grandes, tienen más dependencias y por lo tanto más dificiles de mantener.

Pero cuando las fechas de entrega apremian, a veces no hay tiempo para estar haciendo muchos tests a nivel unitario. En esos casos con tests de este estilo se puede testear la implementación manteniendo un estilo TDD (uno escribe el "user story test" antes de empezar), sin necesidad de crear un test por cada una de las clases que se usan "internamente". En este caso las herramientas de covertura pueden ayudar a ver que se cubre con el "user story test" y que no.

Si bien esta es una alternativa que complementa a los tests de unidad, no los reemplaza: los tests de unidad son una buena forma de trabajar a "nivel micro", pueden ejecutarse todo el tiempo durante el desarrollo y son más faciles de mantener.

Espero no haberlos dormido con este post enorme, trate de resumir un poco las "lecciones aprendidas" con TDD.
Hasta la proxima :)

12 agosto 2009

Side effects

El post anterior estuvo dedicado a la representación de "estados" en el dominio, donde mencione al pasar que el código del estilo: "objeto.getEstado()/objeto.setEstado(X)" tiene además otras complejidades, producto de algo que los programadores de lenguajes funcionales odian bastante: side effects.

Para los que se desayunan con el término voy a contarles de que se trata. Supongamos el siguiente programa:

a := 10
b := a + 10
if (b = a + 10) then retornar "OK" else retornar "FALLA"

Sabemos de ante mano que el programa retorna "OK", asi que podemos simplificar el código y ahorrarnos muchas vueltas (incluso el compilador podría realizar estas simplificaciones por nosotros). Ahora añadimos un procedimiento "inofensivo":

a := 10
b := a + 10
procedimientoRaro()
if (b = a + 10) then retornar "OK" else retornar "FALLA"

¿Seguimos sabiendo que el programa retorna siempre "OK"?
Antes de decir si, voy a poner una condición tramposa:
Dado que este es un programa en pseudo-código y no sabemos como se evalua nuestro supuesto lenguaje, supongamos que la variable "a" se puede acceder y modificar desde "procedimientoRaro()".

Ahora si volvamos a la pregunta: ¿El programa retorna siempre "OK"?
No lo sabemos :-(.
Tendríamos que examinar el "procedimientoRaro()" (si es que tenemos el código) un lindo procedimiento con más de 200 lineas de código y millones de ifs/fors anidados.

Sobre la "modularidad" del ejemplo podríamos decir que la variable "a" es una global y "procedimientoRaro" no debería usar globales, etc, etc. Asi que vamos a cambiarlo:

a := 10
b := a + 10
procedimientoRaro(a)
if (b = a + 10) then retornar "OK" else retornar "FALLA"

¿Seguimos sabiendo que el programa retorna siempre "OK"?
Quizás no, el problema es si "procedimientoRaro" puede modificar o no el valor de "a", y si nuestro lenguaje copia el argumento o lo pasa por referencia.

Podemos seguir con innumerables ejemplos, la cuestión es que en nuestro lenguaje imperativo "procedimientoRaro" puede tener side effects: afecta estados compartidos con el resto del programa.
La raíz de los problemas reside en la asignación. Al incorporar la capacidad de asignar valores a la variable "a", incorporamos implícitamente la noción de estado de ejecución.
Es decir la verificación "b = a + 10" ya no depende de la solo de la expresión, si no que depende además del estado. (hay una explicación muy buena sobre asignación y estado en el capitulo 3 del libro Structure and Interpretation of Programs).

“Todo esto parece muy teórico. En el trabajo diario ¿De qué me sirve?”

Supongamos que estamos trabajando en un sistema que se encarga de registrar la reserva de salas para una empresa de capacitación:
  • CalendarioDeSalas: Lleva un control de la reserva de salas. Una sala no puede reservarse en un intervalo de tiempo que ya este en uso.
  • IntervaloDeTiempo: Representa un rango de fecha-hora.



Con estas clases podríamos escribir un código así:

// reserva la sala del día 16 al 17
// el calendario verifica la disponibilidad
intervaloDeTiempo = new IntervaloDeTiempo();
intervaloDeTiempo.setDesde("16/07/2009");
intervaloDeTiempo.setHasta("17/07/2009");
calendario.reservar(unaSala, intervaloDeTiempo);

// reserva la sala del día 20 al 21
otroIntervaloDeTiempo = new IntervaloDeTiempo();
otroIntervaloDeTiempo.setDesde("20/07/2009");
otroIntervaloDeTiempo.setHasta("21/07/2009");
calendario.reservar(unaSala, otroIntervaloDeTiempo);

// cambia la fecha de la primer reserva..
// OOPS! el calendario ni se entera!
// este cambio rompe con la especificación del calendario
intervaloDeTiempo.setDesde("20/07/2009");
intervaloDeTiempo.setHasta("21/07/2009");

“Pero si esto es un problema: ¿Por qué no me encuentro con estos errores en mi mega sistema J2EE/Spring/Hibernate/etc/etc…?”

El problema del ejemplo puede aliviarse superficialmente si el calendario se encarga de copiar el intervalo antes de registrarlo.
Esto es básicamente lo que sucede con todos los mapeos Objeto-Relacional: lo valores se copian a la base de datos y luego las instancias son regeneradas.
Pero el problema aunque oculto sigue estando. Basta con agregar un “cache”, o usar la instancia de IntervaloDeTiempo de forma compartida sin darse cuenta (por ejemplo en un widget de UI), para que empiecen a ocurrir bugs inesperados.

También surgen un montón de complicaciones innecesarias en el código debido a las “validaciones”.

Supongamos que quiero que en un IntervaloDeTiempo la fecha inicial sea siempre menor a la final. Empiezo a agregar una guarda en “setDesde” otra en “setHasta”, y genero una excepción.

Pero ahora tengo que tener en cuenta en qué orden establezco los valores.
Bueno podemos hacer que cuando uno de los valores es null no se haga el chequeo… ¡HORRIBLE! Complicaciones y más complicaciones.

Simplifiquemos un poco, hacemos un método setIntervalo(desde, hasta), nos ahorramos problemas de validaciones. Pero seguimos teniendo problemas de otro tipo: para que el calendario funcione correctamente hay que asegurarse que el intervalo de la reserva no se cambie sin conocimiento el calendario.

Como mencione antes podemos aliviar el problema haciendo que calendario se encargue de copiar la instancia de intervalo que recibe. Pero esta es solo una solución superficial, supongamos que tras varias iteraciones decidimos hacer un refactoring, en lugar de:

calendario.reservar(unaSala, intervalo);

Ahora tenemos un objeto que representa la reserva:



unaReserva = new Reserva();
unaReserva.setSala(unaSala);
unaReserva.setIntervalo(intervalo);
calendario.registrar(unaReserva);

¿Cómo nos aseguramos que nadie nos cambie el intervalo de la reserva? Tenemos que:
  • Copiar la reserva al registrar en el calendario.
  • Copiar el intervalo a establecerlo en la reserva.
  • Asegurarnos de hacer una nueva copia del intervalo al retornar getIntervalo() en Reserva.
  • Asegurarnos de hacer una copia de la reserva al retornarla en el calendario, o decorarla de alguna manera para que nadie cambie la reserva sin que se entere el calendario.
  • Inicializar correctamente la sala y el intervalo, y chequear en “registrar” que tengan valores validos.
  • ¡HORRIBLE! Seguimos teniendo mucha complejidad.

“¿Cómo puedo aliviar/evitar estos problemas?”

Evitando side effects, es decir eliminar la asignación (setters) usando objetos inmutables.
En el ejemplo es preferible que el valor de las variables de instancia de IntervaloDeTiempo no se puedan cambiar. Es decir que no exista un “setDesde” y “setHasta”.

Para eso solo hay que definir un constructor adecuado:

new IntervaloDeTiempo(desde, hasta);

Lo mismo sucede con Reserva:

new Reserva(unaSala, intervalo);

Remando contra la corriente

"¿Hasta que punto se puede diseñar pensando en objetos inmutables?"

Es evidente que hay casos donde conviene que los objetos sean mutables, por ejemplo en entornos donde la configuración puede cambiar dinamicamte (sin ir más lejos un ejemplo de este tipo de cambios dinamicos es el entorno de Smalltalk).
En otros casos la necesidad de objetos mutables tiene que ver con cuestiones de performance e interacción con otros frameworks (donde por framework incluyo también a las librerías standard del lenguaje, por ejemplo las colecciones).

Este ultimo punto es para mi el más molesto para los programadores Java: la gran mayoría de los frameworks Java adoptaron la convención de JavaBeans (con la que ya me ensañe bastante). El problema es que muchas veces adoptan esa convención de manera totalmente innecesaria, fomentando malas prácticas.

Por ejemplo, Spring puede hacer inyección de dependencias usando constructores. Sin embargo el manual de referencia menciona que la forma preferida es usar "setters", eso significa que la mayoría de los tutoriales usan inyección por setters y por lo tanto la mayoría de los "usuarios" del framework también.

Pero si uno le presta más atención al por qué, el manual dice: "Setter methods also make objects of that class amenable to reconfiguration or re-injection later", lo que me genera la duda ¿Cuantas veces uno diseña una aplicación con este grado de re-configuración en runtime? Mi punto es: existen tecnologías como JMX que facilitan los cambios en runtime, sin embargo no es el requerimiento común, y si fuese un requerimiento de la aplicación uno tendría que diseñar pensando en la complejidad que pueden generar estos cambios dinámicos.

La otra justificación para usar setters en Spring es cuando existen dependencias circulares, sin embargo este tipo de dependencias a veces pueden evitarse incorporando en el diseño un tercer objeto que haga de mediador.

Otros frameworks son peores: al no permitir otro uso que no sea mediante la convención de JavaBeans fuerzan este tipo de prácticas.

Aún así hay algunas prácticas de diseño que ayudan evitar side effects:
  • Mientras sea posible usar objetos inmutables.
  • Hacer las validaciones en el constructor, de esta manera si uno tiene una instancia ya se sabe que la misma fue construida correctamente.
  • Si el constructor queda enorme (cosa que es muy molesta para manejar), pensar de "dividir" el objeto en conceptos más pequeños. Por ejemplo en el ejemplo anterior la clase Reserva podría recibir el valor desde y hasta del intervalo: Reserva(desde,hasta,sala). Sin embargo separando el concepto de intervalo es mucho más simple (incluso para las validaciones): Reserva(intervalo,sala).
  • Si la construcción es complicada utilizar un builder. Lo bueno es que si la interfaz del builder se diseña con cuidado se pueden hacer DSL internos, logrando un código muy comunicativo.
  • Si en el dominio hay distintos "estados", pensar como se representan esos estados y utilizar algunas de las técnicas que mencione en el post previo. Por ejemplo, si en la UI tenemos que modificar una Reserva, podríamos tener un objeto ReservaBorrador que sea mutable y que actúe como builder de una Reserva inmutable.
  • No implementar equals en objetos mutables: es una muy mala práctica. Si no se dan cuenta por que: creen un objeto mutable, calculen equals y hashCode en base a variables de instancia que pueden cambiar, agreguen el objeto a un Set, modifiquen la instancia y vuelvan a probar si la misma esta en el Set.
  • Tener cuidado al retornar colecciones en un setter. Para que la colección no sea modificada pueden hacer una copia, o en Java decorarla para evitar modificaciones.

Estas cuestiones de side effects suelen ser importantes, no solo para el diseño, si no también para la escalabilidad de un sistema. Por eso cada vez más se empiezan a incorporar a los lenguajes orientados a objetos conceptos que vienen de lenguajes funcionales, el lenguaje Scala es un buen ejemplo que les recomiendo ver.

13 julio 2009

Estados

Cuando encuentro código del siguiente estilo:

if (factura.getEstado() == EstadoFactura.PAGA) {
    // código
} else if (factura.getEstado() == EstadoFactura.ENTREGADA) {
    // código
} else {
    // otro
}

no puedo evitar pensar: "¡horrible!". Por que conozco los problemas que genera este tipo de código y sé que con otro diseño pueden evitarse.

La cuestión es explicar como mejorar el diseño. En estas situaciones, mis intentos de explicación son frases un tanto pedantes como: "este uso del 'if' es feo, por que las decisiones sobre que hacer según un 'estado', en objetos se pueden resolver utilizando polimorfismo".

Lamentablemente este esbozo de explicación no ayuda, y noto que en general produce la siguiente reacción: 
"Todo muy lindo esto de objetos, mensajes y polimorfismo. Pero tengo en mi tabla FACTURA un campo ESTADO y esto refleja directamente eso. Además ¿Cual es la solución? ¿Usar el patrón State? ¿Hacer un montón de clases para algo que resuelvo en un 'if'? Dejemos la 'estética de objetos' para la teoría." 

Una explicación técnica...

Voy a comenzar por el camino común de dejar en claro por que en objetos este tipo de "if" puede evitarse.

El siguiente ejemplo:

if (figura.getTipo() == Figura.RECTANGULO) {
    area = figura.getBase() * figura.getAltura();
} else if (figura.getTipo() == Figura.TRIANGULO) {
    area = figura.getBase() * figura.getAltura() / 2;
} else if (figura .... ya se hacen a la idea de como sigue

puede resolverse mediante clases que representen a los rectángulos, triángulos, etc. 
Si todas estas clases son polimorficas con el mensaje "getArea()" entonces toda la seguidilla de "if" puede resolverse en una sola línea: figura.getArea().
Donde el receptor del mensaje es quien encapsula la decisión de como hacer las cosas.


Complicando el problema

Muchas veces es deseable que el algoritmo a ejecutar según el tipo de objeto este separado de los objetos.
Por ejemplo si Figura posee un método "dibujar" es probable que el algoritmo de dibujo genere dependencias con un framework de dibujo, y quizás no quiero "atar" al modulo de figuras con un framework de dibujo en particular, o quizás el algoritmo de dibujo varíe según el contexto.

Para este tipo de casos las alternativas son usar Double Dispatch o bien algún tipo de interfaz entre distintos frameworks/implementaciones donde en este caso dibujar seria algo así como "dibujarSobre(unCanvas)" siendo "unCanvas" es un objeto que implementa esa interfaz común.

Pero volviendo al problema original (de la Factura y Estado), hay algunas cuestiones a tener en cuenta:
  1. En el caso de la factura el "if" se hace en base al estado y no a un "tipo de factura", eso significa que a diferencia del ejemplo de las figuras el estado puede variar una vez creada la factura.
  2. Crear un objeto que represente el estado y usar Double Dispatch no parece ser en este caso una buena solución. (El por que lo dejo como ejercicio)

¿Cuál es la solución a este problema?
Rta: Examinar mejor el dominio:

  • ¿Tiene sentido hablar de "estado" de una factura?
  • ¿Que representa la factura?
  • ¿Realmente el cambio de "estado" representa el cambio de estado de una misma cosa o en realidad representa cosas distintas?

Examinando el dominio

NOTA: El ejemplo de Factura/Estado es ficticio, pero creo que captura muchos de los casos de negocio donde encontré código del estilo "if estado then ...".

En "Stan's Shop" la gente agrega productos a su pedido, paga en la caja donde se le entrega una factura, finalmente en el mostrador de entregas un empleado prepara los productos y una vez que se los dio al cliente coloca un lindo sello de "ENTREGADO" en la factura (un esquema similar al que siguen todos los locales de comida rápida del micro-centro).

Los diseñadores del sistema pensaron que era bueno tener un objeto "factura" especificado por la siguiente clase:


Este diseño tiene varios problemas:
  • Si la factura se ve como un comprobante de pago (al momento de pensar este ejemplo ficticio desconozco si en el negocio tiene otros usos), entonces la factura debe ser inmutable. Este hecho se refleja en que agregarItem y quitarItem generan error: solución problemática por que en todos los lugares donde quiera enviar estos mensajes tengo que tener en cuenta la posibilidad de error. Lo mismo ocurre con getNumero donde se debe chequear por null.
  • La "mutabilidad" de factura genera otros problemas de implementación, pero prefiero dejar los detalles para otro post sobre "side effects".
  • ¿Cómo sé cuando una factura es "valida"? Puedo chequear por getEstado() == PAGA o ENTREGADA, puedo chequear por getNumero() != null o agregar un nuevo método esValida (de intention revealing ni hablar). En la práctica encontré que se mezclan las tres formas dependiendo del programador, lo que genera algunos dolores de cabeza al momento de hacer un refactoring.
  • ¿Tiene sentido tener un estado "ENTREGADA"? ¿El hecho de saber si los productos fueron entregados o no, no es responsabilidad de otra área de negocio? Este es un ejemplo ficticio y no aporta mucho escarbar en detalles de negocio, simplemente lo menciono por que en la mayoría de los casos los problemas de diseño muestran una falla en reconocer objetos del dominio.

La solución a este problema es sencilla:
  • La factura representa para mi un comprobante de pago y nada más. Por lo tanto una vez generada no puede modificarse ya que tiene implicaciones contables (y quizás legales).
  • Cuando el cliente llega al local y elige los productos que quiere no esta trabajando sobre una "factura", si no sobre una especie de "carrito de compras".
  • Saber si se entregaron o no los productos no es responsabilidad de la factura, si no del sistema que lleva en control de las entregas.



Con este diseño la separación de responsabilidades es clara, no hay forma que en el código modifique items en una factura, o de pedirle el número de factura a un CarritoDeCompras, por lo tanto no hay necesidad de verificar un "estado".
El caso de "ENTREGADA" lo voy a discutir a continuación por que me sirve para ejemplificar otro "patrón" común.

Cambios de estado sin cambios de comportamiento

En el ejemplo el cambio de CarritoDeCompras a Factura implica un cambio en la semantica de los objetos. ¿Pero que pasa con el sello de "ENTREGADA"? ¿No se puede pensar como un simple flag booleano en una Factura?

No quiero entrar en detalles de este dominio ficticio, pero quizás un "flag" no alcance. Es probable que quiera registrar quien realizo la entrega, a que hora, etc.
Por eso voy a hacer una simplificación: no me interesan esos detalles, solo quiero el equivalente al sello de "ENTREGADA".

Y dado que en este ejemplo yo pongo las reglas, voy a suponer que el operador en el mostrador de entregas tiene una pantalla que muestra las facturas pendientes de entrega, donde con un click puede cambiar el estado a "ENTREGADA".

Una forma de diseñar esto es pensar que uno tiene un "sistema" (modulo, o como deseen nombrar) que lleva el control de los pedidos, con dos "recipientes": uno para la facturas pedientes y otro para las entregadas. Entonces pasar de estado es mover la Factura de un recipiente a otro:





¿Pero el booleano no era mejor? No, este esquema tiene muchas ventajas más:
  • Factura sigue siendo inmutable, e independiente de como llevo el control de entregas.
  • SistemaDePedidos puede variar fácilmente, llevando por ejemplo el control de la fecha de entrega, etc.

¿Y la UI? "Quisiera hacer una página web que muestre el listado de facturas entregadas y no entregadas, con getEstado() o el flag es mucho más fácil".

En este caso también es fácil por que puede resolverse de la siguiente manera:
  1. La UI puede pedir al SistemaDePedidos directamente las Facturas entregadas o las no entregadas.
  2. Supongamos que la solución 1. no es satisfactoria: por como se utiliza la UI realmente se quiere tener un objeto al cual se le pueda pedir el estado. En ese caso conviene hacer un objeto especifico para la UI, este objeto puede verse como un DTO que se usa solo para transferir datos a la capa de UI.

¿Y la base de datos?
"Tengo una tabla Factura que tiene la columna ENTREGADA"

No hay problema: las herramientas de mapeo O/R permiten hacer mapeos complejos de asociaciones para resolver el caso entregada/pendiente en el SistemaDePedidos, o si es necesario discriminar la subclase a mapear.

Conclusiones

En el ejemplo ficticio que di en este post aplica a muchos dominios, algunos ejemplos que me vienen a la mente son: estados de un documento (publicado/no publicado) en un sistema de CMS, registración de compras, distinción entre un estado de "edición/en uso" para una configuración (ejercicio: examinen el código fuente de Struts y vean como la implementación de configuración puede refactorizarse y mejorarse usando lo que comente en este articulo), etc.

Creo que en la balanza de "bueno/malo" el código del estilo "if estado then codigo" tiene muchos problemas:
  • Probablemente no modela adecuadamente el negocio.
  • El "contrato" de los objetos es complicado: todo el tiempo dependo del estado para saber si puedo o no usar ciertos métodos.
  • En consecuencia el código se empieza a "contaminar" con este tipo de chequeos, con el agravante que quizás cada programador lo haga de una forma distinta según la situación, dificultando el mantenimiento y refactoring.
  • Se necesitan objetos mutables, en casos donde no conviene que lo sean (prometo hablar sobre "side effects" en otro post).

Mientras que las únicas ventajas que le veo son que es simple de implementar para ejemplos chicos y fácil de mapear a una tabla en la base de datos.

Por todas estas razones conviene evitar este tipo de "ifs" y en lo posible evitar el uso de Enums ya que fomentan este tipo de código.