Arquitectura limpia a través de los ojos de un desarrollador de Python

¡Hola! Mi nombre es Eugene, soy desarrollador de Python. Durante el último año y medio, nuestro equipo comenzó a aplicar activamente los principios de Arquitectura limpia, alejándose del modelo clásico de MVC. Y hoy hablaré sobre cómo llegamos a esto, qué nos da y por qué la transferencia directa de enfoques de otros PL no siempre es una buena solución.



Python ha sido mi principal herramienta de desarrollo durante más de siete años. Cuando me preguntan qué es lo que más me gusta de él, respondo que esta es su excelente legibilidad . El primer conocido comenzó con una lectura del libro "Programación de una mente colectiva" . Estaba interesado en los algoritmos descritos en él, pero todos los ejemplos estaban en un lenguaje que aún no me resultaba familiar. Esto no era habitual (Python todavía no era corriente en el aprendizaje automático), las listas a menudo se escribían en pseudocódigo o usando diagramas. Pero después de una rápida introducción al lenguaje, aprecié su concisión: todo fue fácil y claro, nada superfluo y distractor, solo la esencia del proceso descrito.. El principal mérito de esto es el sorprendente diseño del lenguaje, el azúcar sintáctico muy intuitivo. Esta expresividad siempre ha sido apreciada en la comunidad. Lo que " importa esto ", necesariamente presente en las primeras páginas de cualquier libro de texto, cuesta: parece un supervisor invisible, evalúa constantemente sus acciones. En los foros, valía la pena el uso de CamelCase de alguna manera en el nombre de la variable en la lista, por lo que inmediatamente el ángulo de discusión cambió hacia el idioma del código propuesto con referencias a PEP8.
La búsqueda de la elegancia más el poderoso dinamismo del lenguaje han creado muchas bibliotecas con una API realmente encantadora. 

Sin embargo, Python, aunque potente, es solo una herramienta que le permite escribir código expresivo y autodocumentado, pero no garantiza esto , ni el cumplimiento de PEP8. Cuando nuestra tienda en línea aparentemente simple en Django comienza a ganar dinero y, como resultado, aumenta las funciones, en un momento nos damos cuenta de que no es tan simple, e incluso hacer cambios básicos requiere más y más esfuerzo, y lo más importante, esta tendencia está creciendo. ¿Qué pasó y cuándo salió todo mal?

Código malo


El código incorrecto no es uno que no sigue PEP8 o no cumple con los requisitos de complejidad ciclomática. El código incorrecto es, en primer lugar, dependencias no controladas , lo que lleva al hecho de que un cambio en un lugar del programa conduce a cambios impredecibles en otras partes. Estamos perdiendo el control sobre el código; expandir la funcionalidad requiere un estudio detallado del proyecto. Dicho código pierde su flexibilidad y, por así decirlo, se resiste a realizar cambios, mientras que el programa en sí mismo se vuelve "frágil". 

Arquitectura limpia


La arquitectura elegida de la aplicación debería evitar este problema, y ​​no somos los primeros en encontrarlo: ha habido una discusión en la comunidad Java sobre la creación de un diseño de aplicación óptimo durante mucho tiempo.

En 2000, Robert Martin (también conocido como tío Bob) en su artículo " Principios de diseño y diseño " reunió cinco principios para diseñar aplicaciones OOP bajo el acrónimo memorable SOLID. Estos principios han sido bien recibidos por la comunidad y han ido mucho más allá del ecosistema de Java. Sin embargo, son de naturaleza muy abstracta. Más tarde hubo varios intentos de desarrollar un diseño de aplicación general basado en principios SÓLIDOS. Estos incluyen: "Arquitectura hexagonal", "Puertos y adaptadores", "Arquitectura bulbosa" y todos tienen mucho en común, aunque diferentes detalles de implementación. Y en 2012, el mismo Robert Martin publicó un artículo, donde propuso su propia versión llamada " Arquitectura limpia ".



Según el tío Bob, la arquitectura es principalmente " fronteras y barreras ", es necesario comprender claramente las necesidades y limitar las interfaces de software para no perder el control sobre la aplicación. Para hacer esto, el programa se divide en capas.. Al pasar de una capa a otra, solo se pueden transferir datos (las estructuras simples y los objetos DTO pueden actuar como datos ): esta es la regla de los límites. Otra frase citada con mayor frecuencia de que "la aplicación debería gritar " significa que lo principal en la aplicación no es el marco utilizado o la tecnología de almacenamiento de datos, sino lo que realmente hace esta aplicación, qué función desempeña: la lógica empresarial de la aplicación . Por lo tanto, las capas no tienen una estructura lineal, sino una jerarquía . De ahí dos reglas más:

  • La regla de prioridad para la capa interna : es la capa interna la que determina la interfaz a través de la cual interactuará con el mundo exterior;
  • Regla de dependencia : las dependencias deben dirigirse desde la capa interna hacia la externa.

La última regla es bastante atípica en el mundo de Python. Para aplicar cualquier escenario complicado de lógica de negocios, siempre necesita acceder a servicios externos (por ejemplo, una base de datos), pero para evitar esta dependencia, la capa de lógica de negocios debe declarar la interfaz por la cual interactuará con el mundo exterior. Esta técnica se llama " inversión de dependencia " (la letra D en SÓLIDO) y se usa ampliamente en lenguajes con escritura estática. Según Robert Martin, esta es la principal ventaja que vino con la OOP .

Estas tres reglas son la esencia de la arquitectura limpia:

  • Regla de cruce de fronteras;
  • Regla de dependencia;
  • La regla de prioridad de la capa interna.

Las ventajas de este enfoque incluyen:

  • Facilidad de prueba : las capas están aisladas, respectivamente, se pueden probar sin parches de mono, puede configurar granularmente el recubrimiento para diferentes capas, dependiendo del grado de su importancia;
  • La facilidad de cambiar las reglas de negocio , ya que todas se recopilan en un solo lugar, no se extienden por el proyecto y no se mezclan con código de bajo nivel;
  • Independencia de los agentes externos : la presencia de abstracciones entre la lógica empresarial y el mundo exterior en ciertos casos le permite cambiar las fuentes externas sin afectar las capas internas. Funciona si no ha vinculado la lógica de negocios a las características específicas de los agentes externos, por ejemplo, transacciones de bases de datos;
  • , , , .

, . . , . « Clean Architecture».

Python


Esta es una teoría, se pueden encontrar ejemplos de aplicación práctica en el artículo original, los informes y el libro de Robert Martin. Se basan en varios patrones de diseño comunes del mundo de Java: Adaptador, Gateway, Interactor, Fasade, Repository, DTO , etc.

Bueno, ¿qué pasa con Python? Como dije, el laconismo se valora en la comunidad de Python. Lo que se ha arraigado en otros está lejos del hecho de que arraigará con nosotros. La primera vez que volví a este tema hace tres años, entonces no había muchos materiales sobre el tema del uso de Arquitectura limpia en Python, pero el primer enlace en Google fue el proyecto Leonardo Giordani: el autor describe en detalle el proceso de creación de una API para un sitio de búsqueda de propiedades utilizando el método TDD, aplicando arquitectura limpia.
Desafortunadamente, a pesar de la explicación escrupulosa y siguiendo todos los cánones del tío Bob, este ejemplo es bastante aterrador

La API del proyecto consta de un método: obtener una lista con un filtro disponible. Creo que incluso para un desarrollador novato, el código para dicho proyecto no tomará más de 15 líneas. Pero en este caso, tomó seis paquetes. Puede referirse a un diseño no completamente exitoso, y esto es cierto, pero en cualquier caso, es difícil para alguien explicar la efectividad de este enfoque , refiriéndose a este proyecto.

Hay un problema más grave, si no lee el artículo y comienza a estudiar el proyecto de inmediato, entonces es bastante difícil de entender. Considere la implementación de la lógica de negocios:

from rentomatic.response_objects import response_objects as res

class RoomListUseCase(object):
   def __init__(self, repo):
       self.repo = repo
   def execute(self, request_object):
       if not request_object:
           return res.ResponseFailure.build_from_invalid_request_object(
               request_object)
       try:
           rooms = self.repo.list(filters=request_object.filters)
           return res.ResponseSuccess(rooms)
       except Exception as exc:
           return res.ResponseFailure.build_system_error(
               "{}: {}".format(exc.__class__.__name__, "{}".format(exc)))

La clase RoomListUseCase que implementa la lógica empresarial (no muy similar a la lógica empresarial, ¿verdad?) Del proyecto se inicializa mediante el objeto repo . Pero, ¿qué es un repositorio ? Por supuesto, desde el contexto, podemos entender que el repositorio implementa la plantilla de Repositorio para acceder a los datos, si miramos el cuerpo de RoomListUseCase, entendemos que debe tener un método de lista, cuya entrada es una lista de filtros, que no está clara en la salida, debe mirar en ResponseSuccess. ¿Y si el escenario es más complejo, con acceso múltiple a la fuente de datos? Resulta entender qué es un repositorio, solo puede referirse a la implementación. ¿Pero dónde está ella ubicada? Se encuentra en un módulo separado, que de ninguna manera está asociado con RoomListUseCase. Por lo tanto, para comprender lo que está sucediendo, debe subir al nivel superior (el nivel del marco) y ver qué se alimenta a la entrada de la clase al crear el objeto.

Puede pensar que enumero las desventajas de la escritura dinámica, pero esto no es del todo cierto. Es una escritura dinámica que le permite escribir código expresivo y compacto . Me viene a la mente la analogía con los microservicios, cuando cortamos un monolito en microservicios, el diseño adquiere una rigidez adicional, ya que todo se puede hacer dentro del microservicio (PL, marcos, arquitectura), pero debe cumplir con la interfaz declarada. Así que aquí: cuando dividimos nuestro proyecto en capas,Las relaciones entre las capas deben ser consistentes con el contrato , mientras que dentro de la capa, el contrato es opcional. De lo contrario, debe mantener un contexto bastante amplio en su cabeza. Recuerde, dije que el problema con el código incorrecto son las dependencias y, por lo tanto, sin una interfaz explícita, nuevamente volvemos a donde queríamos escapar, a la ausencia de relaciones explícitas de causa y efecto .

repo RoomListUseCase, execute — . - , , . - . , , , repo .

En general, en ese momento abandoné Clean Architecture en un nuevo proyecto, nuevamente aplicando el clásico MVC. Pero, después de llenar el siguiente lote de conos, volvió a esta idea un año después, cuando, por fin, comenzamos a lanzar servicios en Python 3.5+. Como saben, trajo anotaciones de tipo y clases de datos.: Dos potentes herramientas de descripción de interfaz. Basándome en ellos, dibujé un prototipo del servicio, y el resultado ya fue mucho mejor: las capas dejaron de dispersarse, a pesar del hecho de que todavía había mucho código, especialmente cuando se integraba con el marco. Pero eso fue suficiente para comenzar a aplicar este enfoque en pequeños proyectos. Poco a poco, comenzaron a aparecer marcos que se centraron en el uso máximo de anotaciones de tipo: apistar (ahora starlette), marco fundido. El paquete pydantic / FastAPI ahora es común, y la integración con dichos marcos se ha vuelto mucho más fácil. Así es como se vería el ejemplo anterior de restomatic / services.py:

from typing import Optional, List
from pydantic import BaseModel

class Room(BaseModel):
   code: str
   size: int
   price: int
   latitude: float
   longitude: float

class RoomFilter(BaseModel):
   code: Optional[str] = None
   price_min: Optional[int] = None
   price_max: Optional[int] = None

class RoomStorage:
   def get_rooms(self, filters: RoomFilter) -> List[Room]: ...

class RoomListUseCase:
   def __init__(self, repo: RoomStorage):
       self.repo = repo
   def show_rooms(self, filters: RoomFilter) -> List[Room]:
       rooms = self.repo.get_rooms(filters=filters)
       return rooms

RoomListUseCase: una clase que implementa la lógica empresarial del proyecto. No debe prestar atención al hecho de que todo lo que hace el método show_rooms es llamar a RoomStorage (no se me ocurrió este ejemplo). En la vida real, también puede haber un cálculo de descuento, clasificar una lista basada en anuncios, etc. Sin embargo, el módulo es autosuficiente. Si queremos usar este escenario en otro proyecto, tendremos que implementar RoomStorage. Y lo que se necesita para esto es claramente visible desde el módulo. A diferencia del ejemplo anterior, dicha capa es autosuficiente , y al cambiarla no es necesario tener en cuenta todo el contexto. A partir de dependencias no sistémicas solo piramidales, por qué, quedará claro en el complemento del marco. Sin dependencias, otra forma de mejorar la legibilidad del código, no un contexto adicional, incluso un desarrollador novato podrá comprender lo que hace este módulo.

Un escenario de lógica de negocios no tiene que ser una clase; a continuación se muestra un ejemplo de un escenario similar en forma de función:

def rool_list_use_case(filters: RoomFilter, repo: RoomStorage) -> List[Room]:
   rooms = repo.get_rooms(filters=filters)
   return rooms


Y aquí está la conexión con el marco:

from typing import List
from fastapi import FastAPI, Depends
from rentomatic import services, adapters
app = FastAPI()

def get_use_case() -> services.RoomListUseCase:
   return services.RoomListUseCase(adapters.MemoryStorage())

@app.post("/rooms", response_model=List[services.Room])
def rooms(filters: services.RoomFilter, use_case=Depends(get_use_case)):
   return use_case.show_rooms(filters)

Usando la función get_use_case, FastAPI implementa el patrón de inyección de dependencia . No tenemos que preocuparnos por la serialización de datos, FastAPI realiza todo el trabajo junto con pydantic. Por desgracia, los datos no son siempre formato de lógica de negocio adecuado para la emisión en directo en el restaurante <y, por el contrario, la lógica de negocio no sabe dónde venían los datos - con barras, solicitud cuerpo, galletas, etc . En este caso, el cuerpo de la función de sala tendrá una cierta conversión de datos de entrada y salida, pero en la mayoría de los casos, si trabajamos con la API, una función proxy tan fácil es suficiente. 

, , , RoomStorage. , 15 , , , .

No separé intencionalmente la capa de lógica empresarial, como sugiere el modelo canónico de Arquitectura Limpia. Se suponía que la clase Room estaba en la capa de región de dominio Entity que representa la región de dominio, pero para este ejemplo no hay necesidad de esto. Al combinar las capas Entity y UseCase, el proyecto no deja de ser una implementación de Arquitectura limpia. El propio Robert Martin ha dicho repetidamente que el número de capas puede variar tanto hacia arriba como hacia abajo. Al mismo tiempo, el proyecto cumple con los criterios principales de Arquitectura limpia:

  • Regla de cruce de fronteras: los modelos piradínicos, que son esencialmente DTO, cruzan fronteras ;
  • Regla de dependencia : la capa de lógica de negocios es independiente de otras capas;
  • La regla de prioridad para la capa interna : es la capa de lógica de negocios que define la interfaz (RoomStorage), a través de la cual la lógica de negocios interactúa con el mundo exterior.

Hoy, varios proyectos de nuestro equipo, implementados usando el enfoque descrito, están trabajando en la producción. Intento organizar incluso los servicios más pequeños de esta manera. Se entrena bien, surgen preguntas en las que no había pensado antes. Por ejemplo, ¿qué es la lógica de negocios aquí? Esto está lejos de ser siempre obvio, por ejemplo, si está escribiendo algún tipo de proxy. Otro punto importante es aprender a pensar de manera diferente.. Cuando recibimos una tarea, generalmente comenzamos a pensar en los marcos, los servicios utilizados, sobre si será necesaria una línea aquí, donde es mejor almacenar estos datos, que se pueden almacenar en caché. En el enfoque que dicta la arquitectura limpia, primero debemos implementar la lógica de negocios y solo luego pasar a implementar la interacción con la infraestructura, ya que, según Robert Martin, la tarea principal de la arquitectura es retrasar el momento en que la conexión con cualquier La capa de infraestructura será una parte integral de su aplicación.

En general, veo una perspectiva favorable para usar Clean Architecture en Python. Pero la forma, muy probablemente, será significativamente diferente de cómo se acepta en otros PL. En los últimos años, he visto un aumento significativo en el interés en el tema de la arquitectura en la comunidad. Por lo tanto, en la última PyCon hubo varios informes sobre el uso de DDD, y los chicos de los laboratorios secos deben notarse por separado . En nuestra empresa, muchos equipos ya están implementando el enfoque descrito en un grado u otro. Todos estamos haciendo lo mismo, hemos crecido, nuestros proyectos han crecido, la comunidad de Python tiene que trabajar con esto, definir el estilo y el lenguaje comunes que, por ejemplo, una vez se convirtieron para todos los Django.

All Articles