¿Vale la pena crear pruebas unitarias? Mis reflexiones

Ante la expectación creada en twitter y en los comentarios decidí responder al post de Lucas http://geeks.ms/blogs/lontivero/archive/2011/03/31/191-vale-la-pena-crear-pruebas-unitarias.aspx pero el comentario se fué alargando tanto que dije… mejor lo salvo para por si acaso, y ya se alargó tanto y tanto que dije… mejor escribo un post al respecto. Os advierto que es un post LARGO LARGO LARGO así que suerte leyéndolo.

Yo he de decir que entiendo tanto la postura de Lucas como la de Ibon. Así que aquí va mi visión:

1º La parte de Strategic Design de DDD que cuenta Eric Evans, viene a decir que debes implementar con más cuidado el core de tu dominio, es decir, lo que te da más valor de negocio, y que puedes implementar “peor” las partes periféricas que no son críticas para tu negocio. De igual manera, en las pruebas no tienes que hacer todo tu código super testeable, debes poner más énfasis en las partes que son core de tu dominio y que te van a dar la mayor parte del valor de negocio del sistema. No deberías poner énfasis en realizar un testing perfecto de todo el sistema en sí, sino basarte en el negocio y en la complejidad ciclomática para priorizar pruebas o evaluar grados de calidad. Es decir, prueba mejor lo que es más crítico y complejo.

2º Probar lo obvio, sí o no. Depende del tiempo y del factor calidad. Caso extremo: Testear que un setter asigna el valor correcto. ¿Aporta valor? Poco, ya que el código es obvio. Por contra, testear algo complejo como, por poner, la planificación automática de turnos de trabajo. ¿Aporta valor? Sí, porque es un código que no es trivial verificar, y entre otras cosas las pruebas unitarias son más sencillas de realizar que cualquier otro tipo de prueba (manual por ejemplo) ya que tienes un entorno más controlable e influyen menos componentes. La gracia de una prueba unitaria es que controlas el comportamiento de las dependencias, lo que facilita aislar errores en la implementación de estas al sustituirlas por algo que te permite indicar la respuesta.

3º La dificultad al escribir pruebas unitarias. ¿Es síntoma de mal código? Sí y no. Puede ser un síntoma de mal código o un síntoma de que no has hecho una buena prueba. Como dices en el primer punto, “masterizar” un framework de pruebas es sencillo, por el contrario hacer una buena prueba unitaria es muy jodido. Es algo que no aprendes solo por saberte de memoria PEX, Nunit, MbUnit o RitaLaCantaoraUnit. Hay muchas cosas que plantearse a la hora de hacer una prueba unitaria como qué verifico, qué pruebas hago, etc. Las pruebas sirven, como dice Ibon con otras palabras, para hacer afirmaciones sobre el código. Esto nos permite comprobar que cierto hecho o propiedad se cumple y que esto sigue siendo así tras refactorizar o realizar modificaciones. No demuestran la ausencia de errores, solo que determinados hechos se cumplen. Mucha gente, (yo incluido) más de una vez ha escrito y escribe pruebas para “validar” su implementación, más que para “especificar” el comportamiento. Esto es un factor a tener muy en cuenta ya que cuando “validamos” una implementación estamos en el ámbito del “como” mientras que cuando “especificamos” estamos en el ámbito del qué, es decir, estamos más arriba en el nivel de abstracción. Lo que quiero decir es que muchas veces ocurre que se hacen las pruebas unitarias mal, tras realizar una implementación, lo que favorece que se tienda a “validar una implementación” más que a “especificar una interfaz”. Por el contrario, una prueba que de verdad “especifica” se centra solo en lo que un cliente de una determinada interfaz o clase espera tras realizar una determinada acción, por ejemplo. La interaz IPuerta con void Abrir(), void Cerrar(), bool Abierta(), bool Cerrada(). Típica prueba de interfaz, “especificando”:

//Arrange – Con la puerta inicialmente cerrada…

puerta.Abrir();

Assert.IsTrue(puerta.Abierta());

//Típica prueba de “validación de implementación”

//Arrange – Con la puerta cerrada…

puerta.Abrir();

Assert.IsTrue(puerta.Abierta());

Assert.IsTrue(puerta.Pomo.Girado);

Assert.IsTrue(puerta.Angulo == 90);

(Larga lista de asserts)

La diferencia radica en que la prueba que “especifica” tiene en cuenta solo el componente que estás probando. Es realmente una prueba “unitaria” ya que solo está especificando/definiendo el comportamiento del sujeto de la prueba. Por el contrario, la prueba que “valida” no es del todo una prueba unitaria, ya que no se limita a comprobar/definir el componente puerta, sino que además realiza afirmaciones sobre otros componentes. Hay que tener una cosa muy clara, cuando haces una prueba unitaria realizas una especificación en dos puntos de la misma. En los asertos de la prueba, ya que especificas el comportamiento del componente bajo prueba, y también al realizar mocks ya que básicamente dices, ante esta entrada quiero esta respuesta y eso es una especificación (por extensión). Por lo tanto, cuando realizas un mock, estás haciendo una afirmación que deberías comprobar posteriormente. Lo que genera una nueva prueba para la dependencia. Por último queda el tema de “este componente llama a esta dependencia” luego tengo que comprobar que lo hace con los argumentos adecuados. En una comunicación solo debo probar los argumentos de las llamadas ya que comprobar que ante un argumento una dependencia devuelve un determinado valor y queda en un estado determinado es una prueba unitaria de la dependencia, no del propio componente. Comprobar que un componente llama a una dependencia se sale del ámbito de las pruebas unitarias ya que lo que esto hace es verificar una determinada comunicación entre dos objetos, y esto es una prueba de integración. Además cuando se añaden argumentos la cosa se complica más. Nuestro componente puede manipular los argumentos antes de llamar a la dependencia y hacerlo de forma no trivial. Esto implica que tenemos que entrar “dentro” del componente para reproducir esa transformación de forma que podamos hacer la comprobación en la prueba unitaria, por lo que entramos de nuevo en el cómo, y en una prueba buscamos el qué. Probar la comunicación con pruebas unitarias es algo ciertamente complicado, principalmente por la posible transformación que pueden sufrir los argumentos antes de llamar internamente a la dependencia. ¿Cómo resolvemos esto? Sencillamente escribiendo buen código. Una cosa que se nos olvida siempre es que un componente tiene la responsabilidad de usar sus dependencias de la forma adecuada. Lo que nos falta siempre es introducir asertos justo antes de la llamada que comprueben que se va a llamar con los argumentos adecuados, es algo que siempre o casi siempre asumimos. Si pensamos en términos de Design by contract, cada método tiene precondiciones y postcondiciones. Cumplir con las precondiciones es responsabilidad del componente que llama (luego tiene que comprobarlas) y las postcondiciones son responsabilidad del propio método (por lo que el método que llama puede “creer en ellas” (creer significa escribir una prueba unitaria para el método que verifique la postcondición). Al usar un framework de mocking con verificación (moq, rhino, etc) mezclamos la prueba unitaria en sí con la prueba de la comunicación entre componentes. La prueba de las comunicaciones no importa a la hora de especificar el comportamiento de un componente/abstracción (visto como una caja negra) sino solo al comprobar el comportamiento de una implementación específica. Estos dos elementos se mezclan a menudo por el hecho de que muchas veces solo se realiza una implementación para una determinada interfaz y se realiza la prueba sobre la implementación cuando debería realizarse sobre la interfaz. En definitiva y para concluir este punto, pongo un ejemplo donde intento explicarlo. Supongamos dos objetos componente y dependencia. La estructura de una prueba unitaria de componente.metodo(args) y de las implementaciones (en lo que se refiere a pruebas) debería ser algo así.

Arrange (Creo componente y dependencias)

Act

componente.metodo(args);

Assert

Asert.IsTrue(componente.propiedad == valor); //Con propiedad me refiero a llamada a método, propiedad, o engeneral cualquier tipo de consulta sobre el estado, pero siempre del propio objeto.

 

estructura de componente.método(args)

//precondiciones

//comprobación de la precondición del método de la dependencia antes de llamar y de que los argumentos son los correctos.

dependencia.metodo(depArgs) //Llamada con los argumentos de la dependencia obtenidos a partir de los valores accesibles desde el método (argumentos, variables locales, de instancia, clase, constantes, etc).

//resto del método siguiendo el mismo esquema

//postcondiciones

 

estructura de dependencia.método(depArgs)

//precondiciones

//cuerpo siguiendo el mismo esquema

//postcondiciones

 

Una clase debe ofrecer mecanismos para validar las precondiciones de los métodos que expone públicamente. Por ejemplo, la un objeto Stream dispone de propiedades como CanRead, CanWrite, etc. que indican si se pueden llamar a ciertos métodos como read, write, etc. El llamante es responsable de validar los argumentos y de utilizar estas propiedades para comprobar que la operación se puede hacer sin problemas. El método Read, Write, etc. comprobará los argumentos y lanzará una excepción en caso de que no sean válidos, pero es responsabilidad del llamante hacer esa comprobación y además comprobar que los argumentos que va a usar son exactamente los que desea (el invariante en ese punto del programa). Pondré un ejemplo más concreto con un programa que escribe la potencia de un número en un fichero.

void cuadrado(int arg, Stream stream){

   int cuadrado = arg*arg;

   Assert((arg == 0 || cuadrado/arg == arg) && stream.CanWrite); //Suponiendo que eliminamos el factor “entorno” (espacio en disco, etc.) queda claro que lo que se comprueba es el estado del programa antes de la llamada y la precondición del método de la dependencia.

   stream.Write(cuadrado);

En conclusión para este punto, una prueba unitaria debe definir el estado antes de una acción y comprobar/realizar afirmaciones sobre el resultado después de la acción. No debe en ningún caso pronunciarse sobre qué ocurre en medio, que es lo que ocurre cuando utilizamos mocks.

4º Estructura/arquitectura/complejidad en las pruebas. Una prueba unitaria es algo muy elemental. Un simple método, sin parámetros y con estructura Arrange-Act-Assert. Típicamente creamos datos de prueba, hacemos la llamada y comprobamos. Repetimos el proceso para cada una de las casuísticas de lo que estamos probando. Cantidad de veces cogemos una prueba, la copiamos/pegamos y cambiamos los datos del arrange. Las pruebas unitarias suponen un gran esfuerzo porque muchas veces no aplicamos la misma “inteligencia” que aplicamos para el diseño del código. Aplicar principios de diseño es tan importante en las pruebas como en la propia aplicación, y hay técnicas que permiten subsanar el problema del copy/paste de pruebas. Esto se ve mucho más cuando se crean pruebas complejas. Los pasos son los siguientes.

Crear un método que realice la “prueba con los datos que le pasamos” (que se encargue de hacer todo el arrange, el act y el assert en base a argumentos).

Dentro de este método extraer el arrange y el assert a métodos aparte.

Escribir en el método del arrange los pasos para hacer un setup de todas las dependencias y hacer este método virtual de forma que se puedan ajustar detalles.

Escribir la postcondición del método (validando solo elementos del propio componente).

Escribir pruebas siguiendo la misma estructura (o añadir casos concretos si la base para la prueba ya está creada)

Escribir métodos que creen argumentos concretos y realicen la prueba usando el método definido en el punto 1.

Realizar herencia de la clase de pruebas para cada una de las implementaciones sustituyendo el arrange correspondiente.

 

Llegados a este punto las comunicaciones (el objeto x llama a la dependencia y haciendo el pino mientras salta a la pata coja y la dependencia y le devuelve el valor adecuado y se queda en el estado esperado) se prueban mediante los asertos que introducimos antes de realizar las llamadas (que comprueban que los argumentos son correctos y los esperados) y mediante las pruebas unitarias de la dependencia y, que comprueban que dado ese argumento la postcondición es la esperada. En principio no se debería hacer, o por lo menos no me gusta, una comprobación del nº/orden de las llamadas, pero es algo que un framework de mocking ayuda y permite hacer sin problemas.

 

5º El tema de las interfaces por testabilidad. Aquí creo rotundamente que una interfaz no se crea para que algo sea testable, sino para definir un comportamiento. Una interfaz no son solo signaturas de miembros, sino también precondiciones y postcondiciones de los mismos que es lo que define a la interfaz y que es lo que todos los implementadores de la misma deben respetar. De no ser así, la interfaz no sirve para nada ya que los implementadores estarían violando el principio de Liskov Substitution. Respecto a lo de que añades más complejidad… Añades más código, pero no más complejidad, de hecho creo que la cosa se simplifica, al pasar las dependencias en el constructor estás cambiando Afferent coupling por Efferent coupling. Las dependencias apuntan hacia fuera y son explícitas, no hay que buscarlas dentro del código y son mucho más visibles. Por otra parte, crear una interfaz para todo es un error. Las interfaces definen un protocolo/comportamiento para el que puede haber múltiples implementaciones, si solo vas a realizar en principio una implementación o puedes proporcionar una implementación base, lo correcto es usar una clase y definir métodos virtuales. Meter una interfaz cuando no se necesita es añadir complejidad por gusto. Construye lo más simple posible, siempre hay tiempo de refactorizar después y una buena arquitectura/diseño no se hace de una patada, sino que madura a partir de la comprensión de los requisitos del proyecto.

Finalmente, como conclusión, creo que en el estado actual de la industria, Lucas tiene cierta razón. Pero creo que es más un problema de acostumbrar a la gente a hacer pruebas y diseñar bien que del hecho de las pruebas unitarias en sí. Las pruebas son necesarias y valen la pena, pero requieren a su vez una planificación y una priorización. Mi visión sobre esto se resume en:

Prueba lo complejo, lo importante, y lo susceptible a cambios futuros.

Diseña las pruebas con el mismo cuidado con el que diseñas el sistema.

 

Bueno, esto empezó siendo un comentario en el blog de Lucas y ha pasado a ser un tocho largo infumable. Si habeis llegado hasta aquí, espero que os haya resultado lo más ameno posible, y espero vuestros comentarios.

Sobre fake objects, stubs y mocks

Las pruebas unitarias son uno de los pilares centrales de los procesos de gestión de la calidad software modernos. Un desarrollador o un tester, escribe pruebas que intentan refutar que el código bajo prueba hace lo que se espera. Es decir, escribimos pruebas para intentar encontrar un “contraejemplo” a nuestro código, y mientras más pruebas escribimos más seguros estamos que dicho contraejemplo no existe.

En los lenguajes orientados a objetos, un programa se compone de distintos objetos que colaboran entre sí para llevar a cabo una funcionalidad determinada. Por esa razón, cuando probamos un método es normal que este llame a su vez a otros métodos de otros objetos o de sí mismo. Para que una prueba sea realmente unitaria, debemos aislar a nuestro código de este tipo de dependencias.

Para aislar nuestro código de las dependencias tomadas con los miembros de otras clases, desde Microsoft Research nos llega Moles. Moles es un framework de stubbing diseñado por Microsoft para facilitar a los desarrolladores y testers la creación de pruebas unitarias de su código.

Cuando desarrollamos pruebas y queremos hacer pruebas unitarias de nuestro código necesitamos en primer lugar seguir el principio de Inyección de dependencias, es decir, pasar cualquier objeto del que dependa el método que vamos a probar bien a través de los parámetros del constructor o bien a través de dar valor a sus propiedades. Como hemos dicho, para que estas pruebas sean unitarias tenemos que aislar las dependencias de nuestro código bajo prueba. Para aislar dichas dependencias tenemos tres opciones:

· Fake objects: Creamos una clase y una instancia para la prueba que devuelve los valores que esperamos que devuelva en nuestra prueba.

· Stubs: Creamos una clase “esqueleto” que podemos configurar fácilmente para que devuelva los valores que esperamos en nuestras pruebas. La diferencia con respecto a un fake object es que un stub es configurable y por tanto sirve para más pruebas.

· Mocks: Creamos mediante un framework un objeto que podemos configurar para que devuelva los valores que esperamos en nuestras pruebas y al cual además podemos preguntar después sobre cómo ha sido usado. (Que miembros han sido invocados, cuantas veces, con qué parámetros, etc).

Para dejar más clara la diferencia entre stub y mock podemos decir que los stubs sirven para hacer state based testing y los mocks permiten a su vez hacer behavior o interaction testing.

Dentro de la plataforma .NET disponemos de múltiples frameworks para realizar tanto stubbing como mocking. A continuación presentamos los principales:

  • Moq: Es un framework de mocking muy sencillo e intuitivo que se puede descargar desde http://code.google.com/p/moq/ y que aunque tiene una limitada potencia es muy sencillo de usar y más que suficiente para la mayoría de los casos.
  • Rhino Mocks: Es otro framework de mocking muy usado, muy potente aunque menos intuitivo que Moq, es muy flexible y está muy bien documentado. Se puede descargar desde http://www.ayende.com/projects/rhino-mocks.aspx.
  • Typemock Isolator: Es un framework de mocking de pago que se apoya en un profiler para ofrecernos más potencia que otros frameworks como moq o rhino mocks a la hora de crear mocks. Mientras que otros frameworks están limitados a hacer mocks de interfaces o miembros declarados virtual, typemock isolator nos permite reemplazar la lógica de cualquier método, propiedad, evento, etc. Ya sea en nuestro código o en código de terceros. Se puede encontrar en http://site.typemock.com/.
  • Moles: Moles es un framework desarrollado por Microsoft y que se puede descargar gratuitamente desde http://research.microsoft.com/en-us/projects/moles/. De la misma forma que Typemock Moles nos permite reemplazar cualquier método, propiedad, evento, etc. por el código de nuestra elección. Para ello, al igual que typemock hace uso de un profiler que modifica en tiempo de ejecución el código del método invocado y lo sustituye por el nuestro. Moles es muy sencillo de usar, simplemente añadimos a nuestra solución un fichero <Nombre-Ensamblado>.moles y la herramienta genera automáticamente una librería dll con todos los moles de los tipos del ensamblado.

Visto en una pequeña tabla comparativa estas son las distintas opciones que tenemos:

Framework

Ventajas

Inconvenientes

Gratis

Moq

Es muy sencillo e intuitivo.

Debido a su sencillez es difícil realizar algunos mocks.

Si

Rhino Mocks

Es muy potente y ofrece gran flexibilidad y muchas alternativas para realizar un mock.

Está limitado a interfaces y miembros virtuales de clases no selladas.

Si

Typemock Isolator

Es muy potente, permite mockear cualquier miembro de cualquier clase .NET y ofrece muchas facilidades extra a la hora de crear pruebas.

Es tan grande y tan potente que la curva de aprendizaje es más larga que con otros frameworks.

No

Moles

Es una herramienta muy potente y muy sencilla de usar ya que ha sido diseñada desde el principio con esos dos objetivos.

Es un framework de stubbing y no permite realizar comprobaciones sobre las llamadas de forma automática, aunque se puede implementar de forma sencilla manualmente.

Si

Testing, porqué, como, cuando y dónde

Bueno, en este mi segundo post voy a hablar de testing. Como ya comentó Rodrigo Corral en un post hace tiempo, En el software, la calidad, no es opcional. La siguiente pregunta que tenemos que hacernos es ¿Qué es la calidad en el software? La primera distinción que debemos hacer es entre calidad interna y calidad externa. Calidad interna es toda aquella propiedad de nuestro software que el usuario final no va a observar directamente como pueden ser la mantenibilidad del mismo, la flexibilidad, etc. Son más bien propiedades de “diseño”. Calidad externa es toda aquella propiedad de nuestro software que es observable directamente por el usuario final como puede ser la fiabilidad, experiencia de usuario, rendimiento, adecuación del sistema a los requisitos, etc.

Por tanto, dado que en nuestros desarrollos tenemos que proporcionar calidad a nuestros clientes, necesitamos establecer mecanismos y métricas que nos permitan garantizar de forma razonable que estamos alcanzando los niveles de calidad requeridos. Es aquí donde entra en juego el Testing. El testing es el mecanismo que establecemos para asegurar la calidad de nuestro software y los criterios de testing que establecemos en nuestros proyectos son las métricas que luego nos permiten afirmar que nuestro software dispone de una determinada calidad. El siguiente paso es categorizar los tipos de tests existentes y ver qué afirmaciones nos permiten establecer sobre nuestro software.

Unit testing: Es toda prueba que se realiza sobre la unidad más pequeña de código ejecutable disponible para comprobar que dicho código se comporta y produce los resultados que esperamos. Estos tests deben ser FIRST: Fast, Isolated, Repeatable, Self-Validating, Timely. Es decir, los tests deben ser rápidos, independientes (aislados del entorno), repetibles (escribir una vez, ejecutar miles), validarse por sí mismos (es correcto, falla) y escribirse justro tras el código que prueban o antes del mismo (TDD en el último caso).

Integration Testing: Es toda prueba que se realiza sobre dos o más componentes para asegurar que su funcionamiento conjunto y sus interacciones con el entorno son correctas en términos de funcionalidad. La idea es asegurar que las propiedades comprobadas para cada componente en los tests unitarios siguen siendo válidas cuando se integran con otros componentes y con el entorno y que las comunicaciones entre componentes son válidas. Es decir, que todos los componentes respetan las condiciones de interacción establecidas por cada uno de los componentes con los que se relacionan.

Smoke Testing: Es toda prueba que se realiza sobre un sistema para asegurar que funciona correctamente durante un tiempo determinado bajo unas condiciones de carga regulares y bajas. Este tipo de tests nos ayuda a comprobar que no se nos hayan escapado fallos durante las pruebas unitarias y los tests de integración y es una prueba directa de que nuestro sistema funciona correctamente bajo condiciones favorables durante un periodo de tiempo prolongado.

Stress Testing: Es toda prueba que se realiza sobre un sistema para asegurar que sigue funcionando o mejor dicho “satisface unas determinadas propiedades” bajo condiciones adversas. Este tipo de pruebas incluyen la simulación de todo tipo de condiciones adversas como gran carga y picos fuertes de trabajo, problemas de conectividad, problemas de memoria, etc. Lo que nos permiten comprobar este tipo de pruebas es que ante hechos adversos como por ejemplo la caida de un nodo del cluster de la base de datos de nuestro sistema, este siga funcionando correctamente.

Performance Testing:  Es toda prueba que realizamos sobre un componente, módulo o sistema para comprobar el tiempo de respuesta y el throughput del mismo. Este tipo de pruebas nos permite determinar el tiempo de respuesta del sistema y la cantidad de peticiones por segundo que puede soportar. Permite identificar cuellos de botella en el sistema y analizar cual es la mejor parte del sistema a mejorar para conseguir un mejor rendimiento. Para ello nos podemos ayudar de la famosa ley de Amdahl.

Capacity Testing: Es toda prueba que realizamos sobre nuestro sistema para ver como se comporta ante distintos niveles de carga. Mediante estas pruebas podemos hacer una valoración de los recursos necesarios para que nuestro sistema cumpla con los requisitos de rendimiento, tiempo de respuesta y carga de trabajo que espera nuestro cliente. Estos tests comienzan con una carga de trabajo baja y la van aumentando regularmente hasta un nivel equivalente al de una prueba de stress testing. De esta forma podemos ver los recursos que necesitaremos y como se comportará el sistema con los recursos actuales ante distintas cargas de trabajo.

Una vez hemos establecido los tests que podemos realizar a nuestro sistema, nos queda resolver cómo hacer dichos tests. Los pasos a seguir son:

Unit testing: Comprobar que nuestro código hace lo que queremos.

Integration Testing: Comprobar que las distintas partes de nuestro código se comunican adecuadamente entre sí y con el entorno.

Smoke testing: Comprobar que nuestro sistema es capaz de funcionar sin fallos durante un periodo prolongado de tiempo bajo condiciones aceptables de carga de trabajo.

Capacity Testing: Comprobar como escala nuestro sistema ante el aumento de carga de trabajo.

Performance Testing: Comprobar que nuestro sistema cumple las espectativas de tiempo de respuesta y carga de trabajo con los recursos actuales e identificar cuellos de botella del sistema.

Stress Testing: Comprobar que nuestro sistema se comporta adecuadamente ante cargas de trabajo superiores a las previstas y posibles problemas de red, memoria, disco, CPU, etc.

 

Y bueno, como tampoco quiero extenderme hasta el infinito lo dejamos aquí por hoy. En el siguiente post Unit Testing a fondo: metodología, técnicas y herramientas. Veremos como hacer un buen test unitario, como diseñar nuestro código para que sea fácil de testear, cómo medir la calidad de nuestros test, como usar contratos y herramientas de verificación estáticas para detectar errores antes de realizar pruebas y cómo utilizar herramientas automáticas como PEX para testear nuestro código en base a especificaciones y generar automáticamente casos de prueba concretos.

 

Un saludo, espero que lo disfrutéis,

Javier.

Domain Driven Design

Foto

Bueno, este es mi primer post así que voy a aprovechar para presentarme. Soy Javier Calvarro Nelson, estudiante de 5º de informática de la universidad de Málaga, llevo varios años trabajando con .NET. He participado en imagine cup en 2009 quedando en 2º lugar y he trabajado como becario en Microsoft en DPE Arquitectura.

Tras el autobombo, voy a hablar de Domain Driven Design. Lo primero que hay que decir sobre esto es que si buscas en internet vas a encontrar 15 formas distintas de definirlo y de implementarlo, aunque todas ellas tienen bastantes puntos en común. Dicho esto voy a citar a continuación mis referencias para el que quiera saber más.

En primer lugar un artículo de MSDN magazine en el que está basado en gran medida este post http://msdn.microsoft.com/en-us/magazine/dd419654.aspx, en segundo lugar un par de libros que lo ilustran bastante bien http://www.amazon.com/dp/0321125215/ref=rdr_ext_sb_ti_sims_1 que es prácticamente la biblia del DDD y http://www.amazon.com/NET-Domain-Driven-Design-Solution-Programmer/dp/0470147563#noop que es un libro que mezcla teoría e implementación. Iré ilustrando todo el ejemplo con una “aplicacion teórica” de gestión de cuentas bancarias. Bueno, dicho esto vamos al grano.

Domain Driven Design son varias cosas. En primer lugar es una forma de diseñar el software centrándonos en lo que el cliente nos pide. El software que hacemos tiene como objetivo resolver un problema de nuestro cliente. Este problema está contextualizado dentro de un dominio, es decir, el cliente maneja una serie de datos, relaciones y operaciones  como por ejemplo, cuentas, movimientos, transacciones, etc, y nuestro software debe reflejar eso en su estructura. Cuando realizamos una aplicación siguiendo Domain Driven Design es importante que nos centremos en entender y resolver el problema de nuestro cliente, es decir, debemos tener un conjunto de clases y operaciones asociadas que resuelvan el problema de nuestro cliente y que sean independientes de cualquier otro aspecto del sistema como la persistencia, exposición como servicios web, etc.

Uno de los principales problemas en el desarrollo software es la comunicación con el cliente. Los clientes hablan un idioma, el idioma de su dominio, es decir cuentas, transacciones, IRPF, IVA, etc. Los desarrolladores por otra parte no entendemos ese lenguaje y si hablamos otro lenguaje con términos como servicio web, persistencia, capa de negocio, etc. Por esto, cuando nos comunicamos con el cliente no somos capaces de entender bien su problema y por tanto no podemos resolverlo bien. Lo que propone Domain Driven Design es que los desarrolladores se empapen del dominio del problema de su cliente, es decir, conozcan los términos del dominio y su significado y en base a ello acuerden con el cliente un lenguaje común para hablar. Los desarrolladores pueden proponer términos, pero siempre es el cliente el que decide.

En segundo lugar, Domain Driven Design es un estilo arquitectural. Se parece bastante a un estilo en N-Capas (N-Layer) en tanto que los dos estilos separan las responsabilidades de la aplicación, pero la forma de hacerlo es ligeramente distinta. Este es un diagrama de capas de una arquitectura Domain Driven a mi entender, perdon por la “belleza del mismo” pero no soy todavía un experto en el layer diagram de VS 2010.

DDD

Comenzaremos por la capa de Dominio. La principal responsabilidad de esta capa es representar y resolver el problema de nuestro cliente, aquí definimos el modelo de dominio del cliente (Entidades de dominio), las operaciones comunes en el dominio del cliente que no están asociadas a ninguna entidad concreta del modelo de dominio (Servicios de dominio) y los datos que puede requerir la aplicación (Repositorios) con esto nos referimos a cosas del estilo dame la cuenta nº xxxxx y cosas así.

Entidades de dominio: Una entidad de dominio es la representación de un concepto del dominio dentro de nuestro sistema y de las operaciones que se pueden realizar sobre él. Una entidad tiene una clave que es única y la diferencia de cualquier otra entidad. Las entidades de dominio no son simples clases de datos, la forma más correcta de ver una entidad es como una “unidad de comportamiento”, haciendo las cosas de esta forma nos beneficiamos del principio de encapsulación y evitamos el anti-patron Anemic Domain Model. Por ejemplo, una entidad podría ser la clase Cuenta que se compondría de Nº de cuenta (Clave), Titular, y un conjunto de movimientos (Ingresos, Cobros, etc) y métodos como SaldoActual que a partir de los movimientos obtendría el saldo de la cuenta o métodos como congelar que impediría realizar más operaciones sobre la cuenta.

Value Objects: Los value objects se utilizan para representar conceptos importantes en nuestro dominio, pero su proposito es muy distinto al de las entidades. El objetivo de los value objects es describir un concepto importante para nuestro dominio como podría ser “Dinero”. Los value objects no son value objects de .NET y tampoco son entidades por que no tienen una clave. De hecho, los objetos por valor deben ser inmutables, es decir, cualquier intento de modificarlo debe producir un elemento nuevo y no modificar original (Como la clase String) de forma que los podamos exponer tranquilamente desde nuestras entidades y al mismo tiempo evitemos efectos laterales en las mismas. (Devuelvo una referencia al objeto, alguien modifica algo y queda modificado en mi entidad violando el principio de encapsulación).

 

Si pensamos un poco, hacer las cosas así puede hacer que la complejidad del sistema se nos dispare. Por ello, existe el concepto de agregados. Cuando definimos un agregado establecemos una jerarquía entre entidades en la que una entidad denominada “raíz” se encarga de controlar al resto de entidades del agregado. Podemos identificar una relación de agregación entre entidades cuando nos encontramos en la situación de que no tiene sentido que exista una entidad por sí misma si no está relacionada con otra. En el ejemplo, las cuentas y los movimientos formarían un agregado en el que la cuenta sería la raíz del mismo y los movimientos serían entidades dependientes.

Servicios de dominio: Típicamente en nuestro dominio van a aparecer operaciones que no encajen bien dentro de ninguna entidad ya sea por que la operación tenga entidad por sí misma o por que la operación involucre a multiples tipos de entidades. En ese caso, esa operación se extrae a un servicio de dominio. En nuestro ejemplo, un servicio de dominio podría ser por ejemplo una transacción. Los servicios de dominio solo deben ser utilizados para orquestar las operaciones entre entidades y no deben jamás tener estado interno.

Ahora que ya hemos visto los principales componentes de la capa de dominio, vamos a tratar un tema algo más complicado. A menudo, nuestras operaciones de dominio tendrán que realizar cosas como auditoría de la operación, enviar un correo, etc. Nuestro objetivo con DDD es conseguir un modelo lo más independiente del resto de la aplicación. Por ello, a la hora de realizar este tipo de operaciones la forma de proceder es crear interfaces que desacoplen este tipo de operaciones. Tendríamos así por ejemplo interfaces tipo ILogger o IEmailer, y tendríamos dos formas de usarlos. La primera consistiría en pasar estas operaciones como parámetros del método de la entidad o servicio de dominio. Por ejemplo, transactionSvc.Transacción(Cuenta c1, Cuenta c2, Money cantidad, ILogger l) y la segunda sería crear un servicio de dominio donde se reciben este tipo de elementos en el constructor y una operación de servicio se encarga de hacer la combinación de las operaciones. Por ejemplo, crearíamos un servicio TransactionProcessor donde realizaríamos la transacción y después llamaríamos a ILogger.log(transaction). Podríamos haber pasado al servicio de transacciones directamente el ILogger, pero en ese caso estaríamos comenzando a violar el principio de Single Responsability ya que una transacción tiene que ser simple y llanamente una transacción, no tiene que ocuparse de auditarse o enviar un mail, etc. Tenemos que tener claro que el dominio tiene que estar lo más desacoplado posible del mundo exterior para facilitar su testeo y su reutilización.

Repositorios: Un repositorio es una colección de objetos, o así es como debe verse para su utilización. En la capa de dominio definimos las interfaces que nuestro nivel de aplicación va a utilizar para para instanciar entidades de nuestro dominio pero no su implementación, esta se delega en la capa de infraestructura. Típicamente en estas interfaces encontramos métodos del tipo GetAccount(int accId) o  Add/Remove/Update(Account ac).

Con esto queda más o menos explicada la capa de dominio, espero que se entienda. La siguiente pregunta que toca es “vale, ya tengo resuelto mi problema, pero ¿Cómo hago una aplicación con esto?” Para eso están las otras capas.

Capa de Infraestructura: Esta capa es la responsable de implementar el mecanismo de persistencia del modelo de dominio y de proporcionar la implementación de todas las operaciones de comunicación, auditoría, etc.

La capa de infraestructura implementa las interfaces de los repositorios definidas en la capa de dominio para el mecanismo escogido (ficheros, base de datos, etc).  y todas aquellas operaciones de comunicación con el mundo exterior que necesite el dominio (Emailer,Logger, etc.). El mecanismo escogido para la persistencia debe ser transparente a la capa de dominio.

Capa de Aplicación: Esta capa sirve de “pegamento” para el sistema. La responsabilidad de esta capa es “controlar” o manipular el dominio. Los componentes de esta capa están directamente orientados a dar implementación a los casos de uso o historias de usuario. Por ejemplo, dado un titular crearle una nueva cuenta o realizar una transferencia desde la cuenta con id1 a la cuenta con id2. En esta capa, los componentes emplean los repositorios del dominio para traerse los objetos del almacenamiento persistente, instanciar los servicios de dominio y otros componentes necesarios, realizar las llamadas adecuadas para que el modelo de dominio resuelva el problema y finalmente salvar los cambios.

En el ejemplo de la transferencia podríamos tener una clase de servicios de aplicación llamada GestorDeCuentas con métodos del tipo CreateNewAccount(int idUsuarioCuenta) o TransferMoney(int account1, int account2, int ammount). Dentro de este último método por ejemplo, se traerían las 2 cuentas mediante los repositorios, se instanciaría una transacción, un TransactionProcessor y un logger y se realizaría la llamada. Por supuesto esto se simplifica bastante si usamos algún IoC como Unity.

En esta capa también crearíamos interfaces e implementaciones para servicios, DTOs etc en caso de que fuese necesario.

Capa de presentación: No voy a hablar prácticamente nada de esta capa puesto que es prácticamente igual que la capa de presentación de un estilo N-Capas. Simplemente en esta capa controlaríamos la acción del usuario y mostraríamos los datos que nos pide.

Bueno, esto ha sido todo por hoy. Espero que os guste el post y comenteis aquello con lo que no esteis de acuerdo, que al fin y al cabo esto está para que discutamos. Se me han quedado algunas cosas en el tintero como los bounded contexts, pero viene explicado en el artículo de MSDN magazine.

En el siguiente post una pequeña implementación de ejemplo.

Un saludo,

Javier.