1.architecture-Patterns-With-Python Spanish
1.architecture-Patterns-With-Python Spanish
Arquitectura
Patrones
con Python
Habilitación del desarrollo basado en pruebas,
Diseño basado en dominios y basado en eventos
microservicios
Harry JW Percival
y Bob Gregory
Machine Translated by Google
Machine Translated by Google
Copyright © 2020 Harry Percival y Bob Gregory. Reservados todos los derechos.
Publicado por O'Reilly Media, Inc., 1005 Gravenstein Highway North, Sebastopol, CA 95472.
Los libros de O'Reilly se pueden comprar con fines educativos, comerciales o de promoción de ventas. Las ediciones en línea también están
disponibles para la mayoría de los títulos (http://oreilly.com). Para obtener más información, comuníquese con nuestro departamento de ventas
corporativo/institucional: 800-998-9938 o corporate@oreilly.com.
El logotipo de O'Reilly es una marca comercial registrada de O'Reilly Media, Inc. Los patrones de arquitectura con Python, la imagen de portada
y la imagen comercial relacionada son marcas comerciales de O'Reilly Media, Inc.
Las opiniones expresadas en este trabajo son las de los autores y no representan las opiniones del editor.
Si bien el editor y los autores se han esforzado de buena fe para garantizar que la información y las instrucciones contenidas en este trabajo
sean precisas, el editor y los autores renuncian a toda responsabilidad por errores u omisiones, incluida, entre otras, la responsabilidad por
daños resultantes del uso o confianza en este trabajo. El uso de la información e instrucciones contenidas en este trabajo es bajo su propio
riesgo. Si alguna muestra de código u otra tecnología que este trabajo contiene o describe está sujeta a licencias de código abierto o a los
derechos de propiedad intelectual de otros, es su responsabilidad asegurarse de que su uso cumpla con dichas licencias y/o derechos.
978-1-492-05220-3
[LSI]
Machine Translated by Google
Tabla de contenido
Prefacio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ix
Introducción. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xvii
1. Modelado de dominio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
¿Qué es un modelo de dominio? 56
Exploración del dominio Lenguaje Unidad de
prueba Modelos de dominio Las clases de 9 10
Los métodos mágicos de Python Permítanos usar nuestros modelos con Python idiomático 20
2. Patrón de repositorio. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Persistiendo nuestro modelo de dominio 24
iii
Machine Translated by Google
Envolver 38
Envolver 53
FakeRepository para realizar pruebas unitarias Una función de servicio típica ¿Por qué todo 61
se denomina servicio? 63
66
Poner las cosas en carpetas para ver dónde pertenece todo Resumen 67
El DIP en acción 68
68
Envolver 79
trabajo colabora con el repositorio Realización de pruebas de una UoW con la unidad de trabajo de pruebas de 83
integración y su administrador de contexto La unidad de trabajo real utiliza SQLAlchemy Sessions Unidad de 84
trabajo falsa para las pruebas Uso de la UoW en la capa de servicio Pruebas explícitas para compromiso/ 85
Comportamiento de reversión 86
87
88
89
IV | Tabla de contenido
Machine Translated by Google
Ejemplos: uso de UoW para agrupar múltiples operaciones en una unidad atómica 91
Ejemplo 1: reasignar 91
Envolver 93
Elegir un agregado Un 99
Envolver 111
Opción 1: la capa de servicio toma eventos del modelo y los coloca en el bus de mensajes
124
Tabla de contenido | v
Machine Translated by Google
Envolver 130
Un feo truco temporal: el bus de mensajes tiene que devolver resultados 141
Implementación 145
Un nuevo método en el modelo de dominio 146
Resumen 149
¿Qué hemos logrado? 150
11. Arquitectura impulsada por eventos: uso de eventos para integrar microservicios. . . . . . . . . . . . . .
161 Bola de barro distribuida y pensamiento en sustantivos Manejo de errores en sistemas 162
distribuidos La alternativa: desacoplamiento temporal usando mensajería asíncrona usando un 165
canal Pub/Sub de Redis para la integración Probarlo todo usando una prueba de extremo a extremo
167
Redis es otro adaptador delgado Alrededor de nuestro autobús de mensajes Nuestro nuevo evento
168
saliente Eventos internos frente a eventos externos 169
170
171
172
Envolver 172
nosotros
| Tabla de contenido
Machine Translated by Google
Hacer una versión falsa para sus pruebas Averigüe cómo la integración 205
Probar lo real Resumen 206
206
207
209
Epílogo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
E. Validación. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 255
Índice. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 265
Prefacio
Al final del último libro de Harry, Test-Driven Development with Python (O'Reilly), se encontró
haciendo un montón de preguntas sobre arquitectura, como: ¿Cuál es la mejor manera de estructurar
su aplicación para que sea fácil de probar? Más específicamente, ¿de modo que su lógica
empresarial central esté cubierta por pruebas unitarias y para que minimice la cantidad de pruebas
de integración y de extremo a extremo que necesita? Hizo referencias vagas a "Arquitectura
hexagonal", "Puertos y adaptadores" y "Núcleo funcional, carcasa imperativa", pero si fuera honesto,
tendría que admitir que no eran cosas que realmente entendiera o que hubiera hecho. en la práctica.
Y luego tuvo la suerte de encontrarse con Bob, que tiene las respuestas a todas estas preguntas.
Bob terminó siendo un arquitecto de software porque nadie más en su equipo lo estaba haciendo.
Resultó ser bastante malo en eso, pero tuvo la suerte de encontrarse con Ian Cooper, quien le
enseñó nuevas formas de escribir y pensar en el código.
Ambos trabajamos para MADE.com, una empresa europea de comercio electrónico que vende
muebles en línea; allí, aplicamos las técnicas de este libro para construir sistemas distribuidos que
modelan problemas de negocios del mundo real. Nuestro dominio de ejemplo es el primer sistema
que Bob creó para MADE, y este libro es un intento de escribir todo lo que tenemos que enseñar a
los nuevos programadores cuando se unen a uno de nuestros equipos.
MADE.com opera una cadena de suministro global de socios y fabricantes de transporte. Para
mantener los costos bajos, tratamos de optimizar la entrega de existencias a nuestros almacenes
para que no tengamos productos sin vender tirados por el lugar.
Lo ideal es que el sofá que quieres comprar llegue a puerto el mismo día que decidas comprarlo, y
te lo enviamos directamente a tu casa sin tener que guardarlo nunca.
ix
Machine Translated by Google
Obtener el momento adecuado es un acto de equilibrio complicado cuando los productos tardan tres
meses en llegar por barco de contenedores. En el camino, las cosas se rompen o se dañan por el agua,
las tormentas causan retrasos inesperados, los socios logísticos manejan mal los productos, se pierde
papeleo, los clientes cambian de opinión y modifican sus pedidos, etc.
Resolvemos esos problemas mediante la creación de software inteligente que representa los tipos de
operaciones que tienen lugar en el mundo real para que podamos automatizar la mayor parte del negocio
posible.
Si está leyendo este libro, probablemente no necesitemos convencerlo de que Python es excelente, por
lo que la verdadera pregunta es "¿Por qué la comunidad de Python necesita un libro como este?" La
respuesta tiene que ver con la popularidad y la madurez de Python: aunque Python es probablemente el
lenguaje de programación de más rápido crecimiento en el mundo y se está acercando a la cima de las
tablas de popularidad absoluta, apenas está comenzando a asumir el tipo de problemas que ha tenido el
mundo de C# y Java. trabajando durante años. Las startups se convierten en negocios reales; Las
aplicaciones web y las automatizaciones con secuencias de comandos se están convirtiendo (susúrralo)
en software empresarial.
En el mundo de Python, a menudo citamos el Zen de Python: "Debe haber una, y preferiblemente solo
una, forma obvia de hacerlo". 1 Desafortunadamente, a medida que crece el tamaño del proyecto, la
forma más obvia de hacer las cosas no siempre es la forma que le ayuda a gestionar la complejidad y los
requisitos cambiantes.
Ninguna de las técnicas y patrones que discutimos en este libro son nuevos, pero en su mayoría son
nuevos en el mundo de Python. Y este libro no es un reemplazo para los clásicos en el campo, como el
Diseño impulsado por el dominio de Eric Evans o los Patrones de arquitectura de aplicaciones
empresariales de Martin Fowler (ambos publicados por Addison-Wesley Professional), a los que nos
referimos con frecuencia y lo alentamos a leer. y leer.
Pero todos los ejemplos de código clásico en la literatura tienden a estar escritos en Java o C++/#, y si
eres una persona de Python y no has usado ninguno de esos lenguajes en mucho tiempo (o nunca), ese
código las listas pueden ser bastante... difíciles. Hay una razón por la cual la última edición de ese otro
texto clásico, Refactorización de Fowler (Addison-Wesley Professional), está en JavaScript.
x | Prefacio
Machine Translated by Google
1. El desarrollo basado en pruebas (TDD) nos ayuda a crear un código correcto y nos permite refactorizar o
agregar nuevas funciones, sin temor a la regresión. Pero puede ser difícil obtener lo mejor de nuestras
pruebas: ¿Cómo nos aseguramos de que se ejecuten lo más rápido posible? ¿Que obtengamos tanta
cobertura y retroalimentación de pruebas unitarias rápidas y libres de dependencias y tengamos la cantidad
mínima de pruebas de extremo a extremo más lentas y escamosas?
2. El diseño basado en el dominio (DDD) nos pide que centremos nuestros esfuerzos en crear un buen modelo
del dominio comercial, pero ¿cómo nos aseguramos de que nuestros modelos no estén sobrecargados con
problemas de infraestructura y no se vuelvan difíciles de cambiar?
3. Los (micro)servicios débilmente acoplados integrados a través de mensajes (a veces llamados microservicios
reactivos) son una respuesta bien establecida para administrar la complejidad en múltiples aplicaciones o
dominios comerciales. Pero no siempre es obvio cómo hacer que encajen con las herramientas establecidas
del mundo de Python: Flask, Django, Celery, etc.
Nuestro objetivo con este libro es presentar varios patrones arquitectónicos clásicos y mostrar cómo admiten
TDD, DDD y servicios basados en eventos. Esperamos que sirva como referencia para implementarlos de forma
pitónica, y que la gente pueda usarlo como un primer paso para futuras investigaciones en este campo.
Aquí hay algunas cosas que asumimos sobre usted, querido lector:
algo del dolor que conlleva tratar de manejar esa complejidad. • No necesariamente sabes nada sobre DDD
Prefacio | xi
Machine Translated by Google
se siente un poco extraño al principio, pero esperamos que pronto se acostumbre a ver el código
"usado" (es decir, desde el exterior) antes de ver cómo está construido por dentro.
Utilizamos algunos marcos y tecnologías específicos de Python, incluidos Flask, SQL-Alchemy y pytest,
así como Docker y Redis. Si ya está familiarizado con ellos, eso no le hará daño, pero no creemos que
sea necesario. Uno de nuestros principales objetivos con este libro es construir una arquitectura para
la cual las opciones tecnológicas específicas se conviertan en detalles menores de implementación.
xi | Prefacio
Machine Translated by Google
Contenido adicional
Estás leyendo un libro, pero probablemente estarás de acuerdo con nosotros cuando decimos que la mejor
manera de aprender sobre código es programar. Aprendimos la mayor parte de lo que sabemos al trabajar en
equipo con personas, escribir código con ellas y aprender haciendo, y nos gustaría recrear esa experiencia
tanto como sea posible para usted en este libro.
Como resultado, hemos estructurado el libro en torno a un solo proyecto de ejemplo (aunque a veces
agregamos otros ejemplos). Construiremos este proyecto a medida que avancen los capítulos, como si se
hubiera asociado con nosotros y estuviéramos explicando lo que estamos haciendo y por qué en cada paso.
Pero para familiarizarse realmente con estos patrones, debe jugar con el código y tener una idea de cómo
funciona. Encontrarás todo el código en GitHub; cada capítulo tiene su propia rama. También puede encontrar
una lista de las sucursales en GitHub.
Prefacio | XIII
Machine Translated by Google
Aquí hay tres formas en que puede codificar junto con el libro:
• Inicie su propio repositorio e intente desarrollar la aplicación como lo hacemos nosotros, siguiendo los ejemplos
de las listas del libro y, de vez en cuando, buscando sugerencias en nuestro repositorio. Sin embargo, una
palabra de advertencia: si ha leído el libro anterior de Harry y ha codificado junto con eso, encontrará que
este libro requiere que descubra más por su cuenta; es posible que deba apoyarse bastante en las versiones
de trabajo en GitHub.
• Trate de aplicar cada patrón, capítulo por capítulo, a su propio proyecto (preferiblemente pequeño/juguete), y
vea si puede hacerlo funcionar para su caso de uso. Esto es alto riesgo/alta recompensa (¡y además un gran
esfuerzo!). Puede tomar bastante trabajo hacer que las cosas funcionen para los detalles de su proyecto,
pero por otro lado, es probable que aprenda más.
• Para un menor esfuerzo, en cada capítulo describimos un "Ejercicio para el lector" y le indicamos una ubicación
de GitHub donde puede descargar un código parcialmente terminado para el capítulo al que le faltan algunas
partes para que lo escriba usted mismo.
Particularmente si tiene la intención de aplicar algunos de estos patrones en sus propios proyectos, trabajar con
un ejemplo simple es una excelente manera de practicar de manera segura.
Licencia
El código (y la versión en línea del libro) tiene una licencia Creative Commons CC BY-NC-ND, lo que significa que
puede copiarlo y compartirlo con quien quiera, sin fines comerciales, siempre que dar atribución. Si desea reutilizar
parte del contenido de este libro y tiene alguna duda sobre la licencia, comuníquese con O'Reilly en
permisos@oreilly.com.
La edición impresa tiene una licencia diferente; por favor vea la página de derechos de autor.
Cursiva
Indica nuevos términos, URL, direcciones de correo electrónico, nombres de archivo y extensiones de archivo.
xiv | Prefacio
Machine Translated by Google
Ancho constante
Se utiliza para listas de programas, así como dentro de párrafos para referirse a elementos de
programas como nombres de variables o funciones, bases de datos, tipos de datos, variables
de entorno, declaraciones y palabras clave.
Muestra el texto que se debe reemplazar con valores proporcionados por el usuario o por valores
determinados por el contexto.
Prefacio | XV
Machine Translated by Google
Tenemos una página web para este libro, donde enumeramos las erratas, ejemplos y cualquier información
adicional. Puede acceder a esta página en https://oreil.ly/architecture-patterns-python.
Envíe un correo electrónico a bookquestions@oreilly.com para comentar o hacer preguntas técnicas sobre
este libro.
Para obtener más información sobre nuestros libros, cursos, conferencias y noticias, visite nuestro sitio web
en http://www.oreilly.com.
Expresiones de gratitud
A nuestros revisores técnicos, David Seddon, Ed Jung y Hynek Schlawack: absolutamente no los merecemos.
Todos ustedes son increíblemente dedicados, concienzudos y rigurosos.
Cada uno de ustedes es inmensamente inteligente y sus diferentes puntos de vista fueron útiles y
complementarios entre sí. Gracias desde el fondo de nuestros corazones.
Un enorme agradecimiento también a nuestros lectores de Early Release por sus comentarios y sugerencias:
Ian Cooper, Abdullah Ariff, Jonathan Meier, Gil Gonçalves, Matthieu Choplin, Ben Judson, James Gregory,
ÿukasz Lechowicz, Clinton Roy, Vitorino Araújo, Susan Goodbody, Josh Harwood, Daniel Butler, Liu Haibin,
Jimmy Davies, Ignacio Vergara Kausel, Gaia Canestrani, Renne Rocha, Pedroabi, Ashia Zawaduk, Jostein
Leira, Brandon Rhodes y muchos más; nuestras disculpas si no te hemos incluido en esta lista.
Súper-mega-gracias a nuestro editor Corbin Collins por su amabilidad y por ser un incansable defensor del
lector. Gracias igualmente superlativas al personal de producción, Katherine Tozer, Sharon Wilkey, Ellen
Troutman-Zaig y Rebecca Demarest, por su dedicación, profesionalismo y atención a los detalles. Este libro
ha mejorado enormemente gracias a ti.
xi | Prefacio
Machine Translated by Google
Introducción
Por ejemplo, un jardín bien cuidado es un sistema altamente ordenado. Los jardineros definen los
límites con caminos y cercas, y marcan macizos de flores o huertos. Con el tiempo, el jardín evoluciona,
haciéndose más rico y espeso; pero sin un esfuerzo deliberado, el jardín se volverá salvaje. Las malas
hierbas y los pastos ahogarán a otras plantas, cubriendo los caminos, hasta que eventualmente cada
parte se vea igual nuevamente: salvaje y sin control.
Los sistemas de software también tienden al caos. Cuando comenzamos a construir un nuevo sistema,
tenemos grandes ideas de que nuestro código estará limpio y bien ordenado, pero con el tiempo nos
damos cuenta de que reúne casos complicados y extremos y termina en un confuso pantano de clases
de administrador y módulos de utilidad. Descubrimos que nuestra arquitectura sensiblemente
estratificada se ha derrumbado sobre sí misma como una bagatela demasiado empapada. Los
sistemas de software caóticos se caracterizan por una similitud de funciones: controladores de API
que tienen conocimiento del dominio y envían correos electrónicos y realizan registros; clases de
"lógica de negocios" que no realizan cálculos pero realizan operaciones de E/S; y todo acoplado a
todo lo demás, de modo que cambiar cualquier parte del sistema se vuelve peligroso. Esto es tan
común que los ingenieros de software tienen su propio término para el caos: el antipatrón Big Ball of Mud (Figura P-1).
xvii
Machine Translated by Google
Figura P-1. Un diagrama de dependencia de la vida real (fuente: "Dependencia empresarial: Gran bola
de hilo" por Alex Papadimoulis)
Afortunadamente, las técnicas para evitar crear una gran bola de barro no son complejas.
Encapsulación y abstracciones
La encapsulación y la abstracción son herramientas que todos buscamos instintivamente como
programadores, incluso si no todos usamos estas palabras exactas. Permítanos detenernos en ellos por
un momento, ya que son un tema de fondo recurrente del libro.
xviii | Introducción
Machine Translated by Google
identificar una tarea que debe realizarse en nuestro código y asignar esa tarea a un objeto o función bien
definidos. Llamamos a ese objeto o función una abstracción.
importar
json desde urllib.request importar
urlopen desde urllib.parse importar urlencode
resultados = analizado['Temas
relacionados'] para r en resultados: si
'Texto' en r:
'
imprimir(r['PrimeraURL'] + - ' + r['Texto'])
solicitudes de importación
Ambas listas de códigos hacen lo mismo: envían valores codificados en forma a una URL para usar una API de
motor de búsqueda. Pero el segundo es más sencillo de leer y comprender porque opera en un nivel más alto
de abstracción.
Podemos dar un paso más allá identificando y nombrando la tarea que queremos que el código realice por
nosotros y usando una abstracción de nivel aún más alto para hacerlo explícito:
import duckduckgo
para r en duckduckgo.query('Salchichas').resultados:
'
print(r.url + - ' + texto r)
Encapsular el comportamiento mediante el uso de abstracciones es una herramienta poderosa para hacer que
el código sea más expresivo, más comprobable y más fácil de mantener.
Introducción | xix
Machine Translated by Google
Abstracciones y ABC
En un lenguaje OO tradicional como Java o C#, puede usar una clase base abstracta (ABC) o una
interfaz para definir una abstracción. En Python puedes (y a veces lo hacemos) usar ABC, pero
también puedes confiar felizmente en la tipificación pato.
La abstracción puede significar simplemente "la API pública de lo que está usando", por ejemplo,
un nombre de función más algunos argumentos.
La mayoría de los patrones de este libro implican elegir una abstracción, por lo que verá
muchos ejemplos en cada capítulo. Además, el Capítulo 3 analiza específicamente algunas
heurísticas generales para elegir abstracciones.
capas
La encapsulación y la abstracción nos ayudan a ocultar detalles y proteger la consistencia de
nuestros datos, pero también debemos prestar atención a las interacciones entre nuestros
objetos y funciones. Cuando una función, módulo u objeto utiliza otro, decimos que uno
depende del otro. Estas dependencias forman una especie de red o gráfico.
En una gran bola de lodo, las dependencias están fuera de control (como vio en la Figura P-1).
Cambiar un nodo del gráfico se vuelve difícil porque tiene el potencial de afectar muchas otras
partes del sistema. Las arquitecturas en capas son una forma de abordar este problema. En
una arquitectura en capas, dividimos nuestro código en categorías o roles discretos e
introducimos reglas sobre qué categorías de código pueden llamarse entre sí.
Uno de los ejemplos más comunes es la arquitectura de tres capas que se muestra en la
Figura P-2.
XX | Introducción
Machine Translated by Google
La arquitectura en capas es quizás el patrón más común para construir software empresarial. En este
modelo tenemos componentes de interfaz de usuario, que pueden ser una página web, una API o una
línea de comandos; estos componentes de la interfaz de usuario se comunican con una capa de lógica
comercial que contiene nuestras reglas comerciales y nuestros flujos de trabajo; y finalmente, tenemos
una capa de base de datos que es responsable de almacenar y recuperar datos.
Durante el resto de este libro, daremos la vuelta sistemáticamente a este modelo obedeciendo un
principio simple.
Desafortunadamente, no podemos ilustrar el DIP usando tres listados de códigos diminutos como lo
hicimos para la encapsulación. Sin embargo, toda la Parte I es esencialmente un ejemplo práctico de
la implementación del DIP en una aplicación, por lo que se llenará de ejemplos concretos.
1. Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deberían
depender de abstracciones.
2. Las abstracciones no deben depender de los detalles. En cambio, los detalles deben depender de
abstracciones
Los módulos de alto nivel son el código que realmente le importa a su organización. Tal vez trabaje
para una compañía farmacéutica y sus módulos de alto nivel traten con pacientes.
2 SOLID es un acrónimo de los cinco principios de diseño orientado a objetos de Robert C. Martin: responsabilidad
única, abierto para extensión pero cerrado para modificación, sustitución de Liskov, segregación de interfaz e inversión
de dependencia. Ver “SOLID: Los Primeros 5 Principios del Diseño Orientado a Objetos” por Samuel Oloruntoba.
Introducción | xxx
Machine Translated by Google
y juicios Tal vez trabaje para un banco y sus módulos de alto nivel administren transacciones e
intercambios. Los módulos de alto nivel de un sistema de software son las funciones, clases y paquetes
que se ocupan de nuestros conceptos del mundo real.
Por el contrario, los módulos de bajo nivel son el código que no le importa a su organización.
Es poco probable que su departamento de recursos humanos se entusiasme con los sistemas de
archivos o los conectores de red. No es frecuente que discuta SMTP, HTTP o AMQP con su equipo de finanzas.
Para nuestros interesados no técnicos, estos conceptos de bajo nivel no son interesantes ni relevantes.
Todo lo que les importa es si los conceptos de alto nivel funcionan correctamente. Si la nómina se ejecuta
a tiempo, es poco probable que a su empresa le importe si se trata de un trabajo cron o de una función
transitoria que se ejecuta en Kubernetes.
Depende de no significa necesariamente importaciones o llamadas, sino más bien una idea más general
de que un módulo conoce o necesita otro módulo.
Todos los problemas en informática se pueden resolver agregando otro nivel de indirección.
—David Rueda
Entonces, la primera parte del DIP dice que nuestro código comercial no debe depender de detalles
técnicos; en cambio, ambos deberían usar abstracciones.
¿Por qué? En términos generales, porque queremos poder cambiarlos independientemente uno del otro.
Los módulos de alto nivel deben ser fáciles de cambiar en respuesta a las necesidades comerciales.
Los módulos de bajo nivel (detalles) suelen ser, en la práctica, más difíciles de cambiar: piense en
refactorizar para cambiar el nombre de una función en lugar de definir, probar e implementar una
migración de base de datos para cambiar el nombre de una columna. No queremos que los cambios en
la lógica empresarial se desaceleren porque están estrechamente relacionados con detalles de
infraestructura de bajo nivel. Pero, del mismo modo, es importante poder cambiar los detalles de su
infraestructura cuando lo necesite (piense en fragmentar una base de datos, por ejemplo), sin necesidad
de realizar cambios en su capa empresarial. Agregar una abstracción entre ellos (la famosa capa
adicional de direccionamiento indirecto) permite que los dos cambien (más) independientemente el uno
del otro.
La segunda parte es aún más misteriosa. “Las abstracciones no deberían depender de los detalles”
parece bastante claro, pero “Los detalles deberían depender de las abstracciones” es difícil de imaginar.
¿Cómo podemos tener una abstracción que no dependa de los detalles que está abstrayendo? Para
cuando lleguemos al Capítulo 4, tendremos un ejemplo concreto que debería aclarar todo esto un poco.
XXII | Introducción
Machine Translated by Google
El Capítulo 1 muestra cómo crear una capa empresarial con un patrón de modelo de dominio. El
resto de los patrones en la Parte I muestran cómo podemos mantener el modelo de dominio fácil de
cambiar y libre de preocupaciones de bajo nivel eligiendo las abstracciones correctas y aplicando
continuamente el DIP.
Introducción | XXIII
Machine Translated by Google
Machine Translated by Google
PARTE I
La mayoría de los desarrolladores nunca han visto un modelo de dominio, solo un modelo de datos.
La mayoría de los desarrolladores con los que hablamos sobre arquitectura tienen la persistente sensación
de que las cosas podrían mejorar. A menudo están tratando de rescatar un sistema que de alguna manera
ha fallado y están tratando de volver a poner alguna estructura en una bola de barro. Saben que su lógica
empresarial no debe estar esparcida por todos lados, pero no tienen idea de cómo solucionarlo.
Hemos encontrado que muchos desarrolladores, cuando se les pide que diseñen un nuevo sistema,
inmediatamente comienzan a construir un esquema de base de datos, con el modelo de objetos tratado
como una idea de último momento. Aquí es donde todo empieza a salir mal. En su lugar, el comportamiento
debe ser lo primero e impulsar nuestros requisitos de almacenamiento. Después de todo, a nuestros clientes
no les importa el modelo de datos. Les importa lo que hace el sistema; de lo contrario, solo usarían una hoja de cálculo.
La primera parte del libro analiza cómo construir un modelo de objetos enriquecido a través de TDD (en el
Capítulo 1), y luego mostraremos cómo mantener ese modelo desvinculado de las preocupaciones técnicas.
Mostramos cómo crear código que ignore la persistencia y cómo crear API estables alrededor de nuestro
dominio para que podamos refactorizar agresivamente.
Service Layer para definir claramente dónde comienzan y terminan nuestros casos de uso
Machine Translated by Google
Si desea una imagen de hacia dónde nos dirigimos, eche un vistazo a la Figura I-1, pero no se preocupe si todavía
no tiene sentido. Introducimos cada casilla de la figura, una a una, a lo largo de esta parte del libro.
También nos tomamos un poco de tiempo para hablar sobre el acoplamiento y las abstracciones, ilustrándolo con
un ejemplo simple que muestra cómo y por qué elegimos nuestras abstracciones.
Machine Translated by Google
• El Apéndice C es un tipo de contenido de "prueba está en el pudín", que muestra lo fácil que es
cambiar toda nuestra infraestructura (la API de Flask, el ORM y Postgres) por un modelo de E/S
totalmente diferente que involucra una CLI y CSV.
• Finalmente, el Apéndice D puede ser de interés si se pregunta cómo se verían estos patrones si usa
Django en lugar de Flask y SQLAlchemy.
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 1
Modelado de dominio
Este capítulo analiza cómo podemos modelar procesos comerciales con código, de una manera 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 de valor y servicio de dominio.
La figura 1-1 es un marcador de posición visual simple para nuestro patrón de modelo de dominio. Completaremos algunos
detalles en este capítulo y, a medida que avancemos a otros capítulos, construiremos cosas en torno al modelo de dominio,
pero siempre debería poder encontrar estas pequeñas formas en el centro.
5
Machine Translated by Google
En la introducción, usamos el término capa de lógica empresarial para describir la capa central de una
arquitectura de tres capas. Para el resto del libro, usaremos el término modelo de dominio en su lugar.
Este es un término de la comunidad DDD que hace un mejor trabajo al capturar nuestro significado
previsto (consulte la siguiente barra lateral para obtener más información sobre DDD).
El dominio es una forma elegante de decir el problema que estás tratando de resolver. Sus autores
trabajan actualmente para una tienda online de muebles. Según el sistema del que esté hablando, el
dominio podría ser compras y adquisiciones, o diseño de productos, o logística y entrega. La mayoría de
los programadores pasan sus días tratando de mejorar o automatizar los procesos comerciales; el dominio
es el conjunto de actividades que esos procesos soportan.
El modelo de dominio es el mapa mental que los empresarios tienen de sus negocios.
Toda la gente de negocios tiene estos mapas mentales: es la forma en que los humanos piensan sobre
procesos complejos.
Puede saber cuándo están navegando en estos mapas porque usan lenguaje comercial. La jerga surge
naturalmente entre las personas que colaboran en sistemas complejos.
Imagina que tú, nuestro desafortunado lector, de repente fueras transportado a años luz de la Tierra a
bordo de una nave extraterrestre con tus amigos y familiares y tuvieras que descubrir, desde los primeros
principios, cómo navegar de regreso a casa.
En sus primeros días, podría presionar botones al azar, pero pronto aprendería qué botones hacían qué,
para que pudieran darse instrucciones unos a otros. “Presiona el botón rojo cerca de la llave intermitente
y luego tira la gran palanca hacia el aparato del radar”, podrías decir.
En un par de semanas, se volvería más preciso al adoptar palabras para describir las funciones de la
nave: "Aumentar los niveles de oxígeno en la bodega de carga tres" o "encender los pequeños propulsores".
Después de unos meses, habrá adoptado un lenguaje para procesos complejos completos: "Iniciar
secuencia de aterrizaje" o "preparar para deformación". Este proceso ocurriría de forma bastante natural,
sin ningún esfuerzo formal para construir un glosario compartido.
En pocas palabras, DDD dice que lo más importante del software es que proporciona un modelo útil de un
problema. Si conseguimos ese modelo correcto, nuestro software ofrece valor y hace posibles cosas
nuevas.
Si nos equivocamos en el modelo, se convierte en un obstáculo que hay que sortear. En este libro,
podemos mostrar los conceptos básicos de la construcción de un modelo de dominio y la construcción de
una arquitectura a su alrededor que deje el modelo lo más libre posible de restricciones externas, de modo
que sea fácil de evolucionar y cambiar.
Pero hay mucho más en DDD y en los procesos, herramientas y técnicas para desarrollar un modelo de
dominio. Sin embargo, esperamos darle una probada y no podemos alentarlo lo suficiente para que
continúe y lea un libro de DDD adecuado:
• El "libro rojo", Implementación del diseño basado en dominios por Vaughn Vernon
(Addison-Wesley Professional)
Así es en el mundo mundano de los negocios. La terminología utilizada por las partes interesadas del negocio
representa una comprensión destilada del modelo de dominio, donde las ideas y los procesos complejos se
reducen a una sola palabra o frase.
Cuando escuchamos a las partes interesadas de nuestro negocio usar palabras desconocidas o usar términos
de una manera específica, debemos escuchar para comprender el significado más profundo y codificar su
experiencia ganada con tanto esfuerzo en nuestro software.
Vamos a utilizar un modelo de dominio del mundo real a lo largo de este libro, específicamente un modelo de
nuestro empleo actual. MADE.com es un exitoso minorista de muebles.
Obtenemos nuestros muebles de fabricantes de todo el mundo y los vendemos en
Europa.
1 DDD no originó el modelado de dominio. 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 basado en la responsabilidad, del
cual DDD es un caso especial que trata el dominio. Pero incluso eso es demasiado tarde, y los entusiastas de OO
le dirán que mire más atrás a Ivar Jacobson y Grady Booch; el término existe desde mediados de la década de 1980.
Cuando compra un sofá o una mesa de café, tenemos que averiguar cuál es la mejor manera de llevar
sus productos de Polonia, China o Vietnam a su sala de estar.
En un nivel alto, tenemos sistemas separados que son responsables de comprar existencias, vender
existencias a los clientes y enviar mercancías a los clientes. Un sistema en el medio necesita coordinar
el proceso asignando existencias a los pedidos de un cliente; consulte la Figura 1-2.
Para los propósitos de este libro, estamos imaginando que la empresa decide implementar una nueva y
emocionante forma de asignar existencias. Hasta ahora, la empresa ha estado presentando las
existencias y los plazos de entrega en función de lo que está físicamente disponible en el almacén. Si se
agota el almacén, un producto aparece como "agotado" hasta que llegue el próximo envío del fabricante.
Aquí está la innovación: si tenemos un sistema que puede realizar un seguimiento de todos nuestros
envíos y cuándo deben llegar, podemos tratar los productos en esos barcos como existencias reales y
parte de nuestro inventario, solo que con plazos de entrega un poco más largos. Parecerá que hay menos
productos agotados, venderemos más y la empresa puede ahorrar dinero al mantener un inventario más
bajo en el almacén nacional.
Pero asignar pedidos ya no es una cuestión trivial de disminuir una sola cantidad en el sistema de
almacén. Necesitamos un mecanismo de asignación más complejo. Tiempo para un poco de modelado
de dominio.
Nos aseguramos de expresar esas reglas en la jerga comercial (el lenguaje omnipresente en la
terminología de DDD). Elegimos identificadores memorables para nuestros objetos para 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
conversábamos con nuestros expertos de dominio sobre la asignación.
• 1 unidad de TASTELESS-LAMP
El departamento de compras ordena pequeños lotes de existencias. Un lote de existencias tiene una
identificación única llamada referencia, un SKU y una cantidad.
Necesitamos asignar líneas de pedido a lotes. Cuando hayamos asignado una línea de pedido a un lote,
enviaremos existencias de ese lote específico a la dirección de entrega del cliente.
Cuando asignamos x unidades de stock a un lote, la cantidad disponible se reduce en x.
Por ejemplo:
No podemos asignar a un lote si la cantidad disponible es menor que la cantidad de la línea de pedido.
Por ejemplo:
• Si volvemos a asignar la línea de pedido al mismo lote, el lote aún debe tener un
cantidad disponible de 8.
Los lotes tienen una ETA si se están enviando actualmente o pueden estar en stock en el almacén.
Asignamos al stock de almacén con preferencia a los lotes de envío. Asignamos los lotes de envío en
orden de cuál tiene la ETA más temprana.
No le mostraremos cómo funciona TDD en este libro, pero queremos mostrarle cómo construiríamos
un modelo a partir de esta conversación empresarial.
Encontrará algunas pruebas unitarias de marcador de posición en GitHub, pero puede comenzar desde
cero o combinarlas/reescribirlas como desee.
def prueba_asignación_a_un_lote_reduce_la_cantidad_disponible():
lote = Lote("lote-001", "TABLA PEQUEÑA", cant. =20, eta=fecha.hoy()) línea = LíneaPedido('ref-
pedido', "TABLA PEQUEÑA", 2)
lote.asignar(línea)
afirmar lote.cantidad_disponible == 18
El nombre de nuestra prueba unitaria describe el comportamiento que queremos ver del sistema, y
los nombres de las clases y variables que usamos se toman del negocio.
jerga. 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.
lote de clase :
def __init__( self,
ref: str, sku: str, qty: int, eta: Opcional[fecha]
):
self.reference = ref
self.sku = sku self.eta =
eta self.disponible_cantidad
= cantidad
No mostramos las importaciones en la mayoría de las listas de códigos, en un intento por mantenerlas limpias.
Esperamos que pueda adivinar que esto vino a través de dataclasses import dataclass; asimismo,
escribiendo.Opcional y fechahora.fecha . Si desea volver a verificar algo, puede ver el código de trabajo
completo para cada capítulo en su rama (por ejemplo, chapter_01_domain_model).
Las sugerencias de tipo siguen siendo un tema de controversia en el mundo de Python. Para los modelos de
dominio, a veces pueden ayudar a aclarar o documentar cuáles son los argumentos esperados, y las personas
con IDE a menudo los agradecen. Puede decidir que el precio pagado en términos de legibilidad es demasiado
alto.
Nuestra implementación aquí es trivial: un Lote simplemente envuelve un número entero disponible_cantidad, y
decrementamos ese valor en la asignación. Hemos escrito bastante código solo para restar un número de otro, pero
creemos que modelar nuestro dominio con precisión valdrá la pena.3
2 En versiones anteriores de Python, podríamos haber usado una tupla con nombre. También puede consultar el artículo de Hynek Schlawack.
excelentes atributos
3 ¿O tal vez piensa que no hay suficiente código? ¿Qué pasa con algún tipo de verificación de que el SKU en OrderLine
coincide con Batch.sku? Guardamos algunos pensamientos sobre la validación para el Apéndice E.
def prueba_puede_asignar_si_disponible_mayor_de_requerido():
lote_grande, línea_pequeña = hacer_lote_y_línea("LÁMPARA ELEGANTE", 20, 2) afirmar
lote_grande.puede_asignar(línea_pequeña)
def test_cannot_allocate_if_available_smaller_than_required():
small_batch, large_line = make_batch_and_line("ELEGANT-LAMP", 2, 20) afirmar
small_batch.can_allocate(large_line) es falso
def prueba_puede_asignar_si_disponible_igual_a_required():
lote, línea = make_batch_and_line("ELEGANT-LAMP", 2, 2) afirmar
lote.can_allocate(línea)
No hay nada demasiado inesperado aquí. Hemos refactorizado nuestro conjunto de pruebas para que no
sigamos 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 simples para un nuevo método can_allocate. Una vez más, tenga en cuenta
que los nombres que usamos reflejan el lenguaje de nuestros expertos en dominios, y los ejemplos que
acordamos se escriben directamente en el código.
En esta prueba, afirmamos que desasignar una línea de un lote no tiene efecto a menos que el
lote haya asignado previamente la línea. Para que esto funcione, nuestro lote debe comprender
qué líneas se han asignado. Veamos la implementación:
lote de clase :
def __init__( self,
ref: str, sku: str, qty: int, eta: Opcional[fecha]
):
self.reference = ref
self.sku = sku self.eta =
eta self._purchased_quantity
= qty self._allocations = set() # type:
Set[OrderLine]
@propiedad
def cantidad_asignada (auto) -> int:
return sum(line.qty for line in self._allocations)
@property
def cantidad_disponible (self) -> int: return
self._cantidad_comprada - self.cantidad_asignada
¡Ahora estamos llegando a alguna parte! Un lote ahora realiza un seguimiento de un conjunto de objetos
de línea de pedido asignados. Cuando asignamos, si tenemos suficiente cantidad disponible, simplemente
agregamos al conjunto. Nuestra cantidad_disponible ahora es una propiedad calculada: cantidad
comprada menos cantidad asignada.
Sí, hay mucho más que podríamos hacer. Es un poco desconcertante que tanto allocate() como
desalocate() puedan fallar silenciosamente, pero tenemos lo básico.
Por cierto, el uso de un conjunto para ._allocations nos simplifica el manejo de la última prueba, porque
los elementos de un conjunto son únicos:
Por el 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, surge cualquier
cantidad de reglas comerciales y casos extremos: los clientes pueden solicitar la entrega en fechas
futuras específicas, lo que significa que es posible que no queramos asignarlos al lote más antiguo.
Algunos SKU no están en lotes, sino que se piden a pedido directamente de los proveedores, por lo que
tienen una lógica diferente. Dependiendo de la ubicación del cliente, podemos asignar solo un subconjunto
de almacenes y envíos que se encuentran en su región, excepto algunos SKU que nos complace entregar
desde un almacén en una región diferente si no tenemos existencias en el hogar. región. Y así. ¡Un
negocio 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 modelo de dominio simple como marcador de posición para algo más complejo,
ampliaremos nuestro modelo de dominio simple en el resto del libro y lo conectaremos al mundo real de
API, bases de datos y hojas de cálculo. Veremos como pega
rígidamente a nuestros principios de encapsulación y una cuidadosa estratificación nos ayudarán a evitar
una bola de barro.
de tipos Si realmente quiere ir a la ciudad con sugerencias de tipos, podría ir tan lejos como
envolver tipos primitivos usando escribiendo.NewType:
lote de clase :
def __init__(self, ref: Referencia, sku: Sku, qty: Cantidad): self.sku = sku
self.reference = ref
self._cantidad_comprada = cantidad
Eso permitiría que nuestro verificador de tipos se asegure de que no pasemos un Sku donde
se espera una Referencia , por ejemplo.
valor Hemos usado la línea generosamente en las listas de códigos anteriores, pero ¿qué es una línea? En
nuestro lenguaje comercial, un pedido tiene varios elementos de línea, donde cada línea tiene un SKU y una
cantidad. Podemos imaginar que un archivo YAML simple que contenga información de pedidos se vería así:
Referencia_pedido: 12345
Líneas:
- sku: RED-SILLA
cantidad: 25 -
sku: BLU-SILLA
cantidad:
25 - sku: GRN-SILLA
cantidad: 25
Tenga en cuenta que mientras un pedido tiene una referencia que lo identifica de forma única, una línea no.
(Incluso si agregamos la referencia del pedido a la clase OrderLine , no es algo que identifique de manera
única la línea en sí).
Cada vez que tenemos un concepto de negocio que tiene datos pero no identidad, a menudo elegimos
representarlo utilizando el patrón de objeto de valor. Un objeto de valor es cualquier objeto de dominio que
se identifica de forma única por los datos que contiene; normalmente los hacemos inmutables:
Una de las cosas buenas que nos dan las clases de datos (o tuplas con nombre) es la igualdad de valores,
que es la forma elegante de decir: "Dos líneas con el mismo orderid, sku y qty son iguales".
@dataclass(congelado=Verdadero)
Nombre de la clase :
clase Dinero(TupleNombrado):
moneda: str valor:
int
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. Asimismo, dos
nombres son iguales si tanto el nombre como el apellido coinciden; y dos líneas son equivalentes si tienen
el mismo pedido de cliente, código de producto y cantidad.
Sin embargo, todavía podemos tener un comportamiento complejo en un objeto de valor. De hecho, es
común soportar operaciones sobre valores; por ejemplo, operadores matemáticos:
def can_subtract_money_values():
afirmar diez - cinco == cinco
def puede_multiplicar_dinero_por_un_número():
afirmar cinco * 5 == Dinero('gbp', 25)
línea de pedido se identifica de forma única por su ID de pedido, SKU y cantidad; si cambiamos
uno de esos valores, ahora tenemos una nueva línea. Esa es la definición de un objeto de
valor: cualquier objeto que se identifica solo por sus datos y no tiene una identidad de larga duración.
Sin embargo, ¿qué pasa con un lote? Que se identifica por una referencia.
Usamos el término entidad para describir un objeto de dominio que tiene una identidad de larga
duración. En la página anterior, presentamos una clase Nombre como objeto de valor. Si tomamos el
nombre Harry Percival y cambiamos una letra, tenemos el nuevo objeto Nombre Barry Percival.
def prueba_nombre_igualdad():
afirmar Nombre("Harry", "Percival") != Nombre("Barry", "Percival")
Pero, ¿qué pasa con Harry como persona? Las personas cambian de nombre, de estado civil e incluso
de género, pero seguimos reconociéndolas como el mismo individuo. Eso es porque los humanos, a
diferencia de los nombres, tienen una identidad persistente:
Persona de clase :
def test_barry_is_harry():
harry = Persona(Nombre("Harry", "Percival"))
barry = harry
Las entidades, a diferencia de los valores, tienen igualdad de identidad. Podemos cambiar sus valores, y
siguen siendo reconociblemente lo mismo. Los lotes, en nuestro ejemplo, son entidades. Podemos asignar
líneas a un lote, o cambiar la fecha en la que esperamos que llegue, y seguirá siendo la misma entidad.
Por lo general, hacemos esto explícito en el código implementando operadores de igualdad en las entidades:
def __hash__(self):
return hash(self.reference)
Tanto para objetos de entidad como de valor, también vale la pena pensar en cómo funcionará __hash__ . Es
el método mágico que usa Python para controlar el comportamiento de los objetos cuando los agregas a
conjuntos o los usas como teclas de dictado; puede 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 sean inmutables. Obtenemos esto gratis especificando @fro zen=True en la clase de datos.
Para las entidades, la opción más simple es decir que el hash es Ninguno, lo que significa que el objeto no se
puede modificar y no puede, por ejemplo, usarse en un conjunto. Si por algún motivo decide que realmente
desea utilizar operaciones de establecimiento o dictado con entidades, el hash debe basarse en los atributos,
como .reference, que definen la identidad única de la entidad a lo largo del tiempo. También debe intentar
hacer que ese atributo sea de solo lectura.
Evans analiza la idea de las operaciones del servicio de dominio que no tienen un lugar natural en
una entidad o un objeto de valor.6 Algo que asigna una línea de pedido, dado un conjunto de lotes,
suena mucho a una función y podemos aprovechar del hecho de que Python es un lenguaje
multiparadigma y simplemente convertirlo en una función.
def test_prefers_current_stock_batches_to_shipments():
in_stock_batch = Batch("en-stock-batch", "RETRO-CLOCK", 100, eta=Ninguno)
shipping_batch = Batch(" shipment -batch", "RETRO-CLOCK", 100, eta=mañana) line =
OrderLine( "oref", "RETRO-RELOJ", 10)
afirmar en_stock_batch.cantidad_disponible == 90
afirmar envío_lote.cantidad_disponible == 100
def test_prefers_earlier_batches():
más temprano = Lote("lote-rápido", "CUCHARA-MINIMALISTA", 100, eta=hoy) medio
= Lote("lote-normal", "CUCHARA-MINIMALISTA", 100, eta=mañana) último =
Lote("lento -lote", "MINIMALISTA-CUCHARA", 100, eta=más tarde) línea =
OrderLine("pedido1", "MINIMALISTA-CUCHARA", 10)
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 comercial, mientras que un servicio de capa de servicio
representa un caso de uso para su aplicación. A menudo, la capa de servicio llamará a un servicio de dominio.
def test_returns_allocated_batch_ref():
in_stock_batch = Batch("in-stock-batch-ref", "HIGHBROW-POSTER", 100, eta=Ninguno)
shipping_batch = Lote(" shipment -batch-ref", "HIGHBROW-POSTER", 100, eta=mañana) línea =
OrderLine("oref", "HIGHBROW-POSTER", 10) asignación = allocate(line, [in_stock_batch,
shipping_batch ]) asertar asignación == in_stock_batch.reference
) lote.asignar(línea)
volver lote.referencia
Los métodos mágicos de Python Permítanos usar nuestros modelos con Python idiomático Puede o no
gustarle el uso de next() en el código anterior, pero estamos bastante seguros de que estará de acuerdo en que
poder usar sorted() en nuestra lista de lotes es Python agradable e idiomático.
Eso es adorable.
Tenemos un último concepto que cubrir: las excepciones también pueden usarse para expresar conceptos de
dominio. En nuestras conversaciones con expertos en dominios, aprendimos sobre la posibilidad de que no se
pueda asignar un pedido porque no tenemos existencias, y podemos capturar eso usando una excepción de
dominio:
Modelado de
dominio Esta es la parte de su código que está más cerca del negocio, la que tiene más
probabilidades de cambiar y el lugar donde entrega el mayor valor al negocio. Que sea fácil
de entender y modificar.
excepto StopIteration:
aumentar OutOfStock(f'Agotado para sku {line.sku}')
¡Eso probablemente sea suficiente por ahora! Tenemos un servicio de dominio que podemos usar para nuestro primer
caso de uso. Pero primero necesitaremos una base de datos...
CAPITULO 2
Patrón de repositorio
Es hora de cumplir nuestra promesa de utilizar el principio de inversión de dependencia como una forma
de desvincular nuestra lógica central de las preocupaciones de infraestructura.
La Figura 2-1 muestra una pequeña vista previa de lo que vamos a construir: un objeto Repositorio que
se encuentra entre nuestro modelo de dominio y la base de datos.
23
Machine Translated by Google
En el Capítulo 1 creamos un modelo de dominio simple que puede asignar pedidos a lotes de existencias.
Es fácil para nosotros escribir pruebas contra este código porque no hay dependencias o infraestructura
para configurar. Si necesitáramos ejecutar una base de datos o una API y crear datos de prueba,
nuestras pruebas serían más difíciles de escribir y mantener.
Lamentablemente, en algún momento tendremos que poner nuestro pequeño modelo perfecto en manos
de los usuarios y enfrentarnos al mundo real de las hojas de cálculo, los navegadores web y las
condiciones de carrera. En los próximos capítulos, veremos cómo podemos conectar nuestro modelo
de dominio idealizado al estado externo.
Esperamos estar trabajando de manera ágil, por lo que nuestra prioridad es llegar a un producto mínimo
viable lo más rápido posible. En nuestro caso, será una API web. En un proyecto real, puede sumergirse
directamente con algunas pruebas de extremo a extremo y comenzar a conectar un marco web,
probando cosas de afuera hacia adentro.
Pero sabemos que, pase lo que pase, vamos a necesitar alguna forma de almacenamiento persistente,
y este es un libro de texto, por lo que podemos permitirnos un poco más de desarrollo de abajo hacia
arriba y comenzar a pensar en el almacenamiento. y bases de datos.
Cuando construimos nuestro primer punto final de API, sabemos que vamos a tener un código que se
parece más o menos al siguiente.
@flask.route.gubbins def
allocate_endpoint(): # extraer
la línea de orden de la línea de solicitud
= OrderLine (request.params, ...) # cargar
todos los lotes de la base de datos lotes = #
llamar a nuestro
... servicio de dominio
allocate(line, lotes) # luego guarde la
asignación en la base de datos de alguna
manera
volver 201
Necesitaremos una forma de recuperar información por lotes de la base de datos e instanciar nuestros objetos de
modelo de dominio a partir de ella, y también necesitaremos una forma de guardarlos en la base de datos.
¿Qué? Oh, "gubbins" es una palabra británica para "cosas". Puedes simplemente ignorar eso. Es pseudocódigo,
¿de acuerdo?
Pero queremos que nuestro modelo de dominio no tenga dependencias de ningún tipo. 1 No queremos que los
problemas de infraestructura se desborden en nuestro modelo de dominio y ralenticen nuestras pruebas unitarias
o nuestra capacidad para realizar cambios.
En cambio, como se discutió en la introducción, pensaremos en nuestro modelo como si estuviera en el “adentro”,
y las dependencias fluyen hacia adentro; esto es lo que la gente a veces llama arquitectura cebolla (vea la Figura
2-3).
1 Supongo que queremos decir "sin dependencias con estado". Depender de una biblioteca auxiliar está bien; dependiendo de un
ORM o un framework web no lo es.
Si ha estado leyendo sobre patrones arquitectónicos, es posible que se esté haciendo preguntas
como esta:
Aunque a algunas personas les gusta criticar las diferencias, todos estos son más o menos
nombres para lo mismo, y todos se reducen al principio de inversión de dependencia: los módulos
de alto nivel (el dominio) no deberían depender de los de bajo nivel. (la infraestructura).2
Recordemos nuestro modelo de dominio (ver Figura 2-4): una asignación es el concepto de vincular
una línea de pedido a un lote. Estamos almacenando las asignaciones como una colección en
nuestro objeto Batch .
días, es poco probable que los miembros de su equipo estén elaborando manualmente sus propias
consultas SQL. En cambio, es casi seguro que esté utilizando algún tipo de marco para generar SQL para
usted en función de sus objetos modelo.
Estos marcos se denominan mapeadores relacionales de objetos (ORM) porque existen para cerrar la
brecha conceptual entre el mundo de los objetos y el modelado de dominios y el mundo de las bases de
datos y el álgebra relacional.
Lo más importante que nos brinda un ORM es la ignorancia de la persistencia: la idea de que nuestro
modelo de dominio sofisticado no necesita saber nada acerca de cómo se cargan o persisten los datos.
Esto ayuda a mantener nuestro dominio libre de dependencias directas de tecnologías de bases de datos
particulares.3
Pero si sigue el tutorial típico de SQLAlchemy, terminará con algo como esto:
Base = base_declarativa()
clase OrderLine(Base):
3 En este sentido, usar un ORM ya es un ejemplo del DIP. En lugar de depender de SQL codificado,
dependen de una abstracción, el ORM. Pero eso no es suficiente para nosotros, ¡no en este libro!
id = Columna(Entero, clave_principal=Verdadero)
sku = Columna(Cadena(250)) cantidad =
Entero(Cadena(250)) id_pedido =
Columna(Entero, ClaveExterna('pedido.id')) orden =
relación(Pedido)
No necesita entender SQLAlchemy para ver que nuestro modelo prístino ahora está lleno de dependencias
en el ORM y, además, está empezando a verse feo. ¿Podemos realmente decir que este modelo ignora
la base de datos? ¿Cómo puede estar separado de las preocupaciones de almacenamiento cuando las
propiedades de nuestro modelo están directamente acopladas a las columnas de la base de datos?
clase OrderLine(modelos.Modelo):
sku = modelos.CharField(longitud_máxima=255)
cantidad = modelos.IntegerField () pedido =
modelos.ForeignKey(Pedido)
El punto es el mismo: nuestras clases modelo heredan directamente de las clases ORM, por lo
que nuestro modelo depende del ORM. Queremos que sea al revés.
esa no es la única forma de usar SQLAlchemy. La alternativa es definir su esquema por separado y definir
un mapeador explícito sobre cómo convertir entre el esquema y nuestro modelo de dominio, lo que
SQLAlchemy llama un mapeo clásico:
modelo de importación
metadatos = metadatos()
order_lines =
Table( 'order_lines', metadatos,
Columna('id', Entero, clave_principal=Verdadero, incremento automático=Verdadero),
Columna('sku', Cadena(255)),
Columna('cantidad', Entero, anulable=Falso),
Columna('orderid', String(255)),
)
...
def start_mappers():
lines_mapper = mapper(modelo.OrderLine, order_lines)
Definimos las tablas y columnas de nuestra base de datos utilizando las abstracciones de SQLAlchemy.4
Cuando llamamos a la función del mapeador , SQLAlchemy hace su magia para vincular nuestras clases
de modelo de dominio a las diversas tablas que hemos definido.
El resultado final será que, si llamamos a start_mappers, podremos cargar y guardar fácilmente instancias de
modelos de dominio desde y hacia la base de datos. Pero si nunca llamamos a esa función, nuestras clases
Esto nos brinda todos los beneficios de SQLAlchemy, incluida la capacidad de usar alambique para migraciones
y la capacidad de consultar de manera transparente utilizando nuestras clases de dominio, como veremos.
ver.
Cuando intenta construir su configuración ORM por primera vez, puede ser útil escribir pruebas para ella, como
en el siguiente ejemplo:
4 Incluso en proyectos en los que no usamos un ORM, a menudo usamos SQLAlchemy junto con Alambic para crear
esquemas declarativamente en Python y administrar migraciones, conexiones y sesiones.
) esperado = [
model.OrderLine("order1", "RED-SILLA", 12),
model.OrderLine("order1", "RED-TABLE", 13),
model.OrderLine("order2", "BLUE-LIPSTICK", 14) ,
def test_orderline_mapper_can_save_lines(session):
new_line = model.OrderLine("order1", "DECORATIVE-WIDGET", 12)
session.add(new_line) session.commit ()
Probablemente no mantendría estas pruebas, como verá en breve, una vez que haya dado el paso de
invertir la dependencia de ORM y el modelo de dominio, es solo un pequeño paso adicional para
implementar otra abstracción llamada patrón de Repositorio, que será más fácil escribir pruebas en
contra y proporcionará una interfaz simple para falsificar más adelante en las pruebas.
En este punto, sin embargo, nuestro punto final de la API podría parecerse a lo siguiente, y podríamos hacer
que funcione correctamente:
@flask.route.gubbins def
allocate_endpoint():
sesión = inicio_sesión()
volver 201
Si tuviéramos memoria infinita en nuestras computadoras portátiles, no necesitaríamos bases de datos torpes.
En cambio, podríamos usar nuestros objetos cuando quisiéramos. Como se veria eso?
importar todos_mis_datos
Aunque nuestros objetos están en la memoria, debemos colocarlos en algún lugar para poder encontrarlos
nuevamente. Nuestros datos en memoria nos permitirían agregar nuevos objetos, como una lista o un conjunto.
Debido a que los objetos están en la memoria, nunca necesitamos llamar a un método .save() ; simplemente
buscamos el objeto que nos importa y lo modificamos en la memoria.
más simple tiene solo dos métodos: add() para colocar un nuevo elemento en el repositorio y get()
para devolver un elemento agregado previamente . nuestro dominio y nuestra capa de servicio.
Esta simplicidad autoimpuesta nos impide acoplar nuestro modelo de dominio a la base de datos.
Así es como se vería una clase base abstracta (ABC) para nuestro repositorio:
@abc.abstractmethod
def add(self, lote: modelo.Lote): generar
NotImplementedError
@abc.abstractmethod
def get(self, reference) -> model.Batch: aumentar
NotImplementedError
Sugerencia de Python: @abc.abstractmethod es una de las únicas cosas que hacen que ABC
realmente "funcione" en Python. Python se negará a permitirle instanciar una clase que no
implemente todos los métodos abstractos definidos en su clase principal.7
6 Puede estar pensando: "¿Qué pasa con la lista , la eliminación o la actualización? " Sin embargo, en un mundo ideal, modificamos
nuestros objetos modelo uno a la vez, y la eliminación generalmente se maneja como una eliminación temporal, es decir, lote.cancel().
Finalmente, la actualización está a cargo del patrón Unidad de trabajo, como verá en el Capítulo 6.
7 Para cosechar realmente los beneficios de los ABC (tales como pueden ser), ejecute ayudantes como pylint y mypy.
En la vida real, a veces nos encontramos eliminando ABC de nuestro código de producción,
porque Python hace que sea demasiado fácil ignorarlos y terminan sin mantenimiento y, en el
peor de los casos, engañosos. En la práctica, a menudo solo confiamos en el tipeo de pato de
Python para habilitar las abstracciones. Para un Pythonista, un repositorio es cualquier objeto
que tiene métodos add(thing) y get(id) .
Una alternativa a considerar son los protocolos PEP 544. Éstos le permiten escribir sin la
posibilidad de herencia, lo que gustará especialmente a los fanáticos de “preferir la composición
sobre la herencia”.
¿Qué es la compensación?
¿Sabes que dicen que los economistas saben el precio de todo y el valor de nada?
Bueno, los programadores conocen los beneficios de todo y las compensaciones de nada.
—Rich Hickey
Siempre que presentemos un patrón arquitectónico en este libro, siempre preguntaremos: “¿Qué
obtenemos con esto? ¿Y cuánto nos cuesta?
Por lo general, como mínimo, introduciremos una capa adicional de abstracción y, aunque podemos
esperar que reduzca la complejidad en general, agrega complejidad localmente y tiene un costo en
términos de la cantidad bruta de partes móviles y en curso
mantenimiento.
Sin embargo, el patrón Repository es probablemente una de las opciones más fáciles del libro, si ya se
está dirigiendo a la ruta de inversión de dependencia y DDD. En lo que respecta a nuestro código, en
realidad solo estamos intercambiando la abstracción de SQLAlchemy (ses sion.query(Batch)) por una
diferente (batches_repo.get) que diseñamos.
Tendremos que escribir algunas líneas de código en nuestra clase de repositorio cada vez que agreguemos
un nuevo objeto de dominio que queramos recuperar, pero a cambio obtenemos una simple abstracción
sobre nuestra capa de almacenamiento, que controlamos. El patrón Repositorio facilitaría la realización
de cambios fundamentales en la forma en que almacenamos las cosas (consulte el Apéndice C) y, como
veremos, es fácil falsificarlo para las pruebas unitarias.
Además, el patrón Repository es tan común en el mundo de DDD que, si colabora con programadores
que llegaron a Python desde los mundos de Java y C#, es probable que lo reconozcan. La figura 2-5
ilustra el patrón.
Como siempre, comenzamos con una prueba. Esto probablemente se clasificaría como una prueba de integración,
ya que estamos verificando que nuestro código (el repositorio) esté correctamente integrado con la base de datos;
por lo tanto, las pruebas tienden a mezclar SQL sin procesar con llamadas y aserciones en nuestro propio código.
A diferencia de las pruebas ORM anteriores, estas pruebas son buenas candidatas
para permanecer como parte de su base de código a largo plazo, particularmente
si alguna parte de su modelo de dominio significa que el mapa relacional de objetos
no es trivial.
repo = repositorio.SqlAlchemyRepository(sesión)
repo.add(lote) sesión.commit ()
filas =
list(session.execute( 'SELECCIONE referencia, sku, _cantidad_comprada, eta DE "lotes"'
))
afirmar filas == [("batch1", "RUSTY-SOAPDISH", 100, None)]
Mantenemos el .commit() fuera del repositorio y lo hacemos responsabilidad de la persona que llama. Hay
ventajas y desventajas para esto; algunas de nuestras razones se aclararán cuando lleguemos al Capítulo 6.
Usamos el SQL sin procesar para verificar que se hayan guardado los datos correctos.
La siguiente prueba consiste en recuperar lotes y asignaciones, por lo que es más compleja:
) [[orderline_id]] = sesión.ejecutar(
'SELECCIONE id DE order_lines DONDE orderid=:orderid AND sku=:sku',
dict(orderid="order1", sku="GENERIC-SOFA")
) devuelve orderline_id
def test_repository_can_retrieve_a_batch_with_allocations(sesión):
orderline_id = insertar_order_line(sesión) lote1_id
= insertar_lote(sesión, "lote1") insertar_lote(sesión,
"lote2") insertar_asignación(sesión, orderline_id ,
lote1_id)
repo = repository.SqlAlchemyRepository(sesión)
recuperado = repo.get("batch1")
Esto prueba el lado de lectura, por lo que el SQL sin formato está preparando los datos para que
los lea repo.get().
Y eso es lo que verificamos aquí. La primera afirmación == verifica que los tipos coincidan y que la
referencia sea la misma (porque, como recordará, Batch es una entidad y tenemos un eq
personalizado para ello).
Por lo tanto, también verificamos explícitamente sus principales atributos, incluido ._allocations, que
es un conjunto de Python de objetos de valor OrderLine .
Ya sea que escriba minuciosamente o no pruebas para cada modelo es una cuestión de juicio. Una vez
que haya probado una clase para crear/modificar/guardar, puede estar feliz de continuar y hacer
los demás con una mínima prueba de ida y vuelta, o incluso nada, si todos siguen un patrón similar.
En nuestro caso, la configuración de ORM que configura el conjunto ._allocations es un poco
compleja, por lo que amerita una prueba específica.
clase SqlAlchemyRepository(AbstractRepository):
def list(self):
return self.session.query(model.Batch).all()
@flask.route.gubbins def
allocate_endpoint():
lotes = SqlAlchemyRepository.list() líneas =
[ OrderLine(l['orderid'], l['sku'], l['qty']) for l in
request.params...
] allocate(líneas, lotes)
session.commit() return 201
Hemos dejado las pruebas del repositorio, pero decidir qué SQL escribir depende de usted.
Quizás sea más difícil de lo que crees; quizás sea más fácil. Pero lo bueno es que al resto de
su aplicación simplemente no le importa.
def list(self):
return list(self._batches)
Debido a que es un envoltorio simple alrededor de un conjunto, todos los métodos son de una sola línea.
Usar un repositorio falso en las pruebas es realmente fácil, y tenemos una abstracción simple que es fácil de usar y
razonar sobre:
Los puertos y adaptadores surgieron del mundo OO, y la definición que mantenemos es que el puerto es la interfaz entre
nuestra aplicación y lo que sea que deseemos abstraer, y el adaptador es la implementación detrás de esa interfaz o
abstracción.
Ahora, Python no tiene interfaces per se, por lo que aunque suele ser fácil identificar un adaptador, definir el puerto puede
ser más difícil. Si está utilizando una clase base abstracta, eso es
el puerto. De lo contrario, el puerto es solo el tipo de pato al que se ajustan sus adaptadores y
que su aplicación principal espera: los nombres de funciones y métodos en uso, y sus nombres
y tipos de argumentos.
Envolver
Teniendo en cuenta la cita de Rich Hickey, en cada capítulo resumimos los costos y beneficios
de cada patrón arquitectónico que presentamos. Queremos dejar claro que no estamos diciendo
que todas las aplicaciones deban construirse de esta manera; solo a veces la complejidad de
la aplicación y el dominio hacen que valga la pena invertir tiempo y esfuerzo en agregar estas
capas adicionales de indirección.
Con eso en mente, la Tabla 2-1 muestra algunos de los pros y los contras del patrón Repositorio
y nuestro modelo de persistencia-ignorante.
ventajas Contras
• Tenemos una interfaz simple entre el almacenamiento persistente y nuestro modelo • Un ORM ya te compra algo de desacoplamiento.
de dominio. Cambiar claves foráneas puede ser difícil, pero debería
• Es fácil crear una versión falsa del repositorio para pruebas unitarias o intercambiar ser bastante fácil cambiar entre MySQL y Postgres si alguna
de pensar en la persistencia nos ayuda a centrarnos en el problema empresarial en cuestión. • El mantenimiento de mapeos ORM a mano requiere trabajo
adicional y código adicional.
Si alguna vez queremos cambiar radicalmente nuestro enfoque, podemos hacerlo en
nuestro modelo, sin necesidad de preocuparnos por las claves externas o las migraciones • Cualquier capa adicional de direccionamiento indirecto siempre
hasta más tarde. • El esquema de nuestra base de datos es realmente simple porque aumenta los costos de mantenimiento y agrega un "factor
tenemos control total sobre cómo asignamos nuestros objetos a las tablas. WTF" para los programadores de Python que nunca antes
La Figura 2-6 muestra la tesis básica: sí, para casos simples, un modelo de dominio
desacoplado es más difícil que un patrón ORM/ActiveRecord simple.8
8 Diagrama inspirado en una publicación llamada “Complejidad global, simplicidad local” de Rob Vens.
Pero cuanto más complejo sea el dominio, más valdrá la pena invertir en liberarse de las preocupaciones de
infraestructura en términos de la facilidad para realizar cambios.
Nuestro código de ejemplo no es lo suficientemente complejo como para dar más que una pista de cómo se ve el lado
derecho del gráfico, pero las pistas están ahí. Imagínese, por ejemplo, si un día decidimos que queremos cambiar las
asignaciones para vivir en OrderLine en lugar de en el objeto Batch : si estuviéramos usando Django, digamos,
tendríamos que definir y pensar en la migración de la base de datos antes de podría ejecutar cualquier prueba. Tal
como están las cosas, debido a que nuestro modelo son simples objetos antiguos de Python, podemos cambiar un set()
para que sea un atributo nuevo, sin necesidad de pensar en la base de datos hasta más tarde.
Te estarás preguntando, ¿cómo instanciamos estos repositorios, falsos o reales? ¿Cómo será realmente nuestra
aplicación Flask? Lo descubrirá en la próxima entrega emocionante, el patrón de la capa de servicio.
Resumen | 39
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 3
Permítanos una breve digresión sobre el tema de las abstracciones, querido lector. Hemos hablado
mucho de abstracciones. El patrón Repository es una abstracción sobre el almacenamiento permanente,
por ejemplo. Pero, ¿qué hace una buena abstracción? ¿Qué queremos de las abstracciones? ¿Y cómo
se relacionan con las pruebas?
Un tema clave en este libro, oculto entre los patrones de fantasía, es que podemos usar abstracciones
simples para ocultar detalles desordenados. Cuando estamos escribiendo código por diversión, o en
un kata,1 podemos jugar con ideas libremente, elaborando cosas y refactorizando agresivamente. Sin
embargo, en un sistema a gran escala, nos vemos limitados por las decisiones que se toman en otras
partes del sistema.
Cuando no podemos cambiar el componente A por temor a romper el componente B, decimos que los
componentes se han acoplado. A nivel local, el acoplamiento es algo bueno: es una señal de que
nuestro código está trabajando en conjunto, cada componente apoya a los demás, todos encajando en
su lugar como los engranajes de un reloj. En la jerga, decimos que esto funciona cuando hay una alta
cohesión entre los elementos acoplados.
1 Un código kata es un desafío de programación pequeño y contenido que a menudo se usa para practicar TDD. Véase “Kata: el único
Manera de aprender TDD” por Peter Provost.
41
Machine Translated by Google
A nivel mundial, el acoplamiento es una molestia: aumenta el riesgo y el costo de cambiar nuestro código, a veces
hasta el punto en que nos sentimos incapaces de realizar ningún cambio. Este es el problema con el patrón Ball
of Mud: a medida que crece la aplicación, si no podemos evitar el acoplamiento entre elementos que no tienen
cohesión, ese acoplamiento aumenta superlinealmente hasta que ya no podemos cambiar nuestros sistemas de
manera efectiva.
Podemos reducir el grado de acoplamiento dentro de un sistema (Figura 3-1) abstrayendo los detalles (Figura 3-2).
En ambos diagramas, tenemos un par de subsistemas, uno dependiente del otro. En la Figura 3-1, hay un alto
grado de acoplamiento entre los dos; el número de flechas indica muchos tipos de dependencias entre los dos. Si
necesitamos cambiar el sistema B, es muy probable que el cambio se extienda al sistema A.
En la figura 3-2, sin embargo, hemos reducido el grado de acoplamiento al insertar una abstracción nueva y más
simple. Debido a que es más simple, el sistema A tiene menos tipos de dependencias en la abstracción. La
abstracción sirve para protegernos del cambio al ocultar los detalles complejos de cualquier cosa que haga el
sistema B: podemos cambiar las flechas de la derecha sin cambiar las de la izquierda.
Veamos un ejemplo. Imagine que queremos escribir código para sincronizar dos directorios de archivos, a
los que llamaremos origen y destino:
origen, pero tiene un nombre diferente al del destino, cambie el nombre del archivo de destino para que
coincida.
Nuestros requisitos primero y tercero son bastante simples: solo podemos comparar dos listas de rutas. Sin
embargo, nuestro segundo es más complicado. Para detectar cambios de nombre, tendremos que
inspeccionar el contenido de los archivos. Para ello, podemos utilizar una función hash como MD5 o SHA-1.
El código para generar un hash SHA-1 a partir de un archivo es bastante simple:
hasher.update(buf) buf =
file.read(BLOCKSIZE)
devolver hasher.hexdigest()
Ahora tenemos que escribir la parte que toma las decisiones sobre qué hacer: la lógica empresarial, por así
decirlo.
Cuando tenemos que abordar un problema desde los primeros principios, generalmente tratamos de escribir
una implementación simple y luego refactorizar hacia un mejor diseño. Usaremos este enfoque a lo largo del
libro, porque es la forma en que escribimos código en el mundo real: comience con una solución para la parte
más pequeña del problema y luego haga que la solución sea más rica y mejor diseñada.
import hashlib
import os import
shutil from pathlib
import Path
source_hashes = {}
para carpeta, archivos
_, en os.walk(fuente): para fn en
archivos:
source_hashes[hash_file(Ruta(carpeta) / fn)] = fn
seen = set() # Realizar un seguimiento de los archivos que hemos encontrado en el objetivo
# para cada archivo que aparece en el origen pero no en el destino, copie el archivo # en
el destino para src_hash, fn en source_hashes.items(): si src_hash no se ve:
¡Fantástico! Tenemos algo de código y se ve bien, pero antes de ejecutarlo en nuestro disco
duro, tal vez deberíamos probarlo. ¿Cómo hacemos para probar este tipo de cosas?
def test_when_a_file_exists_in_the_source_but_not_the_destination():
finalmente: shutil.rmtree
(fuente) shutil.rmtree (destino)
def prueba_cuando_un_archivo_ha_sido_renombrado_en_la_fuente():
finalmente: shutil.rmtree
(fuente) shutil.rmtree (destino)
¡Wowsers, eso es mucha configuración para dos casos simples! El problema es que nuestra lógica de dominio,
"descubrir la diferencia entre dos directorios", está estrechamente relacionada con el código de E/S. No podemos
ejecutar nuestro algoritmo de diferencias sin llamar a pathlib, shutil y
módulos hashlib .
Y el problema es que, incluso con nuestros requisitos actuales, no hemos escrito suficientes pruebas: la
implementación actual tiene varios errores (el shutil.move() es incorrecto, por ejemplo). Obtener una cobertura
decente y revelar estos errores significa escribir más pruebas, pero si todas son tan difíciles de manejar como las
anteriores, eso se volverá muy doloroso muy rápidamente.
Además de eso, nuestro código no es muy extensible. Imagínese intentar implementar un indicador de ejecución
en seco que haga que nuestro código simplemente imprima lo que va a hacer, en lugar de hacerlo realmente. ¿O
qué pasaría si quisiéramos sincronizar con un servidor remoto o con el almacenamiento en la nube?
Nuestro código de alto nivel está acoplado a detalles de bajo nivel y está complicando la vida. A medida que los
escenarios que consideramos se vuelvan más complejos, nuestras pruebas se volverán más difíciles de manejar.
Definitivamente podemos refactorizar estas pruebas (algunas de las tareas de limpieza podrían incluirse en
dispositivos de prueba de pytest, por ejemplo), pero mientras realicemos operaciones del sistema de archivos, se
mantendrán lentas y serán difíciles de leer y escribir.
Primero, debemos pensar en lo que nuestro código necesita del sistema de archivos. Al leer el código, podemos
ver que están sucediendo tres cosas distintas. Podemos pensar en estos como tres responsabilidades distintas
que tiene el código:
1. Interrogamos el sistema de archivos usando os.walk y determinamos hashes para una serie de rutas. Esto
es similar tanto en el caso de origen como en el de destino.
Recuerde que queremos encontrar abstracciones simplificadoras para cada una de estas responsabilidades.
Eso nos permitirá ocultar los detalles desordenados para que podamos concentrarnos en la lógica interesante.2
Para los pasos 1 y 2, ya comenzamos intuitivamente a usar una abstracción, un diccionario de hash para rutas.
Es posible que ya haya estado pensando: "¿Por qué no crea un diccionario para la carpeta de destino, así como
la fuente, y luego simplemente comparamos dos diccionarios?" Esa parece una buena manera de abstraer el
estado actual del sistema de archivos:
¿Qué pasa con pasar del paso 2 al paso 3? ¿Cómo podemos abstraer la interacción real de mover/copiar/
eliminar el sistema de archivos?
Aplicaremos aquí un truco que emplearemos a gran escala más adelante en el libro. Vamos a separar lo que
queremos hacer de cómo hacerlo. Vamos a hacer que nuestro programa genere una lista de comandos que se
vean así:
Ahora podríamos escribir pruebas que solo usen dos dictados del sistema de archivos como entradas, y
esperaríamos listas de tuplas de cadenas que representen acciones como salidas.
2 Si está acostumbrado a pensar en términos de interfaces, eso es lo que estamos tratando de definir aquí.
En lugar de decir: "Dado este sistema de archivos real, cuando ejecute mi función, verifique qué
acciones han sucedido", decimos: "Dada esta abstracción de un sistema de archivos, ¿qué
abstracción de acciones del sistema de archivos sucederá?"
def prueba_cuando_un_archivo_existe_en_el_origen_pero_no_en_el_destino():
src_hashes = {'hash1': 'fn1'} dst_hashes = {} acciones_esperadas = [('COPY', '/
src/fn1', '/dst/fn1')]
...
def prueba_cuando_un_archivo_ha_sido_renombrado_en_la_fuente():
src_hashes = {'hash1': 'fn1'}
dst_hashes = {' hash1': ' fn2 '}
acciones_esperadas == [('MOVER', '/dst/fn2', '/dst/fn1')]
...
Nuestro objetivo es aislar la parte inteligente de nuestro sistema y poder probarlo a fondo sin
necesidad de configurar un sistema de archivos real. Crearemos un "núcleo" de código que no
dependa del estado externo y luego veremos cómo responde cuando le damos información del
mundo exterior (este tipo de enfoque fue caracterizado por Gary Bernhardt como Functional Core,
Imperative Shell o FCIS).
Comencemos por dividir el código para separar las partes con estado de la lógica.
Y nuestra función de nivel superior no contendrá casi ninguna lógica; es solo una serie imperativa de
pasos: recopilar entradas, llamar a nuestra lógica, aplicar salidas:
si acción == 'eliminar':
os.remove(rutas[0])
Aquí está la primera función que eliminamos, read_paths_and_hashes(), que aísla la parte de E/S de
nuestra aplicación.
El código para construir el diccionario de rutas y hashes ahora es trivialmente fácil de escribir:
def read_paths_and_hashes(root):
hashes = {} for folder, files in
os.walk(root): for in files:
_, fnhashes[hash_file(Path(folder) /
fn)] = fn
devolver hashes
La función determine_actions() será el núcleo de nuestra lógica comercial, que dice: "Dados estos dos
conjuntos de hash y nombres de archivo, ¿qué debemos copiar/mover/eliminar?". Toma estructuras de datos
simples y devuelve estructuras de datos simples:
...
def prueba_cuando_un_archivo_ha_sido_renombrado_en_la_fuente():
src_hashes = {'hash1': 'fn1'}
dst_hashes = {'hash1': 'fn2'} acciones
= determine_actions(src_hashes, dst_hashes, Path('/src'), Path('/dst')) asertar lista(acciones ) ==
[('mover', Ruta('/dst/fn2'), Ruta('/dst/fn1'))]
Debido a que hemos desenredado la lógica de nuestro programa, el código para identificar cambios, de los
detalles de bajo nivel de E/S, podemos probar fácilmente el núcleo de nuestro código.
Con este enfoque, pasamos de probar nuestra función de punto de entrada principal, sync(), a probar una
función de nivel inferior, determine_actions(). Puede decidir que está bien porque sync() ahora es muy
simple. O puede decidir mantener algunas pruebas de integración/aceptación para probar esa
sincronización(). Pero hay otra opción, que es modificar la función sync() para que pueda probarse de
forma unitaria y de extremo a extremo; es un enfoque que Bob denomina pruebas de extremo a extremo.
Cuando comenzamos a escribir un nuevo sistema, a menudo nos enfocamos primero en la lógica central,
conduciéndola con pruebas unitarias directas. Sin embargo, en algún momento, queremos probar juntos
partes más grandes del sistema.
Podríamos volver a nuestras pruebas de extremo a extremo, pero siguen siendo tan difíciles de escribir y
mantener como antes. En cambio, a menudo escribimos pruebas que invocan un sistema completo pero
falsifican la E/S, de borde a borde:
source_hashes = lector(source_root)
dest_hashes = lector(dest_root)
Nuestra función de nivel superior ahora expone dos nuevas dependencias, un lector y un sistema de
archivos.
Pruebas usando DI
A Bob le encanta usar listas para crear dobles de prueba simples, aunque sus compañeros de trabajo se
enojen. Significa que podemos escribir pruebas como afirmar foo no en la base de datos.
Cada método en nuestro FakeFileSystem simplemente agrega algo a la lista para que podamos inspeccionarlo
más tarde. Este es un ejemplo de un objeto espía.
La ventaja de este enfoque es que nuestras pruebas actúan exactamente sobre la misma función que utiliza nuestro
código de producción. La desventaja es que tenemos que hacer explícitos nuestros componentes con estado y
pasarlos. David Heinemeier Hansson, el creador de Ruby on Rails, describió esto como "daño de diseño inducido por
pruebas".
En cualquier caso, ahora podemos trabajar para corregir todos los errores en nuestra implementación; ahora es
mucho más fácil enumerar las pruebas para todos los casos extremos.
En este punto, es posible que se esté rascando la cabeza y pensando: "¿Por qué no usa mock.patch y se ahorra el
esfuerzo?"
Evitamos el uso de simulacros en este libro y también en nuestro código de producción. No vamos a entrar en una
Guerra Santa, pero nuestro instinto es que los marcos burlones, en particular los parches mono, son un olor a código.
En cambio, nos gusta identificar claramente las responsabilidades en nuestra base de código y separar esas
responsabilidades en objetos pequeños y enfocados que son fáciles de reemplazar con un doble de prueba.
• Parchear la dependencia que está utilizando hace posible la prueba unitaria del código, pero no hace nada para
mejorar el diseño. El uso de mock.patch no permitirá que su código funcione con un indicador de ejecución en
seco , ni lo ayudará a ejecutarse contra un servidor FTP. Para eso, necesitarás introducir abstracciones.
• Las pruebas que usan simulacros tienden a estar más acopladas a los detalles de implementación del código
base. Eso es porque las pruebas simuladas verifican las interacciones entre las cosas: ¿llamamos a shutil.copy
con los argumentos correctos? Este acoplamiento entre el código y la prueba tiende a hacer que las pruebas
sean más frágiles, según nuestra experiencia. • El uso excesivo de simulacros genera conjuntos de pruebas
• Los simulacros se usan para verificar cómo se usa algo; tienen métodos como afirmar_llamado_una
vez_con(). Están asociados con la escuela de Londres TDD. • Las falsificaciones son
implementaciones funcionales de lo que reemplazan, pero están diseñadas para usarse solo en
pruebas. No funcionarían “en la vida real”; nuestro repositorio en memoria es un buen ejemplo.
Pero puede usarlos para hacer afirmaciones sobre el estado final de un sistema en lugar de los
comportamientos a lo largo del camino, por lo que están asociados con TDD de estilo clásico.
Estamos combinando ligeramente simulacros con espías y falsificaciones con stubs aquí, y puede
leer la respuesta larga y correcta en el ensayo clásico de Martin Fowler sobre el tema llamado "Mocks
Aren't Stubs".
Probablemente tampoco ayude que los objetos MagicMock proporcionados por unittest.mock no
sean, estrictamente hablando, simulacros; son espías, en todo caso. Pero también se usan a menudo
como stubs o dummy. Listo, prometemos que ya hemos terminado con los detalles de doble
terminología de la prueba.
¿Qué hay de la escuela londinense frente al TDD de estilo clásico? Puede leer más sobre estos dos
en el artículo de Martin Fowler que acabamos de citar, así como en el sitio de Software Engineering
Stack Exchange, pero en este libro estamos firmemente en el campo clásico. Nos gusta construir
nuestras pruebas en torno al estado tanto en la configuración como en las aserciones, y nos gusta
trabajar al más alto nivel de abstracción posible en lugar de verificar el comportamiento de los
colaboradores intermediarios.3 Lea más sobre esto en “Sobre la decisión de qué tipo de Pruebas
Vemos TDD como una práctica de diseño en primer lugar y una práctica de prueba en segundo lugar. Las pruebas
actúan como un registro de nuestras elecciones de diseño y sirven para explicarnos el sistema cuando volvemos
al código después de una larga ausencia.
3 Lo cual no quiere decir que pensemos que la gente de la escuela de Londres está equivocada. Algunas personas increíblemente inteligentes trabajan de esa manera.
Las pruebas que utilizan demasiados simulacros se ven abrumadas por el código de configuración que oculta la
historia que nos interesa.
Steve Freeman tiene un gran ejemplo de pruebas superpuestas en su charla "Desarrollo basado en pruebas".
También debe consultar esta charla de PyCon, "Mocking and Patching Pitfalls", de nuestro estimado revisor de
tecnología, Ed Jung, que también aborda la burla y sus alternativas. Y mientras recomendamos charlas, no se pierda
a Brandon Rhodes hablando sobre "Elevación de su E/S", que cubre muy bien los problemas de los que estamos
hablando, usando otro ejemplo simple.
Consulte el Capítulo 13 para obtener más información sobre cómo hacer que nuestra inyección de dependencia sea
más explícita y centralizada.
Envolver
Veremos que esta idea surge una y otra vez en el libro: podemos hacer que nuestros sistemas sean más fáciles de
probar y mantener al simplificar la interfaz entre nuestra lógica comercial y las E/S desordenadas. Encontrar la
abstracción correcta es complicado, pero aquí hay algunas heurísticas y preguntas que debe hacerse:
• ¿Puedo elegir una estructura de datos familiar de Python para representar el estado del sistema desordenado y
luego tratar de imaginar una sola función que pueda devolver ese estado? • ¿Dónde puedo trazar una línea
entre mis sistemas, dónde puedo tallar una costura para pegar esa abstracción?
• ¿Cuál es una forma sensata de dividir las cosas en componentes con diferentes responsabilidades? ¿Qué
conceptos implícitos puedo hacer explícitos?
Resumen | 53
Machine Translated by Google
¡La práctica hace menos imperfecto! Y ahora volvamos a nuestra programación habitual…
CAPÍTULO 4
¡Volvamos a nuestro proyecto de asignaciones! La Figura 4-1 muestra el punto al que llegamos al final
del Capítulo 2, que cubría el patrón Repositorio.
Figura 4-1. Antes: manejamos nuestra aplicación hablando con repositorios y el modelo de dominio
55
Machine Translated by Google
En este capítulo, analizamos las diferencias entre la lógica de orquestación, la lógica comercial y el código de interfaz, y
presentamos el patrón de capa de servicio para encargarse de orquestar nuestros flujos de trabajo y definir los casos de
uso de nuestro sistema.
También hablaremos sobre las pruebas: al combinar la capa de servicio con nuestra abstracción de repositorio sobre la
base de datos, podemos escribir pruebas rápidas, no solo de nuestro modelo de dominio, sino de todo el flujo de trabajo
para un caso de uso.
La Figura 4-2 muestra lo que buscamos: vamos a agregar una API de Flask que se comunicará con la capa de servicio,
que servirá como punto de entrada a nuestro modelo de dominio. Debido a que nuestra capa de servicio depende de
AbstractRepository, podemos realizar pruebas unitarias usando FakeRepository pero ejecutar nuestro código de producción
usando SqlAlchemyRepository.
Figura 4-2. La capa de servicio se convertirá en la principal vía de acceso a nuestra aplicación.
En nuestros diagramas, usamos la convención de que los componentes nuevos se resaltan con texto/líneas
en negrita (y color amarillo/naranja, si está leyendo una versión digital).
Conectemos todas las partes móviles lo más rápido que podamos y luego refactoricemos hacia una arquitectura
más limpia. Este es nuestro plan:
1. Use Flask para colocar un punto final de API frente a nuestro servicio de asignación de dominio. Conecte
la sesión de la base de datos y nuestro repositorio. Pruébelo con una prueba de extremo a extremo y
algo de SQL rápido y sucio para preparar los datos de prueba.
2. Refactorice una capa de servicio que pueda servir como abstracción para capturar el caso de uso y que
se ubicará entre Flask y nuestro modelo de dominio. Construye algún servicio
3. Experimente con diferentes tipos de parámetros para nuestras funciones de capa de servicio; muestran
que el uso de tipos de datos primitivos permite que los clientes de la capa de servicio (nuestras pruebas
y nuestra API de Flask) se desacoplen de la capa del modelo.
Nadie está interesado en entrar en un debate terminológico largo sobre lo que cuenta como una prueba de
extremo a extremo (E2E) frente a una prueba funcional frente a una prueba de aceptación frente a una prueba
de integración frente a una prueba unitaria. Diferentes proyectos necesitan diferentes combinaciones de
pruebas, y hemos visto proyectos perfectamente exitosos que simplemente dividen las cosas en "pruebas
rápidas" y "pruebas lentas".
Por ahora, queremos escribir una o tal vez dos pruebas que van a ejercer un "real"
Punto final de API (usando HTTP) y hable con una base de datos real. Llamémoslas pruebas de extremo a
extremo porque es uno de los nombres que más se explica por sí mismo.
@pytest.mark.usefixtures('restart_api') def
test_api_returns_allocation(add_stock):
sku, otrosku = random_sku(), random_sku('otro') earlybatch
= random_batchref(1) laterbatch = random_batchref(2) otro
lote = random_batchref (3) add_stock([
add_stock es un dispositivo de ayuda que simplemente oculta los detalles de la inserción manual de filas en la
base de datos usando SQL. Mostraremos una mejor manera de hacer esto más adelante en el capítulo.
Todos resuelven estos problemas de diferentes maneras, pero necesitará alguna forma de hacer girar Flask,
posiblemente en un contenedor, y de comunicarse con una base de datos de Postgres. Si desea ver cómo lo hicimos,
consulte el Apéndice B.
La implementación directa
Implementando las cosas de la manera más obvia, podría obtener algo como esto:
importar
configuración
importar modelo importar orm
importar repositorio
orm.start_mappers()
get_session = sessionmaker(bind=create_engine(config.get_postgres_uri())) app =
Flask(__name__)
Hasta aquí todo bien. No hay necesidad de mucho más de su sinsentido de "arquitectura astronauta",
Bob y Harry, pueden estar pensando.
Pero espera un minuto, no hay confirmación. En realidad, no estamos guardando nuestra asignación
en la base de datos. Ahora necesitamos una segunda prueba, ya sea una que inspeccione el estado
de la base de datos después (no muy recuadro negro), o tal vez una que verifique que no podemos
asignar una segunda línea si una primera ya debería haber agotado el lote:
@pytest.mark.usefixtures('restart_api') def
test_allocations_are_persisted(add_stock):
sku = random_sku()
lote1, lote2 = random_batchref(1), random_batchref(2) order1 , order2
= random_orderid(1), random_orderid(2) add_stock([
La implementación sencilla | 59
Machine Translated by Google
Sin embargo, si seguimos así, las cosas se pondrán cada vez más feas.
Supongamos que queremos agregar un poco de manejo de errores. ¿Qué sucede si el dominio genera un error
por un SKU que está agotado? ¿O qué pasa con un SKU que ni siquiera existe? Eso no es
algo que el dominio ni siquiera sabe, ni debería. Es más una verificación de cordura que debemos implementar
en la capa de la base de datos, incluso antes de invocar el servicio de dominio.
@pytest.mark.usefixtures('restart_api') def
test_400_message_for_out_of_stock(add_stock): sku,
smalL_batch, large_order = random_sku(), random_batchref(), random_orderid() add_stock([
@pytest.mark.usefixtures('restart_api') def
test_400_message_for_invalid_sku():
unknown_sku, orderid = random_sku(), random_orderid() data =
{'orderid': orderid, 'sku': unknown_sku, 'qty': 20} url = config.get_api_url()
r = solicitudes.post(f'{url} /asignar', json=datos) afirmar r.status_code ==
400 afirmar r.json()['mensaje'] == f'Sku no válido {unknown_sku}'
En la primera prueba, intentamos asignar más unidades de las que tenemos en stock.
En el segundo, el SKU simplemente no existe (porque nunca llamamos a add_stock), por lo que no es
válido en lo que respecta a nuestra aplicación.
sesión.commit()
devuelve jsonify({'batchref': loteref}), 201
Pero nuestra aplicación Flask está empezando a parecer un poco difícil de manejar. Y nuestro número de
pruebas E2E está empezando a salirse de control, y pronto terminaremos con una pirámide de prueba invertida
(o "modelo de cono de helado", como le gusta llamarlo a Bob).
Si observamos lo que está haciendo nuestra aplicación Flask, hay mucho de lo que podríamos llamar
orquestación: obtener cosas de nuestro repositorio, validar nuestra entrada contra el estado de la base de
datos, manejar errores y comprometerse en el camino feliz. La mayoría de estas cosas no tienen nada que
ver con tener un punto final de API web (las necesitaría si estuviera creando una CLI, por ejemplo; consulte el
Apéndice C), y en realidad no son cosas que deban probarse. por pruebas de extremo a extremo.
A menudo tiene sentido dividir una capa de servicio, a veces llamada capa de orquestación o capa de casos
de uso.
| 61
Introducción a una capa de servicio y uso de FakeRepository para realizar pruebas unitarias
Machine Translated by Google
self._lotes = conjunto(lotes)
def list(self):
return list(self._batches)
Aquí es donde será útil; nos permite probar nuestra capa de servicio con una unidad agradable y rápida
pruebas:
Nuestro módulo de servicios (services.py) definirá una función de capa de servicio allocate() . Se ubicará entre
nuestra función allocate_endpoint() en la capa API y la función de servicio de dominio allocate() de nuestro
modelo de dominio.1
También necesitamos una FakeSession para falsificar la sesión de la base de datos, como se muestra en el
clase FakeSession():
comprometido = Falso
1 Los servicios de la capa de servicio y los servicios de dominio tienen nombres similares que resultan confusos. Abordaremos este tema
más adelante en "¿Por qué todo se llama servicio?" en la página 66.
def commit(self):
self.committed = True
Esta sesión falsa es solo una solución temporal. Nos desharemos de él y haremos las cosas aún mejor pronto, en el
Capítulo 6. Pero mientras tanto, el falso .commit() nos permite migrar una tercera prueba desde la capa E2E:
def test_commits():
línea = modelo.OrderLine('o1', 'OMINOUS-MIRROR', 10) lote
= modelo.Batch('b1', 'OMINOSO-ESPEJO', 100, eta=Ninguno) repo =
FakeRepository([ lote]) sesión = FakeSession()
Hacemos algunas comprobaciones o afirmaciones sobre la solicitud frente al estado actual del mundo.
| 63
Introducción a una capa de servicio y uso de FakeRepository para realizar pruebas unitarias
Machine Translated by Google
Ese último paso es un poco insatisfactorio en este momento, ya que nuestra capa de servicio está
estrechamente acoplada a nuestra capa de base de datos. Mejoraremos eso en el Capítulo 6 con el patrón
Unidad de trabajo.
Depende de un repositorio. Elegimos hacer explícita la dependencia y usamos la sugerencia de tipo para
decir que dependemos de AbstractRepository. Esto significa que funcionará tanto cuando las pruebas le
proporcionen un FakeRepository como cuando la aplicación Flask le proporcione un SqlAlchemyRepository.
Si recuerda "El principio de inversión de dependencia" en la página xxi, esto es lo que queremos decir
cuando decimos que debemos "depender de abstracciones". Nuestro módulo de alto nivel, la capa de
servicio, depende de la abstracción del repositorio. Y los detalles de la implementación de nuestra
elección específica de almacenamiento persistente también dependen de esa misma abstracción.
Consulte las Figuras 4-3 y 4-4.
Consulte también en el Apéndice C un ejemplo resuelto de intercambiar los detalles de qué sistema de
almacenamiento persistente usar mientras se dejan las abstracciones intactas.
Pero lo esencial de la capa de servicio está ahí, y nuestra aplicación Flask ahora se ve mucho más limpia:
)
prueba: batchref = services.allocate(línea, repositorio, sesión)
excepto (model.OutOfStock, services.InvalidSku) como e:
devolver jsonify({'mensaje': str(e)}), 400
Extraemos los comandos del usuario de la solicitud web y los pasamos a un servicio de dominio.
Las responsabilidades de la aplicación Flask son solo cosas web estándar: administración de
sesiones por solicitud, análisis de información de parámetros POST, códigos de estado de respuesta
y JSON. Toda la lógica de orquestación está en la capa de caso de uso/servicio, y la lógica del
dominio permanece en el dominio.
Finalmente, podemos reducir con confianza nuestras pruebas E2E a solo dos, una para el camino
feliz y otra para el camino infeliz:
@pytest.mark.usefixtures('restart_api') def
test_happy_path_returns_201_and_allocated_batch(add_stock): sku, otherku
= random_sku(), random_sku('other') earlybatch = random_batchref(1)
laterbatch = random_batchref(2) otro lote = random_batchref (3) add_stock
([
@pytest.mark.usefixtures('restart_api') def
test_unhappy_path_returns_400_and_error_message():
unknown_sku, orderid = random_sku(), random_orderid() data =
{'orderid': orderid, 'sku': unknown_sku, 'qty': 20} url = config.get_api_url()
r = solicitudes.post(f'{url} /asignar', json=datos) afirmar r.status_code ==
400 afirmar r.json()['mensaje'] == f'Sku no válido {unknown_sku}'
Hemos dividido con éxito nuestras pruebas en dos amplias categorías: pruebas sobre material web,
que implementamos de principio a fin; y pruebas sobre cosas de orquestación, que podemos probar
contra la capa de servicio en la memoria.
Introducción a una capa de servicio y uso de FakeRepository para realizar pruebas unitarias
| sesenta y cinco
Machine Translated by Google
Si eso no es suficiente, continúe con las pruebas E2E y ask_app.py, y refactorice el adaptador Flask para que sea
más RESTful. ¡Observe cómo hacerlo no requiere ningún cambio en nuestra capa de servicio o capa de dominio!
Si decide que desea crear un punto final de solo lectura para recuperar
información de asignación, simplemente haga "lo más simple que
pueda funcionar", que es repo.get() directamente en el controlador Flask.
Hablaremos más sobre lecturas versus escrituras en el Capítulo 12.
Lo sentimos, no elegimos los nombres, o tendríamos formas mucho más geniales y amigables de hablar
sobre estas cosas.
Estamos usando dos cosas llamadas un servicio en este capítulo. El primero es un servicio de aplicación
(nuestra capa de servicio). Su trabajo es manejar las solicitudes del mundo exterior y orquestar una operación.
Lo que queremos decir es que la capa de servicio impulsa la aplicación siguiendo una serie de pasos simples:
Este es el tipo de trabajo aburrido que tiene que ocurrir para cada operación en su sistema, y mantenerlo
separado de la lógica comercial ayuda a mantener las cosas ordenadas.
El segundo tipo de servicio es un servicio de dominio. Este es el nombre de una parte de la lógica que
pertenece al modelo de dominio pero que no se encuentra naturalmente dentro de una entidad con estado o
un objeto de valor. Por ejemplo, si estuviera creando una aplicación de carrito de compras, podría optar por
crear reglas de impuestos como un servicio de dominio. Calcular impuestos es un trabajo separado de
actualizar el carrito y es una parte importante del modelo, pero no parece correcto tener una entidad
persistente para el trabajo. En su lugar, una clase TaxCalculator sin estado o una función de cálculo de
impuestos pueden hacer el trabajo.
Algunas subcarpetas
.
ÿÿÿ config.py ÿÿÿ
dominio ÿ ÿÿÿ
__init__.py
model.py
ÿ ÿÿÿ
ÿÿÿ
service_layer
__init__.py
ÿ ÿÿÿ ÿ
services.py ÿÿÿ adaptadores
ÿ ÿÿÿ __ÿ__.py
repository.py
ÿ orm.py
ÿÿÿ ÿ
ÿÿÿ
puntos de entrada
__init__.py
ÿ ÿÿÿ
ÿfrasco_app.py ÿÿÿ pruebas
ÿÿÿ __init__.py
conftest.pyÿÿÿ
ÿÿÿ
unidadtest_batches.py
ÿ ÿÿÿpy ÿ ÿÿÿ ÿ
ÿÿÿ
test_services.py
integración
ÿÿÿ ÿ ÿÿÿ
test_orm.py ÿ test_repository.py
ÿÿÿ e2e ÿÿÿ
ÿÿÿ
ÿÿÿ
ÿÿÿ
prueba_api.py
Tengamos una carpeta para nuestro modelo de dominio. Actualmente es solo un archivo, pero para una aplicación
más compleja, puede tener un archivo por clase; es posible que tenga clases principales auxiliares para Entity,
ValueObject y Aggregate, y puede agregar un archivoExceptions.py para las excepciones de la capa de dominio y,
como verá en la Parte II, commands.py y events.py.
Distinguiremos la capa de servicio. Actualmente, ese es solo un archivo llamado services.py para nuestras funciones
de capa de servicio. Podría agregar excepciones de capa de servicio aquí y, como verá en el Capítulo 5, agregaremos
unit_of_work.py.
Adaptadores es un guiño a la terminología de puertos y adaptadores. Esto se completará con cualquier otra
abstracción en torno a la E/S externa (por ejemplo, un redis_client.py). Estrictamente hablando, llamaría a
estos adaptadores secundarios o adaptadores impulsados, oa veces adaptadores orientados hacia adentro.
Los puntos de entrada son los lugares desde los que manejamos nuestra aplicación. En la terminología
oficial de puertos y adaptadores, estos también son adaptadores y se denominan adaptadores primarios,
impulsores o orientados hacia el exterior.
¿Qué pasa con los puertos? Como recordará, son las interfaces abstractas que implementan los adaptadores.
Tendemos a mantenerlos en el mismo archivo que los adaptadores que los implementan.
Envolver
Agregar la capa de servicio realmente nos ha comprado mucho:
• Nuestros extremos de API de Flask se vuelven muy delgados y fáciles de escribir: su única responsabilidad
es hacer "cosas web", como analizar JSON y producir los códigos HTTP correctos para casos felices o
desafortunados.
• Hemos definido una API clara para nuestro dominio, un conjunto de casos de uso o puntos de entrada que
puede usar cualquier adaptador sin necesidad de saber nada sobre las clases de nuestro modelo de
dominio, ya sea una API, una CLI (consulte el Apéndice C), o las pruebas!
También son un adaptador para nuestro dominio.
• Podemos escribir pruebas a toda velocidad mediante el uso de la capa de servicio, lo que nos deja libres
para refactorizar el modelo de dominio de cualquier forma que consideremos adecuada. Siempre que
podamos ofrecer los mismos casos de uso, podemos experimentar con nuevos diseños sin necesidad de
volver a escribir un montón de pruebas.
• Y nuestra pirámide de prueba se ve bien: la mayor parte de nuestras pruebas son pruebas unitarias rápidas,
con solo el mínimo de E2E y pruebas de integración.
El DIP en acción
La Figura 4-3 muestra las dependencias de nuestra capa de servicio: el modelo de dominio y AbstractRepository
(la terminología de puerto, en puertos y adaptadores).
Cuando ejecutamos las pruebas, la Figura 4-4 muestra cómo implementamos las dependencias abstractas
usando FakeRepository (el adaptador).
Y cuando realmente ejecutamos nuestra aplicación, intercambiamos la dependencia "real" que se muestra en la
Figura 4-5.
Resumen | 69
Machine Translated by Google
Maravilloso.
Hagamos una pausa en la Tabla 4-1, en la que consideramos los pros y los contras de tener una
capa de servicio.
ventajas Contras
• Contamos con un solo lugar para capturar todos los casos de uso de • Si su aplicación es puramente una aplicación web, sus controladores/funciones de
nuestra aplicación. • Hemos colocado nuestra lógica de dominio vista pueden ser el único lugar para capturar todos los casos de uso. • Es otra
inteligente detrás de una API, lo que nos deja libres para refactorizar. capa más de abstracción. • Poner demasiada lógica en la capa de servicio puede
• Hemos separado claramente "stu que habla HTTP" de "stu que habla detectar la lógica de orquestación que se infiltra en los controladores.
asignación".
FakeRepository, tenemos una buena manera de escribir pruebas en • Puede obtener muchos de los beneficios que se derivan de tener un rico
un nivel más alto que la capa de dominio; podemos probar más de modelos de dominio simplemente sacando la lógica de sus controladores y
nuestro flujo de trabajo sin necesidad de usar pruebas de integración bajando a la capa del modelo, sin necesidad de agregar una capa adicional en el
(lea el Capítulo 5 para obtener más detalles al respecto). medio (también conocido como "modelos pesados, controladores delgados").
• La capa de servicio todavía está estrechamente unida al dominio, porque su API se expresa en
términos de objetos OrderLine . En el Capítulo 5, solucionaremos eso y hablaremos sobre la
forma en que la capa de servicio permite un TDD más productivo.
CAPÍTULO 5
Hemos introducido la capa de servicio para capturar algunas de las responsabilidades de orquestación
adicionales que necesitamos de una aplicación en funcionamiento. La capa de servicio nos ayuda a definir
claramente nuestros casos de uso y el flujo de trabajo para cada uno: qué necesitamos obtener de
nuestros repositorios, qué comprobaciones previas y validación del estado actual debemos hacer, y qué
ahorramos al final.
Pero actualmente, muchas de nuestras pruebas unitarias operan a un nivel inferior, actuando directamente
sobre el modelo. En este capítulo, discutiremos las compensaciones involucradas en mover esas pruebas
al nivel de la capa de servicio, y algunas pautas de prueba más generales.
Harry dice: Ver una pirámide de prueba en acción fue un momento de iluminación
Aquí hay algunas palabras de Harry directamente: Al principio era escéptico de todos los
patrones arquitectónicos de Bob, pero ver una pirámide de prueba real me convirtió.
Una vez que implementa el modelado de dominio y la capa de servicio, realmente puede llegar a una etapa
en la que las pruebas unitarias superan en número a las pruebas de integración y de extremo a extremo en
un orden de magnitud. Después de haber trabajado en lugares donde la compilación de la prueba E2E
tomaría horas ("esperar hasta mañana", esencialmente), no puedo decirle qué diferencia hace poder ejecutar
todas sus pruebas en minutos o segundos.
Siga leyendo para obtener algunas pautas sobre cómo decidir qué tipo de pruebas escribir y en qué nivel.
La forma de pensar de marcha alta versus marcha baja realmente cambió mi vida de prueba.
71
Machine Translated by Google
Veamos qué le hace a nuestra pirámide de prueba este movimiento hacia el uso de una capa de servicio, con sus propias
pruebas de capa de servicio:
pruebas/integración/test_orm.py:6
pruebas/integración/test_repository.py:2
pruebas/e2e/test_api.py:2
¡Nada mal! Tenemos 15 pruebas unitarias, 8 pruebas de integración y solo 2 pruebas de extremo a extremo. Eso ya es
una pirámide de prueba de aspecto saludable.
Veamos qué sucede si llevamos esto un paso más allá. Dado que podemos probar nuestro software contra la capa de
servicio, ya no necesitamos más pruebas para el modelo de dominio.
En su lugar, podríamos reescribir todas las pruebas a nivel de dominio del Capítulo 1 en términos de la capa de servicio:
# prueba de capa de
dominio: def test_prefers_current_stock_batches_to_shipments():
in_stock_batch = Batch("en-stock-batch", "RETRO-CLOCK", 100, eta=Ninguno)
shipping_batch = Batch(" shipment -batch", "RETRO-CLOCK", 100, eta=mañana) line =
OrderLine( "oref", "RETRO-RELOJ", 10)
# prueba de capa de
servicio: def test_prefers_warehouse_batches_to_shipments():
in_stock_batch = Batch("en-stock-batch", "RETRO-CLOCK", 100, eta=Ninguno)
shipping_batch = Batch(" shipment -batch", "RETRO-CLOCK", 100, eta=mañana) repo =
FakeRepository( [in_stock_batch, shipping_batch ]) sesión = FakeSession()
afirmar en_stock_batch.cantidad_disponible == 90
afirmar envío_lote.cantidad_disponible == 100
Se supone que las pruebas nos ayudan a cambiar nuestro sistema sin miedo, pero a menudo vemos equipos que
escriben demasiadas pruebas contra su modelo de dominio. Esto causa problemas cuando cambian su base de
código y descubren que necesitan actualizar decenas o incluso cientos de pruebas unitarias.
Esto tiene sentido si se detiene a pensar en el propósito de las pruebas automatizadas. Usamos pruebas para
hacer cumplir que una propiedad del sistema no cambia mientras estamos trabajando. Usamos pruebas para
verificar que la API continúa devolviendo 200, que la sesión de la base de datos continúa comprometiéndose y
que las órdenes aún se están asignando.
Si accidentalmente cambiamos uno de esos comportamientos, nuestras pruebas fallarán. Sin embargo, la otra
cara de la moneda es que si queremos cambiar el diseño de nuestro código, cualquier prueba que dependa
directamente de ese código también fallará.
A medida que avanzamos en el libro, verá cómo la capa de servicio forma una API para nuestro sistema que
podemos controlar de varias maneras. Probar contra esta API reduce la cantidad de código que necesitamos
cambiar cuando refactorizamos nuestro modelo de dominio. Si nos limitamos a realizar pruebas solo en la capa
de servicio, no tendremos ninguna prueba que interactúe directamente con métodos o atributos "privados" en
nuestros objetos modelo, lo que nos deja más libres para refactorizarlos.
Cada línea de código que ponemos en una prueba es como una gota de pegamento
que mantiene el sistema en una forma particular. Cuantas más pruebas de bajo nivel
tengamos, más difícil será cambiar las cosas.
La programación extrema (XP) nos exhorta a “escuchar el código”. Cuando estamos escribiendo pruebas,
podemos encontrar que el código es difícil de usar o notar un olor a código. Este es un desencadenante
para que refactoricemos y reconsideremos nuestro diseño.
Sin embargo, solo recibimos esa retroalimentación cuando trabajamos de cerca con el código de destino.
Una prueba de la API HTTP no nos dice nada sobre el diseño detallado de nuestros objetos, porque se
encuentra en un nivel de abstracción mucho más alto.
Por otro lado, podemos reescribir toda nuestra aplicación y, mientras no cambiemos las URL o los
formatos de solicitud, nuestras pruebas HTTP seguirán pasando. Esto nos da la confianza de que los
cambios a gran escala, como cambiar el esquema de la base de datos, no han roto nuestro código.
En el otro extremo del espectro, las pruebas que escribimos en el Capítulo 1 nos ayudaron a desarrollar
nuestra comprensión de los objetos que necesitamos. Las pruebas nos guiaron hacia un diseño que tiene
sentido y se lee en el idioma del dominio. Cuando nuestras pruebas se leen en el idioma del dominio, nos
sentimos cómodos de que nuestro código coincida con nuestra intuición sobre el problema que estamos
tratando de resolver.
Debido a que las pruebas están escritas en el lenguaje del dominio, actúan como documentación viva
para nuestro modelo. Un nuevo miembro del equipo puede leer estas pruebas para comprender
rápidamente cómo funciona el sistema y cómo se interrelacionan los conceptos básicos.
A menudo, "esbozamos" nuevos comportamientos escribiendo pruebas en este nivel para ver cómo se
vería el código. Sin embargo, cuando queramos mejorar el diseño del código, necesitaremos reemplazar
o eliminar estas pruebas, ya que están estrechamente vinculadas a una implementación particular.
Por ejemplo, al escribir una función add_stock o una función cancel_order , podemos trabajar más rápido
y con menos acoplamiento al escribir pruebas en la capa de servicio.
La metáfora que usamos es la de cambiar de marcha. Al iniciar un viaje, la bicicleta debe estar en una marcha
baja para que pueda vencer la inercia. Una vez que estamos en marcha, podemos ir más rápido y de manera
más eficiente cambiando a una marcha más alta; pero si de repente nos encontramos con una colina empinada o
nos vemos obligados a reducir la velocidad por un peligro, bajamos de nuevo a una marcha más baja hasta que
podamos volver a acelerar.
Todavía tenemos dependencias directas en el dominio en nuestras pruebas de capa de servicio, porque usamos
objetos de dominio para configurar nuestros datos de prueba e invocar nuestras funciones de capa de servicio.
Para tener una capa de servicio completamente desacoplada del dominio, necesitamos reescribir su API para
que funcione en términos de primitivas.
Pero nuestras pruebas aún dependen del dominio, porque aún creamos instancias de objetos Batch de forma
manual. Entonces, si algún día decidimos refactorizar masivamente cómo funciona nuestro modelo Batch ,
tendremos que cambiar un montón de pruebas.
menos podríamos abstraer eso en una función auxiliar o un accesorio en nuestras pruebas. Aquí hay una
forma de hacerlo, agregando una función de fábrica en FakeRepository:
@staticmethod
def for_batch(ref, sku, qty, eta=Ninguno): return
FakeRepository([ model.Batch(ref, sku, qty,
eta),
])
...
Al menos eso movería todas las dependencias de nuestras pruebas en el dominio a un solo lugar.
embargo, podríamos ir un paso más allá. Si tuviéramos un servicio para agregar stock, podríamos usarlo y
hacer que nuestras pruebas de capa de servicio se expresen completamente en términos de los casos de
uso oficiales de la capa de servicio, eliminando todas las dependencias en el dominio:
def añadir_lote(
ref: str, sku: str, qty: int, eta: Optional[date], repo: AbstractRepository,
session,
):
repo.add(model.Batch(ref, sku, qty, eta)) session.commit()
...
Eso ahora nos permite reescribir todas nuestras pruebas de la capa de servicio puramente en términos de los servicios
mismos, usando solo primitivas y sin ninguna dependencia del modelo:
def test_allocate_errors_for_invalid_sku():
repositorio, sesión = FakeRepository([]), FakeSession()
services.add_batch("b1", "AREALSKU", 100, Ninguno, repositorio, sesión)
Este es un lugar realmente agradable para estar. Nuestras pruebas de capa de servicio dependen solo de la capa de
servicio en sí, lo que nos deja completamente libres para refactorizar el modelo como mejor nos parezca.
Gracias a nuestra función de servicio, agregar el punto final es fácil, con solo un poco de JSON y una sola
llamada de función requerida:
Y nuestras consultas SQL codificadas de conftest.py se reemplazan con algunas llamadas a la API, lo que
significa que las pruebas de la API no tienen otras dependencias que la API, lo que también es bueno:
Las pruebas de API ahora pueden agregar sus propios lotes (tests/e2e/test_api.py)
@pytest.mark.usefixtures('postgres_db')
@pytest.mark.usefixtures('restart_api') def
test_happy_path_returns_201_and_allocated_batch(): sku, otherku
= random_sku(), random_sku('other')
Envolver
Una vez que tenga una capa de servicio en su lugar, realmente puede mover la mayor parte de su cobertura de
prueba a pruebas unitarias y desarrollar una pirámide de prueba saludable.
Esto podría escribirse contra una API HTTP, por ejemplo. El objetivo es demostrar que la característica
funciona y que todas las partes móviles están pegadas correctamente.
1 Una preocupación válida acerca de escribir pruebas en un nivel más alto es que puede conducir a una explosión combinatoria para más
casos de uso complejos. En estos casos, puede ser útil descender a pruebas unitarias de nivel inferior de los diversos objetos de dominio que
colaboran. Pero consulte también el Capítulo 8 y “Opcionalmente: Controladores de eventos de pruebas unitarias de forma aislada con un
bus de mensajes falsos” en la página 147.
Resumen | 79
Machine Translated by Google
un mundo ideal, tendrá todos los servicios que necesita para poder probar completamente contra
la capa de servicio, en lugar de piratear el estado a través de repositorios o la base de datos.
Esto también vale la pena en sus pruebas de extremo a extremo.
CAPÍTULO 6
En este capítulo, presentaremos la pieza final del rompecabezas que une los patrones de Repositorio
y Capa de Servicio: el patrón de Unidad de Trabajo.
La Figura 6-1 muestra que, actualmente, se produce mucha comunicación entre las capas de nuestra
infraestructura: la API se comunica directamente con la capa de la base de datos para iniciar una
sesión, se comunica con la capa del repositorio para inicializar SQLAlchemyRepository y se comunica
con el servicio capa para pedirle que asigne.
81
Machine Translated by Google
Figura 6-1. Sin UoW: API habla directamente con tres capas
La figura 6-2 muestra nuestro estado objetivo. La API de Flask ahora solo hace dos cosas: inicializa una unidad de
trabajo e invoca un servicio. El servicio colabora con la UoW (nos gusta pensar que la UoW es parte de la capa de
servicio), pero ni la función del servicio en sí ni Flask ahora necesitan comunicarse directamente con la base de
datos.
Y lo haremos todo utilizando una hermosa pieza de sintaxis de Python, un administrador de contexto.
Figura 6-2. Con UoW: UoW ahora administra el estado de la base de datos
lotes = uow.lotes.lista()
...
loteref = modelo.asignar(línea, lotes) uow.commit()
uow.batches es el repositorio de lotes, por lo que la UoW nos proporciona acceso a nuestro almacenamiento
permanente.
La UoW actúa como un único punto de entrada a nuestro almacenamiento persistente y realiza un seguimiento de los
objetos que se cargaron y del estado más reciente.1
• Una instantánea estable de la base de datos con la que trabajar, de modo que los objetos que usamos no cambien
a la mitad de una operación. • Una forma de conservar todos nuestros cambios a la vez, de modo que si algo
sale mal, no
terminar en un estado inconsistente
• Una API simple para nuestras preocupaciones de persistencia y un lugar práctico para obtener un repositorio
uow = unidad_de_trabajo.SqlAlchemyUnitOfWork(session_factory)
with uow: lote = uow.lotes.get(referencia='lote1') línea =
modelo.OrderLine('o1', 'HIPSTER-WORKBENCH', 10)
lote.asignar(línea) uow .comprometerse()
1 Es posible que haya encontrado el uso de la palabra colaboradores para describir objetos que trabajan juntos para lograr un
objetivo. La unidad de trabajo y el repositorio son un gran ejemplo de colaboradores en el sentido de modelado de objetos. En
el diseño impulsado por la responsabilidad, los grupos de objetos que colaboran en sus roles se denominan vecindarios de
objetos, lo que es, en nuestra opinión profesional, totalmente adorable.
Inicializamos el UoW usando nuestra fábrica de sesión personalizada y obtenemos un objeto uow
para usar en nuestro bloque with .
En nuestras pruebas, hemos definido implícitamente una interfaz para lo que debe hacer una UoW.
Hagámoslo explícito usando una clase base abstracta:
clase AbstractUnitOfWork(abc.ABC):
lotes: repositorio.AbstractRepository
@abc.abstractmethod def
commit(self): aumentar
NotImplementedError
@abc.abstractmethod
def rollback(self):
aumentar NotImplementedError
La UoW proporciona un atributo llamado .batches, que nos dará acceso al repositorio de lotes.
Si nunca has visto un administrador de contexto, __enter__ y __exit__ son los dos métodos mágicos que se
ejecutan cuando ingresamos al bloque with y cuando salimos de él, respectivamente. Son nuestras fases de
instalación y desmontaje.
Llamaremos a este método para confirmar explícitamente nuestro trabajo cuando estemos listos.
Si no nos comprometemos, o si salimos del administrador de contexto generando un error, hacemos una
reversión. (La reversión no tiene efecto si se ha llamado a commit() . Siga leyendo para obtener más información
sobre esto).
DEFAULT_SESSION_FACTORY =
sesionista(bind=create_engine( config.get_postgres_uri(),
))
clase SqlAlchemyUnitOfWork(AbstractUnitOfWork):
def __enter__(self):
self.session = self.session_factory() # tipo: Sesión self.batches =
repository.SqlAlchemyRepository(self.session) return super().__enter__()
def commit(self):
self.session.commit()
def revertir(auto):
self.session.rollback()
El módulo define una fábrica de sesiones predeterminada que se conectará a Postgres, pero permitimos que se
anule en nuestras pruebas de integración para que podamos usar SQLite en su lugar.
El método __enter__ es responsable de iniciar una sesión de base de datos e instanciar un repositorio real que
pueda usar esa sesión.
Finalmente, proporcionamos métodos concretos commit() y rollback() que utilizan nuestra sesión de base de datos.
Así es como usamos una UoW falsa en nuestras pruebas de capa de servicio:
def __init__(self):
self.lotes = FakeRepository([])
self.committed = False
def commit(self):
self.committed = True
def test_add_batch():
uow = FakeUnitOfWork()
services.add_batch("b1", "CRUNCHY-ARMCHAIR", 100, None, uow)
afirmar uow.batches.get("b1") no es Ninguno afirmar uow.committed
...
Observe la similitud con la función commit() falsa de FakeSession (de la que ahora podemos
deshacernos). Pero es una mejora sustancial porque ahora estamos falsificando código que
escribimos nosotros en lugar de código de terceros. Algunas personas dicen: “No te burles de
lo que no tienes”.
En nuestras pruebas, podemos instanciar una UoW y pasarla a nuestra capa de servicio, en
lugar de pasar un repositorio y una sesión. Esto es considerablemente menos engorroso.
Si solo nos preocupamos por escribir pruebas que se ejecuten rápidamente, podríamos crear simulacros
que reemplacen a SQLAlchemy y usarlos en toda nuestra base de código. El problema es que Session
es un objeto complejo que expone muchas funciones relacionadas con la persistencia. Es fácil usar
Session para realizar consultas arbitrarias en la base de datos, pero eso lleva rápidamente a que el
código de acceso a los datos se esparza por todo el código base. Para evitar eso, queremos limitar el
acceso a nuestra capa de persistencia para que cada componente tenga exactamente lo que necesita
y nada más.
“No te burles de lo que no tienes” es una regla general que nos obliga a construir estas abstracciones
simples sobre subsistemas desordenados. Esto tiene el mismo beneficio de rendimiento que burlarse
de la sesión de SQLAlchemy, pero nos anima a pensar detenidamente en nuestros diseños.
uow: unidad_de_trabajo.AbstractUnitOfWork
):
con uow:
uow.batches.add(model.Batch(ref, sku, qty, eta))
uow.commit()
Nuestra capa de servicio ahora tiene solo una dependencia, una vez más en un resumen
UoW.
uow = unidad_de_trabajo.SqlAlchemyUnitOfWork(session_factory)
with pytest.raises(MyException): with uow: insert_batch(uow.session,
'batch1', 'LARGE-FORK', 100, None) raise MyException()
nueva_sesión = session_factory()
No lo hemos mostrado aquí, pero puede valer la pena probar algunos de los
comportamientos de base de datos más "oscuros", como las transacciones,
con la base de datos "real", es decir, el mismo motor. Por ahora, nos estamos
saliendo con la nuestra usando SQLite en lugar de Postgres, pero en el Capítulo
7 cambiaremos algunas de las pruebas para usar la base de datos real. ¡Es
conveniente que nuestra clase UoW lo haga fácil!
Ahora divagamos brevemente sobre las diferentes formas de implementar el patrón UoW.
Podríamos imaginar una versión ligeramente diferente de la UoW que se compromete de forma predeterminada
y retrocede solo si detecta una excepción:
clase AbstractUnitOfWork(abc.ABC):
def __enter__(auto):
retornar auto
Nos permitiría guardar una línea de código y eliminar la confirmación explícita de nuestro código de cliente:
Esta es una decisión de juicio, pero tendemos a preferir requerir el compromiso explícito para que tengamos
que elegir cuándo vaciar el estado.
Aunque usamos una línea adicional de código, esto hace que el software sea seguro por defecto. El
comportamiento predeterminado es no cambiar nada. A su vez, eso hace que nuestro código sea más fácil de leer.
Son about porque solo hay una ruta de código que conduce a cambios en el sistema: éxito total y
una confirmación explícita. Cualquier otra ruta de código, cualquier excepción, cualquier salida
anticipada del alcance de la UoW lleva a un estado seguro.
Del mismo modo, preferimos retroceder de forma predeterminada porque es más fácil de entender;
esto retrocede hasta la última confirmación, por lo que el usuario hizo una o eliminamos sus cambios.
Duro pero simple.
Aquí hay algunos ejemplos que muestran el patrón de Unidad de trabajo en uso. Puede ver cómo
conduce a un razonamiento simple sobre qué bloques de código suceden juntos.
Ejemplo 1: reasignar
Supongamos que queremos poder desasignar y luego reasignar pedidos:
compañía naviera nos llama para decirnos que una de las puertas del contenedor se abrió y la mitad
de nuestros sofás se han caído al Océano Índico. ¡Ups!
Cambiar cantidad
Ejemplos: uso de UoW para agrupar varias operaciones en una unidad atómica | 91
Machine Translated by Google
Aquí es posible que necesitemos desasignar cualquier número de líneas. Si obtenemos una falla en
cualquier etapa, probablemente no queramos confirmar ninguno de los cambios.
test_allocate.py
test_services.py
ÿÿÿ test_batches.py
ÿÿÿ
ÿÿÿ
Siempre debe sentirse libre de desechar las pruebas si cree que no agregarán valor a largo plazo. Diríamos que
test_orm.py fue principalmente una herramienta para ayudarnos a aprender SQLAlchemy, por lo que no lo
necesitaremos a largo plazo, especialmente si las cosas principales que hace están cubiertas en test_repository.py.
Puede conservar esa última prueba, pero ciertamente podríamos ver un argumento para mantener todo en el
nivel más alto posible de abstracción (tal como lo hicimos para las pruebas unitarias).
Hemos eliminado tanto la UoW real como las falsificaciones, así como también hemos reducido la
UoW abstracta. ¿Por qué no nos envía un enlace a su repositorio si se le ocurre algo de lo que está
particularmente orgulloso?
Envolver
Esperamos haberlo convencido de que el patrón Unidad de trabajo es útil y que el administrador de contexto
es una forma Pythonic realmente agradable de agrupar visualmente el código en bloques que queremos que
suceda atómicamente.
Este patrón es tan útil, de hecho, que SQLAlchemy ya usa una UoW con la forma del objeto Session . El
objeto Session en SQLAlchemy es la forma en que su aplicación carga datos de la base de datos.
Cada vez que carga una nueva entidad desde la base de datos, la sesión comienza a realizar un seguimiento
de los cambios en la entidad y, cuando se cancela la sesión, todos los cambios persisten juntos. ¿Por qué
nos esforzamos por abstraer la sesión de SQLAlchemy si ya implementa el patrón que queremos?
ventajas Contras
• Tenemos una buena abstracción sobre el concepto de operaciones atómicas, y el administrador • Su ORM probablemente ya tenga algunos perfectamente
de contexto facilita ver, visualmente, qué bloques de código se agrupan atómicamente. buenas abstracciones en torno a la atomicidad. SQLAlchemy incluso
• Tenemos un control explícito sobre cuándo comienza y finaliza una transacción, y simplemente pasando una sesión. • Hemos hecho que parezca fácil,
nuestra aplicación falla de una manera segura por defecto. pero debe pensar con mucho cuidado en cosas como reversiones,
Nunca tenemos que preocuparnos de que una operación esté parcialmente comprometida. subprocesos múltiples y transacciones anidadas. Tal vez simplemente
• Es un buen lugar para colocar todos sus repositorios para que el código del cliente pueda acceder apegarse a lo que le brinda Django o Flask-SQLAlchemy hará que su
Por un lado, la API de sesión es rica y admite operaciones que no queremos ni necesitamos en nuestro
dominio. Nuestra UnitOfWork simplifica la sesión a su núcleo esencial: se puede iniciar, confirmar o desechar.
Por otro lado, estamos usando UnitOfWork para acceder a nuestros objetos del Repositorio . Esta es una
buena parte de la usabilidad del desarrollador que no podríamos hacer con una simple sesión de SQLAlchemy.
Resumen | 93
Machine Translated by Google
Ayuda a hacer cumplir la consistencia de nuestro modelo de dominio y mejora el rendimiento, al permitirnos realizar una sola
patrón de Unidad de Trabajo completa nuestras abstracciones sobre el acceso a los datos al representar actualizaciones
atómicas. Cada uno de nuestros casos de uso de la capa de servicio se ejecuta en una sola unidad de trabajo que tiene éxito
o falla como un bloque.
Los administradores de contexto son una forma idiomática de definir el alcance en Python. Podemos usar un administrador de
contexto para revertir automáticamente nuestro trabajo al final de una solicitud, lo que significa que el sistema es seguro de
forma predeterminada.
abstracción aún más simple sobre el objeto de sesión de SQLAlchemy para "estrechar" la interfaz entre el ORM y nuestro
Por último, estamos nuevamente motivados por el principio de inversión de dependencia: nuestra
capa de servicio depende de una abstracción delgada y adjuntamos una implementación concreta
en el borde exterior del sistema. Esto se alinea muy bien con las propias recomendaciones de
SQLAlchemy:
CAPÍTULO 7
En este capítulo, nos gustaría revisar nuestro modelo de dominio para hablar sobre invariantes y restricciones, y ver cómo nuestros
objetos de dominio pueden mantener su propia consistencia interna, tanto conceptualmente como en almacenamiento persistente.
Discutiremos el concepto de un límite de consistencia y mostraremos cómo hacerlo explícito puede ayudarnos a construir software
La Figura 7-1 muestra una vista previa de hacia dónde nos dirigimos: presentaremos un nuevo objeto de modelo llamado Producto
para envolver varios lotes, y en su lugar, haremos que el antiguo servicio de dominio allocate() esté disponible como un método en
Producto .
95
Machine Translated by Google
¿Cuál es el punto de un modelo de dominio, de todos modos? ¿Cuál es el problema fundamental que estamos
tratando de abordar?
¿No podríamos ejecutar todo en una hoja de cálculo? Muchos de nuestros usuarios estarían encantados con
eso. A los usuarios comerciales les gustan las hojas de cálculo porque son simples, familiares y, sin embargo,
enormemente poderosas.
De hecho, una enorme cantidad de procesos de negocios funcionan mediante el envío manual de hojas de
cálculo por correo electrónico. Esta arquitectura “CSV sobre SMTP” tiene una complejidad inicial baja, pero
tiende a no escalar muy bien porque es difícil aplicar la lógica y mantener la consistencia.
¿Quién puede ver este campo en particular? ¿Quién puede actualizarlo? ¿Qué sucede cuando tratamos de
ordenar 350 sillas o 10,000,000 de mesas? ¿Puede un empleado tener un salario negativo?
Estas son las restricciones de un sistema. Gran parte de la lógica de dominio que escribimos existe para
hacer cumplir estas restricciones a fin de mantener las invariantes del sistema. Los invariantes son las cosas
que tienen que ser verdaderas cada vez que terminamos una operación.
Las dos palabras son algo intercambiables, pero una restricción es una regla que restringe los posibles
estados en los que puede entrar nuestro modelo, mientras que un invariante se define con un poco más de
precisión como una condición que siempre es verdadera.
Por supuesto, a veces es posible que necesitemos doblar las reglas temporalmente. Tal vez necesitemos
cambiar las habitaciones debido a una reserva VIP. Mientras movemos las reservas en la memoria, es posible
que tengamos reservas dobles, pero nuestro modelo de dominio debe garantizar que, cuando terminemos,
terminemos en un estado consistente final, donde se cumplen las invariantes. Si no podemos encontrar una
manera de acomodar a todos nuestros invitados, debemos generar un error y negarnos a completar la
operación.
Veamos un par de ejemplos concretos de nuestros requisitos comerciales; Empezaremos con este:
Esta es una regla de negocio que impone un invariante. Lo invariable es que una línea de pedido se
asigna a cero o a un lote, pero nunca a más de uno. Necesitamos asegurarnos de que nuestro código
nunca llame accidentalmente a Batch.allocate() en dos lotes diferentes para la misma línea y, actualmente,
no hay nada que nos impida explícitamente hacerlo.
No podemos asignar a un lote si la cantidad disponible es menor que la cantidad de la línea de pedido.
-El negocio
Aquí, la restricción es que no podemos asignar más stock del que está disponible a un lote, por lo que
nunca sobrevendemos el stock asignando dos clientes al mismo colchón físico, por ejemplo. Cada vez
que actualizamos el estado del sistema, nuestro código debe asegurarse de que no rompamos el
invariante, que es que la cantidad disponible debe ser mayor o igual a cero.
En una aplicación de un solo hilo y de un solo usuario, es relativamente fácil para nosotros mantener este
invariante. Podemos simplemente asignar existencias una línea a la vez y generar un error si no hay
existencias disponibles.
Esto se vuelve mucho más difícil cuando introducimos la idea de concurrencia. De repente, podríamos
estar asignando existencias para múltiples líneas de pedido simultáneamente. Incluso podríamos estar
asignando líneas de pedido al mismo tiempo que procesamos los cambios en los lotes.
Solemos resolver este problema aplicando bloqueos a las tablas de nuestra base de datos. Esto evita que
ocurran dos operaciones simultáneamente en la misma fila o en la misma tabla.
Cuando comenzamos a pensar en ampliar nuestra aplicación, nos damos cuenta de que nuestro modelo
de asignación de líneas en todos los lotes disponibles puede no escalar. Si procesamos decenas de miles
de pedidos por hora y cientos de miles de líneas de pedido, no podemos mantener un bloqueo sobre toda
la tabla de lotes para cada uno de ellos; como mínimo, tendremos interbloqueos o problemas de rendimiento.
¿Qué es un agregado?
Bien, si no podemos bloquear toda la base de datos cada vez que queremos asignar una línea de pedido,
¿qué debemos hacer en su lugar? Queremos proteger las invariantes de nuestro sistema pero permitir el
mayor grado de concurrencia. Mantener nuestras invariantes inevitablemente significa evitar escrituras
simultáneas; si varios usuarios pueden asignar DEADLY-SPOON al mismo tiempo, corremos el riesgo de
sobreasignar.
Por otro lado, no hay ninguna razón por la que no podamos asignar DEADLY-SPOON al mismo tiempo que
FLIMSY-DESK. Es seguro asignar dos productos al mismo tiempo porque no hay una invariante que los cubra
a ambos. No necesitamos que sean consistentes entre sí.
El patrón Aggregate es un patrón de diseño de la comunidad DDD que nos ayuda a resolver esta tensión. Un
agregado es solo un objeto de dominio que contiene otros objetos de dominio y nos permite tratar la colección
completa como una sola unidad.
La única forma de modificar los objetos dentro del agregado es cargar todo y llamar a métodos en el agregado
mismo.
A medida que un modelo se vuelve más complejo y crece con más entidades y objetos de valor, haciendo
referencia entre sí en un gráfico enredado, puede ser difícil hacer un seguimiento de quién puede modificar qué.
Especialmente cuando tenemos colecciones en el modelo (nuestros lotes son una colección), es una buena
idea designar algunas entidades para que sean el único punto de entrada para modificar sus objetos
relacionados. Hace que el sistema sea conceptualmente más simple y fácil de razonar si nombra algunos
objetos para que estén a cargo de la consistencia para los demás.
ers.
Por ejemplo, si estamos construyendo un sitio de compras, el carrito podría ser un buen agregado: es una
colección de artículos que podemos tratar como una sola unidad. Es importante destacar que queremos cargar
la cesta completa como un único blob desde nuestro almacén de datos. No queremos dos solicitudes para
modificar la cesta al mismo tiempo, o corremos el riesgo de errores de concurrencia extraños.
En su lugar, queremos que cada cambio en la cesta se ejecute en una sola transacción de base de datos.
No queremos modificar varias cestas en una transacción, porque no hay caso de uso para cambiar las cestas
de varios clientes al mismo tiempo. Cada canasta es un único límite de consistencia responsable de mantener
sus propias invariantes.
Un AGREGADO es un grupo de objetos asociados que tratamos como una unidad para el propósito
de cambios de datos.
Según Evans, nuestro agregado tiene una entidad raíz (el carrito) que encapsula el acceso a los artículos.
Cada artículo tiene su propia identidad, pero otras partes del sistema siempre se referirán al Carrito solo como
un todo indivisible.
Elegir un agregado
¿Qué agregado debemos usar para nuestro sistema? La elección es algo arbitraria, pero es
importante. El agregado será el límite donde nos aseguremos de que cada operación termine en un
estado consistente. Esto nos ayuda a razonar sobre nuestro software y evitar problemas extraños
de carrera. Queremos dibujar un límite alrededor de una pequeña cantidad de objetos (cuanto más
pequeños, mejor para el rendimiento) que tienen que ser coherentes entre sí, y debemos darle un
buen nombre a este límite.
El objeto que estamos manipulando bajo las sábanas es Batch. ¿Cómo llamamos a una colección
de lotes? ¿Cómo debemos dividir todos los lotes en el sistema en islas discretas de consistencia?
Podríamos usar Envío como nuestro límite. Cada envío contiene varios lotes, y todos viajan a
nuestro almacén al mismo tiempo. O tal vez podríamos usar Almacén como nuestro límite: cada
almacén contiene muchos lotes, y contar todo el stock al mismo tiempo podría tener sentido.
Sin embargo, ninguno de estos conceptos nos satisface realmente. Deberíamos poder asignar
DEADLY-SPOON y FLIMSY-DESK al mismo tiempo, incluso si están en el mismo almacén o en el
mismo envío. Estos conceptos tienen la granularidad incorrecta.
Cuando asignamos una línea de pedido, solo nos interesan los lotes que tienen el mismo SKU que
la línea de pedido. Podría funcionar algún tipo de concepto como GlobalSkuStock : una colección
de todos los lotes para un SKU determinado.
Sin embargo, es un nombre difícil de manejar, por lo que después de algunos cambios en SkuStock,
Stock, Pro ductStock, etc., decidimos llamarlo simplemente Producto; después de todo, ese fue el
primer concepto con el que nos encontramos en nuestra exploración del lenguaje del dominio. en
el Capítulo 1.
Así que el plan es este: cuando queremos asignar una línea de pedido, en lugar de la Figura 7-2,
donde buscamos todos los objetos Batch del mundo y los pasamos al servicio de dominio allocate() ...
Elección de un agregado | 99
Machine Translated by Google
Figura 7-2. Antes: asignar contra todos los lotes utilizando el servicio de dominio
…pasaremos al mundo de la Figura 7-3, en el que hay un nuevo objeto Producto para el SKU
particular de nuestra línea de pedido, y estará a cargo de todos los lotes para ese SKU, y podemos
llamar a un método .allocate() en ese lugar.
intente: lote =
siguiente ( b para b en ordenados (auto. lotes) si b.can_allocate (línea)
) lote.asignar(línea)
volver lote.referencia
excepto StopIteration:
aumentar OutOfStock(f'Agotado para sku {line.sku}')
Nuestra clase Producto contiene una referencia a una colección de lotes para ese SKU.
Finalmente, podemos mover el servicio de dominio allocate() para que sea un método en el
agregado del Producto .
En esencia, esta fue una reacción contra los intentos de capturar empresas enteras en un
solo modelo. La palabra cliente significa diferentes cosas para las personas en ventas,
servicio al cliente, logística, soporte, etc. Los atributos necesarios en un contexto son
irrelevantes en otro; más pernicioso, los conceptos con el mismo nombre pueden tener
significados completamente diferentes en diferentes contextos. En lugar de intentar construir
un solo modelo (o clase o base de datos) para capturar todos los casos de uso, es mejor
tener varios modelos, trazar límites alrededor de cada contexto y manejar la traducción entre
diferentes contextos explícitamente.
Este concepto se traduce muy bien en el mundo de los microservicios, donde cada
microservicio es libre de tener su propio concepto de "cliente" y sus propias reglas para
traducirlo hacia y desde otros microservicios con los que se integra.
En nuestro ejemplo, el servicio de asignación tiene Producto (sku, lotes), mientras que el
comercio electrónico tendrá Producto (sku, descripción, precio, image_url, dimensiones, etc.).
Como regla general, sus modelos de dominio deben incluir solo los datos que necesitan para
realizar los cálculos.
Ya sea que tenga o no una arquitectura de microservicios, una consideración clave al elegir
sus agregados es también elegir el contexto delimitado en el que operarán. Al restringir el
contexto, puede mantener una cantidad baja de agregados y su tamaño manejable.
Una vez más, nos vemos obligados a decir que no podemos darle a este tema el tratamiento
que se merece aquí, y solo podemos alentarlo a que lo lea en otro lugar. El enlace de Fowler
al comienzo de esta barra lateral es un buen punto de partida, y cualquier libro de DDD (o
cualquier otro) tendrá un capítulo o más sobre contextos acotados.
Un agregado = un repositorio
Una vez que defina ciertas entidades para que sean agregados, debemos aplicar la regla de que
son las únicas entidades a las que el mundo exterior puede acceder públicamente. En otras
palabras, los únicos repositorios que se nos permiten deben ser repositorios que devuelvan agregados.
...
clase AbstractProductRepository(abc.ABC):
@abc.abstractmethod
def add(self, producto):
...
@abc.abstractmethod
def get(self, sku) -> modelo.Producto:
...
La capa ORM necesitará algunos ajustes para que los lotes correctos se carguen automáticamente
y se asocien con los objetos del Producto . Lo bueno es que el patrón Repository significa que
no tenemos que preocuparnos por eso todavía. Simplemente podemos usar nuestro
FakeRepository y luego pasar el nuevo modelo a nuestra capa de servicio para ver cómo se ve
con Product como su punto de entrada principal:
Primero, estamos modelando nuestros datos a propósito para que podamos hacer una sola consulta a la base de
datos para leer y una sola actualización para conservar nuestros cambios. Esto tiende a funcionar mucho mejor
que los sistemas que emiten muchas consultas ad hoc. En los sistemas que no se modelan de esta manera, a
menudo encontramos que las transacciones se vuelven más largas y complejas a medida que el software
evoluciona.
En segundo lugar, nuestras estructuras de datos son mínimas y comprenden unas pocas cadenas y números
enteros por fila. Podemos cargar fácilmente decenas o incluso cientos de lotes en unos pocos milisegundos.
Tercero, esperamos tener solo 20 o más lotes de cada producto a la vez. Una vez que se agota un lote, podemos
descontarlo de nuestros cálculos. Esto significa que la cantidad de datos que estamos obteniendo no debería
salirse de control con el tiempo.
Si esperáramos tener miles de lotes activos para un producto, tendríamos un par de opciones. Por un lado,
podríamos usar la carga diferida para los lotes de un producto. Desde la perspectiva de nuestro código, nada
cambiaría, pero en segundo plano, SQLAlchemy revisaría los datos por nosotros. Esto daría lugar a más
solicitudes, cada una de las cuales obtendría un número menor de filas. Debido a que solo necesitamos encontrar
un solo lote con suficiente capacidad para nuestro pedido, esto podría funcionar bastante bien.
Por supuesto, puede hacer trampa y copiar/pegar de las listas anteriores, pero incluso si lo hace, aún tendrá que
resolver algunos desafíos por su cuenta, como agregar el modelo al ORM y asegurarse de que todas las partes
móviles pueden hablar entre ellos, lo que esperamos sea instructivo.
Encontrarás el código en GitHub. Hemos incluido una implementación de "trampa" en los delegados de la función
allocate() existente , por lo que debería poder evolucionar hacia la realidad.
Hemos marcado un par de pruebas con @pytest.skip(). Después de leer el resto de este capítulo, vuelva a estas
pruebas para intentar implementar números de versión.
¡Puntos de bonificación si puede hacer que SQLAlchemy los haga por usted por arte de magia!
Si todo lo demás falla, simplemente buscaríamos un agregado diferente. Tal vez podríamos dividir los lotes
por región o por almacén. Tal vez podríamos rediseñar nuestra estrategia de acceso a datos en torno al
concepto de envío. El patrón Agregado está diseñado para ayudar a administrar algunas restricciones
técnicas relacionadas con la consistencia y el rendimiento. No hay un agregado correcto, y deberíamos
sentirnos cómodos cambiando de opinión si descubrimos que nuestros límites están causando problemas
de rendimiento.
No queremos mantener un bloqueo sobre toda la tabla de lotes , pero ¿cómo implementaremos mantener
un bloqueo solo sobre las filas de un SKU en particular?
Una respuesta es tener un único atributo en el modelo de producto que actúe como marcador de que se
ha completado todo el cambio de estado y utilizarlo como el único recurso por el que los trabajadores
simultáneos pueden luchar. Si dos transacciones leen el estado del mundo por lotes al mismo tiempo, y
ambas quieren actualizar las tablas de asignaciones , forzamos a ambas a intentar actualizar también
version_number en la tabla de productos , de tal manera que solo una de ellas pueda hacerlo. gana y el
mundo se mantiene constante.
La figura 7-4 ilustra dos transacciones simultáneas que realizan sus operaciones de lectura al mismo
tiempo, por lo que ven un Producto con, por ejemplo, versión=3. Ambos llaman a Prod uct.allocate() para
modificar un estado. Pero configuramos nuestras reglas de integridad de la base de datos de modo que
solo uno de ellos puede confirmar el nuevo Producto con la versión = 4, y la otra actualización se rechaza.
Los números de versión son solo una forma de implementar el bloqueo optimista.
Podría lograr lo mismo configurando el nivel de aislamiento de transacciones de
Postgres en SERIALIZABLE, pero eso a menudo tiene un alto costo de
rendimiento. Los números de versión también hacen explícitos los conceptos
implícitos.
Figura 7-4. Diagrama de secuencia: dos transacciones que intentan una actualización simultánea en
el Producto
El control de concurrencia pesimista funciona bajo el supuesto de que dos usuarios van a causar conflictos y
queremos evitar conflictos en todos los casos, por lo que bloqueamos todo solo para estar seguros. En nuestro
ejemplo, eso significaría bloquear toda la tabla de lotes o usar SELECCIONAR PARA ACTUALIZAR; estamos
fingiendo que los hemos descartado por razones de rendimiento, pero en la vida real querrás hacer algunas
evaluaciones y mediciones. tuyo.
Con el bloqueo pesimista, no necesita pensar en manejar fallas porque la base de datos las evitará por usted
(aunque sí necesita pensar en interbloqueos).
Con el bloqueo optimista, debe manejar explícitamente la posibilidad de fallas en el caso (con suerte poco
probable) de un conflicto.
La forma habitual de manejar un error es volver a intentar la operación fallida desde el principio.
Imagine que tenemos dos clientes, Harry y Bob, y cada uno envía un pedido de SHINY-TABLE. Ambos
subprocesos cargan el producto en la versión 1 y asignan stock. La base de datos impide la actualización
simultánea y el pedido de Bob falla con un error. Cuando volvemos a intentar la operación, el pedido de Bob
carga el producto en la versión 2 e intenta asignarlo nuevamente. Si queda suficiente stock, todo está bien; de
lo contrario, recibirá OutOfStock.
La mayoría de las operaciones se pueden volver a intentar de esta manera en el caso de un problema de simultaneidad.
Obtenga más información sobre los reintentos en “Recuperación de errores sincrónicamente” en la página 158
y “Footguns” en la página 226.
2. ¡La capa de servicio podría hacerlo! El número de versión no es estrictamente una preocupación del
dominio, por lo que nuestra capa de servicio podría suponer que el repositorio adjunta el número de
versión actual al Producto , y la capa de servicio lo incrementará antes de realizar la confirmación ().
3. Dado que podría decirse que es un problema de infraestructura, la UoW y el repositorio podrían
hacerlo por arte de magia. El repositorio tiene acceso a los números de versión de cualquier producto que
recupera, y cuando la UoW realiza una confirmación, puede incrementar el número de versión
de cualquier producto que conozca, suponiendo que hayan cambiado.
La opción 3 no es ideal, porque no hay una forma real de hacerlo sin tener que asumir que todos
los productos han cambiado, por lo que incrementaremos los números de versión cuando no sea
necesario.1
Entonces, al final, aunque los números de versión no tienen que ser una preocupación del dominio,
puede decidir que la compensación más limpia es colocarlos en el dominio:
intente: lote =
siguiente ( b para b en ordenados (auto. lotes) si b.can_allocate (línea)
) batch.allocate(line)
self.version_number += 1
return batch.reference
excepto StopIteration: raise
OutOfStock(f'Outofstock for sku {line.sku}')
¡Ahí está!
1 Tal vez podríamos obtener algo de magia ORM/SQLAlchemy para decirnos cuándo un objeto está sucio, pero ¿cómo podría eso
funciona en el caso genérico, por ejemplo, para un CsvRepository?
Ahora, para asegurarnos de que podemos obtener el comportamiento que queremos: si tenemos dos intentos
simultáneos de hacer una asignación en el mismo Producto, uno de ellos debería fallar, porque no pueden actualizar el
número de versión.
Primero, simulemos una transacción "lenta" usando una función que realiza la asignación y luego realiza una suspensión
explícita:2
Luego hacemos que nuestra prueba invoque esta asignación lenta dos veces, al mismo tiempo, usando subprocesos:
def test_concurrent_updates_to_version_are_not_allowed(postgres_session_factory):
sku, lote = random_sku(), random_batchref() session
= postgres_session_factory() insert_batch(session,
batch, sku, 100, eta=Ninguno, product_version=1) session.commit()
[[version]] =
session.execute( "SELECCIONE version_number DESDE productos DONDE sku=:sku",
2 time.sleep() funciona bien en nuestro caso de uso, pero no es la forma más confiable o eficiente de reproducir errores de
concurrencia. Considere usar semáforos o primitivas de sincronización similares compartidas entre sus subprocesos para
obtener mejores garantías de comportamiento.
dict(sku=sku),
) afirmar versión == 2
[excepción] = excepciones afirmar
'no se pudo serializar el acceso debido a una actualización simultánea' en str (excepción)
ordenes = lista(sesion.ejecutar(
"SELECCIONE orderid DE asignaciones"
"
ÚNASE lotes EN asignaciones.batch_id = lotes.id"
"
ÚNASE a order_lines EN asignaciones.orderline_id = order_lines.id"
" DONDE order_lines.sku=:sku",
dict(sku=sku),
))
afirmar len(pedidos) == 1 con
unit_of_work.SqlAlchemyUnitOfWork() como uow:
uow.session.execute('select 1')
Comenzamos dos subprocesos que producirán de manera confiable el comportamiento de concurrencia que
queremos: lectura1, lectura2, escritura1, escritura2.
Para que la prueba pase tal como está, podemos establecer el nivel de aislamiento de transacciones en nuestra sesión:
))
Los niveles de aislamiento de transacciones son complicados, por lo que vale la pena
dedicar tiempo a comprender la documentación de Postgres. 3
3 Si no está utilizando Postgres, deberá leer otra documentación. De manera molesta, las diferentes bases de datos tienen
definiciones bastante diferentes. SERIALIZABLE de Oracle es equivalente a REPEATABLE READ de Postgres, por
ejemplo.
varias formas de abordar esto, pero mostraremos una. SELECCIONAR PARA ACTUALIZAR produce un
comportamiento diferente; dos transacciones concurrentes no podrán hacer una lectura en las mismas filas
al mismo tiempo:
SELECCIONAR PARA ACTUALIZAR es una forma de elegir una fila o filas para usar como candado (aunque
esas filas no tienen que ser las que actualice). Si dos transacciones intentan SELECCIONAR PARA
ACTUALIZAR una fila al mismo tiempo, una ganará y la otra esperará hasta que se libere el bloqueo. Este
es un ejemplo de control de concurrencia pesimista.
Así es como puede usar SQLAlchemy DSL para especificar FOR UPDATE en el momento de la consulta:
Algunas personas se refieren a esto como el modo de falla de "lectura-modificación-escritura". Lea "Antipatrones de
PostgreSQL: ciclos de lectura, modificación y escritura" para obtener una buena descripción general.
Realmente no tenemos tiempo para discutir todas las compensaciones entre LECTURA REPETIBLE y
SELECCIONAR PARA ACTUALIZAR, o bloqueo optimista versus pesimista en general. Pero si tiene una
prueba como la que mostramos, puede especificar el comportamiento que desea y ver cómo cambia. También
puede utilizar la prueba como base para realizar algunos experimentos de rendimiento.
Envolver
Las opciones específicas sobre el control de concurrencia varían mucho en función de las circunstancias
comerciales y las opciones de tecnología de almacenamiento, pero nos gustaría volver a llevar este capítulo
a la idea conceptual de un agregado: modelamos explícitamente un objeto como el punto de entrada principal
a algún subconjunto. de nuestro modelo, y como encargado de hacer cumplir las invariantes y reglas
comerciales que se aplican a todos esos objetos.
Elegir el agregado correcto es clave, y es una decisión que puede revisar con el tiempo. Puede leer más
sobre esto en varios libros de DDD. También te recomendamos estos tres
Resumen | 111
Machine Translated by Google
documentos en línea sobre el diseño efectivo de agregados por Vaughn Vernon (el autor del "libro
rojo").
La Tabla 7-1 tiene algunas ideas sobre las ventajas y desventajas de implementar el patrón Agregado.
ventajas Contras
• Es posible que Python no tenga métodos públicos y privados "oficiales", pero • Otro nuevo concepto que pueden asumir los nuevos
tenemos la convención de guiones bajos, porque a menudo es útil tratar de indicar desarrolladores. Explicar entidades versus objetos
qué es para uso "interno" y qué es para uso de "código externo". de valor ya era una carga mental; ahora hay un
Elegir agregados es solo el siguiente nivel: le permite decidir cuáles de las clases tercer tipo de objeto de modelo de dominio?
de su modelo de dominio son las públicas y cuáles no. • Modelar nuestras • Cumplir rígidamente con la regla que modificamos
operaciones en torno a límites de coherencia explícitos nos ayuda a evitar problemas solo un agregado a la vez es un gran cambio mental.
de rendimiento con nuestro ORM. • Poner al agregado a cargo exclusivo de los
cambios de estado en sus modelos subsidiarios hace que sea más fácil razonar sobre • Tratar con la eventual consistencia entre los
el sistema y facilita el control de las invariantes. agregados puede ser complejo.
Resumen de la parte I
¿Recuerda la Figura 7-5, el diagrama que mostramos al comienzo de la Parte I para ver hacia dónde nos dirigíamos?
Así que ahí es donde estamos al final de la Parte I. ¿Qué hemos logrado? Hemos visto cómo construir un modelo
de dominio que es ejercitado por un conjunto de pruebas unitarias de alto nivel. Nuestras pruebas son documentación
viva: describen el comportamiento de nuestro sistema, las reglas que acordamos con las partes interesadas de
nuestro negocio, en un código agradable y legible. Cuando nuestros requisitos comerciales cambian, confiamos en
que nuestras pruebas nos ayudarán a probar la nueva funcionalidad, y cuando los nuevos desarrolladores se unen
al proyecto, pueden leer nuestras pruebas para comprender cómo funcionan las cosas.
Hemos desacoplado las partes de infraestructura de nuestro sistema, como la base de datos y los
controladores de API, para que podamos conectarlos al exterior de nuestra aplicación. Esto nos ayuda
a mantener nuestra base de código bien organizada y evita que construyamos una gran bola de barro.
Por último, hemos hablado sobre la idea de los límites de consistencia. No queremos bloquear todo
nuestro sistema cada vez que hacemos un cambio, por lo que tenemos que elegir qué partes son
consistentes entre sí.
Para un sistema pequeño, esto es todo lo que necesita para jugar con las ideas del diseño basado en
dominios. Ahora tiene las herramientas para crear modelos de dominio independientes de la base de
datos que representan el lenguaje compartido de sus expertos comerciales. ¡Hurra!
En la Parte II, nos alejaremos y hablaremos sobre un tema más amplio: si los agregados son nuestro
límite y solo podemos actualizar uno a la vez, ¿cómo modelamos procesos que cruzan los límites de
consistencia?
PARTE II
Lamento haber acuñado hace mucho tiempo el término "objetos" para este tema porque hace que muchas
personas se concentren en la idea menor.
La gran idea es la "mensajería".... La clave para crear sistemas excelentes y que puedan crecer es mucho
más diseñar cómo se comunican sus módulos en lugar de cuáles deberían ser sus propiedades y
comportamientos internos.
—Alan Kay
Está muy bien poder escribir un modelo de dominio para administrar un solo bit de proceso comercial,
pero ¿qué sucede cuando necesitamos escribir muchos modelos? En el mundo real, nuestras aplicaciones
se ubican dentro de una organización y necesitan intercambiar información con otras partes del sistema.
Puede recordar nuestro diagrama de contexto que se muestra en la Figura II-1.
Ante este requisito, muchos equipos buscan microservicios integrados a través de API HTTP. Pero si no
tienen cuidado, terminarán produciendo el desorden más caótico de todos: la gran bola de barro
distribuida.
En la Parte II, mostraremos cómo las técnicas de la Parte I se pueden extender a los sistemas distribuidos.
Nos alejaremos para ver cómo podemos componer un sistema a partir de muchos componentes
pequeños que interactúan a través del paso de mensajes asincrónicos.
Veremos cómo nuestros patrones de capa de servicio y unidad de trabajo nos permiten reconfigurar
nuestra aplicación para que se ejecute como un procesador de mensajes asíncrono, y cómo los sistemas
controlados por eventos nos ayudan a desacoplar agregados y aplicaciones entre sí.
Machine Translated by Google
Figura II-1. Pero, ¿exactamente cómo se comunicarán todos estos sistemas entre sí?
Eventos de dominio
Bus de mensajes
Proporcione una forma unificada de invocar casos de uso desde cualquier punto final.
CQRS
La separación de lecturas y escrituras evita compromisos incómodos en una arquitectura basada en eventos y
permite mejoras en el rendimiento y la escalabilidad.
Además, agregaremos un marco de inyección de dependencia. Esto no tiene nada que ver con la arquitectura
impulsada por eventos per se, pero arregla una gran cantidad de cabos sueltos.
Machine Translated by Google
CAPÍTULO 8
Hasta ahora hemos dedicado mucho tiempo y energía a un problema simple que podríamos haber resuelto
fácilmente con Django. Quizás se esté preguntando si el aumento de la capacidad de prueba y la expresividad
realmente valen la pena todo el esfuerzo.
En la práctica, sin embargo, encontramos que no son las características obvias las que ensucian nuestras
bases de código: es la sustancia pegajosa alrededor del borde. Son los informes, los permisos y los flujos de
trabajo los que afectan a un millón de objetos.
Nuestro ejemplo será un requisito de notificación típico: cuando no podemos asignar un pedido porque no
tenemos stock, debemos alertar al equipo de compras. Irán y solucionarán el problema comprando más
acciones, y todo irá bien.
Para una primera versión, el propietario de nuestro producto dice que podemos enviar la alerta por correo electrónico.
Veamos cómo resiste nuestra arquitectura cuando necesitamos conectar algunas de las cosas mundanas que
componen gran parte de nuestros sistemas.
Comenzaremos haciendo lo más simple y rápido, y hablaremos de por qué es exactamente este tipo de
decisión lo que nos lleva a la Gran Bola de Lodo.
Luego, mostraremos cómo usar el patrón de eventos de dominio para separar los efectos secundarios de
nuestros casos de uso y cómo usar un patrón de bus de mensajes simple para desencadenar comportamientos
basados en esos eventos. Mostraremos algunas opciones para crear esos eventos y cómo pasarlos al bus de
mensajes y, finalmente, mostraremos cómo se puede modificar el patrón de la Unidad de trabajo para conectar
los dos de forma elegante, como se muestra en la Figura 8-1. .
117
Machine Translated by Google
Simplemente golpéelo en el punto final, ¿qué podría salir mal? (src/asignación/puntos de entrada/ask_app.py)
@app.route("/asignar", métodos=['POST']) def
allocate_endpoint():
línea = modelo.OrderLine(
solicitud.json['orderid'],
solicitud.json['sku'],
solicitud.json['cantidad'],
)
intente: uow = unit_of_work.SqlAlchemyUnitOfWork()
batchref = services.allocate(line, uow) excepto
(model.OutOfStock, services.InvalidSku) as e: send_mail( 'out of
stock', 'stock_admin@made.com', f '{línea.orderid} -
{línea.sku}'
…pero es fácil ver cómo podemos terminar rápidamente en un lío arreglando las cosas de esta manera. El
envío de correo electrónico no es el trabajo de nuestra capa HTTP y nos gustaría poder probar esta nueva
función de forma unitaria.
intente: lote =
siguiente ( b para b en ordenados (auto. lotes) si b.can_allocate (línea)
) #...
excepto StopIteration:
email.send_mail('stock@made.com', f'Agotado para {line.sku}') raise
OutOfStock(f'Agotado para sku {line.sku}')
¡Pero eso es aún peor! No queremos que nuestro modelo tenga ninguna dependencia de problemas de
infraestructura como email.send_mail.
Esto de enviar correos electrónicos no es bienvenido y arruina el buen flujo limpio de nuestro sistema. Lo que
nos gustaría es mantener nuestro modelo de dominio enfocado en la regla "No puede asignar más cosas de
las que realmente están disponibles".
El trabajo del modelo de dominio es saber que no tenemos stock, pero la responsabilidad de enviar una alerta
pertenece a otra parte. Deberíamos poder activar o desactivar esta función, o cambiar a notificaciones por
SMS en su lugar, sin necesidad de cambiar las reglas de nuestro modelo de dominio.
¡O la capa de servicio!
El requisito “Intente asignar algo de stock y envíe un correo electrónico si falla” es un ejemplo de orquestación
de flujo de trabajo: es un conjunto de pasos que el sistema debe seguir para lograr un objetivo.
Hemos escrito una capa de servicio para administrar la orquestación por nosotros, pero incluso aquí la función
se siente fuera de lugar:
¿Atrapar una excepción y volver a subirla? Podría ser peor, pero definitivamente nos está haciendo infelices.
¿Por qué es tan difícil encontrar un hogar adecuado para este código?
Una formulación del SRP es que cada clase debe tener una sola razón para cambiar. Cuando
cambiamos de correo electrónico a SMS, no deberíamos tener que actualizar nuestra función allo
cate() , porque esa es claramente una responsabilidad separada.
Para resolver el problema, vamos a dividir la orquestación en pasos separados para que las
diferentes preocupaciones no se enreden.2 El trabajo del modelo de dominio es saber que no
tenemos existencias, pero la responsabilidad de enviar un alerta pertenece a otra parte. Deberíamos
poder activar o desactivar esta función, o cambiar a notificaciones por SMS en su lugar, sin necesidad
de cambiar las reglas de nuestro modelo de dominio.
También nos gustaría mantener la capa de servicio libre de detalles de implementación. Queremos
aplicar el principio de inversión de dependencia a las notificaciones para que nuestra capa de
servicio dependa de una abstracción, de la misma forma que evitamos depender de la base de datos
usando una unidad de trabajo.
Los patrones que vamos a presentar aquí son los eventos de dominio y el bus de mensajes.
Podemos implementarlos de varias maneras, así que mostraremos un par antes de decidirnos por
el que más nos guste.
simples Un evento es un tipo de objeto de valor. Los eventos no tienen ningún comportamiento,
porque son estructuras de datos puras. Siempre nombramos eventos en el idioma del dominio y los
consideramos parte de nuestro modelo de dominio.
2 A nuestro revisor técnico Ed Jung le gusta decir que el paso del control de flujo imperativo al basado en eventos
cambia lo que solía ser orquestación en coreografía.
evento de clase :
pasar
@dataclass
clase Agotado(Evento): sku: str
Una vez que tengamos una cantidad de eventos, nos resultará útil tener una clase principal que pueda
almacenar atributos comunes. También es útil para escribir sugerencias en nuestro bus de mensajes,
como verá en breve.
Cuando nuestro modelo de dominio registra un hecho que sucedió, decimos que genera un evento.
Así es como se verá desde el exterior; si le pedimos a Product que asigne pero no puede, debería generar
un evento:
Nuestro agregado expondrá un nuevo atributo llamado .events que contendrá una lista de hechos
sobre lo que sucedió, en forma de objetos Event .
probar:
#...
excepto StopIteration:
self.events.append(events.OutOfStock(line.sku)) # raise
OutOfStock(f'Outofstock for sku {line.sku}') return Ninguno
También vamos a dejar de generar una excepción para el caso de falta de existencias. El evento
hará el trabajo que estaba haciendo la excepción.
bus de mensajes básicamente dice: "Cuando veo este evento, debo invocar la siguiente función de
controlador". En otras palabras, es un sistema simple de publicación y suscripción. Los controladores
están suscritos para recibir eventos, que publicamos en el bus. Suena más difícil de lo que es, y
generalmente lo implementamos con un dict:
MANEJADORES = {
eventos.OutOfStock: [send_out_of_stock_notification],
El apio es una herramienta popular en el mundo de Python para diferir partes de trabajo independientes
a una cola de tareas asincrónicas. El bus de mensajes que presentamos aquí es muy diferente, por lo
que la respuesta corta a la pregunta anterior es no; nuestro bus de mensajes tiene más en común con
una aplicación Node.js, un bucle de eventos de interfaz de usuario o un marco de actor.
Si tiene un requisito para mover el trabajo del hilo principal, aún puede usar nuestras metáforas basadas
en eventos, pero le sugerimos que use eventos externos para eso. Hay más discusión en la Tabla 11-1,
pero esencialmente, si implementa una forma de persistir eventos en un almacén centralizado, puede
suscribir otros contenedores u otros microservicios a ellos. Luego, ese mismo concepto de usar eventos
para separar responsabilidades entre unidades de trabajo dentro de un solo proceso/servicio puede
extenderse a múltiples procesos, que pueden ser diferentes contenedores dentro del mismo servicio o
microservicios totalmente diferentes.
Si nos sigue en este enfoque, su API para distribuir tareas son sus clases de eventos, o una
representación JSON de ellas. Esto le permite mucha flexibilidad a la hora de distribuir las tareas; no
necesariamente tienen que ser servicios de Python. La API de Celery para distribuir tareas es
esencialmente "nombre de función más argumentos", que es más restrictiva y solo para Python.
La forma más sencilla de hacer esto es agregando algo de código en nuestra capa de servicio:
finalmente:
messagebus.handle(producto.eventos)
Eso ya evita algunas de las fealdades que teníamos en nuestra implementación ingenua, y tenemos
varios sistemas que funcionan como este, en el que la capa de servicio recopila explícitamente eventos
de agregados y los pasa al bus de mensajes.
si loteref es Ninguno:
messagebus.handle(events.OutOfStock(line.sku)) return loteref
Como antes, nos comprometemos incluso si fallamos en la asignación porque el código es más
simple de esta manera y es más fácil razonar: siempre nos comprometemos a menos que algo
salga mal. Confirmar cuando no hemos cambiado nada es seguro y mantiene el código
despejado.
def commit(self):
self._commit()
self.publish_events()
@abc.abstractmethod
def _commit(self):
aumentar NotImplementedError
...
clase SqlAlchemyUnitOfWork(AbstractUnitOfWork):
...
def _commit(self):
self.session.commit()
Después de confirmar, ejecutamos todos los objetos que nuestro repositorio ha visto y pasamos
sus eventos al bus de mensajes.
Eso depende de que el repositorio realice un seguimiento de los agregados que se han
cargado utilizando un nuevo atributo, .seen, como verá en la siguiente lista.
def __init__(self):
self.seen = set() # tipo: Set[modelo.Producto]
@abc.abstractmethod
def _add(self, producto: modelo.Producto):
aumentar NotImplementedError
@abc.abstractmethod
def _get(self, sku) -> modelo.Producto:
aumentar NotImplementedError
clase SqlAlchemyRepository(AbstractRepository):
Para que la UoW pueda publicar nuevos eventos, debe poder preguntar al repositorio para
qué objetos del Producto se han utilizado durante esta sesión. Usamos
un conjunto llamado .seen para almacenarlos. Eso significa que nuestras implementaciones deben
llamar a super().__init__().
El método padre add() agrega cosas a .seen, y ahora requiere subclases para implementar ._add().
De manera similar, .get() delega a una función ._get() , para ser implementada por subclases, para
capturar los objetos vistos.
Después de que la UoW y el repositorio colaboren de esta manera para realizar un seguimiento automático
de los objetos en vivo y procesar sus eventos, la capa de servicio puede estar totalmente libre de problemas
de manejo de eventos:
También debemos recordar cambiar las falsificaciones en la capa de servicio y hacer que llamen a super()
en los lugares correctos, e implementar métodos de subrayado, pero los cambios son mínimos:
clase FakeRepository(repositorio.AbstractRepository):
...
clase FakeUnitOfWork(unidad_de_trabajo.AbstractUnitOfWork):
...
def _commit(self):
self.committed = True
Una forma de composición sobre la herencia sería implementar una clase contenedora:
Al envolver el repositorio, podemos llamar a los métodos .add() y .get() reales , evitando
métodos extraños de subrayado.
Vea si puede aplicar un patrón similar a nuestra clase UoW para deshacerse también de
esos métodos Java-y _commit() . Puedes encontrar el código en GitHub.
Cambiar todo el ABC a escribir. El protocolo es una buena manera de obligarse a evitar el
uso de la herencia. ¡Cuéntanos si se te ocurre algo bonito!
Es posible que esté comenzando a preocuparse de que el mantenimiento de estas falsificaciones sea una carga
de mantenimiento. No hay duda de que es trabajo, pero en nuestra experiencia no es mucho trabajo. Una vez
que su proyecto está en funcionamiento, la interfaz para su repositorio y las abstracciones de UoW realmente no
cambian mucho. Y si está usando ABC, le ayudarán a recordar cuando las cosas se desincronicen.
Envolver
Los eventos de dominio nos brindan una forma de manejar los flujos de trabajo en nuestro sistema. A menudo,
al escuchar a nuestros expertos en el dominio, encontramos que expresan los requisitos de una manera causal
o temporal, por ejemplo, "Cuando tratamos de asignar existencias pero no hay ninguna disponible, entonces
debemos enviar un correo electrónico al equipo de compras".
Las palabras mágicas “Cuando X, entonces Y” muchas veces nos hablan de un evento que podemos concretar
en nuestro sistema. Tratar los eventos como cosas de primera clase en nuestro modelo nos ayuda a hacer que
nuestro código sea más comprobable y observable, y ayuda a aislar las preocupaciones.
ventajas Contras
• Un bus de mensajes nos brinda una buena • El bus de mensajes es algo adicional para entender; la
manera de separar responsabilidades cuando La implementación en la que la unidad de trabajo plantea eventos para nosotros es ordenada
tenemos que realizar varias acciones en respuesta a pero también mágica. No es obvio cuando llamamos a commit que también vamos a enviar
una solicitud. • Los controladores de eventos están correos electrónicos a las personas. • Además, ese código oculto de manejo de eventos se
excelente manera de modelar el mundo real y podemos en sus puntos finales web (es posible agregar procesamiento asincrónico, pero hace que las
usarlos como parte de nuestro lenguaje comercial cosas sean aún más confusas). • De manera más general, los flujos de trabajo basados en
cuando modelamos con las partes interesadas. eventos pueden ser confusos porque después
las cosas se dividen en una cadena de múltiples controladores, no hay un solo lugar en el
Sin embargo, los eventos son útiles para algo más que enviar correos electrónicos. En el Capítulo 7 dedicamos
mucho tiempo a convencerlo de que debe definir agregados o límites donde garantizamos la consistencia. La
gente suele preguntar: "¿Qué debo hacer si necesito cambiar varios agregados como parte de una solicitud?"
Ahora tenemos las herramientas que necesitamos para responder a esa pregunta.
Si tenemos dos cosas que pueden aislarse transaccionalmente (p. ej., un pedido y un producto), podemos hacer
que eventualmente sean consistentes usando eventos. Cuando un
se cancela el pedido, debemos encontrar los productos que se le asignaron y eliminar las asignaciones.
Opción 1: la capa de servicio genera eventos y los pasa al bus de mensajes La forma más
sencilla de comenzar a usar eventos en su sistema es generarlos desde los controladores llamando a bus.handle
Opción 2: el modelo de dominio genera eventos, la capa de servicio los pasa al bus de mensajes
La lógica sobre cuándo generar un evento realmente debe vivir con el modelo, de modo que podamos mejorar el diseño
y la capacidad de prueba de nuestro sistema al generar eventos desde el modelo de dominio. Es fácil para nuestros
controladores recopilar eventos de los objetos del modelo después de confirmarlos y pasarlos al bus.
Agregar bus.handle(aggregate.events) a cada controlador es molesto, por lo que podemos arreglarlo haciendo que
nuestra unidad de trabajo sea responsable de generar eventos generados por objetos cargados. Este es el diseño más
complejo y puede depender de la magia ORM, pero es limpio y fácil de usar una vez que está configurado.
En el Capítulo 9, veremos esta idea con más detalle a medida que construimos un flujo de trabajo más
complejo con nuestro nuevo bus de mensajes.
Resumen | 131
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 9
En este capítulo, comenzaremos a hacer que los eventos sean más fundamentales para la estructura
interna de nuestra aplicación. Pasaremos del estado actual en la Figura 9-1, donde los eventos son un
efecto secundario opcional...
133
Machine Translated by Google
…a la situación de la Figura 9-2, donde todo pasa por el bus de mensajes, y nuestra aplicación se ha
transformado fundamentalmente en un procesador de mensajes.
Figura 9-2. El bus de mensajes es ahora el principal punto de entrada a la capa de servicio.
Este software es complicado de escribir porque suceden cosas inesperadas todo el tiempo en el
mundo real de objetos físicos y humanos poco confiables. Por ejemplo:
• Durante un inventario, descubrimos que tres COLCHONES SPRINGY han sido dañados por el
agua debido a una gotera en el techo. • A un envío de RELIABLE-FORK le falta la
de BRILLANTE-LIBRERÍA.
En este tipo de situaciones, aprendemos sobre la necesidad de cambiar las cantidades de lotes
cuando ya están en el sistema. Quizás alguien se equivocó en el número del manifiesto, o quizás
algunos sofás se cayeron de un camión. Luego de una conversación con el negocio,1 modelamos
la situación como en la Figura 9-3.
Tal vez ya esté anticipando que nuestros eventos y bus de mensajes internos pueden ayudar a
implementar este requisito. Podríamos definir un servicio llamado change_batch_quantity que sepa
cómo ajustar las cantidades de lote y también cómo desasignar cualquier línea de pedido en
exceso, y luego cada desasignación puede emitir un evento AllocationRequired que
1 El modelado basado en eventos es tan popular que se ha desarrollado una práctica llamada tormenta de eventos para facilitar la
recopilación de requisitos basados en eventos y la elaboración de modelos de dominio.
se puede reenviar al servicio de asignación existente , en transacciones separadas. Una vez más, nuestro bus
de mensajes nos ayuda a hacer cumplir el principio de responsabilidad única y nos permite tomar decisiones
sobre las transacciones y la integridad de los datos.
Pero antes de lanzarnos, piense hacia dónde nos dirigimos. Hay dos tipos de flujos a través de nuestro sistema:
• Llamadas API que son manejadas por una función de capa de servicio
• Eventos internos (que pueden generarse como un efecto secundario de una función de capa de servicio) y
sus controladores (que a su vez llaman a funciones de capa de servicio)
¿No sería más fácil si todo fuera un controlador de eventos? Si reconsideramos nuestras llamadas API como
eventos de captura, las funciones de la capa de servicio también pueden ser controladores de eventos, y ya no
necesitamos hacer una distinción entre controladores de eventos internos y externos:
Todo suena como un poco demasiado? Trabajemos para lograrlo todo gradualmente. Seguiremos el flujo de
trabajo de refactorización preparatoria , también conocido como “Facilita el cambio; entonces haz el cambio fácil”:
2 Si ha leído un poco sobre arquitecturas basadas en eventos, puede estar pensando: "¡Algunos de estos eventos
suenan más como comandos!" ¡Tenga paciencia con nosotros! Estamos tratando de introducir un concepto a la vez.
En el próximo capítulo, presentaremos la distinción entre comandos y eventos.
2. Creamos una prueba de extremo a extremo que coloca los eventos BatchQuantityChanged en el sistema .
y busca los eventos asignados que salen.
3. Nuestra implementación será conceptualmente muy simple: un nuevo controlador para eventos Batch
CantidadChanged , cuya implementación emitirá eventos AllocationRequired , que a su vez serán
manejados exactamente por el mismo controlador para asignaciones que usa la API.
Comenzamos definiendo los dos eventos que capturan nuestras entradas API actuales—Asignación
Requerido y BatchCreated:
...
@dataclass
class AllocationRequired (Evento):
orderid: str sku: str qty: int
email.send( 'stock@made.com',
f'Agotado para {event.sku}',
)
def añadir_lote(
- ref: str, sku: str, qty: int, eta: Optional[date], uow:
- unit_of_work.AbstractUnitOfWork event: events.BatchCreated,
+ uow: unit_of_work.AbstractUnitOfWork
):
con uow:
- producto = uow.products.get(sku=sku)
+ producto = uow.products.get(sku=event.sku)
...
+
+def enviar_out_of_stock_notification( event:
+ events.OutOfStock, uow: unit_of_work.AbstractUnitOfWork,
+):
+ Enviar correo electrónico(
...
En el camino, hemos hecho que la API de nuestra capa de servicio sea más estructurada y más consistente. Era una
dispersión de primitivas, y ahora usa objetos bien definidos (vea la siguiente barra lateral).
Algunos de ustedes pueden recordar “Desvinculación completa de las pruebas de la capa de servicio del dominio” en la
página 75, en el que cambiamos nuestra API de la capa de servicio de estar en términos de objetos de dominio a
primitivas. Y ahora nos estamos moviendo hacia atrás, ¿pero a diferentes objetos?
¿Lo que da?
En los círculos OO, la gente habla de la obsesión primitiva como un anti-patrón: evita las primitivas en las API públicas
y, en su lugar, envuélvelas con clases de valor personalizadas, dirían.
En el mundo de Python, muchas personas serían bastante escépticas al respecto como regla general. Cuando se aplica
sin pensar, ciertamente es una receta para una complejidad innecesaria.
Así que eso no es lo que estamos haciendo per se.
El paso de los objetos de dominio a los primitivos nos trajo un poco de desacoplamiento: nuestro código de cliente ya no
estaba acoplado directamente al dominio, por lo que la capa de servicio podría presentar una API que permanece igual
incluso si decidimos realizar cambios en nuestro modelo, y viceversa.
Entonces, ¿hemos retrocedido? Bueno, nuestros objetos del modelo de dominio central aún pueden variar libremente,
pero en su lugar, hemos acoplado el mundo externo a nuestras clases de eventos. También son parte del dominio, pero
la esperanza es que varíen con menos frecuencia, por lo que son un artefacto sensato para combinar.
¿Y qué nos hemos comprado? Ahora, cuando invocamos un caso de uso en nuestra aplicación, ya no necesitamos
recordar una combinación particular de primitivas, sino solo una clase de evento única que representa la entrada a
nuestra aplicación. Eso es conceptualmente bastante bueno. Además de eso, como verá en el Apéndice E, esas clases
de eventos pueden ser un buen lugar para realizar alguna validación de entrada.
controladores de eventos ahora necesitan una UoW. Además, a medida que nuestro bus de
mensajes se vuelve más central para nuestra aplicación, tiene sentido ponerlo explícitamente
a cargo de recopilar y procesar nuevos eventos. Había un poco de dependencia circular entre
la UoW y el bus de mensajes hasta ahora, por lo que será unidireccional:
Extraemos eventos desde el frente de la cola e invocamos a sus controladores (el dictamen MANEJADORES
no ha cambiado; aún asigna tipos de eventos a funciones de controlador).
Después de que finaliza cada controlador, recopilamos cualquier evento nuevo que se haya generado y lo
agregamos a la cola.
clase AbstractUnitOfWork(abc.ABC): @@
-23,13 +21,11 @@ clase AbstractUnitOfWork(abc.ABC):
def commit(self):
self._commit()
- self.publish_events()
Y la UoW ya no coloca activamente eventos en el bus de mensajes; simplemente los pone a disposición.
Nuestras pruebas ahora funcionan creando eventos y colocándolos en el bus de mensajes, en lugar
de invocar funciones de la capa de servicio directamente:
clase TestAddBatch:
...
clase PruebaAsignar:
Un truco feo temporal: el bus de mensajes tiene que devolver resultados Nuestra API y
nuestra capa de servicio actualmente quieren saber la referencia del lote asignado cuando invocan
nuestro controlador allocate() . Esto significa que debemos implementar un truco temporal en nuestro
bus de mensajes para que pueda devolver eventos:
queue.extend(uow.collect_new_events())
+ devolver resultados
Es porque estamos mezclando las responsabilidades de lectura y escritura en nuestro sistema. Volveremos
para arreglar esta verruga en el Capítulo 12.
En lugar de llamar a la capa de servicio con un montón de primitivas extraídas de la solicitud JSON...
Instanciamos un evento.
Y deberíamos volver a una aplicación completamente funcional, pero que ahora está completamente
impulsada por eventos:
eventos. • Eso las hace iguales a las funciones que invocamos para manejar eventos internos generados
por nuestro modelo de dominio. • Usamos eventos como nuestra estructura de datos para capturar
El evento que nos dice que la cantidad de un lote ha cambiado es simple; solo necesita una referencia de
lote y una nueva cantidad:
cantidad: int
afirmar lote.cantidad_disponible == 50
] para e en event_history:
messagebus.handle(e, uow)
[batch1, batch2] = uow.products.get(sku="INDIFFERENT-TABLE").batches afirmar
lote1.cantidad_disponible == 10 afirmar lote2.cantidad_disponible == 50
El caso simple sería trivialmente fácil de implementar; solo modificamos una cantidad.
Implementación
Nuestro nuevo controlador es muy simple:
Nos damos cuenta de que necesitaremos un nuevo tipo de consulta en nuestro repositorio:
clase AbstractRepository(abc.ABC):
...
@abc.abstractmethod
def _add(self, producto: modelo.Producto):
aumentar NotImplementedError
@abc.abstractmethod
def _get(self, sku) -> modelo.Producto:
aumentar NotImplementedError
@abc.abstractmethod
def _get_by_batchref(self, loteref) -> modelo.Producto:
aumentar NotImplementedError
...
clase SqlAlchemyRepository(AbstractRepository):
...
clase FakeRepository(repositorio.AbstractRepository):
...
Estamos agregando una consulta a nuestro repositorio para que este caso
de uso sea más fácil de implementar. Siempre que nuestra consulta devuelva
un solo agregado, no estamos infringiendo ninguna regla. Si se encuentra
escribiendo consultas complejas en sus repositorios, es posible que desee
considerar un diseño diferente. Métodos como get_most_popular_products
o find_products_by_order_id en particular definitivamente activarían nuestro
sentido arácnido. El Capítulo 11 y el epílogo tienen algunos consejos sobre
la gestión de consultas complejas.
Agregamos el nuevo método al modelo, que realiza el cambio de cantidad y la(s) desasignación(es)
en línea y publica un nuevo evento. También modificamos la función de asignación existente para
publicar un evento:
)
...
lote de clase :
...
Nuestra prueba principal para el flujo de trabajo de reasignación es de extremo a extremo (consulte el código de
ejemplo en "Prueba de conducción de un nuevo controlador" en la página 144). Utiliza el bus de mensajes real y
prueba todo el flujo, donde el controlador de eventos BatchQuantityChanged activa la desasignación y emite nuevos
eventos AllocationRequired , que a su vez son manejados por sus propios controladores. Una prueba cubre una
cadena de múltiples eventos y controladores.
Dependiendo de la complejidad de su cadena de eventos, puede decidir que desea probar algunos controladores de
forma aislada unos de otros. Puede hacer esto usando un bus de mensajes "falso".
def __init__(self):
super().__init__()
self.eventos_publicados = [] # tipo: Lista[eventos.Evento]
Opcionalmente: Controladores de eventos de pruebas unitarias aislados con un bus de mensajes falsos | 147
Machine Translated by Google
prueba: en lugar de verificar todos los efectos secundarios, solo verificamos que
BatchQuantityChanged conduce a AllocationRequired si la cantidad cae por debajo del total ya asignado:
# configuración de prueba
como antes event_history
= [ events.BatchCreated("batch1", "INDIFFERENT-TABLE", 50, None),
events.BatchCreated("batch2", "INDIFFERENT-TABLE", 50, date.today()),
events.AllocationRequired("order1", "INDIFFERENT-TABLE", 20),
events.AllocationRequired("order2", "INDIFFERENT-TABLE", 20),
] para e en event_history:
messagebus.handle(e, uow)
[batch1, batch2] = uow.products.get(sku="INDIFFERENT-TABLE").batches afirmar
lote1.cantidad_disponible == 10 afirmar lote2.cantidad_disponible == 50
Si cambiamos el bus de mensajes para que sea una clase,3 entonces construir un FakeMessageBus
es más sencillo:
clase MessageBus(AbstractMessageBus):
MANEJADORES = {
eventos.OutOfStock: [send_out_of_stock_notification],
class FakeMessageBus(messagebus.AbstractMessageBus):
def __init__(self): self.events_published = [] # type:
List[events.Event] self.handlers = { events.OutOfStock: [lambda e:
self.events_published.append(e) ]
Entonces salte al código en GitHub y vea si puede hacer funcionar una versión basada en clases, y
luego escriba una versión de test_reallocates_if_necessary_isolated() anterior.
Usamos un bus de mensajes basado en clases en el Capítulo 13, si necesita más inspiración.
Envolver
Miremos hacia atrás a lo que hemos logrado y pensemos por qué lo hicimos.
3 La implementación “simple” en este capítulo utiliza esencialmente el propio módulo messagebus.py para implementar el
Patrón Singleton.
Resumen | 149
Machine Translated by Google
Los eventos son clases de datos simples que definen las estructuras de datos para entradas y mensajes
internos dentro de nuestro sistema. Esto es bastante poderoso desde el punto de vista de DDD, ya que los
eventos a menudo se traducen muy bien al lenguaje comercial (busque tormenta de eventos si aún no lo ha
hecho).
Los controladores son la forma en que reaccionamos a los eventos. Pueden llamar a nuestro modelo o
llamar a servicios externos. Podemos definir múltiples controladores para un solo evento si queremos.
Los controladores también pueden generar otros eventos. Esto nos permite ser muy granulares sobre lo que
hace un controlador y realmente ceñirnos al SRP.
Aquí hemos agregado un caso de uso bastante complicado (cambiar la cantidad, desasignar, iniciar una
nueva transacción, reasignar, publicar una notificación externa), pero desde el punto de vista arquitectónico,
no ha habido ningún costo en términos de complejidad. Hemos agregado nuevos eventos, nuevos
controladores y un nuevo adaptador externo (para correo electrónico), todos los cuales son categorías
existentes de cosas en nuestra arquitectura que entendemos y sabemos cómo razonar, y que son fáciles de
explicar. recién llegados Cada una de nuestras partes móviles tiene un trabajo, están conectadas entre sí
de maneras bien definidas y no hay efectos secundarios inesperados.
ventajas Contras
• Los controladores y los servicios son • Un bus de mensajes sigue siendo una forma ligeramente impredecible de hacer las cosas desde el punto
lo mismo, así que es más simple. • de vista web. No sabes de antemano cuándo van a terminar las cosas.
Tenemos una buena estructura de datos • Habrá duplicidad de campos y estructura entre objetos modelo y eventos, lo que tendrá un costo de
para las entradas al sistema. mantenimiento. Agregar un campo a uno generalmente significa agregar un campo a al menos uno de
los otros.
Ahora, puede que se pregunte, ¿de dónde van a venir esos eventos BatchQuantityChanged ? La respuesta
se revela en un par de capítulos. Pero primero, hablemos de eventos versus comandos.
CAPÍTULO 10
En el capítulo anterior, hablamos sobre el uso de eventos como una forma de representar las entradas
de nuestro sistema y convertimos nuestra aplicación en una máquina de procesamiento de mensajes.
Para lograrlo, convertimos todas nuestras funciones de casos de uso en controladores de eventos.
Cuando la API recibe un POST para crear un nuevo lote, genera un nuevo evento BatchCreated y lo
maneja como si fuera un evento interno. Esto puede parecer contradictorio. Después de todo, el lote
aún no se ha creado; por eso llamamos a la API. Vamos a arreglar esa verruga conceptual
introduciendo comandos y mostrando cómo pueden ser manejados por el mismo bus de mensajes
pero con reglas ligeramente diferentes.
Comandos y Eventos
Al igual que los eventos, los comandos son un tipo de mensaje: instrucciones enviadas por una parte
de un sistema a otra. Por lo general, representamos comandos con estructuras de datos tontas y
podemos manejarlos de la misma manera que los eventos.
Los comandos son enviados por un actor a otro actor específico con la expectativa de que algo en
particular suceda como resultado. Cuando publicamos un formulario en un controlador de API,
151
Machine Translated by Google
están enviando un comando. Nombramos comandos con frases verbales de modo imperativo como
"asignar existencias" o "retrasar el envío".
Los comandos capturan la intención. Expresan nuestro deseo de que el sistema haga algo. Como
resultado, cuando fallan, el remitente necesita recibir información de error.
Los eventos son transmitidos por un actor a todos los oyentes interesados. Cuando publicamos
Lotes de cantidad cambiada, no sabemos quién lo recogerá. Nombramos eventos con frases
verbales en tiempo pasado como "pedido asignado a stock" o "envío retrasado".
Los eventos capturan hechos sobre cosas que sucedieron en el pasado. Dado que no sabemos
quién está manejando un evento, a los remitentes no debería importarles si los receptores tuvieron
éxito o fallaron. La tabla 10-1 resume las diferencias.
Comando de clase :
pasar
@dataclass
class Asignar (Comando):
orderid: str sku:
str qty: int
@dataclass
class CreateBatch(Comando): ref: str
sku: str
@dataclass
class ChangeBatchQuantity(Command): ref: str
qty: int
devolver resultados
Todavía tiene un punto de entrada principal handle() que toma un mensaje, que puede ser un comando
o un evento.
Los eventos van a un despachador que puede delegar a varios controladores por evento.
def
manejar_comando( comando:
comandos.Comando, cola:
Lista[Mensaje], uow: unidad_de_trabajo.AbstractUnitOfWork
):
logger.debug(' comando de manejo %s', comando)
intente: controlador = COMMAND_HANDLERS[tipo(comando)]
resultado = controlador(comando, uow=uow)
queue.extend(uow.collect_new_events()) devolver
resultado
excepto Excepción:
logger.exception(' Comando de manejo de excepciones %s', comando)
aumentar
el resultado devuelto es solo temporal; como se menciona en “Un truco feo temporal: el bus de
mensajes tiene que devolver resultados” en la página 141, es un truco temporal para permitir que
el bus de mensajes devuelva la referencia de lote para que la use la API. Arreglaremos esto en el
Capítulo 12.
También cambiamos el dictado único de MANEJADORES a otros diferentes para comandos y eventos.
Los comandos solo pueden tener un controlador, según nuestra convención:
EVENT_HANDLERS
= { eventos.OutOfStock: [handlers.send_out_of_stock_notification],
} # tipo: Dict[Tipo[eventos.Evento], Lista[Llamable]]
COMMAND_HANDLERS
= { commands.Allocate: handlers.allocate,
commands.CreateBatch: handlers.add_batch,
commands.ChangeBatchQuantity: handlers.change_batch_quantity, } # type:
Dict[Type[commands.Command], Callable]
Comencemos con el peor de los casos: fallamos en manejar un evento y el sistema queda en un estado
inconsistente. ¿Qué tipo de error causaría esto? A menudo, en nuestros sistemas, podemos terminar en
un estado inconsistente cuando solo se completa la mitad de una operación.
Por ejemplo, podríamos asignar tres unidades de DESIRABLE_BEANBAG al pedido de un cliente, pero
de alguna manera no logramos reducir la cantidad de existencias restantes. Esto causaría un estado
inconsistente: las tres unidades de stock están asignadas y disponibles, dependiendo de cómo se mire.
Más tarde, podríamos asignar esos mismos pufs a otro cliente, causando un dolor de cabeza para la
atención al cliente.
Sin embargo, en nuestro servicio de asignación, ya hemos tomado medidas para evitar que eso suceda.
Hemos identificado cuidadosamente los agregados que actúan como límites de coherencia y hemos
introducido una UoW que gestiona el éxito o el fracaso atómico de una actualización de un agregado.
Por ejemplo, cuando asignamos existencias a un pedido, nuestro límite de consistencia es el agregado del
Producto . Esto significa que no podemos sobreasignar accidentalmente: o se asigna una línea de pedido
particular al producto, o no lo es; no hay lugar para incoherencias.
estados de tienda.
Por definición, no requerimos que dos agregados sean consistentes de inmediato, por lo que si fallamos
en procesar un evento y actualizamos solo un agregado, nuestro sistema aún puede volverse consistente
eventualmente. No debemos violar ninguna restricción del sistema.
Con este ejemplo en mente, podemos comprender mejor la razón de dividir los mensajes en comandos y
eventos. Cuando un usuario quiere que el sistema haga algo, representamos su solicitud como un
comando. Ese comando debería modificar un solo agregado y tener éxito o fallar en su totalidad. Cualquier
otra contabilidad, limpieza y notificación que necesitemos hacer puede ocurrir a través de un evento. No
requerimos que los controladores de eventos funcionen correctamente para que el comando funcione
correctamente.
Veamos otro ejemplo (de un proyecto imaginario diferente) para ver por qué no.
Imagine que estamos construyendo un sitio web de comercio electrónico que vende artículos de lujo caros.
Nuestro departamento de marketing quiere recompensar a los clientes por visitas repetidas. Marcaremos
a los clientes como VIP después de que realicen su tercera compra, y esto les dará derecho a un
tratamiento prioritario y ofertas especiales. Nuestros criterios de aceptación para esta historia son los
siguientes:
Usando las técnicas que ya hemos discutido en este libro, decidimos que queremos construir
un nuevo agregado de historial que registre pedidos y pueda generar eventos de dominio
cuando se cumplen las reglas. Estructuraremos el código así:
si entrada en self.orders:
devolver
self.orders.add(entrada)
if len(self.orders) == 3:
self.events.append( CustomerBecameVIP(self.customer_id)
)
f'¡Felicitaciones {cliente.nombre}!'
)
El agregado Historial captura las reglas que indican cuándo un cliente se convierte en VIP. Esto nos coloca en un
buen lugar para manejar los cambios cuando las reglas se vuelvan más complejas en el futuro.
Nuestro primer controlador crea un pedido para el cliente y genera un evento de dominio
Pedido creado.
Nuestro segundo controlador actualiza el objeto Historial para registrar que se creó un pedido.
Usando este código, podemos ganar algo de intuición sobre el manejo de errores en un sistema controlado por eventos.
En nuestra implementación actual, generamos eventos sobre un agregado después de que persistimos nuestro estado
en la base de datos. ¿Qué pasaría si planteáramos esos eventos antes de persistir y cometiéramos todos nuestros
cambios al mismo tiempo? De esa manera, podríamos estar seguros de que todo el trabajo estaba completo. ¿No sería
eso más seguro?
Sin embargo, ¿qué sucede si el servidor de correo electrónico está ligeramente sobrecargado? Si todo el trabajo tiene
que completarse al mismo tiempo, un servidor de correo electrónico ocupado puede impedir que tomemos dinero para
los pedidos.
Al separar estas preocupaciones, hemos hecho posible que las cosas fallen de forma aislada, lo que mejora la
confiabilidad general del sistema. La única parte de este código que debe completarse es el controlador de comandos
que crea un pedido. Esta es la única parte que le importa a un cliente, y es la parte que las partes interesadas de nuestro
negocio deberían priorizar.
Observe cómo hemos alineado deliberadamente nuestros límites transaccionales con el inicio y el final de los procesos
comerciales. Los nombres que usamos en el código coinciden con la jerga utilizada por las partes interesadas de
nuestro negocio, y los controladores que hemos escrito coinciden con los pasos de nuestros criterios de aceptación de
lenguaje natural. Esta concordancia de nombres y estructura nos ayuda a razonar sobre nuestros sistemas a medida
que crecen y se vuelven más complejos.
Lo primero que necesitamos es saber cuándo se ha producido un error, y para ello solemos confiar en los logs.
Cuando manejamos un mensaje en nuestro sistema, lo primero que hacemos es escribir una línea de registro
para registrar lo que estamos a punto de hacer. Para nuestro caso de uso CustomerBecameVIP , los registros
podrían ser los siguientes:
Debido a que elegimos usar clases de datos para nuestros tipos de mensajes, obtenemos un resumen claramente
impreso de los datos entrantes que podemos copiar y pegar en un shell de Python para volver a crear el objeto.
Cuando ocurre un error, podemos usar los datos registrados para reproducir el problema en una prueba unitaria
o reproducir el mensaje en el sistema.
La reproducción manual funciona bien para los casos en los que necesitamos corregir un error antes de que
podamos volver a procesar un evento, pero nuestros sistemas siempre experimentarán algún nivel de fondo de
falla transitoria. Esto incluye cosas como contratiempos en la red, interbloqueos de tablas y breve tiempo de
inactividad causado por las implementaciones.
Para la mayoría de esos casos, podemos recuperarnos elegantemente volviendo a intentarlo. Como dice el
proverbio, "si al principio no tiene éxito, vuelva a intentar la operación con un período de retroceso
exponencialmente creciente".
...
def
handle_event( evento:
eventos.Evento, cola:
Lista[Mensaje], uow: unidad_de_trabajo.AbstractUnitOfWork
):
con intento:
logger.debug('manejar evento %s con controlador %s', evento, controlador)
controlador(evento, uow=uow) queue.extend(uow.collect_new_events())
Tenacity es una biblioteca de Python que implementa patrones comunes para reintentar.
Aquí configuramos nuestro bus de mensajes para reintentar operaciones hasta tres veces, con
una espera entre intentos que aumenta exponencialmente.
Reintentar las operaciones que podrían fallar es probablemente la mejor forma de mejorar la resiliencia
de nuestro software. Una vez más, los patrones de Unidad de trabajo y Manejador de comandos
significan que cada intento comienza desde un estado consistente y no dejará las cosas a medio
terminar.
Envolver
En este libro decidimos introducir el concepto de eventos antes que el concepto de comandos, pero otras
guías suelen hacerlo al revés. Hacer explícitas las solicitudes a las que nuestro sistema puede responder
dándoles un nombre y su propia estructura de datos es algo bastante fundamental. A veces verá que la
gente usa el nombre Patrón de controlador de comandos para describir lo que estamos haciendo con
Eventos, Comandos y Bus de mensajes.
La tabla 10-2 analiza algunas de las cosas en las que debe pensar antes de embarcarse.
ventajas Contras
• Tratar los comandos y los eventos de manera diferente nos • Las diferencias semánticas entre comandos y eventos pueden ser sutiles.
ayuda a comprender qué cosas deben tener éxito y qué cosas Espere argumentos de bikeshedding sobre las diferencias.
podemos arreglar más adelante.
• CreateBatch es definitivamente un nombre menos confuso que • Estamos invitando expresamente al fracaso. Sabemos que a veces las
BatchCreated. Estamos siendo explícitos sobre la intención de cosas se estropean, y elegimos manejar eso haciendo que las fallas
nuestros usuarios, y explícito es mejor que implícito, ¿verdad? sean más pequeñas y más aisladas. Esto puede hacer que sea más difícil
razonar sobre el sistema y requiere una mejor supervisión.
CAPÍTULO 11
En el capítulo anterior, en realidad nunca hablamos sobre cómo recibiríamos los eventos de "cantidad
de lote modificada" o, de hecho, cómo podríamos notificar al mundo exterior sobre las reasignaciones.
Tenemos un microservicio con una API web, pero ¿qué pasa con otras formas de hablar con otros
sistemas? ¿Cómo sabremos si, por ejemplo, se retrasa un envío o se modifica la cantidad?
¿Cómo le informaremos al sistema de almacén que se ha asignado un pedido y que debe enviarse
a un cliente?
En este capítulo, nos gustaría mostrar cómo la metáfora de los eventos puede extenderse para
abarcar la forma en que manejamos los mensajes entrantes y salientes del sistema. Internamente,
el núcleo de nuestra aplicación ahora es un procesador de mensajes. Hagamos un seguimiento de
eso para que también se convierta en un procesador de mensajes externo. Como se muestra en la
Figura 11-1, nuestra aplicación recibirá eventos de fuentes externas a través de un bus de mensajes
externo (usaremos las colas de publicación/suscripción de Redis como ejemplo) y publicará sus
salidas, en forma de eventos, allí también. .
161
Machine Translated by Google
¿Qué sustantivos hemos introducido hasta ahora en nuestro sistema? Bueno, tenemos lotes de stock,
pedidos, productos y clientes. Por lo tanto, un intento ingenuo de dividir el sistema podría haberse parecido
a la figura 11-2 (observe que hemos nombrado nuestro sistema con el nombre Lotes, en lugar de Asignación).
162 | Capítulo 11: Arquitectura impulsada por eventos: uso de eventos para integrar microservicios
Machine Translated by Google
Cada "cosa" en nuestro sistema tiene un servicio asociado, que expone una API HTTP.
Trabajemos con un ejemplo de flujo de camino feliz en la Figura 11-3: nuestros usuarios visitan un sitio web y
pueden elegir entre los productos que están en stock. Cuando añaden un artículo a su cesta, reservamos algo de
stock para ellos. Cuando se completa un pedido, confirmamos la reserva, lo que hace que enviemos instrucciones
de despacho al almacén. Digamos también que si este es el tercer pedido del cliente, queremos actualizar el
registro del cliente para marcarlo como VIP.
Podemos pensar en cada uno de estos pasos como un comando en nuestro sistema:
ReserveStock, ConfirmReservation, DispatchGoods, MakeCustomerVIP, etc.
Este estilo de arquitectura, donde creamos un microservicio por tabla de base de datos y tratamos
nuestras API HTTP como interfaces CRUD para modelos anémicos, es la forma inicial más común
para que las personas se acerquen al diseño orientado a servicios.
Esto funciona bien para sistemas que son muy simples, pero puede degradarse rápidamente en una
bola de lodo distribuida.
Para ver por qué, consideremos otro caso. A veces, cuando las existencias llegan al almacén,
descubrimos que los artículos se han dañado por el agua durante el tránsito. No podemos vender
sofás dañados por el agua, por lo que tenemos que tirarlos y solicitar más stock a nuestros socios.
También necesitamos actualizar nuestro modelo de stock, y eso podría significar que necesitamos
reasignar el pedido de un cliente.
Bien, el sistema de Almacén sabe que el stock se ha dañado, por lo que tal vez debería ser el
propietario de este proceso, como se muestra en la Figura 11-4.
164 | Capítulo 11: Arquitectura impulsada por eventos: uso de eventos para integrar microservicios
Machine Translated by Google
Esto también funciona, pero ahora nuestro gráfico de dependencia es un desastre. Para asignar existencias,
el servicio de Pedidos maneja el sistema de Lotes, que maneja el Almacén; pero con el fin de manejar los
problemas en el almacén, nuestro sistema de almacén impulsa los lotes, que impulsan los pedidos.
Multiplique esto por todos los otros flujos de trabajo que necesitamos proporcionar, y podrá ver cómo los
servicios se enredan rápidamente.
Aquí tenemos dos opciones: podemos realizar el pedido de todos modos y dejarlo sin asignar, o podemos
negarnos a aceptar el pedido porque no se puede garantizar la asignación. El estado de falla de nuestro
servicio de lotes se ha disparado y está afectando la confiabilidad de nuestro servicio de pedidos.
Cuando hay que cambiar dos cosas juntas, decimos que están acopladas. Podemos pensar en esta
cascada de fallas como una especie de acoplamiento temporal: cada parte del sistema tiene que funcionar
al mismo tiempo para que cualquier parte funcione. A medida que el sistema se hace más grande, existe
una probabilidad exponencialmente creciente de que alguna parte se degrade.
Conocimiento
Estamos usando el término acoplamiento aquí, pero hay otra forma de describir las relaciones entre
nuestros sistemas. Connascence es un término utilizado por algunos autores para describir los diferentes
tipos de acoplamiento.
La connascencia no es mala, pero algunos tipos de connascencia son más fuertes que otros. Queremos
tener una connascencia fuerte localmente, como cuando dos clases están estrechamente relacionadas,
pero una connascencia débil a distancia.
En nuestro primer ejemplo de una bola de barro distribuida, vemos Connascence of Execution: múltiples
componentes necesitan conocer el orden correcto de trabajo para que una operación sea exitosa.
Cuando pensamos en las condiciones de error aquí, estamos hablando de Connascence of Timing:
varias cosas tienen que suceder, una tras otra, para que la operación funcione.
Cuando reemplazamos nuestro sistema de estilo RPC con eventos, reemplazamos ambos tipos de
conciencia con un tipo más débil. Eso es Connascence of Name: varios componentes deben estar de
acuerdo solo en el nombre de un evento y los nombres de los campos que lleva.
Nunca podemos evitar por completo el acoplamiento, excepto si nuestro software no se comunica con
ningún otro software. Lo que queremos es evitar un acoplamiento inapropiado. Connascence proporciona
un modelo mental para comprender la fuerza y el tipo de acoplamiento inherente a los diferentes estilos
arquitectónicos. Lea todo sobre esto en connascence.io.
166 | Capítulo 11: Arquitectura impulsada por eventos: uso de eventos para integrar microservicios
Machine Translated by Google
Cuando separamos las cosas de esta manera, es un poco más fácil ver qué sistema debería ser responsable de
qué. Cuando pensamos en hacer un pedido, realmente queremos asegurarnos de que cuando hagamos un
pedido, se haga el pedido. Todo lo demás puede suceder más tarde, siempre que suceda.
Al igual que los agregados, los microservicios deben ser límites de consistencia. Entre dos servicios, podemos
aceptar la consistencia eventual, y eso significa que no necesitamos depender de llamadas síncronas. Cada
servicio acepta comandos del mundo exterior y genera eventos para registrar el resultado. Otros servicios pueden
escuchar esos eventos para desencadenar los siguientes pasos en el flujo de trabajo.
Para evitar el antipatrón Distributed Ball of Mud, en lugar de llamadas a la API HTTP acopladas temporalmente,
queremos usar mensajería asíncrona para integrar nuestros sistemas.
Queremos que nuestros mensajes BatchQuantityChanged entren como mensajes externos de los sistemas
ascendentes, y queremos que nuestro sistema publique eventos asignados para que los escuchen los sistemas
descendentes.
¿Por qué es esto mejor? Primero, debido a que las cosas pueden fallar de forma independiente, es más fácil
manejar el comportamiento degradado: aún podemos tomar pedidos si el sistema de asignación está teniendo
un mal día.
En segundo lugar, estamos reduciendo la fuerza de acoplamiento entre nuestros sistemas. Si necesitamos
cambiar el orden de las operaciones o introducir nuevos pasos en el proceso, podemos hacerlo localmente.
En MADE.com, usamos Event Store; Kafka o RabbitMQ son alternativas válidas. Una solución liviana
basada en los canales pub/sub de Redis también puede funcionar bien, y debido a que Redis es mucho
más familiar para las personas, pensamos que lo usaríamos para este libro.
Nuestro nuevo flujo se parecerá a la Figura 11-6: Redis proporciona el evento BatchQuantityChanged que
inicia todo el proceso, y nuestro evento Allocated se vuelve a publicar en Redis al final.
168 | Capítulo 11: Arquitectura impulsada por eventos: uso de eventos para integrar microservicios
Machine Translated by Google
Así es como podríamos comenzar con una prueba de extremo a extremo. Podemos usar nuestra
API existente para crear lotes y luego probaremos los mensajes entrantes y salientes:
def test_change_batch_quantity_leading_to_reallocation():
# comenzar con dos lotes y un pedido asignado a uno de ellos orderid, sku
= random_orderid(), random_sku() before_batch, later_batch =
random_batchref('old'), random_batchref('newer') api_client.post_to_add_batch(earlier_batch,
sku, qty =10, eta='2011-01-02') api_client.post_to_add_batch(later_batch, sku, qty=10,
eta='2011-01-02') respuesta = api_client.post_to_allocate(orderid, sku, 10) afirmar respuesta.
json()['batchref'] == lote_anterior
suscripción = redis_client.subscribe_to('line_allocated')
# cambiar la cantidad en el lote asignado para que sea menor que nuestro
pedido redis_client.publish_message('change_batch_quantity', { 'batchref':
before_batch, 'qty': 5
})
# espere hasta que veamos un mensaje que indique que el pedido ha sido
reasignado . mensajes.agregar(mensaje) imprimir(mensajes) datos =
json.loads(mensajes[-1]['datos']) afirmar datos['orderid'] == orderid afirmar
datos['batchref'] == later_batch
Puede leer la historia de lo que está sucediendo en esta prueba en los comentarios: queremos
enviar un evento al sistema que provoque la reasignación de una línea de pedido, y vemos
que la reasignación también aparece como un evento en Redis.
api_client es un pequeño ayudante que refactorizamos para compartir entre nuestros dos tipos de
prueba; envuelve nuestras llamadas a request.post.
Debido a la naturaleza asíncrona del sistema que se está probando, necesitamos usar la biblioteca de
tenacidad nuevamente para agregar un ciclo de reintento; primero, porque nuestro nuevo mensaje
line_allocated puede demorar un tiempo en llegar, pero también porque no será el único mensaje en
ese canal.
Nuestro oyente pub/sub de Redis (lo llamamos consumidor de eventos) es muy parecido a Flask: se traduce
del mundo exterior a nuestros eventos:
def main():
orm.start_mappers()
pubsub = r.pubsub(ignore_subscribe_messages=True)
pubsub.subscribe('change_batch_quantity')
para m en pubsub.listen():
handle_change_batch_quantity(m)
def handle_change_batch_quantity(m):
logging.debug('handling %s', m) data =
json.loads(m['data']) cmd =
commands.ChangeBatchQuantity(ref=data['batchref'], qty=data[ 'cantidad'])
messagebus.handle(cmd, uow=unidad_de_trabajo.SqlAlchemyUnitOfWork())
Nuestro trabajo principal como punto de entrada al sistema es deserializar JSON, convertirlo en un
comando y pasarlo a la capa de servicio, como lo hace el adaptador Flask.
También construimos un nuevo adaptador descendente para hacer el trabajo opuesto: convertir eventos de
dominio en eventos públicos:
r = redis.Redis(**config.get_redis_host_and_port())
170 | Capítulo 11: Arquitectura impulsada por eventos: uso de eventos para integrar microservicios
Machine Translated by Google
Tomamos un canal codificado aquí, pero también puede almacenar un mapeo entre clases/nombres de
eventos y el canal apropiado, permitiendo que uno o más tipos de mensajes vayan a diferentes canales.
Captura todo lo que necesitamos saber sobre una asignación: los detalles de la línea de pedido y a qué lote se
asignó.
Lo agregamos al método allocate() de nuestro modelo (habiendo agregado una prueba primero, naturalmente):
lote.allocate(línea)
self.version_number += 1
self.events.append(events.Allocated(
orderid=línea.orderid, sku=línea.sku, qty=línea.qty,
loteref=lote.referencia,
))
volver lote.referencia
El controlador para ChangeBatchQuantity ya existe, por lo que todo lo que necesitamos agregar es un controlador
que publique el evento saliente:
La publicación del evento utiliza nuestra función auxiliar del contenedor de Redis:
def publicar_asignado_evento(
event: events.Allocated, uow: unit_of_work.AbstractUnitOfWork,
):
redis_eventpublisher.publish('line_allocated', evento)
Es una buena idea mantener clara la distinción entre eventos internos y externos.
Algunos eventos pueden provenir del exterior y algunos eventos pueden actualizarse y publicarse externamente, pero
no todos lo harán. Esto es particularmente importante si te involucras en el abastecimiento de eventos (aunque, en
gran medida, es un tema para otro libro).
Los eventos salientes son uno de los lugares en los que es importante aplicar
la validación. Consulte el Apéndice E para ver algunos ejemplos y filosofía de
validación.
Es probable que desee agregar una nueva prueba E2E y realizar algunos cambios en
redis_eventconsumer.py.
Envolver
Los eventos pueden provenir del exterior, pero también pueden publicarse externamente: nuestro controlador de
publicación convierte un evento en un mensaje en un canal de Redis. Usamos eventos para hablar con el mundo
exterior. Este tipo de desacoplamiento temporal nos da mucha flexibilidad en nuestras integraciones de aplicaciones,
pero como siempre, tiene un costo.
La notificación de eventos es buena porque implica un bajo nivel de acoplamiento y es bastante simple
de configurar. Sin embargo, puede volverse problemático si realmente hay un flujo lógico que se ejecuta
en varias notificaciones de eventos... Puede ser difícil ver dicho flujo ya que no está explícito en ningún
texto del programa... Esto puede hacer que sea difícil depurar y modificar.
172 | Capítulo 11: Arquitectura impulsada por eventos: uso de eventos para integrar microservicios
Machine Translated by Google
ventajas Contras
• Evita la gran bola de barro distribuida. • Los • Los flujos generales de información son más difíciles de ver. •
servicios están desacoplados: es más fácil cambiar La consistencia eventual es un nuevo concepto a tratar. •
servicios individuales y agregar otros nuevos.
Fiabilidad de los mensajes y opciones entre al menos una vez y como máximo
una vez que la entrega necesita pensar bien.
Resumen | 173
Machine Translated by Google
Machine Translated by Google
CAPÍTULO 12
En este capítulo, vamos a comenzar con una idea bastante no controvertida: las lecturas (consultas)
y las escrituras (comandos) son diferentes, por lo que deben tratarse de manera diferente (o segregar
sus responsabilidades, por así decirlo). Entonces vamos a llevar esa idea tan lejos como podamos.
Si eres como Harry, todo esto parecerá extremo al principio, pero con suerte podemos argumentar
que no es totalmente irrazonable.
175
Machine Translated by Google
Hemos dedicado mucho tiempo en este libro a hablar sobre cómo crear software que haga cumplir
las reglas de nuestro dominio. Estas reglas, o restricciones, serán diferentes para cada aplicación
y constituyen el núcleo interesante de nuestros sistemas.
En este libro, hemos establecido restricciones explícitas como "No puede asignar más stock del
que está disponible", así como restricciones implícitas como "Cada línea de pedido se asigna a un
solo lote".
lote.asignar(línea)
afirmar lote.cantidad_disponible == 18
...
def test_cannot_allocate_if_available_smaller_than_required():
small_batch, large_line = make_batch_and_line("ELEGANT-LAMP", 2, 20) afirmar
small_batch.can_allocate(large_line) es falso
Para aplicar estas reglas correctamente, necesitábamos asegurarnos de que las operaciones fueran coherentes, por
lo que introdujimos patrones como Unidad de trabajo y Agregado que nos ayudan a realizar pequeñas porciones de
trabajo.
Para comunicar los cambios entre esos pequeños fragmentos, introdujimos el patrón de eventos de dominio para que
podamos escribir reglas como "Cuando el stock se dañe o se pierda, ajuste la cantidad disponible en el lote y reasigne
los pedidos si es necesario".
Toda esta complejidad existe para que podamos hacer cumplir las reglas cuando cambiamos el estado de nuestro
sistema. Hemos creado un conjunto flexible de herramientas para escribir datos.
Sin embargo, en ese mismo día ajetreado, podríamos tener cien vistas de productos por segundo.
Cada vez que alguien visita la página de un producto, o una página de listado de productos, debemos averiguar si el
producto todavía está en stock y cuánto tiempo nos llevará entregarlo.
El dominio es el mismo: nos preocupan los lotes de existencias, su fecha de llegada y la cantidad que aún está
disponible, pero el patrón de acceso es muy diferente. Por ejemplo, nuestros clientes no se darán cuenta si la consulta
está desactualizada unos segundos, pero si nuestro servicio de asignación es inconsistente, desordenaremos sus
pedidos. Podemos aprovechar esta diferencia haciendo que nuestras lecturas sean eventualmente consistentes para
Esta idea de intercambiar consistencia por rendimiento pone nerviosos a muchos desarrolladores al principio,
así que hablemos rápidamente sobre eso.
Imaginemos que nuestra consulta "Obtener existencias disponibles" tiene 30 segundos de desactualización
cuando Bob visita la página de ASYMETRICAL-DRESSER. Mientras tanto, sin embargo, Harry ya ha comprado
el último artículo. Cuando intentemos asignar el pedido de Bob, obtendremos un error y tendremos que
cancelar su pedido o comprar más existencias y retrasar su entrega.
Las personas que han trabajado solo con almacenes de datos relacionales se ponen muy nerviosas con este
problema, pero vale la pena considerar otros dos escenarios para obtener cierta perspectiva.
Primero, imaginemos que Bob y Harry visitan la página al mismo tiempo. Harry se va a hacer café y, cuando
regresa, Bob ya ha comprado la última cómoda. Cuando Harry hace su pedido, lo enviamos al servicio de
asignación, y como no hay suficiente stock, tenemos que reembolsar su pago o comprar más stock y retrasar
su entrega.
Tan pronto como mostramos la página del producto, los datos ya están obsoletos. Esta información es clave
para comprender por qué las lecturas pueden ser inconsistentes de manera segura: siempre necesitaremos
verificar el estado actual de nuestro sistema cuando lleguemos a la asignación, porque todos los sistemas
distribuidos son inconsistentes. Tan pronto como tenga un servidor web y dos clientes, tiene el potencial de
datos obsoletos.
Bien, supongamos que resolvemos ese problema de alguna manera: creamos mágicamente una aplicación
web totalmente consistente donde nadie ve datos obsoletos. Esta vez, Harry llega primero a la página y
compra su tocador.
Desafortunadamente para él, cuando el personal del almacén intenta despachar sus muebles, estos se caen
de la carretilla elevadora y se rompen en un millón de pedazos. ¿Ahora que?
Las únicas opciones son llamar a Harry y reembolsar su pedido o comprar más existencias y retrasar la
entrega.
No importa lo que hagamos, siempre encontraremos que nuestros sistemas de software son inconsistentes
con la realidad, por lo que siempre necesitaremos procesos comerciales para hacer frente a estos casos
extremos. Está bien intercambiar rendimiento por consistencia en el lado de la lectura, porque los datos
obsoletos son esencialmente inevitables.
Podemos pensar que estos requisitos forman dos mitades de un sistema: el lado de lectura y el lado de
escritura, que se muestran en la tabla 12-1.
Para el lado de escritura, nuestros elegantes patrones arquitectónicos de dominio nos ayudan a evolucionar
nuestro sistema con el tiempo, pero la complejidad que hemos construido hasta ahora no compra nada para
leer datos. La capa de servicio, la unidad de trabajo y el modelo de dominio inteligente son simplemente inflados.
Comportamiento
lectura sencilla Lógica empresarial compleja
Publicar/Redireccionar/Obtener y CQS
Si se dedica al desarrollo web, probablemente esté familiarizado con el patrón Publicar/Redireccionar/Obtener. En esta
técnica, un punto final web acepta un HTTP POST y responde con una redirección para ver el resultado. Por ejemplo,
podríamos aceptar un POST a /lotes para crear un nuevo lote y redirigir al usuario a /lotes/123 para ver su lote recién creado.
Este enfoque soluciona los problemas que surgen cuando los usuarios actualizan la página de resultados en su navegador
o intentan marcar una página de resultados. En el caso de una actualización, puede hacer que nuestros usuarios envíen
datos dos veces y, por lo tanto, compren dos sofás cuando solo necesitaban uno. En el caso de un marcador, nuestros
desafortunados clientes terminarán con una página rota cuando intenten OBTENER un punto final POST.
Ambos problemas ocurren porque estamos devolviendo datos en respuesta a una operación de escritura. Post/Redirect/Get
evita el problema al separar las fases de lectura y escritura de nuestra operación.
Esta técnica es un ejemplo simple de separación de consulta de comando (CQS). En CQS seguimos una regla simple: las
funciones deben modificar el estado o responder preguntas, pero nunca ambas cosas. Esto hace que el software sea más
fácil de razonar: siempre deberíamos poder preguntar: "¿Están encendidas las luces?" sin accionar el interruptor de la luz.
Como verá, podemos usar el principio de CQS para hacer que nuestros sistemas sean más rápidos y más escalables, pero
primero, corrijamos la violación de CQS en nuestro código existente. Hace mucho tiempo, introdujimos un punto final de
asignación que toma un pedido y llama a nuestra capa de servicio para asignar algunas existencias. Al final de la llamada,
devolvemos un 200 OK y la identificación del lote. Eso ha llevado a algunos feos defectos de diseño para que podamos
obtener los datos que necesitamos. Cambiémoslo para que devuelva un mensaje OK simple y, en su lugar, proporcionemos
un nuevo punto final de solo lectura para recuperar la asignación.
estado:
@pytest.mark.usefixtures('postgres_db')
@pytest.mark.usefixtures('restart_api') def
test_happy_path_returns_202_and_batch_is_allocated(): orderid =
random_orderid() sku, otherku = random_sku(), random_sku('other')
earlybatch = random_batchref( 1) laterbatch = random_batchref(2)
otro lote = random_batchref (3) api_client.post_to_add_batch(laterbatch,
sku, 100, '2011-01-02') api_client.post_to_add_batch(earlybatch,
sku, 100, '2011-01-01') api_client.post_to_add_batch(otherbatch,
otherku, 100, Ninguno)
r = api_client.get_allocation(orderid) afirmar
r.ok afirmar r.json() == [ {'sku': sku, 'batchref':
earlybatch},
@pytest.mark.usefixtures('postgres_db')
@pytest.mark.usefixtures('restart_api') def
test_unhappy_path_returns_400_and_error_message():
unknown_sku, orderid = random_sku(), random_orderid() r =
api_client.post_to_allocate( orderid, unknown_sku, qty=20,
expect_success=False,
r = api_client.get_allocation(orderid) afirmar
r.status_code == 404
Muy bien, un views.py, bastante justo; podemos mantener cosas de solo lectura allí, y será un view.py real, no como el de
Django, algo que sabe cómo crear vistas de solo lectura de nuestros datos...
Si eres como Harry y te encuentras con este patrón por primera vez, te preguntarás qué diablos ha estado fumando Bob. ¿Estamos
procesando a mano nuestro propio SQL ahora y convirtiendo las filas de la base de datos directamente en dictados? ¿Después
de todo el esfuerzo que pusimos en construir un buen modelo de dominio? ¿Y qué pasa con el patrón Repositorio? ¿No está
destinado a ser nuestra abstracción en torno a la base de datos? ¿Por qué no reutilizamos eso?
Bueno, primero exploremos esa alternativa aparentemente más simple y veamos cómo se ve en la práctica.
Todavía mantendremos nuestra vista en un módulo views.py separado; hacer cumplir una distinción clara entre lecturas y
escrituras en su aplicación sigue siendo una buena idea. Aplicamos la separación de consultas de comandos y es fácil ver qué
código modifica el estado (los controladores de eventos) y qué código solo recupera el estado de solo lectura (las vistas).
def test_allocations_view(sqlite_session_factory):
uow = unidad_de_trabajo.SqlAlchemyUnitOfWork(sqlite_session_factory)
messagebus.handle(commands.CreateBatch('sku1batch', 'sku1', 50, None), uow)
messagebus.handle(commands.CreateBatch('sku2batch', 'sku2', 50, hoy ), uow)
messagebus.handle(commands.Allocate('order1', 'sku1', 20), uow)
messagebus.handle(commands.Allocate('order1', 'sku2', 20), uow) # agregar un falso lote y
ordene para asegurarnos de obtener los correctos
messagebus.handle(commands.CreateBatch('sku1batch-later', 'sku1', 50, today), uow)
messagebus.handle(commands.Allocate('otherorder', 'sku1', 30), uow)
messagebus.handle(commands.Allocate('otherorder', 'sku2', 10), uow)
Nuestro repositorio devuelve objetos de Producto y necesitamos encontrar todos los productos
para los SKU en un orden determinado, por lo que crearemos un nuevo método auxiliar
llamado .for_order() en el repositorio.
Ahora tenemos productos, pero en realidad queremos referencias de lotes, por lo que obtenemos todos
los lotes posibles con una lista de comprensión.
Filtramos nuevamente para obtener solo los lotes para nuestro pedido específico. Eso, a su vez, depende
de que nuestros objetos Batch puedan decirnos qué ID de pedido ha asignado.
@property
def orderids(self):
return {l.orderid for l in self._allocations}
Puede comenzar a ver que reutilizar nuestras clases de modelo de dominio y repositorio existentes no es tan
sencillo como podría haber asumido. Tuvimos que agregar nuevos métodos de ayuda a ambos, y estamos
haciendo un montón de bucles y filtros en Python, que es un trabajo que la base de datos haría de manera
mucho más eficiente.
Así que sí, en el lado positivo estamos reutilizando nuestras abstracciones existentes, pero en el lado negativo,
todo parece bastante torpe.
Esta es la justificación del arquitecto que se acaricia la barbilla para CQRS. Como dijimos antes, un modelo
de dominio no es un modelo de datos: estamos tratando de capturar la forma en que funciona el negocio: flujo
de trabajo, reglas sobre cambios de estado, mensajes intercambiados; preocupaciones sobre cómo reacciona
el sistema a los eventos externos y la entrada del usuario. La mayoría de estas cosas son totalmente
irrelevantes para las operaciones de solo lectura.
Esta justificación de CQRS está relacionada con la justificación del patrón del
modelo de dominio. Si está creando una aplicación CRUD simple, las lecturas
y las escrituras estarán estrechamente relacionadas, por lo que no necesita
un modelo de dominio o CQRS. Pero cuanto más complejo sea su dominio,
más probable es que necesite ambos.
Para hacer un punto sencillo, sus clases de dominio tendrán múltiples métodos para modificar el estado, y no
necesitará ninguno de ellos para operaciones de solo lectura.
A medida que crece la complejidad de su modelo de dominio, se encontrará tomando más y más
decisiones sobre cómo estructurar ese modelo, lo que lo hace cada vez más difícil de usar para
operaciones de lectura.
) devuelve
[ {'sku': b.sku, 'batchref': b.batchref} para b en
lotes
]
Pero, ¿es realmente más fácil de escribir o comprender que la versión SQL sin procesar del ejemplo
de código en “Aguanta tu almuerzo, amigos” en la página 181? Puede que no se vea tan mal allí
arriba, pero podemos decirle que tomó varios intentos y mucho de excavar a través de los documentos
de SQLAlchemy. SQL es solo SQL.
Más allá de SELECT N+1, es posible que tenga otras razones para querer desvincular la forma en que persiste los cambios
de estado de la forma en que recupera el estado actual. Un conjunto de tablas relacionales completamente normalizadas es
una buena manera de asegurarse de que las operaciones de escritura nunca dañen los datos. Pero la recuperación de
datos mediante muchas uniones puede ser lenta. En tales casos, es común agregar algunas vistas desnormalizadas, crear
réplicas de lectura o incluso agregar capas de almacenamiento en caché.
Entonces, razonable o no, esa consulta SQL codificada es bastante fea, ¿verdad? ¿Y si lo hiciéramos más bonito...?
…manteniendo un almacén de datos totalmente separado y desnormalizado para nuestro modelo de vista?
Je, je, je, sin claves foráneas, solo cadenas, YOLO (src/allocation/adapters/
orm.py) asignaciones_vista = Table( 'asignaciones_vista', metadatos, Columna ('orderid', Cadena (255)),
Columna ('sku' , Cadena (255)), Columna ('referencia de lote', Cadena (255)),
De acuerdo, las consultas SQL más atractivas no serían una justificación para nada realmente, pero crear una copia
desnormalizada de sus datos que esté optimizada para operaciones de lectura no es poco común, una vez que haya
alcanzado los límites de lo que puede hacer con los índices.
Incluso con índices bien ajustados, una base de datos relacional utiliza una gran cantidad de CPU
para realizar uniones. Las consultas más rápidas siempre serán SELECT * from mytable WHERE
key = :value.
Sin embargo, más que velocidad bruta, este enfoque nos compra escala. Cuando estamos escribiendo datos en una base
de datos relacional, debemos asegurarnos de obtener un bloqueo sobre las filas que estamos cambiando para que no
tengamos problemas de coherencia.
Si varios clientes están cambiando datos al mismo tiempo, tendremos condiciones de carrera extrañas. Sin embargo,
cuando estamos leyendo datos, no hay límite para la cantidad de clientes que pueden ejecutarse simultáneamente.
Por esta razón, las tiendas de solo lectura se pueden escalar horizontalmente
afuera.
Debido a que las réplicas de lectura pueden ser inconsistentes, no hay límite para
la cantidad que podemos tener. Si tiene dificultades para escalar un sistema con
un almacén de datos complejo, pregúntese si podría crear un modelo de lectura
más simple.
¡Mantener el modelo de lectura actualizado es el desafío! Las vistas de la base de datos (materializadas o no) y los
disparadores son una solución común, pero eso lo limita a su base de datos.
En su lugar, nos gustaría mostrarle cómo reutilizar nuestra arquitectura basada en eventos.
EVENT_HANDLERS
= { eventos. Asignados:
[ handlers.publish_allocated_event,
handlers.add_allocation_to_read_model
],
) uow.commit()
Lo creas o no, ¡eso funcionará bastante bien! Y funcionará con las mismas pruebas de integración que el resto de
nuestras opciones.
events.Deallocated:
[ handlers.remove_allocation_from_read_model,
handlers.realocate
],
...
En la Figura 12-2, puede ver dos transacciones en la operación POST/escritura, una para actualizar el modelo
de escritura y otra para actualizar el modelo de lectura, que puede usar la operación GET/read.
¿Cómo lidiamos con un modelo de vista que no se ha actualizado debido a un error o una interrupción temporal?
Bueno, este es solo otro caso en el que los eventos y los comandos pueden fallar de forma independiente.
Si nunca actualizamos el modelo de vista y ASYMMETRICAL-DRESSER siempre estuvo en stock, eso sería
molesto para los clientes, pero el servicio de asignación aún fallaría y tomaríamos medidas para solucionar el
problema.
Sin embargo, reconstruir un modelo de vista es fácil. Como estamos usando una capa de servicio para actualizar
nuestro modelo de vista, podemos escribir una herramienta que haga lo siguiente:
• Consulta el estado actual del lado de escritura para averiguar qué está asignado actualmente. • Llama al
Podemos usar esta técnica para crear modelos de lectura completamente nuevos a partir de datos históricos.
Sólo mira:
def get_readmodel(orderid):
devuelve r.hgetall(orderid)
(Tal vez el nombre redis_eventpublisher.py sea un nombre inapropiado ahora, pero se entiende la idea).
Y exactamente las mismas pruebas de integración que teníamos antes aún pasan, porque están escritas en un
nivel de abstracción que está desacoplado de la implementación: la configuración coloca mensajes en el bus
de mensajes y las afirmaciones están en contra de nuestra vista.
Los controladores de eventos son una excelente manera de administrar las actualizaciones
de un modelo de lectura, si decide que necesita uno. También facilitan el cambio de la
implementación de ese modelo de lectura en una fecha posterior.
Aquí, las compensaciones entre el uso de SQL codificado y el uso de un repositorio deberían
ser mucho más borrosas. Pruebe algunas versiones (tal vez incluyendo ir a Redis) y vea cuál
prefiere.
Envolver
La Tabla 12-2 propone algunos pros y contras para cada una de nuestras opciones.
Da la casualidad de que el servicio de asignación de MADE.com utiliza CQRS "completo", con un modelo de
lectura almacenado en Redis e incluso una segunda capa de caché proporcionada por Varnish.
Pero sus casos de uso son bastante diferentes de lo que hemos mostrado aquí. Para el tipo de servicio de
asignación que estamos creando, parece poco probable que necesite usar un modelo de lectura y controladores
de eventos separados para actualizarlo.
Pero a medida que su modelo de dominio se vuelve más rico y complejo, un modelo de lectura simplificado se
vuelve cada vez más atractivo.
Resumen | 189
Machine Translated by Google
Solo usa repositorios Enfoque simple y consistente. Espere problemas de rendimiento con patrones de consulta
complejos.
Utilice consultas Permite la reutilización de la configuración de la base de datos y las Agrega otro lenguaje de consulta con sus propias peculiaridades y
Usar SQL enrollado a mano Ofrece un control preciso sobre el rendimiento con una Los cambios en el esquema de la base de datos deben realizarse
limitaciones de rendimiento.
Cree tiendas de lectura Las copias de solo lectura son fáciles de escalar. Las vistas Técnica compleja. Harry siempre sospechará de tus gustos y motivos.
separadas con eventos se pueden construir cuando los datos cambian para que las
A menudo, sus operaciones de lectura actuarán sobre los mismos objetos conceptuales que su modelo
de escritura, por lo que usar el ORM, agregar algunos métodos de lectura a sus repositorios y usar
clases de modelo de dominio para sus operaciones de lectura está bien.
En nuestro ejemplo de libro, las operaciones de lectura actúan sobre entidades conceptuales bastante
diferentes a nuestro modelo de dominio. El servicio de asignación piensa en términos de lotes para un
solo SKU, pero los usuarios se preocupan por las asignaciones para un pedido completo, con múltiples
SKU, por lo que usar el ORM termina siendo un poco incómodo. Estaríamos bastante tentados de optar
por la vista de SQL sin formato que mostramos al principio del capítulo.
CAPÍTULO 13
La inyección de dependencia (DI) se mira con recelo en el mundo de Python. ¡Y nos las hemos arreglado
muy bien sin él hasta ahora en el código de ejemplo de este libro!
En este capítulo, exploraremos algunos de los puntos débiles en nuestro código que nos llevan a considerar
el uso de DI, y presentaremos algunas opciones sobre cómo hacerlo, dejando que usted elija cuál cree que
es más conveniente. pitónico.
También agregaremos un nuevo componente a nuestra arquitectura llamado bootstrap.py; estará a cargo de
la inyección de dependencia, así como de otras cosas de inicialización que a menudo necesitamos.
Explicaremos por qué este tipo de cosa se llama raíz de composición en los lenguajes orientados a objetos y
por qué el script de arranque está bien para nuestros propósitos.
La Figura 13-1 muestra cómo se ve nuestra aplicación sin un programa previo: los puntos de entrada realizan
una gran cantidad de inicializaciones y transferencias de nuestra dependencia principal, la UoW.
191
Machine Translated by Google
Para nuestra dependencia de la base de datos, hemos creado un marco cuidadoso de dependencias
explícitas y opciones sencillas para anularlas en las pruebas. Nuestras principales funciones de
controlador declaran una dependencia explícita en la UoW:
Y eso facilita el intercambio de una UoW falsa en nuestras pruebas de capa de servicio:
uow = FakeUnitOfWork()
mensajebus.handle ([...], uow)
clase SqlAlchemyUnitOfWork(AbstractUnitOfWork):
Lo aprovechamos en nuestras pruebas de integración para poder utilizar en ocasiones SQLite en lugar de Postgres:
Las pruebas de integración intercambian el valor predeterminado de Postgres session_factory por un SQLite
una.
Envío de correo electrónico como una dependencia normal basada en la importación (src/allocation/service_layer/handlers.py)
Importación codificada
¿Por qué contaminar el código de nuestra aplicación con argumentos innecesarios solo por el bien de nuestras
pruebas? mock.patch hace que el parcheo de monos sea fácil y agradable:
El problema es que hemos hecho que parezca fácil porque nuestro ejemplo de juguete no envía un correo
electrónico real (email.send_mail solo imprime), pero en la vida real, terminaría teniendo que llamar a mock.patch
para cada prueba que podría causar una notificación de falta de existencias. Si ha trabajado en bases de código
con muchos simulacros utilizados para evitar efectos secundarios no deseados, sabrá lo molesto que se vuelve
ese modelo simulado.
Y sabrás que los simulacros nos acoplan estrechamente a la implementación. Al elegir monopatch
email.send_mail, estamos obligados a hacer la importación de correo electrónico, y si alguna vez queremos
hacer from email import send_mail, un refactor trivial, tendríamos que cambiar todos nuestros simulacros.
Así que es una compensación. Sí, declarar dependencias explícitas es innecesario, estrictamente hablando, y
usarlas haría que el código de nuestra aplicación fuera un poco más complejo. Pero a cambio, obtendríamos
pruebas que son más fáciles de escribir y administrar.
Además de eso, declarar una dependencia explícita es un ejemplo del principio de inversión de dependencia:
en lugar de tener una dependencia (implícita) de un detalle específico, tenemos una dependencia (explícita) de
una abstracción:
):
send_mail( 'stock@made.com',
f'Agotado para {event.sku}',
)
Pero si cambiamos para declarar todas estas dependencias explícitamente, ¿quién las inyectará y cómo? Hasta
ahora, realmente hemos estado lidiando con solo pasar el UoW:
nuestras pruebas usan FakeUnitOfWork, mientras que los puntos de entrada de consumidores de eventos
de Flask y Redis usan la UoW real, y el bus de mensajes los pasa a nuestros controladores de comandos.
Si agregamos clases de correo electrónico reales y falsas, ¿quién las creará y las transmitirá?
Eso es cruft extra (duplicado) para Flask, Redis y nuestras pruebas. Además, poner toda la responsabilidad
de pasar las dependencias al controlador correcto en el bus de mensajes se siente como una violación del
SRP.
En su lugar, buscaremos un patrón llamado Composición raíz (un script de arranque para usted y para mí),1
y haremos un poco de "DI manual" (inyección de dependencia sin un marco). Consulte la Figura 13-3.
2
1 Debido a que Python no es un lenguaje OO "puro", los desarrolladores de Python no están necesariamente acostumbrados al concepto de
necesidad de componer un conjunto de objetos en una aplicación de trabajo. Simplemente elegimos nuestro punto de entrada y ejecutamos el
código de arriba a abajo.
La diferencia entre los cierres (lambdas o funciones con nombre) y func tools.partial es que los primeros
utilizan el enlace tardío de variables, lo que puede ser una fuente de confusión si alguna de las dependencias
es mutable.
Aquí está el mismo patrón nuevamente para el controlador send_out_of_stock_notification() , que tiene diferentes
dependencias:
send_mail( 'stock@made.com',
...
...
# más tarde, en tiempo
de ejecución: sosn_composed(event) # tendrá email.send_mail ya inyectado en
DI usando clases
# reemplazamos el viejo `def allocate(cmd, uow)` con:
clase AllocateHandler:
...
# más tarde en el tiempo de ejecución, podemos llamar a la instancia del controlador,
y tendrá # el UoW ya inyectado allocate(cmd)
La clase está diseñada para producir una función invocable, por lo que tiene un método de llamada .
Pero usamos el init para declarar las dependencias que requiere. Este tipo de cosas le resultará familiar si
alguna vez ha creado descriptores basados en clases, o un administrador de contexto basado en clases
que toma argumentos.
Un guión de arranque
Queremos que nuestro script de arranque haga lo siguiente:
las cosas de "inicio" que necesitamos para iniciar nuestra aplicación 3. Inyectar
si start_orm:
orm.start_mappers()
} manejadores_comandos_inyectados = {
tipo_de_comando : inyectar_dependencias(manejador, dependencias) para
tipo_de_comando, manejador en handlers.COMMAND_HANDLERS.items()
}
volver mensajebus.MessageBus(
uow = uow,
event_handlers=injected_event_handlers,
command_handlers=injected_command_handlers,
)
orm.start_mappers() es nuestro ejemplo de trabajo de inicialización que debe realizarse una vez al
comienzo de una aplicación. También vemos cosas como configurar el módulo de registro .
Podemos usar los valores predeterminados del argumento para definir cuáles son los valores predeterminados normales/
de producción. Es bueno tenerlos en un solo lugar, pero a veces las dependencias tienen algunos efectos secundarios
en el momento de la construcción, en cuyo caso es posible que prefieras las predeterminadas.
ellos a Ninguno en su lugar.
Construimos nuestras versiones inyectadas de las asignaciones de controladores usando una función llamada
inject_dependencies(), que mostraremos a continuación.
que el código de inspección anterior es un poco más difícil de asimilar, esta versión aún más
simple puede resultarle atractiva.
Harry escribió el código para inject_dependencies() como un primer corte de cómo hacer una
inyección de dependencia “manual”, y cuando lo vio, Bob lo acusó de sobrediseñar y escribir su
propio marco DI.
Honestamente, ni siquiera se le ocurrió a Harry que podrías hacerlo de manera más sencilla,
pero puedes, así:
} manejadores_comandos_inyectados = {
comandos.Asignar: lambda c: handlers.allocate(c, uow),
commands.CreateBatch: \ lambda c: handlers.add_batch(c, uow),
commands.ChangeBatchQuantity: \ lambda c:
handlers.change_batch_quantity(c, uow),
Harry dice que ni siquiera podía imaginar escribir tantas líneas de código y tener que buscar tantos
argumentos de función manualmente. Sin embargo, esta es una solución perfectamente viable, ya
que es solo una línea de código más o menos por controlador que agrega y, por lo tanto, no es una
carga de mantenimiento masiva, incluso si tiene docenas de controladores.
Nuestra aplicación está estructurada de tal manera que siempre queremos realizar la inyección de
dependencia en un solo lugar, el controlador funciona, por lo que esta solución supermanual y la
basada en inspect() de Harry funcionarán bien.
Si se da cuenta de que quiere hacer DI en más cosas y en diferentes momentos, o si alguna vez
entra en cadenas de dependencia (en las que sus dependencias tienen sus propias dependencias,
etc.), puede sacar algo de provecho de un “ verdadero marco DI.
En MADE, hemos usado Inject en algunos lugares, y está bien, aunque hace que Pylint no esté
contento. También puede consultar Punq, tal como lo escribió el propio Bob, o las dependencias del
equipo de DRY Python.
clase MessageBus:
def
self.command_handlers = command_handlers
La función principal handle() es sustancialmente la misma, con solo unos pocos atributos y métodos
trasladados a sí mismo.
El uso de self.queue como este no es seguro para subprocesos, lo que podría ser un problema si está
utilizando subprocesos, ya que la instancia de bus es global en el contexto de la aplicación Flask tal como lo
hemos escrito. Sólo algo a tener en cuenta.
En lugar de pasar una UoW al controlador, esperamos que los controladores ya tengan todas sus
dependencias, por lo que todo lo que necesitan es un solo argumento, el evento o comando específico.
aplicación = Flask(__name__)
-orm.start_mappers() +bus =
bootstrap.bootstrap()
-
) uow = unidad_de_trabajo.SqlAlchemyUnitOfWork()
-
mensajebus.handle(cmd, uow) bus.handle(cmd) return 'OK',
+ 201
Ya no necesitamos llamar a start_orm(); las etapas de inicialización del script de arranque harán eso.
Ya no necesitamos construir explícitamente un tipo particular de UoW; los valores predeterminados del
script de arranque se encargan de ello.
Y nuestro bus de mensajes ahora es una instancia específica en lugar del módulo global.3
3 Sin embargo, sigue siendo global en el ámbito del módulo de la aplicación de matraz, si tiene sentido. Esto puede causar problemas
si alguna vez desea probar su aplicación Flask en proceso utilizando Flask Test Client en lugar de usar Docker como lo hacemos
nosotros. Vale la pena investigar las fábricas de aplicaciones Flask si te involucras en esto.
En las pruebas, podemos usar bootstrap.bootstrap() con valores predeterminados anulados para obtener un bus de
mensajes personalizado. Aquí hay un ejemplo en una prueba de integración:
@pytest.fixture
def sqlite_bus(sqlite_session_factory): bus =
bootstrap.bootstrap( start_orm=True,
uow=unit_of_work.SqlAlchemyUnitOfWork(sqlite_session_factory),
send_mail=lambda *argumentos: Ninguno, publicar=lambda *argumentos:
Ninguno,
) producir
bus clear_mappers()
def test_allocations_view(sqlite_bus):
sqlite_bus.handle(comandos.CreateBatch('sku1batch', 'sku1', 50, Ninguno))
sqlite_bus.handle(comandos.CreateBatch('sku2batch', 'sku2', 50, date.today()))
...
aseverar vistas.allocaciones('order1', sqlite_bus.uow) == [ {'sku':
'sku1', 'batchref': 'sku1batch'}, {'sku': 'sku2', 'batchref':
'sku2batch' },
]
…porque vamos a utilizar una UoW real, aunque con una base de datos en memoria.
Pero no necesitamos enviar un correo electrónico o publicar, así que hacemos esos noops.
def bootstrap_test_app():
return
bootstrap.bootstrap( start_orm=False,
uow=FakeUnitOfWork(), send_mail=lambda *argumentos: Ninguno, publicar=lambda *argumentos: Ninguno,
)
Eso elimina un poco la duplicación, y hemos movido un montón de configuraciones y valores predeterminados
sensibles a un solo lugar.
El UoW tiene una clase base abstracta. Esta es la opción de peso pesado para declarar y administrar su
dependencia externa. Usaríamos esto para el caso cuando la dependencia es relativamente compleja.
S3 • Un cliente de almacenamiento de
La mayoría de estos tendrán API más complejas que no puede capturar como una sola función: lectura y
escritura, GET y POST, etc.
Aunque es simple, usemos send_mail como ejemplo para hablar sobre cómo podría definir una dependencia
más compleja.
API de notificaciones más genérica. Podría ser un correo electrónico, podría ser un SMS, podrían ser
publicaciones de Slack algún día.
@abc.abstractmethod
def send(self, destino, mensaje):
aumentar NotImplementedError
...
mensaje=mensaje
def __init__(self):
self.sent = defaultdict(list) # type: Dict[str, List[str]]
self.sent[destino].append(mensaje)
...
def test_sends_email_on_out_of_stock_error(self):
fake_notifs = FakeNotifications() bus =
bootstrap.bootstrap( start_orm=False,
uow=FakeUnitOfWork(), Notifications=fake_notifs,
publishing =lambda *args: Ninguno,
cosa real, por lo general con una prueba de integración o de extremo a extremo. Hemos utilizado MailHog
como un servidor de correo electrónico real para nuestro entorno de desarrollo de Docker:
versión: "3"
servicios:
redis_pubsub:
construir:
contexto: .
archivo acoplable: archivo acoplable
imagen: asignación-imagen
...
api:
imagen: imagen de asignación
...
postgres:
imagen: postgres:9.6
...
redis:
imagen: redis: alpino
...
correo:
@pytest.fixture def
bus(sqlite_session_factory): bus =
bootstrap.bootstrap( start_orm=True,
uow=unidad_de_trabajo.SqlAlchemyUnitOfWork(sqlite_session_factory),
notificaciones=notificaciones.Notificaciones por correo electrónico(),
publicación=lambda *args: Ninguno,
) producir
bus clear_mappers()
Usamos nuestro programa previo para crear un bus de mensajes que se comunique con la clase de notificaciones
reales.
Descubrimos cómo obtener correos electrónicos de nuestro servidor de correo electrónico "real".
Y eso es todo.
1. Intente cambiar nuestras notificaciones de correo electrónico a notificaciones de SMS usando Twilio,
por ejemplo, o notificaciones de Slack. ¿Puede encontrar un buen equivalente a MailHog para las
pruebas de integración?
2. De manera similar a lo que hicimos al pasar de send_mail a una clase de Notificaciones , intente
refactorizar nuestro redis_eventpublisher que actualmente es solo un Callable a algún tipo de
adaptador/clase base/protocolo más formal.
Resumen
Una vez que tenga más de un adaptador, comenzará a sentir mucho dolor al pasar las
dependencias manualmente, a menos que haga algún tipo de inyección de dependencia.
Configurar la inyección de dependencia es solo una de las muchas actividades típicas de configuración/
inicialización que debe realizar solo una vez al iniciar su aplicación. Reunir todo esto en un script de arranque
suele ser una buena idea.
El script de arranque también es bueno como un lugar para proporcionar una configuración predeterminada
razonable para sus adaptadores, y como un lugar único para anular esos adaptadores con falsificaciones para su
pruebas
Un marco de inyección de dependencia puede ser útil si necesita hacer DI en varios niveles, si tiene
dependencias encadenadas de componentes que necesitan DI, por ejemplo.
Este capítulo también presentó un ejemplo práctico de cómo cambiar una dependencia implícita/simple en un
adaptador "adecuado", factorizando un ABC, definiendo sus implementaciones reales y falsas, y pensando en
las pruebas de integración.
Resumen | 209
Machine Translated by Google
Resumen de DI y Bootstrap
En resumen:
2. Implemente lo real.
4. Encuentre una versión menos falsa que pueda poner en su entorno de Docker.
Estos fueron los últimos patrones que queríamos cubrir, lo que nos lleva al final de la Parte II.
En el epílogo, trataremos de darle algunos consejos para aplicar estas técnicas en el Mundo
RealTM.
Epílogo
¿Ahora que?
¡Uf! Hemos cubierto mucho terreno en este libro, y para la mayoría de nuestra audiencia todas estas
ideas son nuevas. Con eso en mente, no podemos esperar convertirlos en expertos en estas técnicas.
Todo lo que realmente podemos hacer es mostrarle las ideas generales y el código suficiente para que
pueda seguir adelante y escribir algo desde cero.
El código que mostramos en este libro no es un código de producción endurecido por la batalla: es un
conjunto de bloques de Lego con los que puedes jugar para hacer tu primera casa, nave espacial y
rascacielos.
Eso nos deja con dos grandes tareas. Queremos hablar sobre cómo comenzar a aplicar estas ideas de
verdad en un sistema existente, y debemos advertirle sobre algunas de las cosas que tuvimos que
omitir. Le hemos dado todo un nuevo arsenal de formas de dispararse en el pie, por lo que deberíamos
analizar algunos aspectos básicos de la seguridad con las armas de fuego.
esto: “Está bien, Bob y Harry, todo está muy bien, y si alguna vez me contratan para trabajar en un
nuevo servicio totalmente nuevo, sé qué hacer. Pero mientras tanto, estoy aquí con mi gran bola de
barro de Django, y no veo ninguna forma de llegar a su modelo agradable, limpio, perfecto, inmaculado
y simplista. No de aquí.
Te oimos. Una vez que ya has construido una gran bola de barro, es difícil saber cómo empezar a
mejorar las cosas. Realmente, tenemos que abordar las cosas paso a paso.
Lo primero es lo primero: ¿qué problema estás tratando de resolver? ¿Es demasiado difícil cambiar el
software? ¿Es el rendimiento inaceptable? ¿Tienes bichos extraños e inexplicables?
Tener un objetivo claro en mente lo ayudará a priorizar el trabajo que debe realizarse y, lo que es más
importante, comunicar las razones para hacerlo al resto del equipo.
211
Machine Translated by Google
Las empresas tienden a tener enfoques pragmáticos para la deuda técnica y la refactorización, siempre
que los ingenieros puedan presentar un argumento razonado para arreglar las cosas.
Este fue el sistema en el que Bob aprendió por primera vez cómo romper una bola de barro, y fue genial.
Había lógica en todas partes: en las páginas web, en los objetos de administrador, en los ayudantes, en
las clases de servicio gruesas que habíamos escrito para abstraer a los administradores y ayudantes, y en
los objetos de comando peludos que habíamos escrito para separar los servicios.
212 | Epílogo
Machine Translated by Google
Si está trabajando en un sistema que ha llegado a este punto, la situación puede parecer desesperada, pero
nunca es demasiado tarde para empezar a desmalezar un jardín demasiado grande. Finalmente, contratamos
a un arquitecto que sabía lo que estaba haciendo y nos ayudó a recuperar el control.
Comience por resolver los casos de uso de su sistema. Si tiene una interfaz de usuario, ¿qué acciones
realiza? Si tiene un componente de procesamiento de back-end, tal vez cada trabajo cron o trabajo de Celery
sea un caso de uso único. Cada uno de sus casos de uso debe tener un nombre imperativo: aplicar cargos
de facturación, limpiar cuentas abandonadas o generar orden de compra, por ejemplo.
En nuestro caso, la mayoría de nuestros casos de uso formaban parte de las clases de administrador y tenían
nombres como Crear espacio de trabajo o Eliminar versión del documento. Cada caso de uso se invocó
desde una interfaz web.
Nuestro objetivo es crear una sola función o clase para cada una de estas operaciones admitidas que se
ocupe de orquestar el trabajo a realizar. Cada caso de uso debe hacer lo siguiente:
Cada caso de uso debe tener éxito o fallar como una unidad atómica. Es posible que deba llamar a un caso
de uso desde otro. Está bien; simplemente anótelo e intente evitar transacciones de bases de datos de
ejecución prolongada.
Uno de los mayores problemas que tuvimos fue que los métodos de
administrador llamaban a otros métodos de administrador, y el acceso a los
datos podía ocurrir desde los propios objetos del modelo. Era difícil entender
lo que hacía cada operación sin emprender una búsqueda del tesoro a través
del código base. Reunir toda la lógica en un solo método y usar una UoW
para controlar nuestras transacciones hizo que el sistema fuera más fácil de razonar.
Epílogo | 213
Machine Translated by Google
grande Hace muchos años, Bob trabajó para una empresa de software que había subcontratado la
primera versión de su aplicación, una plataforma de colaboración en línea para compartir archivos y
trabajar con ellos.
Cuando la empresa trajo el desarrollo interno, pasó por varias generaciones de desarrolladores, y cada ola
de nuevos desarrolladores agregó más complejidad a la estructura del código.
En esencia, el sistema era una aplicación ASP.NET Web Forms, construida con un ORM de NHibernate. Los
usuarios cargarían documentos en espacios de trabajo, donde podrían invitar a otros miembros del espacio
de trabajo a revisar, comentar o modificar su trabajo.
La mayor parte de la complejidad de la aplicación estaba en el modelo de permisos porque cada documento
estaba contenido en una carpeta, y las carpetas permitían permisos de lectura, escritura y edición, muy
parecido a un sistema de archivos de Linux.
Además, cada espacio de trabajo pertenecía a una cuenta y la cuenta tenía cuotas adjuntas a través de un
paquete de facturación.
Como resultado, cada operación de lectura o escritura en un documento tenía que cargar una enorme
cantidad de objetos de la base de datos para probar los permisos y las cuotas.
La creación de un nuevo espacio de trabajo involucró cientos de consultas a la base de datos a medida que configuramos
la estructura de permisos, invitamos a los usuarios y configuramos el contenido de muestra.
Parte del código para las operaciones estaba en controladores web que se ejecutaban cuando un usuario
hacía clic en un botón o enviaba un formulario; parte estaba en objetos de administrador que contenían
código para orquestar el trabajo; y parte de ella estaba en el modelo de dominio. Los objetos del modelo
hacían llamadas a la base de datos o copiaban archivos en el disco, y la cobertura de la prueba era pésima.
Para solucionar el problema, primero introdujimos una capa de servicio para que todo el código para crear
un documento o espacio de trabajo estuviera en un solo lugar y pudiera entenderse. Esto implicó extraer el
código de acceso a datos del modelo de dominio y colocarlo en controladores de comandos. Del mismo
modo, extrajimos el código de orquestación de los administradores y los controladores web y lo insertamos
en los controladores.
Los controladores de comandos resultantes eran largos y desordenados, pero comenzamos a introducir
orden en el caos.
214 | Epílogo
Machine Translated by Google
Esta es una buena oportunidad para extraer cualquier código de orquestación o de acceso a datos del modelo
de dominio y llevarlo a los casos de uso. También deberíamos tratar de sacar las preocupaciones de E/S (p. ej.,
enviar correos electrónicos, escribir archivos) fuera del modelo de dominio y llevarlos a las funciones de casos
de uso. Aplicamos las técnicas del Capítulo 3 sobre abstracciones para mantener nuestra unidad de controladores
comprobable incluso cuando están realizando operaciones de E/S.
Estas funciones de casos de uso serán principalmente sobre registro, acceso a datos y manejo de errores. Una
vez que haya realizado este paso, tendrá una idea de lo que realmente hace su programa y una forma de
asegurarse de que cada operación tenga un comienzo y un final claramente definidos.
Habremos dado un paso hacia la construcción de un modelo de dominio puro.
Lea Trabajar de manera efectiva con el código heredado de Michael C. Feathers (Prentice Hall) para obtener
orientación sobre cómo poner a prueba el código heredado y comenzar a separar responsabilidades.
No puedes expresar todo el horror de la cosa en un diagrama de clases. Por un lado, no había realmente una
sola cuenta relacionada con un usuario. En cambio, había una regla extraña que requería que enumeraras todas
las cuentas asociadas al usuario a través de los espacios de trabajo y tomaras la que tuviera la fecha de creación
más antigua.
Cada objeto en el sistema formaba parte de una jerarquía de herencia que incluía el objeto seguro y la versión.
Esta jerarquía de herencia se reflejó directamente en el esquema de la base de datos, de modo que cada
consulta tenía que unirse a 10 tablas diferentes y buscar en una columna discriminadora solo para indicar con
qué tipo de objetos estaba trabajando.
El código base facilitó el "punto" de su camino a través de estos objetos de esta manera:
usuario.cuenta.espacios de trabajo[0].documentos.versiones[1].propietario.cuenta.configuración[0];
Construir un sistema de esta manera con Django ORM o SQLAlchemy es fácil pero debe evitarse. Aunque es
conveniente, hace que sea muy difícil razonar sobre el rendimiento porque cada propiedad puede desencadenar
una búsqueda en la base de datos.
Epílogo | 215
Machine Translated by Google
Un montón de operaciones requerían que recorriéramos los objetos de esta manera, por ejemplo:
def lock_account(usuario):
para el espacio de trabajo en user.account.workspaces:
espacio de trabajo.archive()
def lock_documents_in_folder(carpeta):
Estas operaciones acabaron con el rendimiento, pero corregirlas significaba renunciar a nuestro gráfico de objeto
único. En cambio, comenzamos a identificar agregados ya romper los vínculos directos entre objetos.
216 | Epílogo
Machine Translated by Google
Epílogo | 217
Machine Translated by Google
Los enlaces bidireccionales suelen ser una señal de que sus agregados no son correctos.
En nuestro código original, un Documento conocía la Carpeta que lo contenía , y la
Carpeta tenía una colección de Documentos. Esto facilita atravesar el gráfico de objetos,
pero nos impide pensar correctamente en los límites de consistencia que necesitamos.
Separamos los agregados usando referencias en su lugar. En el nuevo modelo, un
Documento tenía referencia a su carpeta_principal pero no tenía forma de acceder
directamente a la carpeta.
Carpeta.
Si necesitábamos leer datos, evitamos escribir bucles y transformaciones complejos e intentamos reemplazarlos
con SQL directo. Por ejemplo, una de nuestras pantallas era una vista de árbol de carpetas y documentos.
Esta pantalla era increíblemente pesada en la base de datos, porque se basaba en bucles for anidados que
activaban un ORM de carga diferida.
Usamos esta misma técnica en el Capítulo 11, donde reemplazamos un bucle anidado
sobre objetos ORM con una consulta SQL simple. Es el primer paso en un enfoque CQRS.
Después de mucho rascarnos la cabeza, reemplazamos el código ORM con un procedimiento almacenado grande
y feo. El código se veía horrible, pero era mucho más rápido y ayudó a romper los vínculos entre la carpeta y el
documento.
El patrón Strangler Fig implica crear un nuevo sistema alrededor de los bordes de un sistema antiguo, mientras se
mantiene en funcionamiento. Las partes de la funcionalidad anterior se interceptan y reemplazan gradualmente,
hasta que el sistema anterior no hace nada y se puede apagar.
Cuando construimos el servicio de disponibilidad, usamos una técnica llamada interceptación de eventos para
mover la funcionalidad de un lugar a otro. Este es un proceso de tres pasos:
218 | Epílogo
Machine Translated by Google
1. Genere eventos para representar los cambios que ocurren en un sistema que desea reemplazar.
2. Cree un segundo sistema que consuma esos eventos y los use para construir su propio
modelo de dominio
a la Figura E-3.
Figura E-3. Después: acoplamiento flexible con eventos asincrónicos (puede encontrar una
versión de alta resolución de este diagrama en cosmicpython.com)
Prácticamente, este fue un proyecto de varios meses. Nuestro primer paso fue escribir un modelo de
dominio que pudiera representar lotes, envíos y productos. Usamos TDD para construir un sistema de
juguetes que pudiera responder a una sola pregunta: "Si quiero N unidades de HAZARDOUS_RUG, ¿cuánto
tiempo tardarán en ser entregadas?"
Epílogo | 219
Machine Translated by Google
Con el tiempo, este proceso de sincronización se hizo cada vez más lento hasta que, una Navidad,
tomó más de 24 horas importar los pedidos de un solo día. Bob fue contratado para dividir el sistema en
un conjunto de servicios basados en eventos.
Primero, identificamos que la parte más lenta del proceso era calcular y sincronizar el stock disponible.
Lo que necesitábamos era un sistema que pudiera escuchar eventos externos y mantener un total
actualizado de la cantidad de stock disponible.
Expusimos esa información a través de una API, de modo que el navegador del usuario pudiera
preguntar cuánto stock había disponible para cada producto y cuánto tardaría en enviarse a su dirección.
Cada vez que un producto se agotaba por completo, generamos un nuevo evento que la plataforma de
comercio electrónico podría usar para retirar un producto de la venta. Debido a que no sabíamos cuánta
carga necesitaríamos manejar, escribimos el sistema con un patrón CQRS.
Cada vez que cambiaba la cantidad de existencias, actualizábamos una base de datos de Redis con un
modelo de vista en caché. Nuestra API de Flask consultó estos modelos de vista en lugar de ejecutar el
modelo de dominio complejo.
Si todo esto te suena un poco familiar, bueno, ¡ahora sabes de dónde vino nuestra aplicación de ejemplo!
Una vez que tuvimos un modelo de dominio de trabajo, pasamos a construir algunas piezas de
infraestructura. Nuestro primer despliegue de producción fue un sistema diminuto que podía recibir un
evento creado por lotes y registrar su representación JSON. Este es el "Hola mundo" de la arquitectura
impulsada por eventos. Nos obligó a implementar un bus de mensajes, conectar un productor y un
consumidor, crear una canalización de implementación y escribir un controlador de mensajes simple.
220 | Epílogo
Machine Translated by Google
Recomendamos el modelado de dominio como primer paso. En muchos sistemas demasiado grandes,
los ingenieros, los propietarios de productos y los clientes ya no hablan el mismo idioma. Las partes
interesadas del negocio hablan sobre el sistema en términos abstractos y centrados en el proceso,
mientras que los desarrolladores se ven obligados a hablar sobre el sistema tal como existe físicamente
en su estado salvaje y caótico.
usuario Mencionamos anteriormente que la cuenta y el modelo de usuario en nuestro primer sistema
estaban unidos por una "regla extraña". Este es un ejemplo perfecto de cómo las partes interesadas
en ingeniería y negocios pueden distanciarse.
En este sistema, las cuentas tenían como padres los espacios de trabajo y los usuarios eran miembros de los espacios de trabajo.
Los espacios de trabajo eran la unidad fundamental para la aplicación de permisos y cuotas. Si un
usuario se unió a un espacio de trabajo y aún no tenía una cuenta, lo asociaríamos con la cuenta que
poseía ese espacio de trabajo.
Esto fue desordenado y ad hoc, pero funcionó bien hasta el día en que el propietario del producto
solicitó una nueva función:
Cuando un usuario se une a una empresa, queremos agregarlo a algunos espacios de trabajo predeterminados para
la empresa, como el espacio de trabajo de recursos humanos o el espacio de trabajo de anuncios de la empresa.
Tuvimos que explicarles que no existía tal cosa como una empresa, y que no tenía ningún sentido que
un usuario se uniera a una cuenta. Además, una “empresa” puede tener muchas cuentas propiedad de
diferentes usuarios, y un nuevo usuario puede ser invitado a cualquiera de ellas.
Años de agregar trucos y soluciones alternativas a un modelo roto nos alcanzaron y tuvimos que
reescribir toda la función de administración de usuarios como un sistema completamente nuevo.
Averiguar cómo modelar su dominio es una tarea compleja que es el tema de muchos libros decentes por
derecho propio. Nos gusta usar técnicas interactivas como la tormenta de eventos y el modelado CRC,
porque los humanos son buenos para colaborar a través del juego.
El modelado de eventos es otra técnica que reúne a ingenieros y propietarios de productos para
comprender un sistema en términos de comandos, consultas y eventos.
Epílogo | 221
Machine Translated by Google
El objetivo es poder hablar sobre el sistema usando el mismo lenguaje ubicuo, para que puedan ponerse
de acuerdo sobre dónde radica la complejidad.
Hemos encontrado mucho valor en el tratamiento de problemas de dominio como kata TDD. Por ejemplo,
el primer código que escribimos para el servicio de disponibilidad fue el modelo de línea de pedido y lote.
Puede tratar esto como un taller a la hora del almuerzo o como un pico al comienzo de un proyecto.
Una vez que pueda demostrar el valor del modelado, es más fácil presentar el argumento para estructurar
el proyecto y optimizarlo para el modelado.
Cuando estuve expuesto por primera vez a los patrones descritos aquí, estaba bastante emocionado. Ya había
utilizado con éxito algunas de las técnicas en proyectos más pequeños, pero aquí había un modelo para sistemas
mucho más grandes respaldados por bases de datos como en el que trabajo en mi trabajo diario. Así que comencé
a tratar de descubrir cómo podría implementar ese modelo en mi organización actual.
Elegí abordar un área problemática del código base que siempre me había molestado. Comencé implementándolo
como un caso de uso. Pero me encontré con preguntas inesperadas.
Había cosas que no había considerado mientras leía que ahora hacían difícil ver qué hacer. ¿Fue un problema si
mi caso de uso interactuaba con dos agregados diferentes?
¿Podría un caso de uso llamar a otro? ¿Y cómo iba a existir dentro de un sistema que seguía diferentes principios
arquitectónicos sin resultar en un desastre horrible?
¿Qué pasó con ese plan tan prometedor? ¿Realmente entendí las ideas lo suficientemente bien como para ponerlas
en práctica? ¿Era incluso adecuado para mi aplicación? Incluso si lo fuera, ¿alguno de mis colegas estaría de
acuerdo con un cambio tan importante? ¿Fueron estas buenas ideas para fantasear mientras seguía con la vida
real?
Me tomó un tiempo darme cuenta de que podía empezar poco a poco. No necesitaba ser purista ni acertar a la
primera: podía experimentar, encontrar lo que me funcionaba.
Y eso es lo que he hecho. He podido aplicar algunas de las ideas en algunos lugares.
He creado nuevas funciones cuya lógica empresarial se puede probar sin la base de datos o simulacros.
Y como equipo, hemos introducido una capa de servicio para ayudar a definir los trabajos que realiza el sistema.
222 | Epílogo
Machine Translated by Google
Si comienza a tratar de aplicar estos patrones en su trabajo, puede experimentar sentimientos similares
al principio. Cuando la buena teoría de un libro se encuentra con la realidad de su base de código,
puede ser desmoralizador.
Mi consejo es que se concentre en un problema específico y se pregunte cómo puede poner en práctica
las ideas relevantes, quizás de una manera inicialmente limitada e imperfecta. Es posible que
descubras, como lo hice yo, que el primer problema que elijas puede ser demasiado difícil; si es así,
pase a otra cosa. No intentes hervir el océano y no tengas demasiado miedo de cometer errores. Será
una experiencia de aprendizaje y puede estar seguro de que se está moviendo aproximadamente en
una dirección que otros han encontrado útil.
Entonces, si usted también siente dolor, pruebe estas ideas. No sienta que necesita permiso para
rediseñar todo. Solo busque un lugar pequeño para comenzar. Y sobre todo, hacerlo para solucionar
un problema concreto. Si tiene éxito en resolverlo, sabrá que hizo algo bien, y los demás también lo
sabrán.
Aquí hay algunas preguntas que escuchamos durante la redacción que no pudimos encontrar un buen lugar para abordar
en otra parte del libro:
¿Tengo que hacer todo esto a la vez? ¿Puedo hacer un poco a la vez?
No, absolutamente puedes adoptar estas técnicas poco a poco. Si tiene un sistema existente, le recomendamos
crear una capa de servicio para tratar de mantener la orquestación en un solo lugar. Una vez que tenga eso, es
mucho más fácil insertar la lógica en el modelo y empujar las preocupaciones de borde como la validación o el
manejo de errores a los puntos de entrada.
Vale la pena tener una capa de servicio incluso si todavía tiene un ORM de Django grande y desordenado porque
es una forma de comenzar a comprender los límites de las operaciones.
La extracción de casos de uso romperá mucho de mi código existente; esta demasiado enredado
Solo copia y pega. Está bien causar más duplicación a corto plazo. Piense en esto como un proceso de varios
pasos. Su código está en mal estado ahora, así que cópielo y péguelo en un lugar nuevo y luego limpie y ordene
ese nuevo código.
Una vez que haya hecho eso, puede reemplazar los usos del código anterior con llamadas a su nuevo código y
finalmente eliminar el desorden. Arreglar grandes bases de código es un proceso complicado y doloroso. No espere
que las cosas mejoren instantáneamente, y no se preocupe si algunas partes de su aplicación siguen desordenadas.
¿Necesito hacer CQRS? Eso suena raro. ¿No puedo simplemente usar repositorios?
¡Por supuesto que puede! Las técnicas que presentamos en este libro están destinadas a facilitarle la vida. No son
una especie de disciplina ascética con la que castigarte a ti mismo.
Epílogo | 223
Machine Translated by Google
En nuestro primer sistema de estudio de caso, teníamos muchos objetos de View Builder que usaban repositorios
para obtener datos y luego realizaban algunas transformaciones para devolver modelos de lectura tonta. La ventaja
es que cuando se encuentra con un problema de rendimiento, es fácil reescribir un generador de vistas para usar
consultas personalizadas o SQL sin formato.
¿Cómo deberían interactuar los casos de uso en un sistema más grande? ¿Es un problema para uno llamar a otro?
Este podría ser un paso intermedio. Nuevamente, en el primer estudio de caso, teníamos controladores que
necesitarían invocar a otros controladores. Sin embargo, esto se vuelve realmente complicado y es mucho mejor usar
un bus de mensajes para separar estas preocupaciones.
Por lo general, su sistema tendrá una sola implementación de bus de mensajes y un grupo de subdominios que se
centran en un agregado o conjunto de agregados en particular.
Cuando su caso de uso haya terminado, puede generar un evento y un controlador en otro lugar
poder correr.
¿Es un olor a código para un caso de uso usar múltiples repositorios/agregados, y si es así, por qué?
Un agregado es un límite de consistencia, por lo que si su caso de uso necesita actualizar dos agregados
atómicamente (dentro de la misma transacción), entonces su límite de consistencia es incorrecto, estrictamente
hablando. Idealmente, debería pensar en pasar a un nuevo agregado que abarque todas las cosas que desea cambiar
al mismo tiempo.
Si en realidad está actualizando solo un agregado y usando los otros para acceso de solo lectura, entonces está
bien, aunque podría considerar construir un modelo de lectura/vista para obtener esos datos en su lugar; hace las
cosas más claras si cada caso de uso tiene un solo agregado.
Si necesita modificar dos agregados, pero las dos operaciones no tienen que estar en la misma transacción/UoW,
entonces considere dividir el trabajo en dos controladores diferentes y usar un evento de dominio para transportar
información entre los dos.
Puede leer más en estos documentos sobre diseño agregado de Vaughn Vernon.
¿Qué pasa si tengo un sistema de solo lectura pero con mucha lógica empresarial?
Los modelos de vista pueden tener una lógica compleja. En este libro, lo alentamos a separar sus modelos de lectura
y escritura porque tienen diferentes requisitos de consistencia y rendimiento. Principalmente, podemos usar una
lógica más simple para las lecturas, pero eso no siempre es cierto. En particular, los modelos de autorización y
permisos pueden agregar mucha complejidad a nuestro lado de lectura.
Hemos escrito sistemas en los que los modelos de vista necesitaban extensas pruebas unitarias. En esos sistemas,
separamos un generador de vistas de un buscador de vistas, como en la Figura E-4.
224 | Epílogo
Machine Translated by Google
Figura E-4. Un generador de vistas y un buscador de vistas (puede encontrar una versión de alta resolución de este
diagrama en cosmicpython.com)
+ Esto facilita la prueba del generador de vistas al proporcionarle datos simulados (por ejemplo, una lista de dictados).
"Fancy CQRS" con controladores de eventos es realmente una forma de ejecutar nuestra lógica de vista compleja cada vez
que escribimos para que podamos evitar ejecutarla cuando leemos.
Epílogo | 225
Machine Translated by Google
escopetas
Vale, te hemos dado un montón de juguetes nuevos con los que jugar. Aquí está la letra pequeña.
Harry y Bob no recomiendan que copie y pegue nuestro código en un sistema de producción y reconstruya su plataforma
de negociación automatizada en Redis pub/sub. Por razones de brevedad y simplicidad, hemos tratado a mano muchos
temas complicados. Aquí hay una lista de cosas que creemos que debes saber antes de probar esto de verdad.
Tyler Treat tiene algunas publicaciones de blog excelentes en su sitio bravonewgeek.com; debería leer al menos “No
puede tener una entrega exactamente una vez” y “Lo que quiere es lo que no: comprensión de las ventajas y
desventajas en la mensajería distribuida”.
Elegimos explícitamente transacciones pequeñas y enfocadas que pueden fallar de forma independiente
En el Capítulo 8, actualizamos nuestro proceso para que la desasignación de una línea de pedido y la reasignación
de la línea sucedan en dos unidades de trabajo separadas. Necesitará monitoreo para saber cuándo fallan estas
transacciones y herramientas para reproducir eventos. Algo de esto se facilita mediante el uso de un registro de
transacciones como intermediario de mensajes (p. ej., Kafka o EventStore). También puede mirar el patrón de
Bandeja de salida.
No discutimos la idempotencia.
No hemos pensado realmente en lo que sucede cuando se vuelven a intentar los controladores. En la práctica, querrá
hacer que los controladores sean idempotentes para que llamarlos repetidamente con el mismo mensaje no haga
cambios repetidos en el estado. Esta es una técnica clave para generar confiabilidad, ya que nos permite volver a
intentar eventos de manera segura cuando fallan.
Hay mucho material bueno sobre el manejo de mensajes idempotentes, intente comenzar con "Cómo garantizar la
idempotencia en una aplicación DDD/CQRS eventualmente consistente" y "(Un) Confiabilidad en la mensajería".
226 | Epílogo
Machine Translated by Google
Envolver
¡Uf! Esas son muchas advertencias y sugerencias de lectura; Esperamos no haberte asustado por
completo. Nuestro objetivo con este libro es brindarle el conocimiento y la intuición suficientes para
que pueda comenzar a construir algo de esto por sí mismo. Nos encantaría saber cómo te va y qué
problemas estás enfrentando con las técnicas en tus propios sistemas, así que ¿por qué no te pones
en contacto con nosotros en www.cosmicpython.com?
Epílogo | 227
Machine Translated by Google
Machine Translated by Google
APÉNDICE A
229
Machine Translated by Google
Dominio Entidad Un objeto de dominio cuyos atributos pueden cambiar pero que
Define la lógica de negocio. tiene una identidad reconocible a lo largo del tiempo.
consistencia.
diferentes componentes. unidad de trabajo Abstracción en torno a la integridad de los datos. Cada unidad de
Implementaciones concretas de una interfaz que va desde Cada agregado tiene su propio repositorio.
Puntos de entrada (adaptadores primarios) Web Recibe solicitudes web y las traduce en comandos, pasándolas
Consumidor de eventos Lee eventos del bus de mensajes externo y los traduce en
N/A Bus de Una pieza de infraestructura que utilizan los diferentes servicios
externo (agente
de mensajes)
APÉNDICE B
Alrededor del Capítulo 4, pasamos de tener todo en una carpeta a un árbol más estructurado, y
pensamos que podría ser interesante delinear las partes móviles.
.
ÿÿÿ Dockerfile
ÿÿÿ Makefile ÿÿÿ
README.md
ÿÿÿ docker-compose.yml
ÿÿÿ licencia.txt ÿÿÿ mypy.ini
ÿÿÿ requisitos.txt ÿÿÿ src ÿ
ÿÿ asignación ÿ ÿÿÿ
__init__.py ÿ ÿ ÿÿÿ
Adaptadores
dominio ÿ ÿ ÿ ÿiqute
ÿÿÿ
__init__.py ÿ ÿ ÿ modelo.py
ÿÿÿ
ÿÿÿ
231
Machine Translated by Google
ÿ ÿfrasco_app.py
ÿ __init__.py
ÿ service_layer
setup.py
ÿ services.py
ÿÿÿ
ÿ ÿÿÿ
pruebas
ÿ ÿÿÿ
ÿÿÿ
conftest.py ÿeÿeÿ
ÿ test_orm.py
test_api.pyÿ test_repository.py
ÿÿÿ
ÿÿÿintegración
pytest.ini ÿÿÿ
ÿ ÿÿÿ
ÿÿÿ
unidad ÿÿÿ test_allocate.py
ÿ test_services.py
ÿÿÿ test_batches.py
ÿÿÿ
ÿ
ÿÿÿ
ÿÿÿ
ÿÿÿ
ÿÿÿ
Nuestro docker-compose.yml y nuestro Dockerfile son los bits principales de configuración para
los contenedores que ejecutan nuestra aplicación, y también pueden ejecutar las pruebas (para
CI). Un proyecto más complejo podría tener varios Dockerfiles, aunque hemos descubierto que
minimizar la cantidad de imágenes suele ser una buena idea.1
Un Makefile proporciona el punto de entrada para todos los comandos típicos que un desarrollador
(o un servidor de CI) podría querer ejecutar durante su flujo de trabajo normal: hacer compilación,
hacer prueba, etc.2 Esto es opcional. Podría simplemente usar docker-compose y pyt est
directamente, pero al menos, es bueno tener todos los "comandos comunes" en una lista en
algún lugar y, a diferencia de la documentación, un Makefile es un código, por lo que tiene menos
tendencia a convertirse en out. de la fecha.
Todo el código fuente de nuestra aplicación, incluido el modelo de dominio, la aplicación Flask y
el código de infraestructura, reside en un paquete de Python dentro de
mediante
src, 3 que
pip instalamos
install -e y el
archivo setup.py. Esto facilita las importaciones. Actualmente, la estructura dentro de este módulo
es totalmente plana, pero para un proyecto más complejo, esperaría aumentar una jerarquía de
carpetas que incluya modelo_dominio/, infraestructura/, servicios/ y api/.
1 La división de imágenes para producción y prueba a veces es una buena idea, pero tendemos a encontrar que ir más allá y
tratar de dividir diferentes imágenes para diferentes tipos de código de aplicación (por ejemplo, API web versus cliente pub/
sub) generalmente termina siendo más problemas de lo que vale; el costo en términos de complejidad y tiempos más largos de
reconstrucción/CI es demasiado alto. YMMV.
2 Una alternativa de Python puro a Makefiles es Invoke, vale la pena echarle un vistazo si todos en su equipo conocen Python.
(¡o al menos lo sabe mejor que Bash!).
3 “Testing and Packaging” de Hynek Schlawack brinda más información sobre las carpetas src.
Las pruebas viven en su propia carpeta. Las subcarpetas distinguen diferentes tipos de pruebas y le
permiten ejecutarlas por separado. Podemos mantener los accesorios compartidos (conftest.py) en la
carpeta principal de pruebas y anidar otros más específicos si lo deseamos. Este es también el lugar
para guardar pytest.ini.
Los documentos de pytest son realmente buenos en cuanto al diseño de prueba y la importabilidad.
El problema básico que intentamos resolver aquí es que necesitamos diferentes ajustes de configuración para
lo siguiente:
• Ejecutar código o pruebas directamente desde su propia máquina de desarrollo, tal vez hablando con
puertos asignados desde contenedores Docker
La configuración a través de variables de entorno como sugiere el manifiesto de 12 factores resolverá este
problema, pero en concreto, ¿cómo lo implementamos en nuestro código y nuestros contenedores?
config.py
Cada vez que nuestro código de aplicación necesite acceso a alguna configuración, la obtendrá de un archivo
llamado config.py. Aquí hay un par de ejemplos de nuestra aplicación:
importarnos _
def get_postgres_uri():
host = os.environ.get('DB_HOST', 'localhost') puerto =
54321 if host == 'localhost' else 5432 contraseña =
os.environ.get('DB_PASSWORD', 'abc123') usuario, db_name
= 'asignación ', 'asignación' devuelve f"postgresql://{usuario}:
{contraseña}@{host}:{puerto}/{db_name}"
Usamos funciones para obtener la configuración actual, en lugar de constantes disponibles en el momento
de la importación, porque eso permite que el código del cliente modifique os.environ si es necesario.
config.py también define algunas configuraciones predeterminadas, diseñadas para funcionar cuando se
ejecuta el código desde la máquina local del desarrollador.4
Vale la pena mirar un elegante paquete de Python llamado environ-config si se cansa de ejecutar manualmente sus
propias funciones de configuración basadas en el entorno.
versión: "3"
servicios:
aplicación:
compilación:
contexto: . dockerfile: Dockerfile
depend_on: - entorno postgres :
- DB_HOST=postgres
- DB_CONTRASEÑA=abc123
- API_HOST=aplicación
- PYTHONDONTWRITEBYTECODE=1
4 Esto nos da una configuración de desarrollo local que “simplemente funciona” (tanto como sea posible). Es posible que prefiera fallar con
fuerza en las variables de entorno que faltan, especialmente si alguno de los valores predeterminados sería inseguro en producción.
5 Harry está un poco cansado de YAML. Está en todas partes y, sin embargo, nunca puede recordar la sintaxis o cómo se supone que debe
sangrar.
volúmenes: - ./
src:/src - ./tests:/tests
puertos: - "5005:80"
postgres:
imagen: postgres:9.6
entorno:
- POSTGRES_USER=asignación
- POSTGRES_CONTRASEÑA=abc123
puertos: - "54321:5432"
En el archivo docker-compose, definimos los diferentes servicios (contenedores) que necesitamos para
nuestra aplicación. Por lo general, una imagen principal contiene todo nuestro código y podemos usarla
para ejecutar nuestra API, nuestras pruebas o cualquier otro servicio que necesite acceso al modelo de
dominio.
Probablemente tendrá otros servicios de infraestructura, incluida una base de datos. En producción, es
posible que no use contenedores para esto; en su lugar, es posible que tenga un proveedor de nube,
pero docker-compose nos brinda una forma de producir un servicio similar para desarrollo o CI.
La estrofa de entorno le permite establecer las variables de entorno para sus contenedores, los nombres
de host y los puertos como se ve desde dentro del clúster de Docker. Si tiene suficientes contenedores
para que la información comience a duplicarse en estas secciones, puede usar el archivo_entorno en su
lugar. Usualmente llamamos al nuestro container.env.
Dentro de un clúster, docker-compose configura la red de modo que los contenedores estén disponibles
entre sí a través de nombres de host con el nombre de su servicio.
Sugerencia profesional: si está montando volúmenes para compartir carpetas de origen entre su máquina
de desarrollo local y el contenedor, la variable de entorno PYTHONDONTWRITEBYTECODE le dice a
Python que no escriba archivos .pyc, y eso le evitará tener millones de archivos de propiedad raíz
salpicados sobre su sistema de archivos local, siendo molesto eliminar y causando errores extraños del
compilador de Python además.
Montar nuestro código fuente y de prueba como volúmenes significa que no necesitamos reconstruir
nuestros contenedores cada vez que hacemos un cambio de código.
La sección de puertos nos permite exponer los puertos desde el interior de los contenedores al mundo exterior6 ;
estos corresponden a los puertos predeterminados que configuramos en config.py.
La carpeta src
ÿÿÿ src ÿ
ÿÿÿ asignación ÿ ÿ ÿÿÿ
config.py ÿ ÿ ÿ
ÿÿÿ
...
ÿÿÿ
configuración.py
Las subcarpetas definen los nombres de los módulos de nivel superior. Puedes tener varios si quieres.
Y setup.py es el archivo que necesita para que sea instalable por pip, como se muestra a continuación.
setup( nombre='asignación',
versión='0.1',
paquetes=['asignación'],
)
Eso es todo lo que necesitas. packages= especifica los nombres de las subcarpetas que desea instalar como módulos de
nivel superior. La entrada del nombre es solo cosmética, pero es obligatoria. Para un paquete que en realidad nunca llegará
a PyPI, funcionará bien.7
6 En un servidor CI, es posible que no pueda exponer puertos arbitrarios de manera confiable, pero es solo una conveniencia para el desarrollador local.
Puede encontrar formas de hacer que estas asignaciones de puertos sean opcionales (por ejemplo, con docker-compose.override.yml).
7 Para obtener más consejos sobre setup.py, consulte este artículo sobre empaquetado de Hynek.
Dockerfile
Los Dockerfiles van a ser muy específicos del proyecto, pero aquí hay algunas etapas clave que esperará ver:
EJECUTAR apk agregar --no-cache --virtual .build-deps gcc postgresql-dev musl-dev python3-dev EJECUTAR apk agregar
libpq
WORKDIR /src
ENV FLASK_APP=asignación/puntos de entrada/flask_app.py FLASK_DEBUG=1 PYTHONUNBUFFERED=1 ejecución
de matraz CMD --host=0.0.0.0 --port=80
Instalar nuestras dependencias de Python (es posible que desee separar su desarrollador de las
dependencias de producción; no lo hemos hecho aquí, por simplicidad)
Pruebas
Nuestras pruebas se mantienen junto con todo lo demás, como se muestra aquí:
ÿalocate.py.
prueba_servicios.py
ÿÿ prueba_lotes.py
ÿÿÿ
ÿÿÿ
Nada particularmente inteligente aquí, solo una cierta separación de diferentes tipos de pruebas que es probable que
desee ejecutar por separado, y algunos archivos para accesorios comunes, configuración, etc.
No hay una carpeta src ni setup.py en las carpetas de prueba porque, por lo general, no hemos necesitado hacer que
las pruebas sean instalables por pip, pero si tiene dificultades con las rutas de importación, es posible que le resulte útil.
Envolver
Estos son nuestros componentes básicos:
configuraciones de Docker para activar un clúster local que refleja la producción hasta ahora
como sea posible
• Configuración a través de variables de entorno, centralizada en un archivo de Python llamado config.py, con valores
predeterminados que permiten que las cosas se ejecuten fuera de los contenedores
Dudamos que alguien termine con exactamente las mismas soluciones que nosotros, pero esperamos que encuentre
algo de inspiración aquí.
APÉNDICE C
Intercambio de la infraestructura:
Hazlo todo con CSV
Este apéndice pretende ser una pequeña ilustración de los beneficios de los patrones Repositorio,
Unidad de trabajo y Capa de servicio. Está destinado a seguir desde el Capítulo 6.
Justo cuando terminamos de construir nuestra Flask API y la preparamos para su lanzamiento, el
negocio se nos acerca disculpándose, diciendo que no están listos para usar nuestra API y
preguntando si podemos construir algo que lea solo lotes y pedidos de un par de CSV y genera un
tercer CSV con asignaciones.
Por lo general, este es el tipo de cosas que podría hacer que un equipo maldiga, escupa y tome
notas para sus memorias. ¡Pero no nosotros! Oh no, nos hemos asegurado de que nuestras
preocupaciones de infraestructura estén bien desacopladas de nuestro modelo de dominio y capa de servicio.
Cambiar a CSV será una simple cuestión de escribir un par de clases nuevas de Repository y
UnitOfWork , y luego podremos reutilizar toda nuestra lógica de la capa de dominio y la capa de
servicio.
Aquí hay una prueba E2E para mostrarle cómo entran y salen los CSV:
def test_cli_app_reads_csvs_with_batches_and_orders_and_outputs_allocations(
hacer_csv
):
sku1, sku2 = referencia_aleatoria('s1'), referencia_aleatoria('s2')
lote1, lote2, lote3 = referencia_aleatoria('b1'), referencia_aleatoria('b2'), referencia_aleatoria('b3')
orden_ref = referencia_aleatoria('o' ) make_csv('lotes.csv', [ ['ref', 'sku', 'qty', 'eta'], [batch1, sku1,
100, ''], [batch2, sku2, 100, '2011-01 -01'], [lote3, sku2, 100, '2011-01-02'],
239
Machine Translated by Google
])
orders_csv = make_csv('orders.csv', [ ['orderid',
'sku', 'qty'], [order_ref, sku1, 3], [order_ref,
sku2, 12],
])
run_cli_script(orders_csv.parent)
if fila['eta']: eta =
datetime.strptime(fila['eta'], '%Y-%m-%d').date() else:
edad = ninguna
lotes.append(modelo.Lote( ref=fila['ref'],
sku=fila['sku'],
qty=int(fila['qty']), eta=eta
))
devolver lotes
def principal
(carpeta): lotes_ruta = Ruta (carpeta) / 'lotes.csv'
lotes = cargar_lotes(ruta_lotes)
si __nombre__ == '__principal__':
principal(sys.argv[1])
¡No se ve tan mal! Y estamos reutilizando nuestros objetos de modelo de dominio y nuestro servicio de dominio.
Pero no va a funcionar. Las asignaciones existentes también deben ser parte de nuestro almacenamiento
CSV permanente. Podemos escribir una segunda prueba para obligarnos a mejorar las cosas:
])
make_csv('allocations.csv',
[ ['orderid', 'sku', 'qty', 'batchref'], [old_order,
sku, 10, lote1],
])
pedidos_csv = make_csv('pedidos.csv', [
['orderid', 'sku', 'qty'], [new_order,
sku, 7],
])
run_cli_script(orders_csv.parent)
Y podríamos seguir pirateando y agregando líneas adicionales a esa función load_batches , y algún
tipo de forma de rastrear y guardar nuevas asignaciones, ¡pero ya tenemos un modelo para hacerlo!
Se llama nuestros patrones Repositorio y Unidad de trabajo.
Todo lo que tenemos que hacer ("todo lo que tenemos que hacer") es volver a implementar esas
mismas abstracciones, pero con CSV subyacentes en lugar de una base de datos. Y como verá,
realmente es relativamente sencillo.
def _load(self):
with self._batches_path.open() como f:
lector = csv.DictReader(f) for fila en
lector: ref, sku = fila['ref'], fila['sku'] qty
= int (fila['cantidad']) si fila['eta']: eta =
datetime.strptime(fila['eta'], '%Y-%m-
%d').date() else:
edad = ninguna
self._lotes[ref] = modelo.Lote(
def list(self):
return list(self._batches.values())
def commit(self):
with self.batches._allocations_path.open('w') como f: escritor
= csv.writer(f) escritor.writerow(['orderid', 'sku', 'qty',
'batchref' ]) para lote en self.batches.list():
Y una vez que tengamos eso, nuestra aplicación CLI para leer y escribir lotes y asignaciones en
CSV se reduce a lo que debería ser: un poco de código para leer líneas de pedido y un poco de
código que invoca nuestra capa de servicio existente:
bob y harry
APÉNDICE D
Suponga que desea usar Django en lugar de SQLAlchemy y Flask. ¿Cómo podrían verse
las cosas? Lo primero es elegir dónde instalarlo. Lo ponemos en un paquete separado al
lado de nuestro código de asignación principal:
ÿÿÿ src ÿ
ÿÿÿ asignación ÿ ÿ ÿÿÿ
__init__.py ÿ__init__.py
adaptadores
ÿ ÿÿÿ ÿ ÿ ÿ ÿÿÿ
...
ÿÿÿ DjanGoProject ÿ ÿÿÿ
__init__.py
Asignado ÿ ÿ ÿÿÿ ÿ ÿ ÿÿÿ
APPS.py ÿ ÿ __init__.py
ÿÿÿÿ0001_initial.py
ÿ ÿÿÿ ÿ ÿ ÿ ÿ ÿ ÿ ÿ ÿ ÿÿ
ÿ models.py ÿ__init__.py
django_project
ÿ views.py
ÿ ÿ ÿÿÿÿ
ÿÿÿ
ÿ ÿÿÿ
settings.py
ÿ ÿ ÿ ÿÿÿ urls.py ÿ ÿ wsgi.py
ÿÿÿ
ÿÿÿ
ÿÿÿ
ÿÿÿ
gestionar.py
ÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
setup.py ÿÿÿ pruebas ÿÿÿ
ÿÿÿ
prueba_api.py
245
Machine Translated by Google
ÿÿÿ integración ÿ
ÿÿÿ test_repository.py
...
Reescribir la primera prueba del repositorio fue un cambio mínimo: solo reescribió SQL sin formato con una llamada al
lenguaje Django ORM/QuerySet:
@pytest.mark.django_db
def test_repository_can_save_a_batch():
lote = modelo.Lote("lote1", "JABONERA OXIDADA", 100, eta=fecha(2011, 12, 25))
repo = repositorio.DjangoRepository()
repo.add(lote)
La segunda prueba es un poco más complicada ya que tiene asignaciones, pero aún está compuesta por código Django
de aspecto familiar:
)
d_b2 = django_models.Lote.objetos.create( referencia="lote2",
sku=sku, cantidad=100, eta=Ninguno
)
django_models.Allocation.objects.create(line=d_line, lote=d_batch1)
repo = repositorio.DjangoRepository()
recuperado = repo.get("batch1")
clase DjangoRepository(AbstractRepository):
def list(self):
return [b.to_domain() for b in django_models.Batch.objects.all()]
Puede ver que la implementación se basa en los modelos de Django que tienen algunos métodos
personalizados para traducir hacia y desde nuestro modelo de dominio.1
Métodos personalizados en las clases ORM de Django para traducir a/desde nuestro
modelo de dominio
Django ORM con métodos personalizados para la conversión de modelos de dominio (src/djangoproject/alloc/models.py)
1 La gente del proyecto DRY-Python ha creado una herramienta llamada mapeadores que parece que podría ayudar a minimizar el modelo
estándar para este tipo de cosas.
@staticmethod
def update_from_domain(lote: modelo_dominio.Lote):
prueba: b = Lote.objetos.get(referencia=lote.referencia)
excepto Batch.DoesNotExist: b
= Batch(referencia=lote.referencia) b.sku =
lote.sku b.qty = lote._cantidad_comprada b.eta =
lote.eta b.save()
b.allocation_set.set( Allocation.from_domain (l, b)
para l en lote._asignaciones
) b._allocations =
set( a.line.to_domain()
for a in self.allocation_set.all()
) volver b
clase OrderLine(modelos.Modelo):
#...
Para objetos de valor, objects.get_or_create puede funcionar, pero para entidades, probablemente necesite
un try-get/except explícito para manejar el upsert.2
Hemos mostrado el ejemplo más complejo aquí. Si decide hacer esto, ¡tenga en cuenta que habrá
repeticiones! Afortunadamente no es un modelo muy complejo.
2 @mr-bo-jangles sugirió que podría usar update_or_create, pero eso va más allá de nuestro Django-fu.
@pytest.mark.django_db(transacción=Verdadero)
def test_uow_can_retrieve_a_batch_and_allocate_to_it():
insert_batch('lote1', 'HIPSTER-WORKBENCH', 100, Ninguno)
uow = unidad_de_trabajo.DjangoUnitOfWork()
with uow: batch =
uow.batches.get(reference='batch1') line =
model.OrderLine('o1', 'HIPSTER-WORKBENCH', 10)
batch.allocate(line) uow. comprometerse()
@pytest.mark.django_db(transacción=Verdadero)
def test_rolls_back_uncommitted_work_by_default():
...
@pytest.mark.django_db(transacción=Verdadero)
def test_rolls_back_on_error():
...
Debido a que teníamos pequeñas funciones auxiliares en estas pruebas, los cuerpos principales
reales de las pruebas son prácticamente los mismos que con SQLAlchemy.
Y la implementación es bastante simple, aunque me tomó algunos intentos encontrar qué invocación de la
magia de transacción de Django funcionaría:
def __enter__(self):
self.lotes = repositorio.DjangoRepository()
transacción.set_autocommit(False) return
super().__enter__()
def commit(self):
para lote en self.batches.seen:
self.lotes.actualizar(lote)
transacción.commit()
def rollback(auto):
transacción.rollback()
Una dificultad: debido a que, a diferencia de SQLAlchemy, no estamos instrumentando las instancias
del modelo de dominio en sí mismas, el comando commit() necesita pasar explícitamente por todos
los objetos que han sido tocados por cada repositorio y actualizarlos manualmente al ORM. .
El archivo views.py de Django termina siendo casi idéntico al antiguo ask_app.py, porque nuestra
arquitectura significa que es un envoltorio muy delgado alrededor de nuestra capa de servicio (que, por
cierto, no cambió en absoluto):
@csrf_exempt
def add_batch(solicitud):
datos = json.loads(solicitud.cuerpo) eta
= datos['eta']
si eta no es Ninguno:
eta = datetime.fromisoformat(eta).date()
services.add_batch( data['ref'], data['sku'], data['qty'],
eta, unit_of_work.DjangoUnitOfWork() ,
@csrf_exempt
def allocate(solicitud): data
= json.loads(request.body) try: batchref
= services.allocate( data['orderid'],
data['sku'], data['qty'],
unit_of_work.DjangoUnitOfWork
(),
)
excepto (model.OutOfStock, services.InvalidSku) como e:
devolver JsonResponse({'mensaje': str(e)}, estado=400)
La razón principal en un nivel bajo es que el ORM de Django no funciona de la misma manera. No
tenemos un equivalente del mapeador clásico SQLAlchemy, por lo que nuestro Active Record y nuestro
modelo de dominio no pueden ser el mismo objeto. En su lugar, tenemos que construir una capa de
traducción manual detrás del repositorio. Eso es más trabajo (aunque una vez hecho, la carga de
mantenimiento continuo no debería ser demasiado alta).
Debido a que Django está tan estrechamente acoplado a la base de datos, debe usar ayudantes como
pytest-django y pensar detenidamente en las bases de datos de prueba, desde la primera línea de código,
de una manera que no teníamos que hacer cuando comenzamos con nuestro modelo de dominio puro.
Pero en un nivel más alto, la única razón por la que Django es tan bueno es que está diseñado en torno
al punto óptimo de facilitar la creación de aplicaciones CRUD con un mínimo de repeticiones. Pero todo
el enfoque de nuestro libro se trata de qué hacer cuando su aplicación ya no es una simple aplicación
CRUD.
En ese punto, Django comienza a entorpecer más de lo que ayuda. Cosas como el administrador de
Django, que son tan increíbles cuando comienzas, se vuelven activamente peligrosas si el objetivo de tu
aplicación es construir un conjunto complejo de reglas y modelado en torno al flujo de trabajo de los
cambios de estado. El administrador de Django pasa por alto todo eso.
• El patrón de la capa de servicio puede ser de su interés si ve mucha duplicación en sus vistas.py.
Puede ser una buena manera de pensar en sus casos de uso por separado de sus puntos finales
web. • Teóricamente, aún puede hacer DDD y modelado de dominio con modelos Django, ya que
están estrechamente acoplados a la base de datos; las migraciones pueden ralentizarlo, pero no
debería ser fatal. Entonces, siempre que su aplicación no sea demasiado compleja y sus pruebas
no sean demasiado lentas, es posible que pueda obtener algo del enfoque de modelos gordos:
aplique tanta lógica a sus modelos como sea posible y aplique patrones como Entidad, Objeto de
valor y Agregado. Sin embargo, vea la siguiente advertencia.
Dicho esto, se dice en la comunidad de Django que las personas encuentran que el enfoque de modelos
pesados se encuentra con problemas de escalabilidad propios, particularmente en torno a la gestión de
interdependencias entre aplicaciones. En esos casos, hay mucho que decir sobre la extracción de una
lógica empresarial o una capa de dominio para ubicarla entre sus vistas y formularios y su models.py,
que luego puede mantener al mínimo posible.
• Un consejo que hemos escuchado es poner un logic.py en cada aplicación de Django desde el primer
día. Esto le brinda un lugar para colocar la lógica de negocios y mantener sus formularios, vistas y
modelos libres de lógica de negocios. Puede convertirse en un trampolín para pasar más adelante
a un modelo de dominio completamente desacoplado y/o una capa de servicio.
• Una capa de lógica de negocios podría comenzar a trabajar con objetos de modelo de Django y solo
más tarde se desvincularía por completo del marco y trabajar en estructuras de datos simples de
Python.
• Para el lado de lectura, puede obtener algunos de los beneficios de CQRS poniendo lecturas en
un solo lugar, evitando llamadas ORM esparcidas por todo el lugar.
• Al separar módulos para lecturas y módulos para lógica de dominio, puede valer la pena
desvincularse de la jerarquía de aplicaciones de Django. Las preocupaciones comerciales los
atravesarán.
Para obtener más ideas y experiencias reales relacionadas con las aplicaciones existentes, consulte
el epílogo.
APÉNDICE E
Validación
Cada vez que enseñamos y hablamos sobre estas técnicas, una pregunta que surge una y otra vez es
“¿Dónde debo hacer la validación? ¿Pertenece eso a mi lógica de negocios en el modelo de dominio, o es
una preocupación de infraestructura?”
La consideración más importante es que queremos mantener nuestro código bien separado para que cada
parte del sistema sea simple. No queremos saturar nuestro código con detalles irrelevantes.
Cuando las personas usan la palabra validación, generalmente se refieren a un proceso mediante el cual
prueban las entradas de una operación para asegurarse de que cumplan con ciertos criterios. Las entradas
que coinciden con los criterios se consideran válidas y las entradas que no lo hacen no son válidas.
Si la entrada no es válida, la operación no puede continuar pero debería salir con algún tipo de error. En otras
palabras, la validación consiste en crear condiciones previas. Encontramos útil separar nuestras condiciones
previas en tres subtipos: sintaxis, semántica y pragmática.
Sintaxis de validación
En lingüística, la sintaxis de una lengua es el conjunto de reglas que rigen la estructura de las oraciones
gramaticales. Por ejemplo, en inglés, la oración "Asignar tres unidades de TASTELESS-LAMP para ordenar
veintisiete" es gramaticalmente correcta, mientras que la frase "hat hat hat hat hat hat wibble" no lo es.
Podemos describir oraciones gramaticalmente correctas como bien formadas.
255
Machine Translated by Google
¿Cómo se relaciona esto con nuestra aplicación? Estos son algunos ejemplos de reglas sintácticas:
• Un comando Asignar debe tener un ID de pedido, un SKU y una cantidad. • Una cantidad es un
Estas son reglas sobre la forma y la estructura de los datos entrantes. Un comando Asignar sin SKU o ID de pedido
no es un mensaje válido. Es el equivalente de la frase "Asignar tres a".
Tendemos a validar estas reglas en el borde del sistema. Nuestra regla general es que un manejador de mensajes
siempre debe recibir solo un mensaje que esté bien formado y que contenga toda la información requerida.
@dataclass
class Asignar (Comando):
_schema =
Schema({ 'orderid':
int, sku: str, qty:
And(Use(int), lambda n: n > 0) },
ignore_extra_keys=True)
@classmethod
def from_json(cls, datos):
datos = json.loads(datos)
return cls(**_schema.validate(datos))
La biblioteca de esquemas nos permite describir la estructura y la validación de nuestros mensajes de una
manera declarativa agradable.
El método from_json lee una cadena como JSON y la convierte en nuestro tipo de mensaje.
Sin embargo, esto puede volverse repetitivo, ya que necesitamos especificar nuestros campos dos veces, por lo que
es posible que deseemos introducir una biblioteca auxiliar que pueda unificar la validación y declaración de nuestros
tipos de mensajes:
def mayor_que_cero(x):
devuelve x > 0
Asignar = comando
( orderid = int, sku
= str, qty = cantidad
AddStock =
comando( sku=str,
qty=cantidad
La función de comando toma un nombre de mensaje, más kwargs para los campos de la carga útil del
mensaje, donde el nombre del kwarg es el nombre del campo y el valor es el analizador.
Usamos la función make_dataclass del módulo dataclass para crear dinámicamente nuestro tipo de
mensaje.
Podemos crear analizadores reutilizables para cantidad, SKU, etc. para mantener las cosas SECAS.
Esto se produce a expensas de perder los tipos en su clase de datos, así que tenga en cuenta esa
compensación.
Validación | 257
Machine Translated by Google
Por ejemplo, nuestro sistema podría validar el formato de un SKU. Hemos estado usando SKU
inventados como UNFORGIVING- COSHION y MISBEGOTTEN-POUFFE. Estos siguen un patrón
simple: dos palabras, separadas por guiones, donde la segunda palabra es el tipo de producto y la
primera palabra es un adjetivo.
A los desarrolladores les encanta validar este tipo de cosas en sus mensajes y rechazar cualquier cosa
que parezca un SKU no válido. Esto causa problemas horribles en el futuro cuando algún anarquista
lanza un producto llamado COMFY-CHAISE-LONGUE o cuando un problema con el proveedor resulta
en un envío de CHEAP-CARPET-2.
Realmente, como sistema de asignación, no es de nuestro incumbencia cuál podría ser el formato de
un SKU. Todo lo que necesitamos es un identificador, por lo que simplemente podemos describirlo como
una cadena. Esto significa que el sistema de adquisiciones puede cambiar el formato cuando lo desee,
y no nos importará.
Este mismo principio se aplica a los números de pedido, los números de teléfono de los clientes y mucho
más. En su mayor parte, podemos ignorar la estructura interna de las cadenas.
Del mismo modo, a los desarrolladores les encanta validar los mensajes entrantes con herramientas
como JSON Schema o crear bibliotecas que validen los mensajes entrantes y los compartan entre
sistemas. Esto también falla la prueba de robustez.
Imaginemos, por ejemplo, que el sistema de compras agrega nuevos campos al mensaje Change
BatchQuantity que registran el motivo del cambio y el correo electrónico del usuario responsable del
cambio.
Dado que estos campos no son importantes para el servicio de asignación, simplemente deberíamos
ignorarlos. Podemos hacerlo en la biblioteca de esquemas pasando la palabra clave arg
ignore_extra_keys=True.
Este patrón, mediante el cual extraemos solo los campos que nos interesan y hacemos una validación
mínima de ellos, es el patrón Tolerant Reader.
Validar lo menos posible. Lea solo los campos que necesita y no especifique
demasiado su contenido. Esto ayudará a que su sistema se mantenga sólido
cuando otros sistemas cambien con el tiempo. Resista la tentación de compartir
definiciones de mensajes entre sistemas: en su lugar, facilite la definición de
los datos de los que depende. Para obtener más información, consulte el
artículo de Martin Fowler sobre el patrón Tolerant Reader.
Mencionar a Postel puede ser bastante estimulante para algunas personas. Te dirán que
Postel es la razón precisa por la que todo en Internet está roto y no podemos tener cosas
bonitas. Pregúntele a Hynek sobre SSLv3 algún día.
Si está a cargo de una API que está abierta al público en Internet, puede haber buenas
razones para ser más conservador con respecto a las entradas que permite.
Validación en el borde
Anteriormente, dijimos que queremos evitar saturar nuestro código con detalles irrelevantes. En
particular, no queremos codificar a la defensiva dentro de nuestro modelo de dominio. En su lugar,
queremos asegurarnos de que se sepa que las solicitudes son válidas antes de que nuestro modelo de
dominio o los controladores de casos de uso las vean. Esto ayuda a que nuestro código se mantenga
limpio y mantenible a largo plazo. A veces nos referimos a esto como validación en el borde del sistema.
En el Capítulo 8, dijimos que el bus de mensajes era un gran lugar para poner preocupaciones
transversales, y la validación es un ejemplo perfecto de eso. Así es como podríamos cambiar nuestro
bus para realizar la validación por nosotros:
Validación
clase MessageBus:
) subir e
Validación | 259
Machine Translated by Google
Así es como podríamos usar ese método desde nuestro punto final de Flask API:
prueba: bus.handle_message('ChangeBatchQuantity', m)
excepto ValidationError:
print('Omitir mensaje inválido')
excepto excepciones . InvalidSku as e:
print(f'No se puede cambiar el stock por el sku faltante {e}')
Tenga en cuenta que nuestros puntos de entrada se refieren únicamente a cómo obtener un mensaje
del mundo exterior y cómo informar el éxito o el fracaso. Nuestro bus de mensajes se encarga de
validar nuestras solicitudes y enrutarlas al controlador correcto, y nuestros controladores se centran
exclusivamente en la lógica de nuestro caso de uso.
Cuando recibe un mensaje no válido, por lo general no hay mucho que pueda
hacer más que registrar el error y continuar. En MADE usamos métricas para
contar la cantidad de mensajes que recibe un sistema y cuántos de ellos se
procesan correctamente, se omiten o no son válidos. Nuestras herramientas
de monitoreo nos alertarán si vemos picos en la cantidad de mensajes
incorrectos.
Validación de la semántica
Mientras que la sintaxis se ocupa de la estructura de los mensajes, la semántica es el estudio del
significado de los mensajes. La oración "Deshacer los perros de los puntos suspensivos cuatro" es
sintácticamente válida y tiene la misma estructura que la oración "Asignar una tetera para pedir cinco",
pero no tiene sentido.
Podemos leer este blob JSON como un comando Allocate pero no podemos ejecutarlo con éxito
porque no tiene sentido:
Tendemos a validar las preocupaciones semánticas en la capa del controlador de mensajes con una especie de
programación basada en contratos:
Usamos una clase base común para los errores que significan que un mensaje no es válido.
El uso de un tipo de error específico para este problema hace que sea más fácil informar y manejar el
error. Por ejemplo, es fácil asignar ProductNotFound a un 404 en Flask.
Esto mantiene limpio y declarativo el flujo principal de nuestra lógica en la capa de servicio:
Validación | 261
Machine Translated by Google
asegurar.product_exists(uow, evento)
producto = uow.products.get(line.sku)
product.allocate(line) uow.commit()
Podemos extender esta técnica para asegurarnos de que aplicamos los mensajes de manera idempotente.
Por ejemplo, queremos asegurarnos de no insertar un lote de stock más de una vez.
Si nos piden que creemos un lote que ya existe, registraremos una advertencia y continuaremos con el
siguiente mensaje:
La introducción de una excepción SkipMessage nos permite manejar estos casos de forma genérica en
nuestro bus de mensajes:
clase MessageBus:
Hay un par de trampas a tener en cuenta aquí. Primero, debemos asegurarnos de que estamos usando la
misma UoW que usamos para la lógica principal de nuestro caso de uso. De lo contrario, nos exponemos a
irritantes errores de concurrencia.
En segundo lugar, debemos tratar de evitar poner toda nuestra lógica empresarial en estas comprobaciones
de condiciones previas. Como regla general, si una regla puede probarse dentro de nuestro modelo de dominio,
entonces debe probarse en el modelo de dominio.
Validación de la pragmática
La pragmática es el estudio de cómo entendemos el lenguaje en contexto. Después de analizar un mensaje y
comprender su significado, aún debemos procesarlo en contexto. Por ejemplo, si recibe un comentario en una
solicitud de extracción que dice: "Creo que esto es muy valiente", puede significar que el revisor admira su
coraje, a menos que sea británico, en cuyo caso, está tratando de decirle que lo que estás haciendo es
increíblemente arriesgado y que solo un tonto lo intentaría. El contexto lo es todo.
Resumen de validación
Validación | 263
Machine Translated by Google
En términos de software, la pragmática de una operación suele estar gestionada por el modelo
de dominio. Cuando recibimos un mensaje como "asignar tres millones de unidades de
SCARCE-CLOCK al pedido 76543", el mensaje es sintácticamente válido y semánticamente
válido, pero no podemos cumplir porque no tenemos el stock disponible.
Índice
265
Machine Translated by Google
229-230
266 | Índice
Machine Translated by Google
reconstruir el modelo de vista desde cero, 188 reproducción del comportamiento con la función time.sleep,
SELECT N+1 y otros problemas de rendimiento, 184 vista 109 conciencia, 166 consistencia, 96 logro de
simple usando el repositorio existente, 182 vistas de consistencia de lectura, 178 lecturas eventualmente consistentes,
96
flujo de comando para reservar stock, confirmar
stock está dañado, 164 flujo de comandos con de control, uso de excepciones para, 123
error, 165 lógica del controlador de comandos en el acoplamiento, 41 evitar acoplamiento inapropiado,
bus de mensajes, 202 eventos versus, 151-152 eventos, 166 desventajas de, 42 lógica de dominio junto con
comandos y manejo de errores, 155-157 E/S, 45 cascada de fallas como acoplamiento
temporal, 165 en pruebas que usan simulacros, 51
reducir mediante la abstracción de los detalles, 42
recuperarse de errores sincrónicamente, separar lo que quiere hacer de cómo hacerlo, 46
158 desacoplamiento temporal mediante mensajería
manejo de excepciones, 154 asíncrona, 167 compensación entre comentarios de diseño y,
manejadores para, 154 en nuestro 73
diagrama de componentes al final de la primera parte, 113 CSVs, haciendo todo con, 239-244
composición sobre herencia en TrackingRepoÿ
clase contenedora de historia, 129 D
raíz de composición, 191, 196
acceso a datos, aplicando el principio de inversión de
concurrencia, 97 agregados y
dependencia a, 25 problemas de integridad de datos
problemas de concurrencia, 112 permitiendo el mayor
que surgen de dividir la operación en dos UoW, 143 pruebas
grado de, 98 aplicación de reglas usando transacciones
para, 109-111
de base de datos,
110
Índice | 267
Machine Translated by Google
manteniendo todas las dependencias de dominio en electrónico falso real, 207 diseño controlado por dominio
funciones de dispositivo, 76 ninguna en el modelo de (DDD), 5, 7 (ver también modelo de dominio; modelado de
dominio, 25 dependencias de capa de servicio real en dominio)
tiempo de ejecución, Patrón agregado, 98
contextos acotados, 102
195
268 | Índice
Machine Translated by Google
usar hojas de cálculo en lugar de, 96 escribir arquitectura basada en eventos que
datos, 176 escribir pruebas en contra, 75 va a microservicios a través del patrón Strangler, 218-220
de dominio de pruebas unitarias, 10-19 clases de cambio de esquema a lo largo del tiempo,
datos para objetos de valor, 15 objetos y 226 comandos versus, 151-153 eventos,
entidades de valor, 17 servicios de dominio, comandos y manejo de errores, 155-157 interno versus
19, 66 función para, 19 adaptadores controlados , 68 externo, 172 división de comandos y eventos,
160
prueba por función, 79 desacoplamiento de la capa de correo electrónico cuando no hay existencias, 118-120
Índice | 269
Machine Translated by Google
toda la aplicación como bus de mensajes, compensaciones, primer corte de la aplicación, 58-60
150 introducción de la capa de servicio y el repositorio falso
UoW publica eventos en el bus de mensajes, 126 lecturas para realizar pruebas unitarias, 61-63 poner el
servicio de dominio, 57
Fowler, Martín, 52, 172
Freeman, Steve, 53 años
fingiendo _
76 nuevo tipo de consulta activado, 146 Publicación "Complejidad global, simplicidad local", 39 Método
usando para probar unitariamente la capa de servicio, mágico __gt__, 20
62 falsificaciones versus simulacros, 52
270 | Índice
Machine Translated by Google
Escuela londinense versus TDD de estilo clásico, 52 manejo de mensajes idempotentes, 226 la
mensajería confiable es difícil, 226 usar el
Objetos MagicMock, 52
mapeadores, 28 corredores
desacoplamiento temporal usando mensajería
de mensajes, 168 bus de
asíncrona, 167 pruebas con prueba de
mensajes bus de mensajes
extremo a extremo, 169-172 compensaciones,
abstractos y sus versiones real y falsa, 149
173 usando el canal pub/sub de Redis para la
integración, 168 enfoque basado en eventos, usando
antes, Message Buse como complemento opcional,
133 el patrón Strangler, 218 -220 producto mínimo
viable, 24 método mock.patch, 51, 195 burlándose
Celery y, 124
evitando el uso de mock.patch, 51 no te burles de lo
manejadores dados de clase en tiempo de
que no tienes, 88 simulacros versus falsificaciones, 52
ejecución, 201 despachando eventos y comandos de
pruebas sobresimuladas, trampas de, 53 "simulacros Aren't
manera diferente, 153 la lógica del controlador
Stubs" (Fowler), 52 modelo (dominio), 6
de eventos y comandos permanece igual, 202
personalizándose con valores predeterminados
de arranque anulados, 204 manejador publicando
eventos salientes, 171 método handle_event, 158
handle_event con reintentos, 158
Índice | 271
Machine Translated by Google
datos) sustantivos, división del sistema en, 162-165 179 separación de consulta de comando (CQS), 179
postgresql
Anti-Patrones: Ciclos de Lectura-Modificación-Escritura,
O
111
vecindarios de objetos, 84 documentación para niveles de aislamiento de
composición orientada a objetos, 53
transacciones, 110 gestión de problemas de
principios de diseño orientado a objetos, 21
concurrencia, 105
mapeadores relacionales de objetos (ORM), 27
Nivel de aislamiento de transacciones SERIALIZABLE,
asociación de lotes correctos con objetos de 105
producto, 103 ejemplo de Django ORM, 28
flujo de trabajo de refactorización preparatoria, 136
Django, métodos personalizados para traducir
primitivas que se mueven de objetos de dominio
hacia/desde el modelo de dominio, 247 ORM
a, en la capa de servicio, 139 obsesión primitiva, 139
depende del modelo de datos, 28 prueba del
ORM, 29 función orm.start_mappers, 199 patrón de
repositorio y, 36 Problema de rendimiento
Objeto del producto, 95
SELECT N+1, 184 vista simple usando ORM, 184
actuando como límite de consistencia, 155
SQLAlchemy, el modelo depende de ORM, 27
solicitando al producto que asigne en sus lotes,
100 código para, 100 capa de servicio
usando, 103 dos transacciones que intentan
actualizar simultáneamente, 105 números de
arquitectura de cebolla, 25
versión implementados en, 107
concurrencia optimista con números de versión, 105-108
orquestación, 61 cambio a coreografía, 121 uso de
servicio de aplicación, 66 capa de orquestación (ver capa de
Objeto ProductRepository, 103 proyectos
servicio)
que se organizan en carpetas, 67
estructuras de proyecto de plantilla,
231-238 protocolos, clases base abstractas,
Patrón de bandeja de salida, 226
tipificación pato y 33 bus de mensajes del sistema de
publicación-suscripción como controladores suscritos
P para recibir eventos,
inyección de
dependencia de funciones parciales con,
196 diferencia de cierres, 197 creación 123
manual en línea, 200 patrones, decidir si paso de publicación, 124
necesita usarlos, 114 utilizando el canal pub/sub de Redis para microserÿ
integración de vicios, 168
PEP 544 protocolos, 33 Charla de PyCon sobre Mocking Pitfalls, 53
límites de coherencia de pytest @pytest.skip, 104 accesorios, 45
rendimiento y, 95, 112 impacto del uso de complemento pytest-django, 246, 251
agregados, 99, 104 ignorancia de persistencia, argumento de sesión, 30
27 compensaciones, 38 concurrencia pesimista, 107
ejemplo, SELECCIONAR PARA ACTUALIZAR,
111 puertos definidos, 37
Q
consultas, 175
272 | Índice
Machine Translated by Google
53 adaptadores secundarios, 68
Redis como adaptador delgado alrededor del bus de Marinero, Mark, entrada de blog, 26
mensajes, 170 Consultas SELECT * FROM WHERE, 185
Redis, cambiando la implementación del modelo de lectura para usar, instrucción SELECCIONAR PARA ACTUALIZAR, 107
188 repositorios agregando el método de lista al objeto del ejemplo de control de concurrencia pesimista con, 111
repositorio existente, 181
SELECT N+1, 184
funciones de servicio
función de capa de servicio según el repositorio abstracto, nuestra aplicación al mundo real, 57 dependencias de,
64 vista simple usando el repositorio existente, 182 68 dependencias reales en tiempo de ejecución, 68 pruebas, 68
diferencia entre el servicio de dominio y, 66 pruebas de capa de
dominio que se trasladan a, 72 razones para, 73 prueba de
Clase contenedora TrackerRepository, 129 extremo a extremo de la API de asignación, prueba de rutas
Unidad de Trabajo colaboradora, 83 felices e infelices, 65 condiciones de error que requieren
Índice | 273
Machine Translated by Google
generar eventos y pasarlos al bus de mensajes, 131 generar usar directamente en el punto final de API, 31
sus propios eventos, 125 enviar alertas por correo usar DSL para especificar PARA ACTUALIZAR, 111 partes
electrónico cuando se agoten, 120 evitar, tomar eventos del interesadas, convencer para probar algo nuevo, 221-223
modelo y colocarlos
estado
en, 88 usando, pirámide de prueba y, 72 escribiendo la mayor Patrón de estrangulador, yendo a microservicios vía, 219-220
parte de las pruebas contra, 79 pruebas de escritura contra,
74 servicios de capa de servicio frente a servicios de dominio, stubbing, simulacros y stubs, 52 super
19 servicios función, 128 ajuste de falsificaciones en la
capa de servicio para llamar, 128 ejecución síncrona de
código de manejo de eventos,
130
274 | Índice
Machine Translated by Google
falso para servicio pruebas de capa, 87 para reglas de modificación para conectar eventos de dominio y bus de
integridad de datos, 109-111 prueba de integración mensajes, 117 pros y contras o compensaciones,
para la vista CQRS, 182 prueba de integración para 93 resumen de puntos importantes, 94 división de
anular los valores predeterminados de arranque, 204 operaciones en dos UoW, 143 prueba de conducción
pruebas de integración para el comportamiento de con pruebas de integración, 84 ordenar pruebas de
reversión, 89 árbol de carpetas de pruebas, 238 pruebas integración, 92
escritas en términos de eventos, 141
bus de mensajes falsos, 147 Unidad de UoW gestionando el éxito o el fracaso de la actualización
trabajo con pruebas de integración, 84 pruebas de agregada, 155
limpieza, 92 pruebas unitarias para bootstrap, 204 UoW publica eventos en el bus de mensajes, 126 usando
pruebas unitarias con falsificaciones en la capa de UoW en la capa de servicio, 88 usando UoW para agrupar
servicio, 62 función time.sleep, 109 reproducción del múltiples operaciones en unidades atómicas, 91-92
comportamiento de concurrencia con,
ejemplo de cambio de cantidad de lote, 91 ejemplo
109 de función de reasignación, 91 con Django,
actas 249-250 sin, API hablando directamente a tres capas,
concurrente, intentando actualizar el Producto,
105 81
Índice | 275
Machine Translated by Google
números de versión vista simple que usa el repositorio, 182 probar vistas
opciones de implementación para, 107 en la CQRS, 182 compensaciones para las opciones de modelo
tabla de productos, implementando bloqueo optimista, 105 de vista, 189 actualizar la tabla de modelo de lectura
usando el controlador de eventos, 186
puntos de vista
276 | Índice
Machine Translated by Google
Bob Gregory es un arquitecto de software con sede en el Reino Unido con MADE.com. Ha estado construyendo
sistemas controlados por eventos con diseño controlado por dominio durante más de una década.
Colofón
El animal de la portada de Architecture Patterns with Python es una pitón birmana (Python bivitattus). Como
era de esperar, la pitón birmana es originaria del sudeste asiático. Hoy vive en selvas y pantanos en el sur de
Asia, Myanmar, China e Indonesia; también es invasivo en los Everglades de Florida.
Las pitones birmanas son una de las especies de serpientes más grandes del mundo. Estos constrictores
carnívoros nocturnos pueden crecer hasta 23 pies y 200 libras. Las hembras son más grandes que los machos.
Pueden poner hasta cien huevos en una nidada. En la naturaleza, las pitones birmanas viven un promedio de
20 a 25 años.
Las marcas en una pitón birmana comienzan con una mancha de color marrón claro en forma de flecha en la
parte superior de la cabeza y continúan a lo largo del cuerpo en rectángulos que se destacan contra sus
escamas de color canela. Antes de que alcancen su tamaño completo, lo que lleva de dos a tres años, las
pitones birmanas viven en los árboles cazando pequeños mamíferos y aves. También nadan durante largos
períodos de tiempo, hasta 30 minutos sin aire.
Debido a la destrucción del hábitat, la pitón de Birmania tiene un estado de conservación Vulnerable. Muchos
de los animales de las portadas de O'Reilly están en peligro de extinción; todos ellos son importantes para el
mundo.
registrada
O'Reilly
O'Reilly
Media,
O'Reilly
marca
Media,
©2019
|175
Inc.
una
Inc.
de
es