Domain-Driven Design: Estrategias y Patrones para Dominar la Complejidad en el Desarrollo de Software.
Blog Detail Page
Blog Detail Page
Blog Detail Page

Domain-Driven Design: Estrategias y Patrones para Dominar la Complejidad en el Desarrollo de Software.

Blog Detail Page
Blog Detail Page
Blog Detail Page

Este artículo explora los principios fundamentales de Domain-Driven Design (DDD) y sus patrones estratégicos clave, como el lenguaje ubicuo y los contextos delimitados, para ayudar a alinear las aplicaciones con las necesidades del negocio, mejorar la comunicación entre equipos y gestionar la complejidad en sistemas de software.

Históricamente, el desarrollo de aplicaciones se había pensado como una actividad separada del negocio, en la que había una toma de requerimientos inicial para conocer necesidades y la entrega de un producto final que no siempre cumplía con las expectativas previstas, ya que con esta forma de trabajar (en cascada) había poca o ninguna flexibilidad para introducir cambios durante el proyecto.

Ya en 2001, con el Agile Manisfesto se empezó a desarrollar pensando en realizar los desarrollos abrazando el cambio. Pero aún había un impedimento a salvar: la diferencia entre el negocio (entendido como las entidades, sus relaciones entre sí, las operaciones que debían hacerse sobre ellas, etc.) y la forma en que este se veía reflejado en la aplicación construida.

Fue a finales del año 2003 cuando apareció el libro Domain-Driven Design: Tackling Complexity in the Heart of Software escrito por Eric Evans, que ofrecía una forma distinta de hacer las cosas. El llamado “libro azul del DDD” no era un libro práctico sobre patrones a aplicar, sino que describía cómo conseguir que la aplicación modelase correctamente el negocio y ofrecía un conjunto de estrategias para conseguir ese objetivo.

El DDD (siglas de Dominio orientado al Dominio en inglés) tiene especial sentido cuando afrontamos el desarrollo de aplicaciones sobre dominios de conocimiento complejos. No es siempre la mejor opción, ya que en dominios relativamente sencillos existen aproximaciones más simples que ofrecen un equilibrio entre la complejidad y el coste de construcción. En cambio, cuando el dominio es complejo, hacer un acercamiento de este tipo permite conseguir una mayor estabilidad de la aplicación a los cambios que aparecen de forma natural al evolucionar el negocio.

A continuación, te damos algunas pautas para aplicar el DDD.

Utilizar un lenguaje ubicuo

En muchas ocasiones, nos encontramos que una aplicación tiene unas tablas, un modelo de datos y unos nombres que, al cabo de unos años, nadie entiende del todo lo que significan; lo que complica el entendimiento entre todas las partes implicadas.

Precisamente, eso es lo que intenta evitar el uso de un lenguaje ubicuo (ubiquitous language en inglés).

El lenguaje ubicuo es un lenguaje común entre todos los equipos involucrados en un proyecto y tiene como objetivo evitar los malentendidos y fomentar la colaboración entre desarrolladores y expertos del dominio.

Un lenguaje ubicuo tiene que:

  • Estar centrado en el dominio: es decir, basarse en la forma en que los expertos del dominio (es decir, las personas que tienen un profundo conocimiento del área de negocio o problema que se está resolviendo con el software) hablan de su trabajo. No tiene sentido que los desarrolladores se expresen en otros términos que pueden llevar a equivoco.
  • Ser consistente: significa utilizar siempre las mismas palabras para referirse a los mismos conceptos. Si existen múltiples formas de referirse a un concepto se tiene que escoger una de ellas y utilizarla siempre de común acuerdo.
  • Ser preciso y claro: debemos evitar las ambigüedades y no utilizar el mismo nombre para referirse a conceptos distintos.
  • Debe integrar el software y el negocio: cuando los términos del lenguaje ubicuo se utilizan en el código (en los nombres de las clases, en los métodos o los módulos) conseguimos que este sea más fácil de comprender por todas las partes involucradas.

Con este enfoque, conseguimos que haya una forma de describir los problemas del dominio y que la solución propuesta a los mismos sea comprensible por todos los equipos. Y lo mismo sucede con el código. Los desarrolladores van a ser capaces de hablar con negocio en unos términos comunes.

Pongamos un ejemplo esclarecedor:

  • Los expertos del dominio nos explican que existen las Cuentas Bancarias y que estas tienen un Titular, un Saldo y una FechaValor para este saldo. En ellas siempre se puede Depositar un importe que se suma al Saldo y se puede Retirar un importe que se resta al Saldo siempre que este sea menor al Saldo existente. En ambos casos se actualiza la FechaValor
  • Entonces el código debería ser algo del estilo:
    • Clase CuentaBancaria
      • Titular
      • Saldo
      • FechaValor
    • Y debería tener métodos de este estilo:
      • Depositar(importe, fecha)
      • Retirar(importe, fecha)
    • Un ejemplo en pseudocódigo sería:
      • Depositar(importe, fecha): Saldo = Saldo + importe, FechaValor = fecha
      • Retirar(importe, fecha): Si Saldo < importe LanzarError Sino Saldo = Saldo – importe, FechaValor = fecha

Tomando como base este ejemplo, los expertos del dominio ahora podrían pedir un listado mensual con las cuentas en las que no ha habido actividad durante el último mes y, por su parte, el equipo de desarrollo podrá proponer seleccionar las CuentaBancaria’s en que la FechaValor sea anterior al mes en curso.

De esta forma, lo que se pide y lo que se implementa estará perfectamente alineado y todo el mundo entenderá lo que se va a hacer y cómo funciona.

En general el lenguaje ubicuo:

  • Mejora la comunicación dentro del equipo y evita malentendidos
  • Permite alinear negocio y tecnología, ya que se consigue que lo implementado cumpla las necesidades reales
  • Favorece un código más legible, preciso y mantenible, evitando confusiones durante el desarrollo
  • Mejora la colaboración entre áreas al hacer las discusiones técnicas más accesibles para negocio

Contexto delimitado

Mediante la división de nuestra aplicación en distintos contextos conseguimos dividir un sistema complejo en piezas más autónomas y más fácilmente mantenibles.

La idea es que cada contexto tenga su propio lenguaje ubicuo y se modele el negocio para resolver una parte específica del problema global. En esa pequeña parcela conseguimos que el dominio tenga un significado específico, que sea coherente y consistente con las reglas de negocio que pueda haber en esa parte del dominio.

A esos distintos contextos los llamamos contextos delimitados (o bounded contexts en inglés).

Una de las fortalezas más importantes de los contextos acotados es que tienen unos límites bien definidos que permiten aislarse del resto de la aplicación y que sus cambios no afecten al resto.

Asimismo, existen patrones con los que favorecer la colaboración entre distintos contextos acotados manteniendo el aislamiento entre ellos (uso de APIs, uso de eventos, uso de capas de anticorrupción, etc.).

Un ejemplo: dentro de una entidad bancaria puede existir el contexto de las cuentas bancarias y el contexto de los préstamos y ambos tienen que poder evolucionar de forma independiente a pesar de poder tener relación.

Además, el concepto de contexto delimitado se puede explotar tanto como sea necesario para reducir la complejidad haciendo que dentro de un mismo contexto haya subcontextos más pequeños que igualmente están cohesionados y en los que se puede implementar una funcionalidad independientemente del resto del contexto.

A nivel práctico, para identificar un contexto delimitado dentro de nuestro dominio tenemos que buscar áreas en que se utilice un conjunto único de términos (el lenguaje ubicuo en ese contexto) o reglas únicas, áreas que tengan unos requisitos funcionales muy diferenciados del resto o áreas que representen una separación natural del flujo de trabajo o los procesos de negocio.

Los contextos delimitados son de vital importancia para impedir que nuestro sistema se convierta en un monolito al dividir de forma clara los distintos modelos que existen dentro del negocio y permitir posibilidades técnicas como el uso de arquitecturas distribuidas.

Mapa de contextos

Como hemos comentado al separar nuestro sistema en distintos contextos delimitados podemos terminar con muchos contextos pequeños y necesitamos tener clara la forma en que van a interactuar entre sí.

Para ello usamos el mapa de contextos (o context map en inglés) que es una representación visual de los contextos delimitados y como interaccionan ya sea de forma jerárquica o mediante colaboración.

Si existe una relación jerárquica entre contextos delimitados, el contexto dominante impone sus términos y los contextos subordinados suelen adaptarse.

En este tipo de relación los patrones de integración más comunes, en este caso, son el patrón Conformista, el patrón Capa de Anticorrupción, el patrón Núcleo Compartido o el patrón Cliente-Proveedor.

Hay ocasiones en que los contextos delimitados colaboran entre sí en plena igualdad de condiciones entonces se tienen que usar otros patrones. Estos son el patrón Asociación, el patrón Núcleo Compartido (en efecto, este patrón se puede usar tanto en interacción jerárquica como en interacción colaborativa), el patrón Lenguaje Publicado y el patrón Vías Separadas.

Como escoger entre este conjunto de patrones forma parte del arte de hacer un diseño guiado por el dominio, pero aquí tienes unas recomendaciones:

  • Si un contexto depende completamente de otro para funcionar correctamente es más conveniente usar un patrón jerárquico. Por ejemplo, contexto de precios está subordinado a contexto de productos.
  • Si ambos contextos son autónomos y pueden funcionar sin que uno domine al otro es más conveniente usar un patrón colaborativo. Por ejemplo, contexto de pedidos y contexto de envíos, aunque estén relacionados pueden colaborar independientemente.
  • Si un contexto siempre ejecuta acciones definidas por otro estamos ante una relación jerárquica. Pero si los dominios solamente necesitan compartir información estamos ante una relación de colaboración.
  • Cuando usamos un patrón jerárquico si el dominio subordinado puede adoptar el modelo del dominio dominante lo más conveniente es utilizar el patrón Conformista. Si por el contrario queremos que el dominio subordinado mantenga su modelo y sus reglas por consistencia interna deberemos usar un patrón como Capa de Anticorrupción o Núcleo Compartido.
  • Si un dominio tiene influencia en cómo se diseña otro dominio es mejor utilizar patrones de colaboración como el Asociación.

Para realizar un mapa de contextos se suelen usar rectángulos o cajas para los contextos y flechas para indicar las relaciones entre ellos. Estas flechas pueden ser unidireccionales si la relación es jerárquica y bidireccionales si la relación es colaborativa. En estas flechas se suele indicar el patrón que queremos aplicar (Conformista, Asociación, etc.). También es común, si se tiene claro en el momento de realizar el mapa, el mecanismo que se utiliza para esa comunicación (API, evento, integración en código, etc.).

A continuación, describiremos brevemente los principales patrones a modo informativo.

> Patrón Conformista

Este patrón (Conformist en inglés) es jerárquico y consiste en que el contexto subordinado acepta completamente el modelo y las reglas del contexto dominante. No intenta modificarlo en absoluto o ni siquiera adaptarse para mantener un modelo propio. La ventaja que ofrece es que es de fácil integración, pero reduce la autonomía del contexto y puede estar afectado por un cambio del contexto dominante.

> Patrón Capa de Anticorrupción

Este patrón (Anticorruption Layer en inglés) es jerárquico y consiste en proteger el modelo propio respecto al modelo del contexto dominante usando una capa de traducción que actúa como mediador. La ventaja es que protege al contexto de los cambios que se puedan producir en el contexto dominante y permite mantener un modelo independiente, pero requiere mayor esfuerzo de implementación y puede introducir problemas de rendimiento por tener que introducir la capa de traducción.

> Patrón Núcleo Compartido

Este patrón (Shared Kernel en inglés) se puede considerar jerárquico o colaborativo. Se fundamente en tener una parte del modelo que se comparte entre ambos contextos, pero poder evolucionar independientemente en el resto del modelo. Su ventaja es que reduce la duplicación de los conceptos comunes y permite consistencia de los datos compartidos, pero los cambios en el núcleo compartido impactan a los dos dominios y requiere una alta colaboración entre los equipos de ambos dominios.

> Patrón Cliente-Proveedor

Este patrón (Customer-Supplier en inglés) es jerárquico y hace que el contexto subordinado actúe como cliente y el contexto dominante actúe como proveedor. En este sentido, el subordinado influye en las decisiones del dominante ya que consume datos o funcionalidades adaptadas a sus necesidades. Su ventaja es que facilita la colaboración y está bien adaptada a las necesidades del subordinado, pero requiere una coordinación continua y activa y si el dominante tiene demasiados subordinados puede generar trabajo adicional.

> Patrón Asociación

Este patrón (Partnership en inglés) es colaborativo y conveniente cuando ambos contextos trabajan estrechamente como socios igualitarios para lograr un objetivo común. La coordinación entre ambos contextos es continua para garantizar la compatibilidad mutua. Su ventaja es que permite una evolución coordinada de ambos contextos y la integración es la justa y necesaria, la desventaja es, como hemos indicado, la coordinación continua y que si ambos equipos no tienen el mismo ritmo de trabajo puede producir problemas.

> Patrón Lenguaje Publicado

Este patrón (Published Language en inglés) es colaborativo y consiste en establecer un protocolo o forma de interaccionar para publicar su modelo de forma clara y consistente a otros contextos. Permite fácilmente relaciones con diversos contextos y no solo uno. El ejemplo técnico de este patrón es la publicación de APIs con mensajerías claramente definidas mediante OpenAPI. Sus ventajas son una alta claridad en la integración y que permite interaccionar con múltiples contextos, pero puede requerir cambios en los consumidores cuando se cambia el lenguaje publicado y requiere que se mantenga actualizado y documentado.

> Patrón Vías Separadas

Este patrón (Separate Ways en inglés) es colaborativo y es conveniente cuando dos contextos no quieren colaborar de ninguna forma y poder resolver sus problemas de forma independiente. Este patrón suele generar duplicidad de datos o de lógica de negocio, pero fomenta la autonomía y la independencia y reduce el riesgo de que un contexto afecte al otro.

Conclusiones

El Domain-Driven Design ofrece una estrategia para abordar la complejidad de un sistema de aplicaciones.

Los contextos delimitados permiten dividir el dominio en partes manejables y los patrones estratégicos facilitan formas de integrarse claras y reconocibles.

Adoptando estas prácticas se promueve la autonomía de los equipos y la flexibilidad en las aplicaciones.

Pero, sobre todo, fomenta la comunicación clara entre los equipos y que el modelo del dominio evolucione junto con las necesidades del negocio.


Sigue leyendo...