sábado, 13 de diciembre de 2008

Patrones Enterprise (parte 1)

En los post anteriores, me dediqué a definir qué es una aplicación enterprise, estableciendo la arquitectura de las mismas mediante cuatro capas estándares: Presentación, Acceso al Negocio, Negocio y Persistencia. Luego me detuve en la capa de dominio para resaltar su extrema importancia, hasta el punto de afirmar que el dominio es lo único importante. Gran parte de este segundo post la dediqué a la separación entre aplicación y negocio, y al objetivo primario de conseguir un dominio portable y rehusable entre distintas aplicaciones.

En este post, mostraré la forma de lograr esta deseada separación entre aplicación y negocio, mediante los famosos patrones de diseño de Martin Fowler, que pueden encontrarse en el "Nuevo Testamento" de los Patrones de Diseño titulado: Patterns of Enterprise Application Architecture (si consideramos al Design Patterns de GoF como el "Antiguo Testamento"). Las ideas que aquí explicaré son parte de ese libro y los diagramas y muchas otras ideas más pertenecen al material de la clase de Arquitectura de Software dictada por el Ing. Guillermo Pantaleo en la FIUBA.

Patrones de Dominio

A grandes rasgos, existen por lo menos tres formas de organizar la arquitectura de una aplicación enterprise:

TransactionScript

Supongamos que la aplicación enterprise que tenemos que diseñar es tan simple que no se justifica separar en demasiadas capas. Supongamos que, al querer forzar esta aplicación, incurrimos en un sobrediseño, que acaba complicando en extremo el producto final. Supongamos un negocio simple, una aplicación con pocas pantallas, en las que apenas mostramos lo que hay en una base de datos para que pueda actualizarse y realmente no tenemos relaciones complejas.

Construir un buen modelo de dominio es costoso y requiere un tiempo de setup bastante grande. Si se sabe que la aplicación será simple y que no evolucionará con el tiempo, no tiene sentido invertir en la construcción de un modelo de dominio. Para eso existe el patrón Transaction Script.

Transaction Script es escencialmente un procedimiento que toma el input de la presentación, lo procesa, trabaja con una base de datos o invoca operaciones de otro sistema y devuelve datos a la presentación para que se los entregue a la vista.

El siguiente ejemplo muestra el cálculo del Pago de un Proyecto:



Es interesante destacar que este diseño no es para nada Orientado a Objetos. No tiene nada que ver que haya clases y métodos. No existe un Modelo de Dominio, por ende no tendremos una clase Pago, ni una clase Proyecto, simplemente un ServicioPago que accede a la BD a través de un simple Gateway, que devuelve un ResultSet con los resultados que el Gateway ejecuta.

Como se imaginarán, un Transaction Script es lo más rápido de construir y lo más difícil de mantener.

Table Module

¿Qué tal si el dominio es un poco más complejo? Obviamente si estructuráramos la arquitectura de la aplicación con un Transaction Script al poco tiempo estaríamos en problemas. Pero, ¿y si la aplicación no es lo suficientemente compleja como para justificar la costosa curva de inicio de un Modelo de Dominio? Para eso contamos con un patrón intermedio llamado Table Module.

Con Table Module no tenemos un Modelo de Dominio puro, pero casi. Ya podemos tener nuestra aplicación dividida en capas físicas (tiers), como estuvimos viendo en los post anteriores. Ya tendremos una capa de Negocio y también una capa de Persistencia. En la capa de Negocio vivirán los objetos de dominio, como Proyecto y Pago si seguimos con el ejemplo anterior, pero la diferencia con un Modelo de Dominio puro es que por cada tabla tendremos una instancia de negocio; habrá una instancia de Proyecto que represente a todos los proyectos y una instancia de Pago que represente a todos los pagos.

El siguiente ejemplo muestra cómo sería el cálculo del Pago de un Proyecto con Table Module:



Table Module está diseñado para trabajar con Record Set. La capa de persistencia podría ser un conjunto de Gateways que tiren consultas a la base de datos y devuelvan los resultados en Record Sets (o DataSet, como se muestra en los diagramas), que no son más que una representación en memoria de los resultados que pudo haber arrojado un query de SQL.

Esta forma de trabajar fue muy popular en .NET y también es la que se podría construir encima de un driver JDBC, por ejemplo. Por supuesto que la forma de estructurar el código con este patrón es mucho más ordenada que Transaction Script, hay menos duplicación y es más fácil de mantener, pero aquí también nos enfrentamos al problema de que al no tener un Modelo de Dominio puro, perdemos muchas de las ventajas de la Orientación a Objetos, como la herencia, el polimorfismo, etc. (Imagínense que heredar de Proyecto no nos proporcionaría las mismas ventajas que en una capa de negocio pura, ya que recordemos que un objeto Proyecto en Table Module es algo así como un Singleton, tendremos una única instancia para todas las representaciones de Proyectos).

Domain Model

Llegamos a la estrella de la Orientación a Objetos. El patrón del que venimos hablando hace dos posts: Domain Model.

En un Modelo de Dominio puro tendremos muchas desventajas y muchas ventajas. Como todo en software, Domain Model no es un bala de plata. Implementar un Domain Model implica un costo altísimo de diseño y construcción de código de infraestructura que sólo es rentable si la aplicación es lo suficientemente grande y compleja, y/o se prevee que deberá escalarse en el futuro.

Volvamos al ejemplo del cálculo del Pago de un Proyecto:



Domain Model es el patrón opuesto a Transaction Script. Ya no tendremos un solo Proyecto para todos los proyectos y un solo Pago para todos los pagos. Habrá una representación en memoria para cada instancia de Proyecto y una para cada instancia de Pago. Además que tendremos objetos exclusivos para resolver cuestiones de lógica de negocio como objetos de Estrategias y demás.

Junto con la implementación de un Domain Model vienen muchas complicaciones de temas de infraestructura que son parte del costo al que antes me refería.

Tendremos la complicación de la impedancia entre el mundo de los objetos y el mundo de las tablas, resueltos por ejemplo por el patrón DataMapper (los ORM son ejemplos de implementación de este patrón: Hibernate, TopLink, etc). En la capa de persistencia necesitaremos patrones más complejos que los simples Gateways. Patrones como DataMapper o Active Record, que se encargarán de mapear de alguna forma el Modelo de Datos que vive en la base de datos, generalmente de tablas relacionales, y el Modelo de Dominio que vive en la capa de Negocio.

Tendremos que encontrar una forma óptima de manejar y manipular estos objetos en memoria. Si el modelo cuenta con diez millones de proyectos, no podremos mantener diez millones de representaciones en memoria al mismo tiempo, ya que no sería óptimo ni físicamente posible. Para resolver esta cuestión existen patrones de infraestructura como Lazy Load, que suelen utilizar los Data Mappers para manejar el "paginado" en las relaciones y colecciones de objetos.

Además, tendremos problemas de concurrencia a nivel capa de dominio, ya que los objetos trabajarán desconectados de la base de datos. Aparecerán lo que se llama transacciones de negocio, donde una transacción de negocio puede incluir muchas transacciones de sistemas (accesos a la base de datos). Los objetos de dominio serán modificados, borrados, dados de alta, por distintos usuarios que estarán manipulando las mismas entidades, pero distintas representaciones locales, y todo esto en memoria, sin que la base de datos se entere. Una Unit of Work se encargará de mantener el registro de las modificaciones en los objetos y de impactar dichos cambios en la base de datos al final de cada transacción (con un commit o un rollback), y otros patrones se encargarán de la concurrencia entre las modificaciones de los distintos usuarios, como Optimistic Offline Lock, Pessimistic Offline Lock y/o Coarse-Grained Lock (el último está basado en el concepto de Agregaciones de Eric Evans de su Domain-Driven Design).

Trabajar con objetos es hermoso, pero implica la construcción de una fuerte y sólida infraestructura. Dependiendo de qué tan robusta sea esta infraestructura, serán más o menos transparentes todas estas cuestiones para un diseñador/programador de la capa de dominio.

A la hora de elegir

A la hora de elegir, Fowler nos propone un excelente gráfico cualitativo/comparativo entre los tres patrones de arquitectura:


En él podemos ver que a medida que crece la complejidad de la lógica de dominio de la aplicación, el esfuerzo en el Transaction Script y en el Table Module se dispara al infinito. Sin embargo, el costo de entender la complejidad de un Modelo de Dominio y de diseñar la infraestructura necesaria para soportar el mismo, es demasiado costosa para una aplicación simple. En la práctica, nadie sabe a ciencia cierta qué tan compleja puede ser la lógica de un negocio, no hay métricas ni formas estándares de medir esa complejidad. Por eso no es fácil ponerle valores a los ejes de este gráfico.

Los tres patrones no son mutuamente exclusivos. Algunas de las funcionalidades de una aplicación podrían estar implementadas con Transaction Script y otras con Domain Model. Otro punto para destacar es que los patrones no muestran una implementación por sí mismos. Los diagramas de clases y de secuencias que expuse no son más que representativos. Los patrones de diseño encapsulan ideas, no implementaciones; el qué y no el cómo.