Recursos de Sergio PereaComo desarrollar un Chatbot con Javascript (parte 1) | Sergio Perea

Arquitectura Limpia con Python (parte 1)


Muchos años después de su libro Clean Code, Robert C. Martin se atrevió a hablar de Clean Architecture. No en vano, ya había sido uno de los responsables del punto 11 del manifiesto ágil:

“The best architectures, requirements, and designs emerge from self-organizing teams.“.

En él nos habla de buscar la independencia en el desarrollo, entre casos de uso, desacoplamiento entre capas, o gestión de duplicidades. También nos habla de las fronteras que delimitan las partes en las que dividir la arquitectura y cómo se cruzan desde el punto de vista técnico: comunicación de hilos, procesos o servicios. Todo ello compartiendo principios ya tratados en la arquitectura hexagonal de Alistair Cockburn: entidades, controladores, puertos, adaptadores, etc.

De este tipo de arquitecturas a las que Robert C. Martin llama "Limpias" muchos aprendimos que la base de datos, como elemento externo a nuestra aplicación, no debe estar acoplada y por tanto no puede ser la base de nuestra aplicación. O lo que es lo mismo: no podemos empezar diseñándola en primer lugar tal como nos enseñaban en los 90. Incluso el framework, que también es algo externo a nuestra aplicación, debe ser algo con lo que evitar acoplarse.

En definitiva, una arquitectura limpia es también una arquitectura en capas. Esto significa que los diversos elementos de uso de su sistema están categorizados y tienen un lugar específico donde estar, según la categoría que les hayamos asignado.

Las capas internas contendrán representaciones de conceptos relacionados con el negocio, mientras que las capas externas contendrán detalles específicos sobre la implementación en "la vida real".

Una capa interna no sabe nada sobre las externas, por lo que no puede entender las estructuras definidas allí. Sus elementos deben hablar hacia afuera usando interfaces, es decir, usando solo la API esperada por un componente, sin tener que conocer su implementación específica. Cuando se crea una capa externa, los elementos que vivan allí se conectarán a esas interfaces.

Principales capas de una arquitectura limpia con Python

Echemos un vistazo a las capas principales de una arquitectura limpia, teniendo en cuenta que su implementación puede requerir crear nuevas capas o dividir algunas de ellas en varias.

Entidades

Esta capa contiene una representación de los modelos de dominio, es decir, todo lo que tu sistema necesitará para interactuar y que además es lo suficientemente complejo como para necesitar una representación específica.

El libro de Robert C. Martin pone el ejemplo de las cadenas. En Python, están representadas por objetos muy complejos y funcionales. Con muchos métodos que puedes usar. De modo que no tiene sentido crear un modelo específico para ello. Sin embargo, una Póliza, un Recibo o un Siniestro en una aseguradora si podemos modelizarlos como entidades, y por tanto necesitarán modelos de dominio específicos.

Pero ojo, no confundas estos modelos de dominio con los que te imponen frameworks como Django. Esos modelos están fuertemente acoplados a una base de datos o un sistema de almacenamiento. Nuestros modelos de dominio deberían ser más ligeros.

Todas nuestras entidades convivirán en una capa concreta de nuestra aplicación, de modo que podremos permitir que interactúen de forma directa entre ellos. De lo que no tienen ninguna información es de las capas externas. Es importante entender esto.

Vamos a ver cómo sería un modelo de entidad para una aplicación que muestra un listado de pólizas de seguros en una compañía aseguradora. Lo primero es escribir un test con el que crearemos un elemento "Póliza" que tiene una prima de 1000 euros y 150 de franquicia. Obviamente, el concepto de póliza se ha simplificado a un extremo absurdo de cara a poneros un ejemplo más sencillo:



import uuid
from insurance.domain import policy as p

def test_policy_model_init():

    code = uuid.uuid4()

    policy = p.Policy(code, price=1000.0, franchise=150.0)

    assert policy.code == code
    assert policy.price == 1000.0
    assert policy.franchise == 150.0


Pero no nos vamos a conformar con esto. Vamos a imponer que nuestra clase Póliza sea capaz de gestionar los datos de la póliza como un diccionario. Lo posdemos especificar con estos dos sencillos test:



def test_policy_model_from_dict():
    code = uuid.uuid4()
    policy = r.Policy.from_dict(
        {
            'code': code,
            'price': 1000.0,
            'franchise': 150.0
        }
    )

    assert policy.code == code
    assert policy.price == 1000.0
    assert policy.franchise == 150.0

def test_policy_model_to_dict():
    policy_dict = {
            'code': code,
            'price': 1000.0,
            'franchise': 150.0
    }

    policy = r.Policy.from_dict(policy_dict)

    assert policy.to_dict() == policy_dict

¿por qué quiero que el modelo de dominio de la Póliza trabaje como un diccionario de Python? Pues porque será mucho más sencillo crear operaciones específicas sobre esos objetos. Por ejemplo, vamos a tratar de imponer, además de lo anterior, que dos pólizas puedan compararse. Fijaos lo fácil que será si utilizamos diccionarios (lo resolveremos a través de una función llamada eq).

Nuevamente, vamos a desarrollar esto mediante TDD. Primero el test:



def test_policy_model_compare():
    policy_dict = {
       'code': code,
       'price': 1000.0,
       'franchise': 150.0
    }
    policy_1 = p.Policy.from_dict(policy_dict)
    policy_2 = p.Policy.from_dict(policy_dict)

    assert policy_1 == policy_2

Y ahora vamos a tratar de escribir una clase de modelo de dominio que permita que todos los test anteriores se pongan en verde:



class Policy:
    def __init__(self, code, price, franchise):
        self.code = code
        self.price = price
        self.franchise = franchise


    @classmethod
    def from_dict(cls, adict):
        return cls(
            code=adict['code'],
            price=adict['price'],
            franchise=adict['franchise'],
        )

    def to_dict(self):
        return {
            'code': self.code,
            'price': self.price,
            'franchise': self.franchise
        }

    def __eq__(self, other):
        return self.to_dict() == other.to_dict()

Ojo, esto no es suficiente. Si nuestra idea es mostrar la información de las pólizas en una API REST; tiene sentido que seamos capaces de serializar esta clase. Así que podemos crear una clase "Serializer" adelantándonos al uso que le darán otras capas. Veamos un sencillo test que podría probar esto:



import json
import uuid

from insurance.serializers import policy_json_serializer as serializer
from insurance.domain import policy as p


def test_serialize_domain_Serializer():
    code = uuid.uuid4()

    policy = p.Serializer(
       code= code,
       price= 1000.0,
       franchise= 150.0
    )

    expected_json = """
        {{
            "code": "{}",
            "price": 2000.0,
            "franchise": 150.0
        }}
    """.format(code)

    json_policy = json.dumps(policy, cls=ser.PolicyJsonEncoder)

    assert json.loads(json_policy) == json.loads(expected_json)

La clase para serializar Póliza es bastante simple:



import json


class PolicyJsonEncoder(json.JSONEncoder):

    def default(self, o):
        try:
            to_serialize = {
                'code': str(o.code),
                'price': o.size,
                'franchise': o.price
            }
            return to_serialize
        except AttributeError:
            return super().default(o)

Casos de uso

Un caso de uso es la descripción de una acción o actividad. Un diagrama de caso de uso es una descripción de las actividades que deberá realizar alguien o algo para llevar a cabo algún proceso. Los personajes o entidades que participarán en un diagrama de caso de uso se denominan actores. En el contexto de ingeniería del software, un diagrama de caso de uso representa a un sistema o subsistema como un conjunto de interacciones que se desarrollarán entre casos de uso y entre estos y sus actores en respuesta a un evento que inicia un actor principal. Los diagramas de casos de uso sirven para especificar la comunicación y el comportamiento de un sistema mediante su interacción con los usuarios y/u otros sistemas.

Entendiendo pues los casos de uso como procesos que suceden en nuestra aplicación, los modelizaremos en una capa por encima de nuestras Entidades. Los casos de uso pueden usar los modelos de su dominio para trabajar con datos reales. Por tanto los casos de uso conocen las entidades, y pueden instanciarlas y utilizarlas.

Aislando estos procesos en casos de uso, tendremos una arquitectura mucho más fácil de probar y mantener.

Por supuesto, al poner los casos de uso en la misma capa de nuestra arquitectura, éstos pueden comunicarse entre sí, como sucedía con los modelos de dominio.

Un caso de uso basado en el modelo de dominio del ejemplo anterior, que consistiera en mostrar una lista de pólizas, debería superar el siguiente test (extremadamente simple, de momento):



import pytest
import uuid
from unittest import mock


@pytest.fixture
def domain_policies():
    policy_1 = r.Policy(
        code=uuid.uuid4(),
        price=1000.0,
        franchise=150.0,
    )

    policy_2 = r.Policy(
        code=uuid.uuid4(),
        price=800.0,
        franchise=250.0,
    )

    policy_3 = r.Policy(
        code=uuid.uuid4(),
        price=500.0,
        franchise=300.0,
    )

    return [policy_1, policy_2, policy_3]


def test_policy_list_without_parameters(domain_policies):
    repository = mock.Mock()
    repository.list.return_value = domain_policies

    policy_list_use_case = uc.PolicyListUseCase(repository)
    result = policy_list_use_case.execute(request)

    repository.list.assert_called_with()
    assert result == domain_policies

Básicamente lo que hace este test es mockear un repositorio a partir de una lista 3 modelos de dominio creados antes. Nuestro caso de uso se podrá reiniciar con dicho repositorio, así que lo ejecutaremos obteniendo el resultado esperado. No hace nada más. Aunque lo iremos complicando próximamente.



class PoliciesListUseCase:
    def __init__(self, repo):
        self.repo = repo

    def execute(self):
        return self.repo.list()

Un montón de sistemas externos

Esta parte de la arquitectura está compuesta por sistemas externos que implementan las interfaces definidas en la capa anterior. Ejemplos de estos sistemas pueden ser un marco específico que expone una API REST o un SGBD específico.

En la parte más externa se encuentra la interfaz de usuario, la infraestructura y el sistema de pruebas. La capa exterior está reservada para las cosas que pueden cambiar más a menudo. Por tanto tiene mucho sentido que todo esto esté aislado del núcleo de la aplicación.

En próximos artículos iré complicando esto creando sistemas externos que permitan almacenar la información a través de una BD, o una API Rest que atienda a la ejecución del caso de uso de ejemplo.

x
Esta web hace uso de cookies únicamente con el objetivo de mejorar la experiencia de usuario y usabilidad de la web. Este aviso es un requisito legal que me obliga molestarte con detalles que te dan igual. Quiero saber más Acepto