Este artículo es un resumen de lo que considero los conceptos más importantes del libro Domain-Driven Design, de Eric Evans. Traté de condensar las ideas más importantes en un solo artículo para cualquier persona interesada en el tema. Intenté incluir toda la información posible, pero no fue una tarea fácil: El libro es una obra muy condensada con muchos ejemplos prácticos. Te recomiendo leer el libro completo si realmente quieres entender de qué se trata todo esto. Dicho esto, echemos un vistazo al diseño orientado al dominio, en un solo artículo
Creo que es una buena idea comenzar definiendo qué queremos decir con dominio.
Construimos software porque queremos resolver un problema relacionado con un área o actividad específica. Esta actividad/área es el dominio de nuestro software y puede ser algo concreto como muñecos amigurumi o tan abstracto como la contabilidad.
Los desarrolladores generalmente no construyen software de forma aislada. La mayoría de las aplicaciones requieren la participación de expertos en el dominio que aportan valiosos conocimientos del dominio que los desarrolladores a menudo no tienen. Sin embargo, no todo el conocimiento sobre el dominio es relevante. Un equipo debe dejar de lado todos los detalles no relevantes del dominio y centrarse en los conceptos más importantes para construir un modelo que sirva como base para nuestro desarrollo.
Este modelo proporcionará al equipo las abstracciones necesarias para construir software que pueda resolver los problemas correctos y adaptarse a futuros cambios en los requisitos. Como puedes imaginar, hay una cantidad infinita de modelos diferentes para cada dominio individual. Todo depende de qué detalles del dominio decides elegir o ignorar. Las siguientes 3 ideas te guiarán en la selección de un buen modelo:
- El modelo es conocimiento destilado: Solo tiene los detalles que son relevantes para resolver el problema en cuestión.
- El modelo forma la base para el lenguaje (hablado y escrito) utilizado por el equipo.
- El modelo y la implementación deben moldearse mutuamente a lo largo del curso del proyecto.
Discutamos las 3 ideas con un poco más de detalle.
1. Destilando conocimiento
Un buen modelo te ayuda a crear objetos que son más que simples estructuras de datos disfrazadas que comparten un nombre con el concepto que representan. Necesitan tener un comportamiento significativo y relaciones con otros objetos en el modelo.
Hay que ser selectivo: agrega al modelo los conceptos importantes y descarta los que no lo son. Esto es difícil de lograr en la práctica, así que probablemente necesitarás probar muchas iteraciones diferentes del modelo antes de descubrir qué lo es importante en tu contexto particular.
Al inicio de cualquier proyecto, los miembros del equipo carecen del conocimiento necesario para crear un buen modelo. Eso es normal y está bien, conforme avanza el proyecto, la base de conocimiento del equipo mejora, y refinar el modelo se vuelve más sencillo. Después de varias iteraciones, los requerimientos se definen mejor, se resuelven ambigüedades y la calidad de las clases mejora.
Crear un buen modelo es difícil, pero comprometerse con un refinamiento continuo hará la diferencia.
2. El modelo como la base del lenguaje del equipo
Los humanos tenemos increíbles habilidades lingüísticas, así que es buena idea usarlas para ayudarnos en el proceso de diseño. En la mayoría de los proyectos, la brecha de lenguaje entre desarrolladores y expertos del dominio puede convertirse en un problema. Hablar en términos de tablas de base de datos o estructuras de datos puede significar muy poco para los expertos del dominio, mientras que los desarrolladores pueden encontrar confuso el lenguaje especializado de otras industrias.
Crear un lenguaje basado en el modelo le da al equipo una herramienta para discutir el proyecto con la suficiente precisión como para crear una implementación técnica.
El equipo debe hacer el esfuerzo de crear un lenguaje basado en el modelo y usarlo tan a menudo como sea posible como parte integral del proceso de desarrollo.
3. Uniendo modelo e implementación
Un modelo es mucho más que una herramienta útil para ayudar en las primeras etapas del diseño. Los modelos son la base del diseño del software que construimos.
Las entidades de software que creamos en nuestro diseño deben ser representaciones de nuestro modelo, pero esto no suele ser fácil. Un modelo producido tras un análisis cuidadoso puede ser correcto y a la vez difícil de implementar.
Un modelo que no se mapea fácilmente a una implementación debe refinarse a través de varias iteraciones. Si se hace correctamente, crearemos un modelo que capture el problema en cuestión y que además se preste a una implementación sencilla.
Por lo general, la mejor estrategia es comenzar con un diseño limitado que refleje el modelo de manera literal, con un mapeo obvio. A partir de ahí, refinamos el modelo iterativamente, haciéndolo cada vez más fácil de implementar sin perder los detalles esenciales.
Los modelos no son simples construcciones elegantes, codifican el conocimiento más importante sobre el dominio. El modelo que produces como parte del proceso de diseño y desarrollo tiene valor en sí mismo; incluso se puede argumentar que el software que haces es valioso porque implementa el modelo.
Recuerda que cada vez que cambias el código (diseño), realizas un cambio implícito en el modelo. Por esta razón, todos los desarrolladores del equipo deben ser incentivados a participar en el proceso de modelado.
Bien. Ahora que discutimos los aspectos básicos de equipo en el diseño guiado por el dominio, hablemos de los bloques de construcción que podemos usar para representar elementos de un modelo.
Bloques de construcción y estructura general
Las arquitecturas en capas son bastante útiles para construir software basado en modelos sólidos. Si no has escuchado este término antes, no te preocupes, la idea es bastante simple: el software se organiza en capas conceptuales con responsabilidades bien definidas. Para mantener la integridad, la comunicación entre capas está restringida: una capa solo puede comunicarse (llamar métodos y mantener referencias) con la capa inmediatamente inferior.
DDD puede implementarse en un esquema con 4 capas:
De todas estas capas, la que más nos interesa cuando hacemos diseño guiado por el dominio es la capa de dominio. Todo el conocimiento destilado se volcará en la creación de los objetos que pueblan esta capa.
Veamos un par de ideas que te ayudarán a crear una capa de dominio expresiva y limpia.
Mantén asociaciones simples
Una asociación entre dos objetos es una dependencia. Mantener una referencia a un objeto, llamar a uno de sus métodos o tener cualquier conocimiento sobre él crea una dependencia.
Las asociaciones no son algo malo, de hecho, sin ellas la mayoría del software sería prácticamente imposible de construir. El problema empieza cuando el número de asociaciones se vuelve inmanejable. El secreto es mantener las cosas simples y deshacerte de tantas asociaciones innecesarias como sea posible. En la práctica, he encontrado que el mayor beneficio se obtiene de dos cosas:
- Eliminar asociaciones bidireccionales (cuando ambos objetos dependen el uno del otro).
- Minimizar el número de asociaciones para cada objeto en tu sistema (sí, obvio).
No es fácil hacerlo, pero existen técnicas bien conocidas para resolver este problema. La mayoría de los libros sobre patrones de diseño o construcción de software te darán herramientas para gestionar dependencias de objetos (Casi cualquier libro sobre el tema es suficientemente bueno).
Si aún no sabes cómo hacerlo, ve y apréndelo: te hará la vida mucho, mucho más fácil.
Entiende la identidad de los objetos
En el libro (DDD), Evans hace una distinción entre objetos basada en su identidad.
Un objeto cuya identidad está definida completamente por sus atributos se llama objeto de valor/value object. Dos objetos de valor con los mismos atributos son esencialmente lo mismo, y el sistema los tratará como tales. Supongamos que estás construyendo software para una fábrica de autos. Debido a consideraciones específicas del proyecto, terminas creando un tipo para las diferentes Marcas de autos que fabrica la empresa. El objeto Marca está definido completamente por los atributos que lo conforman, es lo que lo distingue de los demás.
Si existieran dos marcas iguales al mismo tiempo, el sistema no podría notar la diferencia. Por esto, los autos no necesitan tener sus propias copias de una Marca específica: los autos del mismo fabricante pueden referenciar a una única instancia inmutable de su modelo particular en memoria. Los objetos de valor suelen ser miembros privados de otros objetos, contenedores de datos temporales para pasar como argumentos a funciones, o bien objetos inmutables con múltiples referencias entrantes.
El otro tipo de objeto es la entidad. Las entidades trascienden el contenido de sus atributos: dos entidades pueden tener exactamente los mismos atributos, pero aun así deben tratarse como objetos separados. Imagina una aplicación que, por alguna razón, tiene un objeto Persona con nombre, nacionalidad y sabor de helado favorito. A pesar de tener los mismos atributos, los objetos para mí y mi papá (ambos Juan Luis Orozco, ambos costarricenses y ambos con menta como sabor favorito de helado) deben tratarse como entidades separadas desde su creación hasta su destrucción. En la práctica, la mayoría de los objetos con comportamientos complejos y relaciones en tu aplicación serán entidades.
Es importante entender que el mismo concepto del mundo real puede tomar cualquiera de las dos formas, esto lo dictan las particularidades del problema que intentas resolver. Evans explica esto con el siguiente ejemplo:
Imagina que estás construyendo una aplicación para gestionar los asientos en un estadio para un sistema de boletaje. Si la aplicación necesita tomar en cuenta el asiento específico que reservan los clientes, los objetos Asiento serán entidades: nos importa qué asiento vamos a obtener. Si la aplicación ignora sus posiciones y números, y los clientes pueden sentarse donde quieran, los objetos Asiento serán modelados como objetos de valor porque no hay diferencia importante desde el punto de vista del boleto.
Otros constructos útiles
Servicios
Los servicios te permiten modelar procedimientos de una forma orientada a objetos. Son objetos que realizan tareas procedimentales que no pertenecen a ningún otro objeto de dominio y pueden ubicarse en cualquiera de las capas arquitectónicas que mencionamos antes. Diseñar un buen servicio no es sencillo, pero la mayoría de los servicios bien hechos tienen características en común:
- Las operaciones que realizan se relacionan con un concepto del dominio que no es una responsabilidad natural de tus entidades u objetos de valor.
- La interfaz se define en términos de otros elementos del modelo de dominio (tanto argumentos como la forma en que los métodos encajan en el modelo).
- La operación que realizan puede tener efectos secundarios, pero el objeto que implementa el servicio debe ser stateless/sin estado.
Módulos
Los módulos son una forma de empaquetar juntas las partes estrechamente relacionadas de tu sistema. Idealmente, quieres agrupar conceptos que puedan entenderse y razonarse independientemente de otras partes del modelo. Los módulos abarcan un conjunto cohesivo de conceptos que cuentan una historia cuando se ponen juntos y te ayudan a lograr alta cohesión y bajo acoplamiento.
Como ya podrás sospechar, encontrar los límites y el contenido de cada módulo no es tarea fácil. Adquirir el conocimiento y entendimiento necesarios para crear un buen conjunto de límites de módulos probablemente te llevará varias iteraciones.
Agregados
Los agregados son una forma de transformar una colección de objetos con relaciones complicadas en una construcción consistente que sea fácil de usar y entender. La siguiente imagen muestra un diagrama de clases con un objeto cliente (Customer) interactuando con dos agregados.
Algunas consideraciones importantes al trabajar con agregados:
- Un agregado puede contener cualquier número de objetos, pero la raíz siempre es una Entidad.
- Nada fuera de los límites del agregado puede mantener una referencia a algo dentro de él. Esto significa que toda la funcionalidad del agregado se sirve a través de la raíz (como una Fachada/Facade). En el ejemplo anterior, esto significa que el Customer no puede mantener referencias permanentes a Wheel, Position y/o Tire.
- Las raíces pueden, en algunas circunstancias, devolver una referencia a un objeto dentro del límite, pero solo pueden usarse de manera transitoria por el cliente.
- Los objetos dentro del agregado pueden mantener referencias a las raíces de otros agregados.
- El objeto raíz es responsable de asegurar todas las invariantes del agregado.
- Eliminar la raíz de un agregado elimina todos los objetos dentro de él.
Fábricas
Las fábricas son patrones útiles para facilitar la creación de objetos. En este contexto se vuelven especialmente útiles porque pueden crear agregados completos y asegurar que comienzan en un estado válido donde se satisfacen todas sus invariantes.
Y sí, estoy hablando de los clásicos patrones Abstract Factory y Factory Method. Así que recuerda delegar la creación de agregados a las fábricas de tu sistema.
Persistencia
Casi todas las aplicaciones necesitan algún tipo de almacenamiento persistente para los datos del sistema. Hay dos maneras principales de darle estas capacidades a tu sistema:
- Usar el patrón ActiveRecord y darle a los objetos la responsabilidad de gestionar sus propias capacidades CRUD.
- Crear una familia especial de objetos cuya única responsabilidad sea recuperar y almacenar objetos en una BD u otra forma de almacenamiento. Un objeto que hace esto se llama Repositorio y puede implementarse de muchas formas según las necesidades del proyecto.
El rol de la refactorización en DDD
La palabra refactorización suele referirse al acto de realizar cambios incrementales en la base de código para mejorar la estructura y calidad del mismo. Pero en este contexto, significa algo diferente.
En DDD, refactoring se trata de destilar el modelo en una mejor representación del problema que queremos resolver. Los cambios realizados al modelo (y como resultado, a la implementación) suelen estar motivados por una mayor comprensión de los problemas que intentamos solucionar. Al inicio del proyecto, esto puede ser una tarea difícil, pero después de suficientes refactorizaciones llegamos a un punto donde inversiones modestas de tiempo producen enormes incrementos de valor en funcionalidad.
Pon atención a las ideas que surgen conforme avanza el proyecto, y no ignores la oportunidad de mejorar un modelo. En serio, no tengas miedo de “romperlo”, los modelos son maleables ¡y los beneficios potenciales son enormes!
Los modelos suelen mejorar al tomar un concepto implícito y hacerlo explícito. Y sí, desafortunadamente, no hay atajos para lograr esto.
Necesitarás usar el lenguaje del modelo del proyecto, hablar con expertos del dominio, leer libros y refactorizar hasta encontrar los conceptos más importantes. Es un trabajo duro, pero todas estas cosas te ayudarán a crear modelos consistentes, flexibles y explícitos que harán que implementar nuevas funcionalidades sea rápido y sencillo.
Diseño flexible (Supple design)
Según Evans, un diseño flexible es Un diseño que pone el poder inherente en un modelo profundo en manos de un desarrollador cliente para crear expresiones claras y flexibles que entregan resultados esperados de manera robusta. Igualmente importante, aprovecha ese mismo modelo para hacer que el diseño mismo sea fácil de moldear y remodelar por el implementador para acomodar nuevos conocimientos.
Suena bien, ¿verdad? El objetivo del libro (y de este artículo) es darte herramientas para lograr un diseño flexible en cualquier proyecto que enfrentes. Ya discutimos la mayoría de los fundamentos de DDD que usarás para alcanzar este objetivo. Estas son otras ideas útiles que te ayudarán en esta tarea:
- Crea interfaces que revelen intención. Los nombres de las funciones y sus parámetros deben expresar un propósito.
- Usa aserciones para prevenir comportamientos inesperados y también para expresar intención.
- Descompón los elementos de diseño (interfaces, clases, agregados) en unidades cohesivas con límites claramente definidos.
- Cuando sea posible, usa un estilo declarativo de arquitectura que pueda expresar funcionalidad como combinaciones de comportamiento.
- Refresca tu conocimiento de patrones de diseño y patrones de análisis. Si no sabes por dónde empezar, consigue una copia de Analysis Patterns de Martin Fowler.
Manteniendo la integridad del modelo
Si un sistema es lo suficientemente grande, comenzarán a surgir modelos más pequeños en diferentes partes. Esta es una de las razones por las que mantener la integridad del modelo se vuelve más difícil conforme los sistemas crecen. Veamos algunas cosas que puedes hacer para proteger tu modelo de la corrupción si te encuentras trabajando en un proyecto lo suficientemente grande:
Contexto delimitado (Bounded context)
Cuando se combina código de diferentes módulos, eventualmente surgen errores. Necesitas identificar el contexto (partes del sistema) donde aplica el modelo y protegerlo a toda costa de inconsistencias. Tu equipo debe estar siempre alerta a inconsistencias dentro de esos límites y corregirlas de inmediato.
Mapa de contexto (Context map)
Cuando surgen múltiples modelos en el sistema, necesitas una manera de entender cómo se relacionan los conceptos entre ambos modelos. Para esto, crea un mapa de contexto que especifique explícitamente estas relaciones.
Núcleo compartido (Shared kernel)
A veces, dos o más modelos tendrán una parte en común que no puedes separar. Para esto, designa esas piezas compartidas como el núcleo compartido de tus modelos. Los equipos no pueden hacer cambios en el núcleo compartido sin consultarse entre sí para mantener la consistencia.
Capa anticorrupción (Anticorruption layer)
Una capa anticorrupción les da a los clientes funcionalidad en términos de sus propios modelos. De esta manera, puedes mantener las dos partes del sistema aisladas y consistentes. Si necesitas acceder a funcionalidad de otras partes del sistema, puedes usar las interfaces que ya tienen.
Destilación
Estas son algunas ideas que puedes usar para dividir tu modelo en diferentes secciones:
- Modelo núcleo: El modelo núcleo es la parte más importante del sistema, la esencia del problema que tu software resuelve. Sepáralo de los modelos de soporte.
- Conceptos genéricos separados: Toma las partes de tu sistema que representan características de problemas genéricos y ponlas en su propio modelo.
- Mecanismos separados: Toma las partes procedimentales/mecánicas de tu sistema y escóndelas detrás de una interfaz. Si necesitas darle a tus objetos un comportamiento tipo grafo, codifícalo en su propia construcción y proporciona la funcionalidad a través de una interfaz limpia.
Consideraciones finales
Estas son algunas otras ideas para enriquecer el proceso de diseño guiado por el dominio:
- El proceso de diseño debe absorber retroalimentación. La comunicación y colaboración continua de todos los miembros del equipo es esencial para crear un modelo exitoso.
- Las decisiones deben llegar a todo el equipo. DDD no es un proceso de “arquitecto en torre de marfil” y requiere que los miembros del equipo contribuyan y tengan voz en las decisiones de diseño.
- Haz espacio en tu diseño para la naturaleza dinámica del proceso. Este cambiará, varias veces, antes de llegar a una solución satisfactoria.
- Un buen diseño requiere tanto minimalismo como humildad.
Gracias por leer
Creo firmemente en la importancia de invertir en nuestro propio crecimiento profesional como desarrolladores de software y profesionales de la tecnología. Muchas ideas y procesos importantes han sido documentados en las últimas décadas, pero de alguna manera seguimos aprendiendo las mismas lecciones una y otra vez.
Se han escrito muchos libros y documentos excelentes sobre cómo hacer gran software, y Domain-Driven Design es, en mi opinión, uno de los más importantes. Paso mucho tiempo intentando escribir un artículo con todo lo esencial, pero es casi imposible hacerle justicia con tan poco espacio.
Realmente recomiendo el libro. A pesar de tener casi dos décadas, considero que su contenido sigue siendo refrescante e iluminador para el mundo del desarrollo moderno. Quizás por la importancia que tiene el software hoy en día, me atrevería a decir que nunca ha sido tan relevante como lo es ahora.
Así que, si tienes el tiempo, échale un vistazo, realmente vale la pena.
¡Gracias por leer!
Qué hacer después
- Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que puedan encontrar esta información útil.
- Este artículo está basado en Domain-Driven Design, de Eric Evans.
- Envíame un correo con preguntas, comentarios o sugerencias (lo encuentras en la página Autor)