paint-brush
Introducción práctica al diseño dirigido por dominiospor@tobi
1,435 lecturas
1,435 lecturas

Introducción práctica al diseño dirigido por dominios

por Piotr Tobiasz15m2023/07/22
Read on Terminal Reader

Demasiado Largo; Para Leer

Terminología DDD, bloques de construcción y otros conceptos para comenzar.
featured image - Introducción práctica al diseño dirigido por dominios
Piotr Tobiasz HackerNoon profile picture
0-item
1-item

No debería ser innovador decir que escribir software no se trata simplemente de escribir código, sino de resolver un problema en particular. Aunque son los desarrolladores quienes eventualmente implementan la solución, no son los desarrolladores quienes definen cuál es el problema en primer lugar. Esa tarea la llevan a cabo varios empresarios, que consideran procesos, riesgos y resultados para describir cuál es el problema, por qué existe y cómo debe abordarse. En un contexto impulsado por el dominio, estos empresarios se conocen como expertos en dominios .


Desde una perspectiva de ingeniería, parece que los expertos en dominios tienen un activo valioso: su conocimiento sobre el dominio. Sin embargo, este conocimiento rara vez se comparte en su forma cruda. En cambio, generalmente se traduce en requisitos para que los desarrolladores puedan entenderlo e implementarlo. El problema con este enfoque es que el conocimiento del dominio de los empresarios y los desarrolladores puede divergir. Esto significa que las perspectivas de quienes definen el problema y quienes trabajan para resolverlo pueden no coincidir, lo que genera malentendidos y conflictos.


Entonces, ¿cuál es la salida? Asegúrese de que las personas de negocios y técnicas utilicen el mismo lenguaje y terminología.

¿Qué es el Diseño Dirigido por Dominio?

Domain-Driven Design (DDD) es una metodología que enfatiza la importancia de crear un entendimiento compartido entre los expertos del dominio y las partes interesadas técnicas y alinear la solución de software con los requisitos comerciales subyacentes. Esta parece ser una definición no técnica de alto nivel, pero también se puede dividir en algo más fácil de usar para los desarrolladores:


DDD representa conceptos del mundo real en el código y su estructura al cultivar y usar un lenguaje ubicuo, que se construye modelando el dominio comercial.


Todavía queda algo de terminología por introducir, por lo que es posible que no esté 100 % claro en este momento. Lo más importante es que DDD proporciona herramientas y actividades que permiten escribir y estructurar código alineado con la visión del negocio. Entonces, no se trata solo de la comunicación, sino también de tomar decisiones de diseño que realmente dan forma al lenguaje común.

Terminología

No debería sorprender que el término más importante en el mundo DDD sea el Dominio . Así es como lo describe Vlad Khononov, autor de "Learning Domain-Driven Design":


Un dominio empresarial define el área principal de actividad de una empresa.


Significa que el dominio comercial también podría considerarse como:


  • La principal fuente de ingresos de una empresa,
  • Por qué la empresa es más conocida,
  • Cualquier cosa que la empresa haga mejor que sus competidores.


El dominio también se puede dividir en subdominios : rangos de actividades más específicos. Si bien hay tres tipos diferentes de subdominios, el más importante es el core . Describe cómo la empresa logra la ventaja comercial. Los otros dos son sobre problemas genéricos más comunes, como sistemas de autenticación o paneles de administración internos.


Tener una comprensión profunda del dominio comercial de una empresa es absolutamente crucial para aprovechar al máximo los beneficios del diseño basado en el dominio. La mejor fuente de este entendimiento no es otra que los Expertos del Dominio . Estas son las personas cuyo problema se está abordando con el software: partes interesadas, varios empresarios e incluso usuarios. No quiere decir que los ingenieros no estén informados sobre el dominio en el que están trabajando, sino que los Expertos son la fuente de la verdad del conocimiento del dominio. Al trabajar junto con los expertos en dominios, los desarrolladores pueden garantizar que los modelos del dominio permanezcan precisos y actualizados.


Esto lleva a otro término crítico pero potencialmente ambiguo: modelo . Eric Evans, en su libro sobre el DDD, describe el modelo de la siguiente manera:


Es una interpretación de la realidad que abstrae los aspectos relevantes para resolver el problema en cuestión e ignora los detalles superfluos.


Vlad Khononov continúa explicando esta idea en términos más identificables:


Un modelo no es una copia del mundo real, sino una construcción humana que nos ayuda a dar sentido a los sistemas del mundo real.


En conclusión, un modelo es una representación de un concepto o proceso de negocio que facilita la comprensión y comunicación de la complejidad subyacente del dominio.


Vlad usó un mapa para ilustrar el concepto de un modelo de dominio de manera efectiva. Los mapas son un ejemplo perfecto de cómo solo muestran información relevante para el tipo de mapa, como topografía, carreteras o fronteras. Un mapa que muestre todos los detalles a la vez sería abrumador y bastante inútil. Los modelos de dominio también se pueden encontrar en otras formas, como:


  • Pedidos de clientes, que representan la versión simplificada de todos los procesos que suceden en segundo plano,
  • Menús de restaurantes, donde los elementos enumerados en el menú son los productos finales, en lugar de enumerar cada ingrediente y paso en el proceso de preparación,
  • Reservas de viajes, donde el viaje reservado solo destaca los detalles más críticos, aunque se necesita mucho más para planificar el viaje, el hotel, etc.


La última pieza del rompecabezas terminológico del diseño basado en dominios (DDD) es el lenguaje ubicuo. Se refiere al lenguaje compartido utilizado por las partes interesadas tanto técnicas como comerciales en un proyecto. Tener un lenguaje común para describir el dominio comercial derivado del modelo de dominio es crucial en DDD. Ayuda a garantizar que todos los miembros del equipo tengan una comprensión clara del espacio del problema, sus conceptos y sus relaciones. Esto conduce a una mejor alineación y reduce el riesgo de malentendidos. Mediante el uso de Ubiquitous Language, la solución de software puede reflejar con precisión los requisitos comerciales subyacentes, lo que la convierte en un componente crítico de DDD.


Con la mayor parte de la terminología cubierta, debería ser más fácil entender qué es el diseño basado en dominios . Ahora es el momento de profundizar en el cómo real: los componentes básicos del DDD.

Bloques de construcción

Los componentes básicos de DDD sirven como base para crear un modelo de dominio eficaz y eficiente. Vlad Khononov define el Modelo de Dominio de la siguiente manera:


Un modelo de dominio es un modelo de objetos del dominio que incorpora tanto el comportamiento como los datos.


El modelo de dominio consta de varios bloques de construcción y estructuras. Los más importantes son:


  • objetos de valor,
  • entidades,
  • agregados,
  • eventos de dominio,
  • repositorios,
  • Servicios de dominio.

Objetos de valor

Los objetos de valor son los bloques de construcción más básicos disponibles. Estos son objetos que están definidos por un conjunto de atributos y valores. No tienen un identificador único: sus valores definen su identidad. Son inmutables en el sentido de que diferentes valores ya representan un objeto de valor diferente. Los ejemplos de objetos de valor incluyen:


  • Cantidad monetaria,
  • Rango de fechas,
  • Direccion postal.


Así es como se podría implementar un objeto de valor simple en Python:


 from pydantic import BaseModel class Address(BaseModel):  """Customer address."""  country: str  city: str  street: str  house_number: str  class Config:    frozen = True


Significa que usar el operador de igualdad ( == ) para comparar dos direcciones devolverá True solo si ambos objetos tienen exactamente los mismos valores asignados.

Entidades

Las entidades son el siguiente tipo de bloque de construcción. Las entidades representan objetos individuales en el dominio con una identidad distinta, como una persona o una orden. Son similares a los objetos de valor en la forma en que también almacenan los datos, pero sus atributos pueden cambiar y se espera que cambien y, por lo tanto, necesitan un identificador único. Los pedidos y la información personal son solo dos instancias simples de las Entidades:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.value_objects import Address class Person(BaseModel):  """Personal data."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  first_name: str  last_name: str  address: Address class Order(BaseModel):  """Customer order."""  id: uuid.UUID = Field(default_factory=uuid.uuid4)  description: str  value: float


Debido a que los valores de las instancias son modificables en ambos casos, necesitan una identificación, que puede ser un UUID. Lo que es más importante es que, en la mayoría de los casos, las entidades no están destinadas a administrarse directamente sino a través de un agregado .

Agregar

Un Agregado es un tipo de entidad porque es mutable y requiere un identificador único. Sin embargo, su responsabilidad principal no es almacenar datos, sino agrupar un conjunto de objetos relacionados (entidades y objetos de valor) como una única unidad de coherencia. El agregado es el objeto raíz, con un límite bien definido que encapsula su estado interno y aplica invariantes para garantizar la coherencia de todo el grupo. Los agregados permiten razonar sobre el dominio de una manera más natural e intuitiva al centrarse en las relaciones entre los objetos en lugar de los objetos mismos.


Siguiendo con los ejemplos anteriores, un agregado podría representarse como un cliente:


 import uuid from pydantic import BaseModel, Field from practical_ddd.building_blocks.entities import Person, Order from practical_ddd.building_blocks.value_objects import Address class Customer(BaseModel):  """Customer aggregate.  Manages personal information as well as orders.  """  id: uuid.UUID = Field(default_factory=uuid.uuid4)  person: Person  orders: list[Order] = Field(default_factory=list)  def change_address(self, new_address: Address) -> None:    self.person.address = new_address  def add_order(self, order: Order) -> None:    if self.total_value + order.value > 10000:      raise ValueError("Order cannot have value higher than 10000")    self.orders.append(order)  def remove_order(self, order_id: uuid.UUID) -> None:    order = next((order for order in self.orders if order.id == order_id), None)    if order is None:      raise IndexError("Order not found")    self.orders.remove(order)  @property  def total_value(self) -> float:    return sum(order.value for order in self.orders)


El cliente está directamente vinculado a los datos personales y almacena todos los pedidos. Además de eso, el agregado expone una interfaz para administrar la dirección de la persona, así como para agregar y eliminar pedidos. Esto se debe a que el estado del agregado solo se puede cambiar ejecutando los métodos correspondientes.


Si bien el ejemplo anterior es relativamente sencillo, con solo una restricción (el valor del pedido no puede ser superior a 10000), debe demostrar el uso de los componentes básicos de DDD y sus relaciones. En los sistemas reales, los agregados suelen ser más complejos, con más restricciones, límites y posiblemente más relaciones. Después de todo, su propia existencia es para gestionar esta complejidad. Además, en el mundo real, los agregados normalmente persistirían en un almacén de datos, como una base de datos. Aquí es donde entra en juego el patrón del repositorio .

Repositorio

Todos los cambios de estado del agregado deben comprometerse transaccionalmente en una sola operación atómica. Sin embargo, no es responsabilidad del agregado "perseverar". El patrón de repositorio permite abstraer los detalles del almacenamiento y la recuperación de datos y, en su lugar, trabajar con agregados en un nivel más alto de abstracción. En pocas palabras, un repositorio se puede considerar como una capa entre el agregado y el almacenamiento de datos. Un archivo JSON es un ejemplo bastante simple de tal almacenamiento. El agregado del cliente podría tener un repositorio que funcione con archivos JSON:


 import json import uuid from practical_ddd.building_blocks.aggregates import Customer class CustomerJSONRepository:  """Customer repository operating on JSON files."""  def __init__(self, path: str) -> None:    self.path = path  def get(self, customer_id: uuid.UUID) -> Customer:    with open(self.path, "r") as file:      database = json.load(file)      customer = database["customers"].get(str(customer_id))      if customer is None:        raise IndexError("Customer not found")      person = database["persons"][str(customer["person"])]      orders = [database["orders"][order_id] for order_id in customer["orders"]]    return Customer(      id=customer["id"],      person=person,      orders=orders,    )  def save(self, customer: Customer) -> None:    with open(self.path, "r+") as file:      database = json.load(file)      # Save customer      database["customers"][str(customer.id)] = {        "id": customer.id,        "person": customer.person.id,        "orders": [o.id for o in customer.orders],      }      # Save person      database["persons"][str(customer.person.id)] = customer.person.dict()      # Save orders      for order in customer.orders:        database["orders"][str(order.id)] = order.dict()      file.seek(0)      json.dump(database, file, indent=4, default=str)


Por supuesto, esta clase podría (y tal vez debería) hacer mucho más, pero no pretende ser un ORM perfecto y multifuncional. Debería dar una idea sobre las responsabilidades del repositorio, que en este caso son el almacenamiento y la recuperación del agregado del cliente en el archivo JSON . También vale la pena señalar cómo el repositorio maneja las entidades asociadas con el agregado. Debido a que los datos personales y los pedidos están estrechamente vinculados al ciclo de vida del cliente, deben administrarse con precisión cuando se procesa el agregado.

Servicio de dominio

Otro caso a considerar es cuando existe una lógica de negocios que simplemente no encaja en el agregado o en cualquiera de sus entidades u objetos de valor. Podría ser una lógica que dependa de múltiples agregados o del estado del almacén de datos. En tales casos, una estructura conocida como Servicio de Dominio puede resultar útil. El Servicio de dominio debe poder administrar agregados, por ejemplo, mediante el uso del repositorio, y luego puede almacenar la lógica del dominio que no pertenece al agregado. Por ejemplo, un cliente puede requerir lógica para evitar perder demasiados pedidos:


 import uuid from typing import Protocol from practical_ddd.building_blocks.aggregates import Customer class CustomerRepository(Protocol):  """Customer repository interface."""  def get(self, customer_id: uuid.UUID) -> Customer:    ...  def save(self, customer: Customer) -> None:    ... class CustomerService:  """Customer service."""  def __init__(self, repository: CustomerRepository) -> None:    self.repository = repository  def get_customer(self, customer_id: uuid.UUID) -> Customer | None:    try:      return self.repository.get(customer_id)    except IndexError:      return None  def save_customer(self, customer: Customer) -> None:    existing_customer = self.get_customer(customer.id)    # If customer is already in the database and has more than 2 orders,    # he cannot end up with half of them after a single save.    if (      existing_customer is not None      and len(existing_customer.orders) > 2      and len(customer.orders) < (len(existing_customer.orders) / 2)    ):      raise ValueError(        "Customer cannot lose more than half of his orders upon single save!"      )    self.repository.save(customer)


Agregado no puede asegurar cómo su estado difiere del del archivo JSON porque, en primer lugar, no tiene conocimiento del archivo JSON. Es por eso que la lógica de comparación debe estar incluida en el Servicio de Dominio. También es importante tener en cuenta que el servicio de dominio debería funcionar con la abstracción del repositorio. Esto simplifica el intercambio de la implementación concreta con una alternativa mediante la inserción de dependencias.

Poniendolo todo junto

Ahora que se han cubierto todas las piezas, ahora se pueden ver como un programa de trabajo:


 import uuid from practical_ddd.building_blocks import aggregates, entities, value_objects from practical_ddd.database.repository import CustomerJSONRepository from practical_ddd.service import CustomerService # Initialize domain service with json repository srv = CustomerService(repository=CustomerJSONRepository("test.json")) # Create a new customer customer = aggregates.Customer(  person=entities.Person(    first_name="Peter",    last_name="Tobias",    address=value_objects.Address(      country="Germany",      city="Berlin",      street="Postdamer Platz",      house_number="2/3",    ),  ), ) srv.save_customer(customer) # Add orders to existing customer customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.add_order(entities.Order(description="Order 1", value=10)) customer.add_order(entities.Order(description="Order 2", value=210)) customer.add_order(entities.Order(description="Order 3", value=3210)) srv.save_customer(customer) # Remove orders from existing customer # If there are only 3 orders, it's gonna fail customer = srv.get_customer(uuid.UUID("a32dd73a-6c1b-4581-b1d3-2a1247320938")) assert customer is not None customer.remove_order(uuid.UUID("0f3c0a7f-67fd-4309-8ca2-d007ac003b69")) customer.remove_order(uuid.UUID("a4fd7648-4ea3-414a-a344-56082e00d2f9")) srv.save_customer(customer)


Todo tiene sus responsabilidades y límites. Agregado está a cargo de administrar sus entidades y objetos de valor, así como de hacer cumplir sus restricciones. Domain Service usa el repositorio JSON inyectado para conservar los datos en el archivo JSON y aplicar límites de dominio adicionales. Al final, cada componente tiene una función y un significado distintos dentro del dominio especificado.

Resumen

Domain-Driven Design es, sin duda, una idea compleja de entender. Proporciona prácticas, patrones y herramientas para ayudar a los equipos de software a abordar los problemas comerciales más desafiantes al poner un fuerte énfasis en el dominio comercial. Sin embargo, DDD es más que un conjunto de bloques de construcción. Es una mentalidad que requiere colaboración y comunicación entre las partes interesadas técnicas y comerciales. Una comprensión compartida del dominio, expresada a través de un lenguaje ubicuo, es fundamental para el éxito de un proyecto DDD. Cuando se hace bien, DDD puede conducir a un software mejor alineado con las necesidades del negocio y más efectivo para resolver problemas complejos.

Epílogo y los próximos pasos

Este artículo nunca tuvo la intención de ser algo como "DDD: From Zero To Hero", sino más bien servir como una introducción al universo DDD. Quería demostrar los conceptos más importantes del diseño basado en dominios de una manera muy sencilla y práctica. Creo que aprender diseño basado en dominios es una excelente manera de aumentar la experiencia en programación. Sin embargo, no lo escuchas con demasiada frecuencia, al menos no tanto como "11 consejos y trucos de JavaScript LOCOS: un hilo 🧵".


En cualquier caso, si encuentra algo de esto interesante, puede buscar en la sección de fuentes los libros y artículos que me inspiraron a escribir este artículo en primer lugar. Hay algunos conceptos que no cubrí porque pensé que estaban más allá del alcance de esta introducción, pero vale la pena investigarlos:


  • Contextos acotados
  • Eventos de dominio
  • Tormenta de eventos


Sin duda, los encontrará en las fuentes que se enumeran a continuación.

Fuentes

Los ejemplos de código utilizados en el artículo se pueden encontrar aquí: enlace .


Aprendiendo Diseño Dirigido por Dominio por Vlad Khononov. Un libro increíble que sirvió como una gran fuente de inspiración para mí. Explica todos los conceptos discutidos en este artículo con mayor profundidad.


Patrones de arquitectura en Python por Harry Percival y Bob Gregory. Leí el libro hace casi dos años y tuvo un impacto significativo en mí como desarrollador. Volví a él mientras escribía este artículo, y me ayudó una vez más.


DDD en Python por Przemysław Górecki. Descubrí este blog casi al final de escribir el artículo, pero despertó mi interés por lo increíblemente profesional que es. Dato curioso: trabajé en la misma empresa que Przemysław y no lo sabía por completo.


También publicado aquí .