domingo, 15 de noviembre de 2009

DDD - Parte 3: Refactorizando Hacia una Visión más Profunda #2

Patrón 1: Intention-Revealing Interfaces

Como ya estuvimos viendo en los post anteriores, con Domain-Driven Design (DDD) se busca hacer foco en la lógica de dominio. (Seguro recordarán el título de ese primer post: El Dominio es lo Único Importante.) Cuando hablamos de lógica de dominio compleja queremos hablar de reglas, de conceptos de negocio, que se interrelacionan en un modelo de dominio que puede analizarse usando distintos niveles de abstracción. El paradigma de la orientación a objetos nos permite abstraernos de los detalles del código, permite encapsular en clases e interfaces un conjunto de instrucciones confiriéndoles un nombre, un concepto, una responsabilidad. Así, cuando tengamos que comunicarnos con el experto de dominio, con clientes programadores o hasta con nosotros mismos, podremos hablar de paquetes, entidades, objetos de valor y servicios, pudiendo revisar las interfaces, la lista de operaciones, para darnos cuenta o recordar qué rol cumple cada uno.

Pero si las interfaces de los objetos de dominio están mal diseñadas y no revelan de forma explícita lo que pueden hacer y cómo deben utilizarse, estamos en un problema, porque el programador cliente que tenga que invocarlas deberá internarse en la implementación y seguir el código para entender su uso. De esta forma estamos perdiendo el tremendo valor de la encapsulación. Si un programador debe consultar la implementación de un componente para poder usarlo, el valor de la encapsulación se ha perdido.

El patrón Interfaces que Revelan sus Intenciones habla sobre darle nombres significativos a las clases y a las operaciones que describan sus efectos y propósitos. Una buena elección de nombres, de distribución de métodos, de parámetros, alivia al programador cliente de la necesidad de entender los mecanismos internos del componente. Estos nombres deberían ajustarse a los del lenguaje ubicuo para que los miembros del equipo puedan inferir rápidamente su significado. Todos los mecanismos complicados deben estar encapsulados dentro de una interfaz abstracta que se comunique en términos de intenciones, en lugar de algoritmos.

Para ayudar a la buena elección de nombres, a la feliz distribución de métodos, parámetros, paquetes, también se pueden considerar los test unitarios. Los test pueden comunicar mucho más que un documento verboso o que un diagrama. Los test revelan de forma explícita el comportamiento de las interfaces, desnudando las entradas y las salidas, comunicando directamente al programador cliente la forma de uso del componente.

Patrón 2: Side-Effect-Free Functions

A groso modo, las operaciones pueden dividirse en dos categorías:
  • Comandos (commands)
  • Consultas (queries)

Las consultas obtienen información del sistema sin modificarlo (por ejemplo un getter). Los comandos son operaciones que introducen un cambio en el sistema (por ejemplo un setter). En una aplicación compleja, muchas operaciones llaman a otras operaciones y éstas pueden invocar a muchas operaciones más. De esta forma, se vuelve muy difícil anticipar todas las consecuencias de invocar una operación.

En teoría de programación las operaciones que devuelven un resultado sin producir un efecto secundario (side effect), un cambio en el sistema, son llamadas funciones. Una función puede ser llamada múltiples veces y retornar el mismo valor cada vez. Las funciones son mucho más fáciles de probar que las operaciones que tienen efectos secundarios. Las funciones implican menor riesgo.

Evans aconseja usar Funciones Libres de Efectos Secundarios siempre que sea posible y sobre todo mantener los comandos y las consultas estrictamente separados en diferentes operaciones. Además, siempre que se pueda devolver un Objeto de Valor (ValueObject) como resultado de un cálculo es preferible antes que una Entidad. Un Objeto de Valor puede crearse en el momento, para devolver el resultado. En cambio, como vimos en post anteriores, las Entidades tienen un ciclo de vida bastante más complicado. Los Objetos de Valor son inmutables, lo que implica que, desde el punto de vista del modelo, todas sus operaciones son funciones.

Patrón 3: Assertions

Separar estrictamente las funciones y los comandos es un excelente comienzo. Y si además hacemos que estas operaciones formen parte de una interfaz declarativa, que revela explícitamente sus intenciones, mucho más. Pero lamentablemente no nos podemos librar de las operaciones que producen cambios en el sistema, sino nuestros diseños no tendrían mucha utilidad. El patrón Aserciones (Assertions) hace explícitos los efectos secundarios de los comandos, los declara para facilitar su uso. Las Aserciones describen estados, no procedimientos, por lo tanto son fáciles de analizar.

Con Aserciones podemos establecer las post-condiciones de una operación y las invariantes de clases y agregaciones. Si las Aserciones no pueden codificarse directamente en el lenguaje de programación, siempre podemos escribir pruebas unitarias. Los test unitarios siempre son buenos porque al configurarlos podemos hacer explícitas las pre-condiciones de la operación y, después de la ejecución, chequeamos con Aserciones que las post-condiciones se hayan cumplido.

En Java, por ejemplo, una prueba unitaria muy simple, usando Aserciones podría ser:
public void testSuma() {
int sumando1 = 25;
int sumando2 = 5;
int resultado = suma(sumando1, sumando2);
assertEquals(30, resultado);
}
Evans dice que las Aserciones no sólo deberían usarse en los unit test, también deberían utilizarse en el código mismo, y poderse activar o desactivar por configuración, dependiendo del ambiente en que estemos trabajando.

Patrón 4: Conceptual Contours

El cuarto patrón, Contornos Conceptuales (Conceptual Contours) habla de descomponer los elementos de diseño (operaciones, interfaces, clases, agregaciones, etc) en unidades cohesivas, teniendo en consideración las divisiones de importancia del dominio, observando los ejes de cambio y estabilidad a través de sucesivas refactorizaciones. Es importante modulizar el modelo, alinearlo con los aspectos consistentes del dominio y establecer contornos conceptuales que permitan desglosar un gran sistema en áreas de conocimiento distintas.

El objetivo es un simple conjunto de interfaces que se conbinen de forma lógica, descubriendo así de forma explícita el lenguaje ubicuo en el modelo y en el código, y apartando los aspectos irrelevantes. No hay una receta para esto. No hay procesos fijos establecidos que ayuden a lograr acertados contornos conceptuales. Lamentablemente, sí hay que tener mucha intuición, y mucho conocimiento del dominio. Comúnmente los contornos conceptuales son resultados de muchas refactorizaciones orientadas al negocio, muchas refactorizaciones rumbo hacia una visión más profunda.

Patrón 5: Standalone Classes

Otra verdad cruel: las interdependencias complican el entendimiento de los modelos y de los diseños, hacen que sean más difícil de testear y de mantener y, por lo general, suelen acumularse fácilmente.

El objetivo de los patrones de Módulos y Agregaciones (ver posts: Los Componentes Fundamentales de un Modelo Guiado por el Dominio Parte 1 y Parte 2) es limitar la red de interdependencias entre los objetos. Pero incluso dentro de un Módulo o dentro de una Agregación, la red de interdependencias puede ser lo suficientemente compleja para retrasar severamente la evolución del proyecto. Por eso, cuando se está diseñando, se debe pensar con cuidado cada dependencia. Cada dependencia debe ser sospechosa hasta demostrar que es indispensable para definir al modelo. El bajo acoplamiento entre clases y paquetes es fundamental. El objetivo no es eliminar todas las dependencias, eso es imposible, pero sí eliminar todas las dependencias que no son esenciales.

Refactorizar las más intrincadas dependencias en Clases Independientes (Standalone Classes) es el objeto supremo de este patrón. El más bajo acoplamiento es la forma más básica de reducir la sobrecarga conceptual en un modelo, y una Clase Independiente, una clase suelta, es el acoplamiento más bajo que se puede lograr. (Por supuesto que el nombre del patrón es más bien una metáfora; no se puede construir un sistema con todas clases individuales sin relaciones entre sí, pero sí se puede aspirar a llegar al más bajo acoplamiento posible.)

Patrón 6: Closure of Operations

Los matemáticos son fanáticos acerca de no mezclar conceptos distintos. Cuando uno suma un número entero con otro número entero, el resultado también es un número entero. Entonces se dice que los números enteros son cerrados bajo la operación de la suma. La propiedad de clausura provee un modo de definir una operación sin la necesidad de involucrar ningún otro concepto foráneo. El uso básico de XSLT es transformar un documento XML en otro documento XML. Esta operación es cerrada bajo el conjunto de los documentos XML. La propiedad de clausura simplifica tremendamente la interpretación de una operación y es fácil encadenar o combinar operaciones cerradas.

En diseño y/o programación esto se traduce en: siempre que se pueda, hacer que el objeto que devuelva una operación sea del mismo tipo que sus parámetros. Una operación cerrada provee una interfaz de alto nivel sin introducir una dependencia con otros conceptos.

Este patrón, Clausura de Operaciones (Closure of Operations) se puede aplicar con mayor facilidad en operaciones con ValueObjects. Lo que no quita que pueda aplicarse también a Entidades.


Igual que con los patrones de la Parte 2, el libro da muchos ejemplos, y hasta dedica una sección entera a desarrollar un caso didáctico en el que Evans tiene oportunidad de aplicar todos los patrones aquí nombrados. Yo no suelo poner ejemplos porque me interesa más extractar los conceptos principales (además de que me pasaría la vida entera resumiendo cada capítulo). Para los ejemplos detallados está el libro.

Lo que sigue, creo yo, es la parte más interesante de Domain-Driven Design: Diseños Declarativos y Lenguajes Específicos de Dominio (DSL=Domain Specific Languages).

Diseños Declarativos

No importa qué tan guiado por el modelo sea nuestro diseño, siempre terminaremos escribiendo métodos o procedimientos para producir el efecto de las interacciones conceptuales, y siempre gastaremos tiempo en escribir código repetitivo que no agregue significado o comportamiento.

El Diseño Declarativo es una forma de escribir un sistema, o parte de un sistema, como si se tratara de una especificación ejecutable, una muy precisa descripción de propiedades que controlan el software. A nivel de código, esto puede ser construido de dos maneras:
  • A través de un mecanismo de reflexión, en tiempo de ejecución
  • A través de generación automática de código, en tiempo de compilación, produciendo código convencional basado en la declaración
Este enfoque permite a otro desarrollador tomar la declaración como un valor literal, como una garantía absoluta.

Generar un programa ejecutable a partir de una declaración de propiedades del modelo es una especie de Santo Grial para un diseño guiado por el modelo (Model-Driven Design), pero Evans asegura que en la práctica pueden presentarse problemas. Dos de los problemas que plantea son bastantes comunes e incluso a mí me ha tocado la desgracia de experimentarlos:
  • Un lenguaje declarativo nunca va a expresar lo suficiente como para hacer TODAS las cosas que son necesarias. Entonces llega la hora de tocar "a mano" el código generado. Entonces llegan los dolores de cabeza, porque a menudo las porciones de código auto-generadas son muy difíciles de extender
  • El ciclo iterativo de desarrollo se hace casi imposible de seguir, ya que muchas veces agregar código "a mano" en código que fue auto-generado, provoca que la herramienta no pueda volver a regenerar el código, que al regerar el código se pierda lo escrito "a mano", o que el programa se destruya por completo. En otras palabras: las técnicas de generación de código hacen que perdamos control.
La programación orientada a reglas con un motor de inferencia, como puede ser JBoss Drools (ya mencionado otras veces en este blog y del que pueden encontrar bastante material en el blog de Salaboy) es otro enfoque prometedor de Diseños Declarativos. Sin embargo, a pesar de que la programación orientada a reglas es declarativa en principio, muchos sistemas tienen lo que se llaman predicados de control que permiten agregar configuraciones de rendimiento. Este código de control introduce efectos secundarios, por lo tanto el comportamiento ya no está completamente dictado por las reglas declarativas, sino que hay que tener en cuenta "otros detalles" a la hora de introducir modificaciones.

Aunque Evans no lo menciona, también creo que la programación orientada o procesos haciendo uso un motor de workflows, es otro ejemplo de Diseño Declarativo muy interesante (ejemplo: JBoss jBPM; también pueden encontrar material en el blog de Salaboy).

Tenemos mucho más ejemplos de programación declarativa embebida en la programación imperativa de hoy. Basta con citar muchas herramientas comunes como los ORMs (Hibernate, TopLink, etc), los frameworks MVC para las capas de presentación, las herramientas de inyección de dependencia provistas por Spring, EJB3 o tantas otras especificaciones y/o implementaciones. Este tipo de frameworks trabajan con mecanismos de reflexión. Muchas de ellas son muy estables, cumplen funcionalidades específicas y alivian al programador de mucho código repetitivo y propenso a errores.

DSL: Domain-Specific Language

Los lenguajes específicos de dominio son la expresión máxima de Domain-Driven Design. Los DSL son lenguajes creados para resolver un problema particular de dominio. Pueden ser declarativos o no, no es necesario que lo sean. Ejemplos existen muchos: SQL es un DSL, un lenguaje que sirve para la comunicación con un motor de base de datos, SMI es otro DSL, un subconjunto adaptado de ASN.1, que sirve para definir árboles MIB en el protocolo SNMP de administración de redes, otro ejemplo de DSL puede ser GPSS, un viejo lenguaje declarativo de simulación creado por IBM, donde no se programa de forma imperativa mediante instrucciones, sino mediante bloques declarativos que especifican lo que las transacciones (objetos transitorios) tienen que hacer en un modelo simulado.

Nosotros también podemos crear nuestro propio DSL, si la complejidad del dominio lo amerita; podemos crear un DSL para construir aplicaciones bancarias, un DSL para construir aplicaciones para empresas de transporte de larga distancia, y todo lo que nos podamos imaginar. Estos lenguajes podrían ser interpretados con un intérprete programado en algún lenguaje imperativo (usando un parser y mecanismos de reflexión) o podrían ser "compilados" escupiendo código auto-generado (el código que se genere podría ser imperativo: Java, C++, lo que sea).

La ventaja de diseñar un DSL es que podemos programar de forma extremadamente expresiva, haciendo que los términos del lenguaje ubicuo pasen a formar parte de los tipos de datos y de las expresiones que se pueden escribir en el programa de forma nativa. Sin embargo, a pesar de que suena muy excitante, hay muchas desventajas también:
  • El costo de inversión es muy grande al principio. Hasta que el DSL y el lenguaje ubicuo estén estables y pueda realmente construirse aplicaciones usándolos puede pasar mucho tiempo
  • La habilidad (skill) de los desarrolladores tiene que ser muy grande. Deben tener mucha experiencia en varios lenguajes de programación. Mucha experiencia en el lenguaje sobre el que estén construyendo el DSL. Mucha experiencia en teoría de lenguajes en general
  • El desarrollo en general será más complejo. Las refactorizaciones serán más complicadas. El código que soporta el DSL no será fácil de construir, ni fácil de interpretar. Los cambios en el equipo de desarrollo podrían ser fatales
Para terminar con este post, Evans dice que no siempre la programación orientada a objetos es la mejor opción para construir un DSL. Un lenguaje de la familia de los lenguajes funcionales tal vez podría alinearse mejor al paradigma de los DSL.

jueves, 5 de noviembre de 2009

DDD - Parte 3: Refactorizando Hacia una Visión más Profunda #1


Domain-Driven Design (DDD) dice que el verdadero desafío en el desarrollo de una aplicación empresarial es encontrar un modelo incisivo, que capture hasta los conceptos más sutiles del dominio y los vuelque en un diseño práctico. Refactorizar es una práctica que conduce a ese modelo. Refactorizar es rediseñar de forma tal que no se altere la funcionalidad del sistema. Para esto puede ayudar un buen conjunto de pruebas automatizadas que nos permitan experimentar con el código y el modelo con relativa tranquilidad.

Evans habla de tres niveles de refactorización:
  • Nivel 1: cambios mecánicos que hacen más fácil de leer el código
  • Nivel 2: cambios que permiten aplicar patrones de diseño
  • Nivel 3: cambios que permiten obtener un modelo más profundo
Un modelo profundo (deep model) proporciona una lúcida expresión de las incumbencias principales de un dominio y de su conocimiento más importante, a la vez que aparta los aspectos superficiales.

Progreso

El retorno de inversión de la refactorización no es lineal. Las revelaciones más importantes surgen abruptamente y así un modelo cada vez más profundo va emergiendo gradualmente tras jornadas discontinuas de refactorización. Cada refinamiento del código y del modelo aclara el panorama del equipo de trabajo. Este tipo de progreso (breakthrough) no es una técnica, es un evento.


A menudo, cuando la perspectiva de un progreso se presenta resulta aterrador. El calendario siempre es apretado y decisivo. Pero hay que recordar que estos progresos pueden implicar importantes adelantos en el desarrollo del proyecto. Evans nos recomienda que no nos paralicemos tratando de lograr un progreso. Usualmente los progresos se presentan solos después de varias modestas refactorizaciones. Concentrémonos en masticar el conocimiento y en cultivar un lenguaje ubicuo robusto, como se habló en el post dedicado a la primera parte del libro.

Haciendo Explicitos los Conceptos Implícitos

Muchas transformaciones del modelo de dominio ocurren cuando los desarrolladores reconocen un concepto que acaba de insinuarse en una en una discusión o que estaba implícito en el diseño y encuentran la forma de representarlo de forma explícita con uno o más objetos o relaciones. Por eso, los desarrolladores deben estar atentos a este tipo de insinuaciones y algunas veces deben salir a buscarlas de forma proactiva. Muchos de estos descubrimientos surgen de:
  • Prestar atención al lenguaje del equipo
  • Eliminar torpezas en el diseño y aparentes contradicciones en las declaraciones de los expertos
  • Explorar la literatura del dominio
  • Hacer mucha y mucha experimentación
Cuando el usuario o el experto de dominio usan palabras que no están en el lenguaje ubicuo o en el diseño, estamos frente a un signo de advertencia bien grande. Es una advertencia doblemente fuerte cuando no sólo los usuarios sino también los desarrolladores usan este vocabulario ajeno al modelo. Es cuando tenemos que sentarnos a refactorizar.


Los conceptos que necesitamos no siempre están flotando en la superficie, ni emergiendo en las conversaciones o los documentos. Tendremos que cavar, incluso excavar, incluso inventar. El lugar donde cavar es en la parte más débil de nuestro diseño, el lugar donde los procedimientos son cosas complicadas, retorcidas y difíciles de explicar, el lugar donde cada nuevo requerimiento parece agregar más complejidad.

A veces, para descubrir estos conceptos ocultos contamos con un experto de dominio colaborador, a veces no. A veces contamos con bibliografía donde los conceptos están bien definidos y detallados, a veces no. Muchos dominios no tienen modelos tan refinados como los de contabilidad o finanzas, pero en otros casos podemos dar con pensadores en el campo, quienes quizás se tomaron el trabajo de organizar y abstraer las prácticas comunes del negocio, o artículos especializados, white papers, blogs, etcétera.

En DDD la experimentación es una de las prácticas más importantes. La experimentación es la forma de saber si algo funciona o no. En el camino a un modelo profundo hay mucha prueba y error, mucha obstinación y muchas lecciones aprendidas.

¿Cómo modelar los conceptos menos obvios?
El paradigma de la orientación a objetos nos lleva a buscar y a inventar cierto tipo de conceptos. Son los famosos sustantivos y verbos que nos enseñan cuando somos neófitos en la materia, intentando en vano simplificar el proceso de relevamiento de un dominio. Pero hay otras categorías de conceptos también muy importantes:


1) Restricciones Explícitas

Algunas veces las restricciones (constraints) encuentran un hogar dentro de un objeto o un método cuya responsabilidad no está relacionada con la restricción de forma directa, pero cuando la lógica se torna más complicada, es conveniente aislar la regla en una porción de codigo aparte (sea método o clase), donde la restricción tenga espacio para crecer naturalmente. Si la regla es más complicada debe tener su clase propia y hasta su paquete.


Hay tres advertencias que podemos recibir de que una regla o restricción está contaminando la clase en donde se ubicó y debe mudarse a un lugar más espacioso:
  • Evaluar la restricción requiere datos que no tienen que ver con la definición del objeto
  • Reglas relacionadas aparecen en múltiples objetos, forzando duplicamiento o herencia de objetos que no deberían pertenecer a la misma familia
  • Un montón de diseño, requerimientos y conversaciones giran alrededor de la restricción, pero en la implementación la regla está oculta en un código procedural
DDD propone un patrón para encapsular reglas de negocio un poco más complicadas. Es el patrón Specification que ya mencioné en algún post pasado. En el libro hay una extensa y detallada explicación que no tiene desperdicio. También en el libro de Fowler, Patterns of Enterprise Application Architecture, que ya estuve comentando en los post de Patrones Enterprise Parte 1 y Parte 2, hay una explicación exhaustiva. La versión de Evans es un poco más sofisticada. Para definir los Specifications se inspira en los predicados del paradigma de programación lógica. Además, si se quedaron con ganas de seguir leyendo sobre el tema, hay un paper que Eric Evans y Martin Fowler escribieron juntos y que se puede descargar desde este link. (Algún día, si tengo tiempo, me gustaría escribir un post exclusivo sobre Specifications, pero por ahora voy a saltear el tema.)

2) Procesos como Objetos de Dominio
Reglas de negocio y procesos son dos grandes categorías de conceptos de modelo que no vienen a la mente cuando estamos programando en un lenguaje orientado a objetos. Los procesos pueden expresarse con el patrón Service (ver DDD - Parte 2: Los Componentes Fundamentales de un Modelo Guiado por el Dominio #1, donde hay un breve resumen sobre los Services). La clave para distinguir un proceso que debe ser expuesto de forma explícita en el modelo de otro que debe permanecer escondido entre Entidades y ValueObjects, es simple: ¿El proceso es algo de lo que el experto de dominio habla mucho, o sólo se trata de un aspecto técnico, de un proceso algorítmico del código?

Si bien los patrones Specification (para expresar reglas de negocio) y Service (para expresar procesos) son elegantes y poderosos, hay casos en los que no resultan suficientes y tenemos que adentrarnos en el mundo de los motores de reglas de negocios y de workflows.

Diseño Flexible

El propósito final del software es servir a los usuarios. Pero primero, ese mismo software debe servir a otros desarrolladores. Cuando un sistema con un comportamiento complejo carece de un buen diseño, se vuelve difícil de refactorizar y de manipular. El diseño flexible (supple design) es el complemento de un modelo profundo.

Simple no significa fácil. Para crear elementos que puedan ensamblarse en elaborados sistemas y mantenerse simples se necesita de un estilo de diseño moderadamente riguroso. Para abrazar el cambio, el diseño tiene que ser fácil de entender y, por ende, fácil de modificar. Las primeras versiones suelen ser rígidas, toscas; sólo a través de sucesivas refactorizaciones y sucesivos progresos, podremos alcanzar un diseño flexible.


No hay ciencia ni fórmula para lograr esto; sin embargo, Evans propone un conjunto de patrones que contribuyen a alcanzar el (o por lo menos a mantenerse en el camino hacia el) diseño flexible:
  • Intention-Revealing Interfaces
  • Side-Effect-Free Functions
  • Assertions
  • Conceptual Contours
  • Standalone Classes
  • Closure of Operations


En el próximo post nos dedicaremos a ver un resumen de cada uno de estos patrones y hablaremos un poco sobre Diseños Declarativos y lo que es la máxima expresión de DDD: Domain-Specific Language.