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.
¿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.
[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
)
class
OrderLine
:
orderid
:
str
sku
:
str
qty
:
int
class
Batch
:
def
__init__
(
self
,
ref
:
str
,
sku
:
str
,
qty
:
int
,
eta
:
Optional
[
date
]
)
:
self
.
reference
=
ref
self
.
sku
=
sku
self
.
eta
=
eta
self
.
available_quantity
=
qty
def
allocate
(
self
,
line
:
OrderLine
)
:
self
.
available_quantity
-
=
line
.
qty
OrderLine
es una clase de datos inmutable sin comportamiento.2No 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
ydatetime.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).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_allocate
de 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 Batch
necesita 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.
[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.
¡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.