Capítulo 1. Modelado del dominio

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Este capítulo examina cómo podemos modelar los procesos empresariales con código, de un modo que sea altamente compatible con TDD. Discutiremos por qué es importante el modelado de dominios, y veremos algunos patrones clave para modelar dominios: Entidad, Objeto Valor y Servicio de Dominio.

La Figura 1-1 es un simple marcador de posición visual para nuestro patrón de Modelo de Dominio. Completaremos algunos detalles en este capítulo, y a medida que avancemos en otros capítulos, construiremos cosas alrededor del modelo de dominio, pero siempre deberías poder encontrar estas pequeñas formas en el núcleo.

apwp 0101
Figura 1-1. Ilustración de nuestro modelo de dominio

¿Qué es un modelo de dominio?

En la introducción, hemos utilizado el término capa lógica empresarial para describir la capa central de una arquitectura de tres capas. En el resto del libro, utilizaremos el término modelo de dominio. Se trata de un término de la comunidad DDD que capta mejor el significado que pretendemos (para más información sobre DDD, consulta la siguiente barra lateral).

El dominio es una forma elegante de decir el problema que intentas resolver. Tus autores trabajan actualmente para un minorista online de muebles. Dependiendo del sistema del que estés hablando, el dominio podría ser la compra y el aprovisionamiento, o el diseño del producto, o la logística y la entrega. La mayoría de los programadores se pasan el día intentando mejorar o automatizar procesos empresariales; el dominio es el conjunto de actividades que soportan esos procesos.

Un modelo es un mapa de un proceso o fenómeno que capta una propiedad útil. Los humanos somos excepcionalmente buenos elaborando modelos de las cosas en nuestra cabeza. Por ejemplo, cuando alguien lanza una pelota hacia ti, eres capaz de predecir su movimiento casi inconscientemente, porque tienes un modelo de la forma en que los objetos se mueven en el espacio. Tu modelo no es perfecto ni mucho menos. Los humanos tenemos intuiciones terribles sobre cómo se comportan los objetos a velocidades cercanas a la de la luz o en el vacío, porque nuestro modelo nunca se diseñó para cubrir esos casos. Eso no significa que el modelo sea erróneo, pero sí que algunas predicciones quedan fuera de su dominio.

El modelo de dominio es el mapa mental que los empresarios tienen de sus empresas. Todos los empresarios tienen estos mapas mentales: son la forma en que los humanos piensan sobre los procesos complejos.

Te das cuenta cuando navegan por estos mapas porque utilizan jerga empresarial. La jerga surge de forma natural entre personas que colaboran en sistemas complejos.

Imagina que tú, nuestro desafortunado lector, fueras transportado de repente a años luz de la Tierra a bordo de una nave extraterrestre con tus amigos y familiares y tuvieras que averiguar, a partir de los primeros principios, cómo volver a casa.

Los primeros días os limitabais a pulsar botones al azar, pero pronto aprendisteis qué botones hacían qué cosas, de modo que podíais daros instrucciones unos a otros: "Pulsa el botón rojo que está cerca de la chuchería parpadeante y luego acciona esa palanca grande que está junto al aparato del radar", os decíais.

Al cabo de un par de semanas, te habías vuelto más preciso al adoptar palabras para describir las funciones de la nave: "Aumentar los niveles de oxígeno en el hangar de carga tres" o "encender los pequeños propulsores". Al cabo de unos meses, habrías adoptado un lenguaje para procesos complejos completos: "Iniciar secuencia de aterrizaje" o "prepararse para warp". Este proceso se produciría de forma bastante natural, sin ningún esfuerzo formal para construir un glosario compartido.

Lo mismo ocurre en el mundo mundano de los negocios. La terminología utilizada por las partes interesadas de la empresa representa una comprensión destilada del modelo de dominio, donde las ideas y procesos complejos se reducen a una sola palabra o frase.

Cuando oigamos a nuestros interlocutores empresariales utilizar palabras desconocidas, o emplear términos de una forma específica, debemos escuchar para comprender el significado más profundo y codificar en nuestro software la experiencia que tanto les ha costado adquirir.

Vamos a utilizar un modelo de dominio del mundo real a lo largo de este libro, concretamente un modelo de nuestro empleo actual. MADE.com es un exitoso minorista de muebles. Adquirimos nuestros muebles a fabricantes de todo el mundo y los vendemos en toda Europa.

Cuando compras un sofá o una mesa de centro, tenemos que averiguar cuál es la mejor manera de transportar tu mercancía desde Polonia, China o Vietnam hasta tu salón.

A alto nivel, tenemos sistemas separados que se encargan de comprar existencias, vender existencias a los clientes y enviar mercancías a los clientes. Un sistema intermedio debe coordinar el proceso asignando las existencias a los pedidos de los clientes; véase la Figura 1-2.

apwp 0102
Figura 1-2. Diagrama de contexto del servicio de asignación
[plantuml, apwp_0102]
@startuml Allocation Context Diagram
!include images/C4_Context.puml

System(systema, "Allocation", "Allocates stock to customer orders")

Person(customer, "Customer", "Wants to buy furniture")
Person(buyer, "Buying Team", "Needs to purchase furniture from suppliers")

System(procurement, "Purchasing", "Manages workflow for buying stock from suppliers")
System(ecom, "E-commerce", "Sells goods online")
System(warehouse, "Warehouse", "Manages workflow for shipping goods to customers.")

Rel(buyer, procurement, "Uses")
Rel(procurement, systema, "Notifies about shipments")
Rel(customer, ecom, "Buys from")
Rel(ecom, systema, "Asks for stock levels")
Rel(ecom, systema, "Notifies about orders")
Rel_R(systema, warehouse, "Sends instructions to")
Rel_U(warehouse, customer, "Dispatches goods to")

@enduml

Para los fines de este libro, imaginamos que la empresa decide implantar una nueva e interesante forma de asignar las existencias. Hasta ahora, la empresa ha estado presentando las existencias y los plazos de entrega basándose en lo que está físicamente disponible en el almacén. Cuando el almacén se queda sin existencias, un producto aparece como "agotado" hasta que llega el siguiente envío del fabricante.

He aquí la innovación: si disponemos de un sistema que pueda hacer un seguimiento de todos nuestros envíos y de cuándo deben llegar, podemos tratar las mercancías de esos barcos como existencias reales y parte de nuestro inventario, sólo que con plazos de entrega ligeramente más largos. Aparecerán menos mercancías agotadas, venderemos más y la empresa podrá ahorrar dinero manteniendo menos existencias en el almacén nacional.

Pero asignar pedidos ya no es una cuestión trivial de decrementar una sola cantidad en el sistema de almacén. Necesitamos un mecanismo de asignación más complejo. Es hora de modelar el dominio.

Explorar el lenguaje del dominio

Comprender el modelo de dominio lleva tiempo, y paciencia, y notas Post-it. Mantenemos una conversación inicial con nuestros expertos empresariales y acordamos un glosario y algunas reglas para la primera versión mínima del modelo de dominio. Siempre que sea posible, pedimos ejemplos concretos para ilustrar cada regla.

Nos aseguramos de expresar esas reglas en la jerga empresarial (el lenguaje omnipresente en la terminología DDD). Elegimos identificadores memorables para nuestros objetos, de modo que sea más fácil hablar de los ejemplos.

"Algunas notas sobre la asignación" muestra algunas notas que podríamos haber tomado mientras manteníamos una conversación con nuestros expertos en la materia sobre la asignación.

Pruebas unitarias de modelos de dominio

No vamos a enseñarte cómo funciona TDD en este libro, pero queremos mostrarte cómo construiríamos un modelo a partir de esta conversación empresarial.

He aquí cómo podría ser una de nuestras primeras pruebas:

Una primera prueba de asignación (prueba_lotes.py)

def test_allocating_to_a_batch_reduces_the_available_quantity():
    batch = Batch("batch-001", "SMALL-TABLE", qty=20, eta=date.today())
    line = OrderLine('order-ref', "SMALL-TABLE", 2)

    batch.allocate(line)

    assert batch.available_quantity == 18

El nombre de nuestra prueba unitaria describe el comportamiento que queremos ver del sistema, y los nombres de las clases y variables que utilizamos están tomados de la jerga empresarial. Podríamos mostrar este código a nuestros compañeros de trabajo no técnicos, y estarían de acuerdo en que describe correctamente el comportamiento del sistema.

Y aquí tienes un modelo de dominio que cumple nuestros requisitos:

Primer corte de un modelo de dominio para lotes (model.py)

@dataclass(frozen=True)  12
class OrderLine:
    orderid: str
    sku: str
    qty: int


class Batch:
    def __init__(
        self, ref: str, sku: str, qty: int, eta: Optional[date]  2
    ):
        self.reference = ref
        self.sku = sku
        self.eta = eta
        self.available_quantity = qty

    def allocate(self, line: OrderLine):
        self.available_quantity -= line.qty  3
1

OrderLine es una clase de datos inmutable sin comportamiento.2

2

No mostramos las importaciones en la mayoría de los listados de código, en un intento de mantenerlos limpios. Esperamos que puedas adivinar que esto vino a través de from dataclasses import dataclass; del mismo modo,typing.Optional y datetime.date. Si quieres volver a comprobar algo, puedes ver el código de trabajo completo de cada capítulo en su rama (por ejemplo,chapter_01_domain_model).

3

Las sugerencias de tipo siguen siendo objeto de controversia en el mundo de Python. En el caso de los modelos de dominio, a veces pueden ayudar a aclarar o documentar cuáles son los argumentos esperados, y la gente con IDEs suele agradecerlas. Puede que decidas que el precio pagado en términos de legibilidad es demasiado alto.

Nuestra implementación aquí es trivial: un Batch simplemente envuelve un enteroavailable_quantity, y decrementamos ese valor en la asignación. Hemos escrito bastante código sólo para restar un número de otro, pero creemos que modelar nuestro dominio con precisión valdrá la pena.3

Escribamos nuevas pruebas que fallen:

Lógica de prueba para lo que podemos asignar (test_batches.py)

def make_batch_and_line(sku, batch_qty, line_qty):
    return (
        Batch("batch-001", sku, batch_qty, eta=date.today()),
        OrderLine("order-123", sku, line_qty)
    )


def test_can_allocate_if_available_greater_than_required():
    large_batch, small_line = make_batch_and_line("ELEGANT-LAMP", 20, 2)
    assert large_batch.can_allocate(small_line)

def test_cannot_allocate_if_available_smaller_than_required():
    small_batch, large_line = make_batch_and_line("ELEGANT-LAMP", 2, 20)
    assert small_batch.can_allocate(large_line) is False

def test_can_allocate_if_available_equal_to_required():
    batch, line = make_batch_and_line("ELEGANT-LAMP", 2, 2)
    assert batch.can_allocate(line)

def test_cannot_allocate_if_skus_do_not_match():
    batch = Batch("batch-001", "UNCOMFORTABLE-CHAIR", 100, eta=None)
    different_sku_line = OrderLine("order-123", "EXPENSIVE-TOASTER", 10)
    assert batch.can_allocate(different_sku_line) is False

Aquí no hay nada demasiado inesperado. Hemos refactorizado nuestro conjunto de pruebas para no seguir repitiendo las mismas líneas de código para crear un lote y una línea para el mismo SKU; y hemos escrito cuatro pruebas sencillas para un nuevo métodocan_allocate. De nuevo, fíjate en que los nombres que utilizamos reflejan el lenguaje de nuestros expertos en la materia, y los ejemplos que acordamos se escriben directamente en el código.

También podemos ponerlo en práctica directamente escribiendo el método can_allocatede Batch:

Un nuevo método en el modelo (model.py)

    def can_allocate(self, line: OrderLine) -> bool:
        return self.sku == line.sku and self.available_quantity >= line.qty

Hasta ahora, podemos gestionar la implementación simplemente incrementando y decrementandoBatch.available_quantity, pero a medida que nos adentremos en las pruebas de deallocate(), nos veremos obligados a una solución más inteligente:

Esta prueba va a requerir un modelo más inteligente (test_batches.py)

def test_can_only_deallocate_allocated_lines():
    batch, unallocated_line = make_batch_and_line("DECORATIVE-TRINKET", 20, 2)
    batch.deallocate(unallocated_line)
    assert batch.available_quantity == 20

En esta prueba, estamos afirmando que la desasignación de una línea de un lote no tiene ningún efecto a menos que el lote haya asignado previamente la línea. Para que esto funcione, nuestro Batchnecesita saber qué líneas se han asignado. Veamos la implementación:

El modelo de dominio rastrea ahora las asignaciones (model.py)

class Batch:
    def __init__(
        self, ref: str, sku: str, qty: int, eta: Optional[date]
    ):
        self.reference = ref
        self.sku = sku
        self.eta = eta
        self._purchased_quantity = qty
        self._allocations = set()  # type: Set[OrderLine]

    def allocate(self, line: OrderLine):
        if self.can_allocate(line):
            self._allocations.add(line)

    def deallocate(self, line: OrderLine):
        if line in self._allocations:
            self._allocations.remove(line)

    @property
    def allocated_quantity(self) -> int:
        return sum(line.qty for line in self._allocations)

    @property
    def available_quantity(self) -> int:
        return self._purchased_quantity - self.allocated_quantity

    def can_allocate(self, line: OrderLine) -> bool:
        return self.sku == line.sku and self.available_quantity >= line.qty

La Figura 1-3 muestra el modelo en UML.

apwp 0103
Figura 1-3. Nuestro modelo en UML
[plantuml, apwp_0103, config=plantuml.cfg]

left to right direction
hide empty members

class Batch {
    reference
    sku
    eta
    _purchased_quantity
    _allocations
}

class OrderLine {
    orderid
    sku
    qty
}

Batch::_allocations o-- OrderLine

¡Ahora estamos llegando a alguna parte! Ahora un lote lleva la cuenta de un conjunto de objetosOrderLine asignados. Cuando asignamos, si tenemos suficiente cantidad disponible, simplemente añadimos al conjunto. Nuestro available_quantity es ahora una propiedad calculada: cantidad comprada menos cantidad asignada.

Sí, podríamos hacer mucho más. Es un poco desconcertante que tanto allocate() como deallocate() puedan fallar silenciosamente, pero tenemos lo básico.

Por cierto, utilizar un conjunto para ._allocations nos facilita la última prueba, porque los elementos de un conjunto son únicos:

¡Última prueba de lotes! (prueba_lotes.py)

def test_allocation_is_idempotent():
    batch, line = make_batch_and_line("ANGULAR-DESK", 20, 2)
    batch.allocate(line)
    batch.allocate(line)
    assert batch.available_quantity == 18

En este momento, probablemente sea una crítica válida decir que el modelo de dominio es demasiado trivial para molestarse con DDD (¡o incluso con la orientación a objetos!). En la vida real, surgen numerosas reglas de negocio y casos de perímetro: los clientes pueden pedir la entrega en fechas futuras concretas, lo que significa que quizá no queramos asignarlos al lote más antiguo. Algunas SKU no están en lotes, sino que se piden bajo demanda directamente a los proveedores, por lo que tienen una lógica diferente. Dependiendo de la ubicación del cliente, podemos asignar sólo a un subconjunto de almacenes y envíos que estén en su región -excepto en el caso de algunas SKU que nos complace entregar desde un almacén en una región diferente si no tenemos existencias en la región de origen-. Y así sucesivamente. ¡Una empresa real en el mundo real sabe cómo acumular complejidad más rápido de lo que podemos mostrar en la página!

Pero tomando este sencillo modelo de dominio como marcador de posición para algo más complejo, vamos a ampliar nuestro sencillo modelo de dominio en el resto del libro y enchufarlo al mundo real de las API y las bases de datos y las hojas de cálculo. Veremos cómo ceñirnos rígidamente a nuestros principios de encapsulación y estratificación cuidadosa nos ayudará a evitar una bola de barro.

Las clases de datos son geniales para los objetos de valor

Hemos utilizado libremente line en los listados de códigos anteriores, pero ¿qué es una línea? En nuestro lenguaje empresarial, un pedido tiene varias líneas, donde cada línea tiene un SKU y una cantidad. Podemos imaginar que un simple archivo YAML que contenga información sobre un pedido podría tener este aspecto:

Información del pedido como YAML

Order_reference: 12345
Lines:
  - sku: RED-CHAIR
    qty: 25
  - sku: BLU-CHAIR
    qty: 25
  - sku: GRN-CHAIR
    qty: 25

Observa que mientras una orden tiene una referencia que la identifica unívocamente, unalínea no. (Aunque añadamos la referencia de la orden a la clase OrderLine, no es algo que identifique de forma única a la propia línea).

Siempre que tenemos un concepto de negocio que tiene datos pero no identidad, solemos optar por representarlo utilizando el patrón Objeto Valor. Un objeto valor es cualquier objeto del dominio que se identifica de forma única por los datos que contiene; normalmente los hacemos inmutables:

OrderLine es un objeto de valor

@dataclass(frozen=True)
class OrderLine:
    orderid: OrderReference
    sku: ProductReference
    qty: Quantity

Una de las cosas buenas que nos proporcionan las clases de datos (o namedtuples) es la igualdad de valores, que es la forma elegante de decir: "Dos líneas con los mismos orderid,sku, y qty son iguales".

Más ejemplos de objetos de valor

from dataclasses import dataclass
from typing import NamedTuple
from collections import namedtuple

@dataclass(frozen=True)
class Name:
    first_name: str
    surname: str

class Money(NamedTuple):
    currency: str
    value: int

Line = namedtuple('Line', ['sku', 'qty'])

def test_equality():
    assert Money('gbp', 10) == Money('gbp', 10)
    assert Name('Harry', 'Percival') != Name('Bob', 'Gregory')
    assert Line('RED-CHAIR', 5) == Line('RED-CHAIR', 5)

Estos objetos de valor coinciden con nuestra intuición del mundo real sobre cómo funcionan sus valores. No importa de qué billete de 10€ estemos hablando, porque todos tienen el mismo valor. Del mismo modo, dos nombres son iguales si coinciden el nombre y los apellidos; y dos líneas son equivalentes si tienen el mismo pedido de cliente, código de producto y cantidad. Sin embargo, podemos tener un comportamiento complejo en un objeto valor. De hecho, es habitual admitir operaciones sobre valores; por ejemplo, operadores matemáticos:

Matemáticas con objetos de valor

fiver = Money('gbp', 5)
tenner = Money('gbp', 10)

def can_add_money_values_for_the_same_currency():
    assert fiver + fiver == tenner

def can_subtract_money_values():
    assert tenner - fiver == fiver

def adding_different_currencies_fails():
    with pytest.raises(ValueError):
        Money('usd', 10) + Money('gbp', 10)

def can_multiply_money_by_a_number():
    assert fiver * 5 == Money('gbp', 25)

def multiplying_two_money_values_is_an_error():
    with pytest.raises(TypeError):
        tenner * fiver

Objetos y entidades de valor

Una línea de pedido se identifica unívocamente por su ID de pedido, SKU y cantidad; si cambiamos uno de esos valores, ahora tenemos una nueva línea. Ésa es la definición de un objeto valor: cualquier objeto que se identifica sólo por sus datos y no tiene una identidad duradera. Pero, ¿qué ocurre con un lote? Ése se identifica por una referencia.

Utilizamos el término entidad para describir un objeto de dominio que tiene identidad duradera. En la página anterior, introdujimos una clase Name como objeto de valor. Si tomamos el nombre Harry Percival y le cambiamos una letra, tenemos el nuevo objetoName Barry Percival.

Debe quedar claro que Harry Percival no es igual a Barry Percival:

Un nombre en sí mismo no puede cambiar...

def test_name_equality():
    assert Name("Harry", "Percival") != Name("Barry", "Percival")

¿Pero qué pasa con Harry como persona? Las personas cambian de nombre, de estado civil e incluso de sexo, pero seguimos reconociéndolas como el mismo individuo. Eso es porque los seres humanos, a diferencia de los nombres, tienen unaidentidad persistente:

¡Pero una persona puede!

class Person:

    def __init__(self, name: Name):
        self.name = name


def test_barry_is_harry():
    harry = Person(Name("Harry", "Percival"))
    barry = harry

    barry.name = Name("Barry", "Percival")

    assert harry is barry and barry is harry

Las entidades, a diferencia de los valores, tienen igualdad de identidad. Podemos cambiar sus valores, y seguirán siendo reconociblemente la misma cosa. Los lotes, en nuestro ejemplo, son entidades. Podemos asignar líneas a un lote, o cambiar la fecha en que esperamos que llegue, y seguirá siendo la misma entidad.

Normalmente lo hacemos explícito en el código implementando operadores de igualdad en las entidades:

Implementar operadores de igualdad (model.py)

class Batch:
    ...

    def __eq__(self, other):
        if not isinstance(other, Batch):
            return False
        return other.reference == self.reference

    def __hash__(self):
        return hash(self.reference)

El método mágico __eq__ de Python define el comportamiento de la clase para el operador ==.5

Tanto para los objetos entidad como para los objetos valor, también merece la pena pensar cómo funcionará__hash__. Es el método mágico que utiliza Python para controlar el comportamiento de los objetos cuando los añades a conjuntos o los utilizas como claves dict; puedes encontrar más información en los documentos de Python.

Para los objetos de valor, el hash debe basarse en todos los atributos de valor, y debemos asegurarnos de que los objetos son inmutables. Esto lo conseguimos gratuitamente especificando @frozen=True en la clase de datos.

Para las entidades, la opción más sencilla es decir que el hash es None, lo que significa que el objeto no es hashable y no puede, por ejemplo, utilizarse en un conjunto. Si por alguna razón decides que realmente quieres utilizar operaciones de conjunto o dict con entidades, el hash debe basarse en el atributo o atributos, como.reference, que definen la identidad única de la entidad a lo largo del tiempo. También debes intentar que ese atributo sea de sólo lectura.

Advertencia

Este es un terreno delicado; no debes modificar __hash__ sin modificar también __eq__. Si no estás seguro de lo que haces, te sugerimos que sigas leyendo. "Python Hashes and Equality", de nuestro revisor técnico Hynek Schlawack, es un buen punto de partida.

No todo tiene que ser un objeto: Una Función de Servicio de Dominio

Hemos creado un modelo para representar lotes, pero lo que en realidad necesitamos es asignar líneas de pedido a un conjunto concreto de lotes que representen todas nuestras existencias.

A veces, simplemente no hay nada que hacer.

Eric Evans, Diseño Orientado al Dominio

Evans discute la idea de las operaciones del Servicio de Dominio que no tienen un hogar natural en una entidad u objeto de valor.6 Una cosa que asigna una línea de pedido, dado un conjunto de lotes, se parece mucho a una función, y podemos aprovechar que Python es un lenguaje multiparadigma y convertirla simplemente en una función.

Veamos cómo podríamos probar una función de este tipo:

Probar nuestro servicio de dominio (test_allocate.py)

def test_prefers_current_stock_batches_to_shipments():
    in_stock_batch = Batch("in-stock-batch", "RETRO-CLOCK", 100, eta=None)
    shipment_batch = Batch("shipment-batch", "RETRO-CLOCK", 100, eta=tomorrow)
    line = OrderLine("oref", "RETRO-CLOCK", 10)

    allocate(line, [in_stock_batch, shipment_batch])

    assert in_stock_batch.available_quantity == 90
    assert shipment_batch.available_quantity == 100


def test_prefers_earlier_batches():
    earliest = Batch("speedy-batch", "MINIMALIST-SPOON", 100, eta=today)
    medium = Batch("normal-batch", "MINIMALIST-SPOON", 100, eta=tomorrow)
    latest = Batch("slow-batch", "MINIMALIST-SPOON", 100, eta=later)
    line = OrderLine("order1", "MINIMALIST-SPOON", 10)

    allocate(line, [medium, earliest, latest])

    assert earliest.available_quantity == 90
    assert medium.available_quantity == 100
    assert latest.available_quantity == 100


def test_returns_allocated_batch_ref():
    in_stock_batch = Batch("in-stock-batch-ref", "HIGHBROW-POSTER", 100, eta=None)
    shipment_batch = Batch("shipment-batch-ref", "HIGHBROW-POSTER", 100, eta=tomorrow)
    line = OrderLine("oref", "HIGHBROW-POSTER", 10)
    allocation = allocate(line, [in_stock_batch, shipment_batch])
    assert allocation == in_stock_batch.reference

Y nuestro servicio podría tener este aspecto:

Una función independiente para nuestro servicio de dominio (model.py)

def allocate(line: OrderLine, batches: List[Batch]) -> str:
    batch = next(
        b for b in sorted(batches) if b.can_allocate(line)
    )
    batch.allocate(line)
    return batch.reference

Los métodos mágicos de Python nos permiten utilizar nuestros modelos con Python idiomático

Te puede gustar o no el uso de next() en el código anterior, pero estamos seguros de que estarás de acuerdo en que poder utilizar sorted() en nuestra lista de lotes es un bonito e idiomático Python.

Para que funcione, implementamos __gt__ en nuestro modelo de dominio:

Los métodos mágicos pueden expresar la semántica del dominio (model.py)

class Batch:
    ...

    def __gt__(self, other):
        if self.eta is None:
            return False
        if other.eta is None:
            return True
        return self.eta > other.eta

Qué bonito.

Las excepciones también pueden expresar conceptos de dominio

Nos queda un último concepto por cubrir: las excepciones también pueden utilizarse en para expresar conceptos de dominio. En nuestras conversaciones con expertos en el dominio, nos hemos enterado de la posibilidad de que no se pueda asignar un pedido porque nos hemos quedado sin existencias, y podemos capturarlo utilizando una excepción de dominio:

Probar la excepción de falta de existencias (test_allocate.py)

def test_raises_out_of_stock_exception_if_cannot_allocate():
    batch = Batch('batch1', 'SMALL-FORK', 10, eta=today)
    allocate(OrderLine('order1', 'SMALL-FORK', 10), [batch])

    with pytest.raises(OutOfStock, match='SMALL-FORK'):
        allocate(OrderLine('order2', 'SMALL-FORK', 1), [batch])

No te aburriremos demasiado con la implementación, pero lo principal es que tengamos cuidado al nombrar nuestras excepciones en el lenguaje ubicuo, igual que hacemos con nuestras entidades, objetos de valor y servicios:

Lanzar una excepción de dominio (model.py)

class OutOfStock(Exception):
    pass


def allocate(line: OrderLine, batches: List[Batch]) -> str:
    try:
        batch = next(
        ...
    except StopIteration:
        raise OutOfStock(f'Out of stock for sku {line.sku}')

La Figura 1-4 es una representación visual de dónde hemos llegado.

apwp 0104
Figura 1-4. Nuestro modelo de dominio al final del capítulo

¡Eso probablemente bastará por ahora! Tenemos un servicio de dominio que podemos utilizar para nuestro primer caso de uso. Pero primero necesitaremos una base de datos...

1 El DDD no originó el modelado de dominios. Eric Evans se refiere al libro de 2002 Object Design de Rebecca Wirfs-Brock y Alan McKean (Addison-Wesley Professional), que introdujo el diseño orientado a la responsabilidad, del que el DDD es un caso especial que trata del dominio. Pero incluso eso es demasiado tarde, y los entusiastas de la OO te dirán que mires más atrás, a Ivar Jacobson y Grady Booch; el término existe desde mediados de los años 80.

2 En versiones anteriores de Python, podríamos haber utilizado una namedtuple. También puedes consultar el excelente attrs de Hynek Schlawack.

3 ¿O tal vez crees que no hay suficiente código? ¿Qué tal algún tipo de comprobación de que el SKU en OrderLine coincide con Batch.sku? Hemos guardado algunas ideas sobre la validación para el Apéndice E.

4 Es espantoso. Por favor, por favor, no lo hagas. -Harry

5 El método __eq__ se pronuncia "dunder-EQ". Al menos por algunos.

6 Los servicios de dominio no son lo mismo que los servicios de la capa de servicios, aunque a menudo están estrechamente relacionados. Un servicio de dominio representa un concepto o proceso empresarial, mientras que un servicio de la capa de servicio representa un caso de uso de tu aplicación. A menudo, la capa de servicio llamará a un servicio de dominio.

Get Patrones de Arquitectura con Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.