Herramientas de diseño controladas por dominio

La ballena azul es un gran ejemplo de cómo salió mal el diseño de un proyecto complejo. La ballena parece un pez, pero es un mamífero: alimenta a los cachorros con leche, tiene lana y los huesos del antebrazo y las manos con los dedos, como los de la tierra, todavía se conservan en las aletas. Vive en los océanos, pero no puede respirar bajo el agua, por lo que regularmente sube a la superficie para tragar aire, incluso cuando está durmiendo. La ballena es el animal más grande del mundo, con una longitud de una casa de nueve pisos y un peso de 75 automóviles Volkswagen Touareg, pero no un depredador, sino que se alimenta de plancton.

Cuando los desarrolladores trabajaron en la ballena, no comenzaron a escribir todo desde cero, sino que utilizaron la experiencia de proyectos antiguos. Parece estar improvisado a partir de partes incompatibles del código que no se probaron, y todo el diseño se redujo a elegir un marco y un "ciclo" urgente que ya está en producción. Como resultado, el proyecto resultó ser hermoso en apariencia, pero con piezas de legado denso y muletas debajo del capó.



Para crear proyectos que ayuden a las empresas a ganar dinero, en lugar de parecer un animal marino que no puede respirar bajo el agua, existe DDD. Este es un enfoque que no se centra en las herramientas o el código, sino en el estudio del área temática, los procesos empresariales individuales y cómo funcionan el código o las herramientas para la lógica empresarial.

Qué es DDD y qué herramientas hay en él, lo diremos en un artículo basado en el informeArtyom Malyshev . El enfoque DDD en Python, las herramientas, las trampas, la programación de contratos y el diseño de productos en torno al problema que se está resolviendo, en lugar del marco utilizado, están por debajo.

Presentación completa del informe .

Artem Malyshev (proofit404) - un desarrollador independiente, escribió en Python durante 5 años, ayudó activamente con Django Channels 1.0. Más tarde se centró en los enfoques arquitectónicos: estudió qué herramientas les faltan a los arquitectos de Python y comenzó un proyecto de Python seco . Cofundador de Drylabs.

Complejidad


¿Qué es la programación?
La programación es una lucha constante con la complejidad que los propios desarrolladores crean cuando intentan resolver problemas.
La complejidad se divide en dos tipos: introducida y natural. El introducido se extiende junto con lenguajes de programación, marcos, SO, modelo de asincronía. Este es un desafío técnico que no se aplica a los negocios. La complejidad natural está oculta en el producto y simplifica la vida de los usuarios, ya que esta gente paga dinero.
Los buenos ingenieros deben reducir la complejidad añadida y aumentar la naturalidad para aumentar la utilidad del producto.
Pero los programadores somos personas complejas y nos encanta agregar complejidad técnica a los proyectos. Por ejemplo, no nos molestamos con los estándares de codificación, no usamos linter, prácticas de diseño modular y recibimos mucho código de estilo en los proyectos if c==1.

¿Cómo trabajar con dicho código? Lea muchos archivos, comprenda las variables, las condiciones y cuándo y cómo funcionará todo. Es difícil tener en cuenta este código: una complejidad absolutamente técnica añadida.

Otro ejemplo de complejidad adicional es mi "infierno de devolución de llamada" favorito.



Cuando escribimos en el marco de la arquitectura orientada a eventos (EDA) y elegimos un marco moderno no tan bueno, obtenemos un código en el que no está claro qué sucede y cuándo. Es difícil leer un código de este tipo; esta es nuevamente la complejidad añadida.

Los programadores no solo adoran las dificultades técnicas, sino que también discuten cuál es mejor:

  • AsyncIO o Gevent;
  • PostgreSQL o MongoDB;
  • Python o Go;
  • Emacs o Vim;
  • pestañas o espacios;

La respuesta correcta de un buen programador a todas estas preguntas: "¡No hay diferencia!" Los buenos desarrolladores no discuten sobre los caballos esféricos en el vacío, sino que resuelven problemas comerciales y trabajan en la utilidad del producto. Algunos de ellos han establecido durante mucho tiempo un conjunto de prácticas que reducen la complejidad introducida y lo ayudan a pensar más sobre el negocio.

Uno de ellos es Eric Evans . En 2004, escribió el libro Domain Driven Design. Ella "disparó" y dio un impulso para pensar más sobre el negocio, y empujar los detalles técnicos a un segundo plano.



¿Qué es el DDD?


Primero una solución al problema, y ​​luego herramientas . En primer lugar, Evans invirtió en el concepto de DDD, que esto no es una tecnología, sino una filosofía. En filosofía, primero debe pensar en cómo resolver el problema, y ​​solo entonces, con la ayuda de qué herramientas.

Trabaja en modelos con expertos en la materia y desarrolladores de software. Debemos comunicarnos con personas de negocios: buscar un lenguaje común, construir un modelo del mundo en el que nuestro producto funcionará y resolver problemas.

Escribir software que exprese modelos explícitamente. La diferencia más importante entre DDD y la colaboración simple en un equipo es que debemos escribir software en el mismo estilo que hablamos con expertos en dominios. Toda la terminología, los enfoques para la discusión y la toma de decisiones deben almacenarse en el código fuente para que incluso una persona no técnica pueda entender lo que sucede allí.

Habla el mismo idioma con los negocios . DDD es una filosofía sobre cómo hablar el mismo idioma con expertos en negocios en un campo específico y aplicar terminología a este campo. Tenemos un lenguaje o dialecto común dentro del contexto limitado, que consideramos verdadero. Creamos límites alrededor de soluciones arquitectónicas.

DDD no se trata de tecnología.

Primero, la parte técnica, luego - DDD. El escultor que talla la estatua en piedra no lee el manual sobre cómo sostener un martillo y un cincel; ya sabe cómo trabajar con ellos. Para incorporar DDD a su proyecto, domine la parte técnica: aprenda Django hasta el final, lea el tutorial y deje de discutir sobre si usar PostgreSQL o MongoDB.

La mayoría de los patrones de diseño y patrones son ruidos técnicos. La mayoría de los patrones que conocemos y usamos son técnicos. Dicen cómo reutilizar el código, cómo estructurarlo, pero no dicen cómo usarlo para usuarios, empresas y modelar el mundo exterior. Por lo tanto, las fábricas o clases abstractas están unidas libremente a DDD.

El primer libro "azul" salió hace casi 20 años. La gente trató de escribir en este estilo, caminó un rastrillo y se dio cuenta de que la filosofía es buena, pero en la práctica incomprensible. Por lo tanto, apareció un segundo libro, "rojo", sobre cómo los programadores piensan y escriben en DDD.


Los libros "rojo" y "azul" son los pilares sobre los que se apoya todo DDD.

Nota. Los libros rojo y azul son una fuente única de información sobre DDD, pero son pesados. Los libros no son fáciles de leer: en el original debido a la complejidad del idioma y los términos, y en ruso debido a la mala traducción. Por lo tanto, comience a aprender DDD con un libro verde . Esta es una versión simplificada de los dos primeros con ejemplos más simples y descripciones generales. Pero es mejor que si los libros rojo y azul superaran su deseo de estudiar y aplicar DDD. Es mejor leer en el original.

El libro rojo omite la idea de la mejor manera de incorporar DDD al proyecto, cómo estructurar el trabajo en torno a este enfoque. Aparece una nueva terminología: "Diseño impulsado por modelos", en el que nuestro modelo del mundo exterior se pone en primer lugar.



El único lugar donde se elige la tecnología es Smart UI. Esta es una capa entre el mundo exterior, el usuario y nosotros (una referencia a Robert Martin y su arquitectura limpia con capas). Como puede ver, todo va al modelo.

¿Qué es un modelo? Este es el dolor fantasma de cualquier arquitecto. Todos piensan que esto es UML, pero no lo es.
Un modelo es un conjunto de clases, métodos y enlaces entre ellos que reflejan los escenarios empresariales del programa.
El modelo refleja un objeto real con todas las propiedades y funciones necesarias. Este es un juego de herramientas de alto nivel para tomar decisiones desde el punto de vista de los casos de negocios. Sin embargo, los métodos y las clases son un conjunto de herramientas de bajo nivel para soluciones arquitectónicas.

Pitón seco


Para llenar el nicho de modelos, comencé un proyecto de pitón seco que se ha convertido en una colección de biblioteca arquitectónica de alto nivel para construir Model Driven Design. Cada una de las bibliotecas está intentando cerrar un círculo en la arquitectura y no interfiere con el otro. Las bibliotecas se pueden usar por separado o juntas si lo prueba.



La secuencia de la narración corresponde a la cronología de la adición óptima de DDD al proyecto, por capas. La primera capa son los servicios , una descripción de los escenarios empresariales (procesos) en nuestro sistema. La biblioteca de historias es responsable de esta capa.

Cuentos


Los escenarios empresariales se dividen en tres partes:

  • especificación : una descripción del proceso de negocio;
  • El estado en el que puede existir el escenario empresarial
  • implementación de cada paso del guión.

Estas partes no deben mezclarse. La biblioteca de Historias separa estas partes y dibuja una línea clara entre ellas.

Considere la introducción de DDD e Historias con un ejemplo. Por ejemplo, tenemos un proyecto en Django con una mezcla de señales de Django y modelos oscuros "gruesos". Agregue un paquete de servicios vacío. Usando la biblioteca Stories en partes, reescribimos este hash en un conjunto de scripts claros y comprensibles en nuestro proyecto.

Especificación DSL. La biblioteca le permite escribir una especificación y proporciona DSL para esto. Esta es una manera de describir las acciones del usuario paso a paso. Por ejemplo, para comprar subscription, sigo varios pasos: encontraré un pedido, verificaré la relevancia del precio, verificaré si el usuario puede pagarlo. Esta es una descripción de alto nivel.

Contrato.Debajo de esta clase, redactaremos un contrato para el estado del escenario empresarial. Para hacer esto, denotamos el área de variables que surgen en el proceso de negocio, y para cada variable asignamos un conjunto de validadores.

Tan pronto como alguien intente asignar una variable a esta área como parte del proceso comercial, se resolverá un conjunto de validadores. Nos aseguraremos de que el estado del proceso en tiempo de ejecución siempre funcione. Pero si no, cae dolorosamente y grita ruidosamente al respecto.

Etapa de implementación de cada paso . En la misma clase, subscriptionescribimos un conjunto de métodos cuyos nombres corresponden a los pasos del negocio. Cada método de entrada recibe un estado con el que puede funcionar, pero no tiene derecho a modificarlo. El método puede devolver algún marcador e informar:

  • , () ;
  • - , .


Hay marcadores más complejos: pueden confirmar que el estado está funcionando, sugerir eliminar o cambiar algunas partes del proceso comercial. También puedes escribir en clases.

Lanzamiento de la historia. ¿Cómo ejecutar Story en ejecución? Este es un objeto comercial que funciona como método: transferimos datos a la entrada, los valida, interpreta los pasos. La historia en ejecución recuerda el historial de ejecución, registra el estado que ocurrió en el proceso de negocios y nos dice quién influyó en este estado .

Barra de herramientas de depuración. Si escribimos en Django y usamos el panel de depuración, podemos ver qué escenarios comerciales se procesaron en cada solicitud y su estado.

Py.test. Si escribimos en py.test, para la prueba fallida podemos ver qué scripts de negocios se ejecutaron en cada línea y qué salió mal. Esto es conveniente: en lugar de elegir el código, leemos la especificación y entendemos lo que sucedió.



Centinela. Aún mejor, cuando recibimos el error 500. En un sistema normal, lo soportamos y comenzamos a investigar. En Sentry, aparecerá un informe detallado sobre lo que hizo el usuario para cometer el error. Es conveniente y agradable cuando a las 3 a.m. se recopiló dicha información para usted.


ELK . Ahora estamos trabajando activamente en un complemento que escribe todo esto en Elasticsearch en la pila de Kibana y crea índices competentes.



Por ejemplo, tenemos un contrato para el estado de un proceso comercial. Sabemos lo que hay, por ejemplo,relation IDreporte. En lugar de una investigación arcaica de lo que una vez sucedió allí, escribimos una solicitud en Kibana. Mostrará todas las historias relacionadas con un usuario específico. A continuación, examinamos el estado dentro de nuestros procesos comerciales y escenarios comerciales. No escribimos una sola línea de código de registro, pero el proyecto se registra al nivel de abstracción que nos interesa observar.

Pero quiero algo de nivel superior, por ejemplo, objetos ligeros. Dichos objetos contienen estructuras de datos alfabetizados y métodos que se relacionan con la adopción de decisiones comerciales y no con el trabajo con la base de datos, por ejemplo. Por lo tanto, pasamos a la siguiente parte de la arquitectura dirigida por el modelo: entidades, agregados y objetos de valor.



Entidades, agregados y objetos de valor


¿Cómo se interconecta todo esto? Por ejemplo, un usuario realiza un pedido de producto y nosotros le facturamos. ¿Cuál es la raíz de la agregación y qué es un objeto simple?



Todo lo que está subrayado es la raíz de la agregación. Esto es con lo que quiero trabajar directamente: importante, valioso, holístico.

¿Dónde empezar? Crearemos un paquete vacío en el proyecto, donde colocaremos nuestras unidades. Los agregados se escriben mejor con algo declarativo, como dataclasseso attrs.

Clases de datos . Si dataclassindicamos algún tipo de agregado, escribiremos una anotación en él usando NewType . En la anotación indicamos una referencia explícita, que se expresa en el sistema de tipos. Si dataclasses solo una estructura de datos (entidad), guárdelo dentro del agregado.

En el contexto de Historias, solo los agregados pueden mentir. El acceso a algo incrustado en ellos solo se puede obtener a través de métodos públicos y reglas de alto nivel. Esto le permite construir de manera lógica y competente un modelo sobre el cual trabajamos junto con expertos del área temática. Este es el mismo idioma único .



El problema surge de inmediato: repositorios. Tengo una base de datos con la que trabajo a través de Django, un microservicio vecino al que envío solicitudes, hay JSON y una instancia del modelo Django. ¿Para recibir datos y transferirlos a mano, solo para llamar o probar el método maravillosamente? Por supuesto no. Dry-python tiene una biblioteca Mappers que le permite asignar abstracciones de alto nivel y agregados de dominio a los lugares donde los almacenamos.

Mapeadores


Agregamos un paquete más a nuestro proyecto: un repositorio en el que almacenaremos el nuestro mappers. Así es como transferiremos la lógica empresarial de alto nivel al mundo real.

Por ejemplo, podemos describir cómo mapeamos uno dataclassa un modelo de Django.

Django ORM. Comparamos el modelo de pedido con la descripción de Django ORM: observamos los campos.

Por ejemplo, podemos reescribir algunos campos a través de la configuración opcional. Sucederá lo siguiente: mapperdurante la declaración comparará cómo dataclassse escribe el modelo. Por ejemplo, las anotaciones int( Order dataclasshay un campo costcon anotaciones int) en el modelo Django se corresponden integer fieldcon la opción nullable="true". Aquí se dataclassofrecen para agregar optionala dataclass, o eliminarnullablede field.

A través de Mappers, puede agregar funciones que leen o escriben algo. Los lectores son funciones que reciben un agregado en la entrada y le devuelven una referencia. Los escritores hacen lo contrario: devolver unidades. Bajo el capó, por ejemplo, puede haber una solicitud a la base de datos a través de Django.

Definiciones Swagger Las mismas operaciones se pueden llevar a cabo con microservicios. Puede escribir una parte del esquema de swagger en ellos y verificar cuánto coincide el esquema de swagger de un servicio en particular con sus modelos de dominio. Además, la solicitud devuelta de la biblioteca Solicitud se traducirá de forma transparente dataclass.

Consultas GraphQL. GraphQL y microservicios: el esquema de tipo de interfaz GraphQL funciona bien contradataclass. Puede traducir consultas específicas de GraphQL en estructuras de datos internas.

¿Por qué molestarse con el modelo interno de datos de alto nivel dentro de la aplicación? Para ilustrar el "por qué", contaré una historia "entretenida".

En uno de nuestros proyectos, los sockets web funcionaron a través del servicio Pusher. No nos molestamos, lo envolvimos en una interfaz para no llamar directamente. Esta interfaz estaba vinculada en todas las historias y quedó satisfecha.

Pero los requisitos comerciales han cambiado. Resultó que las garantías que ofrece Pusher para los sockets web no son suficientes. Por ejemplo, necesita entrega garantizada de mensajes e historial de mensajes durante los últimos 2 minutos. Por lo tanto, decidimos pasar al servicio Ably Realtime. También tiene una interfaz: escribiremos un adaptador y lo ataremos a todas partes, todo será genial. Realmente no.

Las abstracciones que utiliza Pusher (argumentos de función) están atrapadas en cada objeto comercial. Tuve que arreglar alrededor de 100 historias y arreglar la formación del canal de usuario al que enviamos algo.

De vuelta a las pruebas.

Pruebas y simulacros


¿Cómo sueles probar este comportamiento con servicios externos? Estamos mojando algo, estamos viendo cómo se llama una biblioteca de terceros, y eso es todo, estamos seguros de que todo está bien. Pero cuando la biblioteca cambia, los formatos de argumento también cambian.

Puede ahorrar una semana reescribiendo miles de pruebas y cientos de casos de negocios si prueba el comportamiento del modelo interno de manera diferente. Por ejemplo, algo similar a las pruebas de integración: escribimos en la secuencia del usuario, y ya dentro del adaptador, Pusher o Ably, traducimos esta secuencia al nombre del canal normal para no escribirlo todo en la lógica empresarial.

Dependencias


En dicha arquitectura modelo, aparecen muchas entidades superfluas. Anteriormente, tomamos algún tipo de función Django y la escribimos: solicitud, respuesta, movimientos mínimos del cuerpo. Aquí debe inicializar los Mapeadores, poner Historias e inicializar, procesar la línea de solicitud de la solicitud HTTP, ver qué respuesta dar. Todo esto da como resultado 30-50 líneas de código de llamada repetitivo Historias dentro de Django-view.

Por otro lado, ya hemos escrito interfaces y mapeadores. Podemos verificar su compatibilidad con un caso de negocios específico, por ejemplo, usando la biblioteca de dependencias. ¿Cómo? A través del patrón de inyección de dependencia, todo se declara pegado a una placa base mínima.

Aquí indicamos la tarea de tomar una clase en el paquete de servicios, poner tresmappers, inicializar el objeto Historias y dárnoslo. Con este enfoque, el número de repeticiones en el código se reduce enormemente.

Mapa de refactorización


Utilizando todo lo que mencioné, desarrollamos un esquema mediante el cual reescribimos un gran proyecto desde las señales de Django ("devolución de llamada implícita") a Django usando DDD.

El primer paso sin DDD . Al principio no teníamos DDD, escribimos MVP. Cuando ganaron su primer dinero, invitaron a los inversores y los convencieron de cambiar a DDD.

Historias sin contratos. Dividimos el proyecto en casos comerciales lógicos sin contratos de datos.

Contratos y agregados . Luego, uno por uno, arrastramos el contrato de datos para cada modelo, que se puede rastrear en nuestra arquitectura.

Creadores de mapas . Los mapeadores escribieron para deshacerse de las plantillas de almacenamiento de datos.

Inyección de dependencia . Deshágase de los patrones de pegado.

Si su proyecto ha superado el MVP y necesita cambiar urgentemente su arquitectura para que no se deslice hacia el legado, mire hacia DDD.

legacy Python-, , , Moscow Python Conf++ 27 . Python . unconference, , , , Drylabs.

DDD Python, TechLead ConfIT-, DDD . 8 , Call for Papers 6 .

All Articles