Capítulo 4. Nuestro primer caso de uso: API de Flask y capa de servicios

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

¡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.

apwp 0401
Figura 4-1. Antes: dirigimos nuestra aplicación hablando con los repositorios y el modelo de dominio

En este capítulo, discutimos las diferencias entre la lógica de orquestación, la lógica de negocio y el código de interfaz, e introducimos el patrón Capa de Serviciopara que se encargue de orquestar nuestros flujos de trabajo y definir los casos de uso de nuestro sistema.

También hablaremos de 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 sólo de nuestro modelo de dominio, sino de todo el flujo de trabajo de un caso de uso.

La Figura 4-2 muestra lo que pretendemos: vamos a añadir una API de Flask que hablará con la capa de servicio, que servirá como punto de entrada a nuestro modelo de dominio. Como nuestra capa de servicio depende deAbstractRepository, podemos probarla unitariamente utilizando FakeRepository, pero ejecutar nuestro código de producción utilizando SqlAlchemyRepository.

apwp 0402
Figura 4-2. La capa de servicio se convertirá en la vía principal de entrada a nuestra app

En nuestros diagramas, utilizamos la convención de que los componentes nuevos se resaltan con texto/líneas en negrita (y color amarillo/naranja, si estás leyendo una versión digital).

Consejo

El código de este capítulo está en la rama chapter_04_service_layer de GitHub:

git clone https://github.com/cosmicpython/code.git
cd code
git checkout chapter_04_service_layer
# or to code along, checkout Chapter 2:
git checkout chapter_02_repository

Conectar nuestra aplicación con el mundo real

Como cualquier buen equipo ágil, nos estamos apresurando para intentar sacar un MVP y ponerlo delante de los usuarios para empezar a recoger opiniones. Tenemos el núcleo de nuestro modelo de dominio y el servicio de dominio que necesitamos para asignar pedidos, y tenemos la interfaz del repositorio para el almacenamiento permanente.

Conectemos todas las piezas móviles tan rápido como podamos y luego refactoricemos hacia una arquitectura más limpia. Éste es nuestro plan:

  1. Utiliza Flask para poner un punto final de API delante de nuestro servicio de dominio allocate. Conecta la sesión de base de datos y nuestro repositorio. Pruébalo con una prueba de extremo a extremo y un poco de SQL rápido y sucio para preparar los datos de prueba.

  2. Refactorizar una capa de servicio que pueda servir como abstracción para capturar el caso de uso y que se sitúe entre Flask y nuestro modelo de dominio. Construir algunas pruebas de la capa de servicio y mostrar cómo pueden utilizarFakeRepository.

  3. Experimenta con distintos tipos de parámetros para nuestras funciones de la capa de servicio; demuestra que utilizar tipos de datos primitivos permite desacoplar los clientes de la capa de servicio (nuestras pruebas y nuestra API de Flask) de la capa del modelo.

Una primera prueba de principio a fin

A nadie le interesa entrar en un largo debate terminológico 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 se limitan a dividir las cosas en "pruebas rápidas" y "pruebas lentas".

Por ahora, queremos escribir una o quizá dos pruebas que van a ejercitar un punto final "real" de la API (utilizando HTTP) y hablar con una base de datos real. Vamos a llamarlas pruebas de extremo a extremo porque es uno de los nombres más autoexplicativos.

A continuación se muestra un primer corte:

Una primera prueba de la API (test_api.py)

@pytest.mark.usefixtures('restart_api')
def test_api_returns_allocation(add_stock):
    sku, othersku = random_sku(), random_sku('other')  1
    earlybatch = random_batchref(1)
    laterbatch = random_batchref(2)
    otherbatch = random_batchref(3)
    add_stock([  2
        (laterbatch, sku, 100, '2011-01-02'),
        (earlybatch, sku, 100, '2011-01-01'),
        (otherbatch, othersku, 100, None),
    ])
    data = {'orderid': random_orderid(), 'sku': sku, 'qty': 3}
    url = config.get_api_url()  3
    r = requests.post(f'{url}/allocate', json=data)
    assert r.status_code == 201
    assert r.json()['batchref'] == earlybatch
1

random_sku(), random_batchref(), etc. son pequeñas funciones de ayuda que generan caracteres aleatorios utilizando el módulo uuid. Como ahora estamos ejecutando contra una base de datos real, ésta es una forma de evitar que varias pruebas y ejecuciones interfieran entre sí.

2

add_stock es un accesorio de ayuda que simplemente oculta los detalles de la inserción manual de filas en la base de datos mediante SQL. Más adelante mostraremos una forma más agradable de hacerlo.

3

config.py es un módulo en el que guardamos información de configuración.

Todo el mundo resuelve estos problemas de formas distintas, pero vas a necesitar alguna forma de poner en marcha Flask, posiblemente en un contenedor, y de comunicarte con una base de datos Postgres. Si quieres ver cómo lo hicimos, consultael Apéndice B.

La aplicación directa

Implementando las cosas de la forma más obvia, en podrías obtener algo así:

Primer corte de la aplicación Flask (flask_app.py)

from flask import Flask, jsonify, request
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

import config
import model
import orm
import repository


orm.start_mappers()
get_session = sessionmaker(bind=create_engine(config.get_postgres_uri()))
app = Flask(__name__)

@app.route("/allocate", methods=['POST'])
def allocate_endpoint():
    session = get_session()
    batches = repository.SqlAlchemyRepository(session).list()
    line = model.OrderLine(
        request.json['orderid'],
        request.json['sku'],
        request.json['qty'],
    )

    batchref = model.allocate(line, batches)

    return jsonify({'batchref': batchref}), 201

Hasta aquí, todo bien. Puede que estéis pensando que no hace falta que sigáis con vuestras tonterías de "astronauta de la arquitectura", Bob y Harry.

Pero espera un momento: 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 es muy black-boxy), o quizá una que compruebe que no podemos asignar una segunda línea si la primera ya debería haber agotado el lote:

Las asignaciones de prueba se mantienen (test_api.py)

@pytest.mark.usefixtures('restart_api')
def test_allocations_are_persisted(add_stock):
    sku = random_sku()
    batch1, batch2 = random_batchref(1), random_batchref(2)
    order1, order2 = random_orderid(1), random_orderid(2)
    add_stock([
        (batch1, sku, 10, '2011-01-01'),
        (batch2, sku, 10, '2011-01-02'),
    ])
    line1 = {'orderid': order1, 'sku': sku, 'qty': 10}
    line2 = {'orderid': order2, 'sku': sku, 'qty': 10}
    url = config.get_api_url()

    # first order uses up all stock in batch 1
    r = requests.post(f'{url}/allocate', json=line1)
    assert r.status_code == 201
    assert r.json()['batchref'] == batch1

    # second order should go to batch 2
    r = requests.post(f'{url}/allocate', json=line2)
    assert r.status_code == 201
    assert r.json()['batchref'] == batch2

No es tan bonito, pero eso nos obligará a añadir el compromiso.

Condiciones de error que requieren comprobaciones de la base de datos

Pero si seguimos así, las cosas se pondrán cada vez más feas.

Supongamos que queremos añadir un poco de gestión de errores. ¿Qué pasa si el dominio genera un error, por una SKU que está agotada? ¿O qué pasa con una SKU que ni siquiera existe? Eso no es algo que el dominio sepa, ni debería saberlo. Se trata más bien de una comprobación de cordura que deberíamos implementar en la capa de la base de datos, antes incluso de invocar el servicio del dominio.

Ahora veremos otras dos pruebas de extremo a extremo:

Más pruebas en la capa E2E (test_api.py)

@pytest.mark.usefixtures('restart_api')
def test_400_message_for_out_of_stock(add_stock):  1
    sku, smalL_batch, large_order = random_sku(), random_batchref(), random_orderid()
    add_stock([
        (smalL_batch, sku, 10, '2011-01-01'),
    ])
    data = {'orderid': large_order, 'sku': sku, 'qty': 20}
    url = config.get_api_url()
    r = requests.post(f'{url}/allocate', json=data)
    assert r.status_code == 400
    assert r.json()['message'] == f'Out of stock for sku {sku}'


@pytest.mark.usefixtures('restart_api')
def test_400_message_for_invalid_sku():  2
    unknown_sku, orderid = random_sku(), random_orderid()
    data = {'orderid': orderid, 'sku': unknown_sku, 'qty': 20}
    url = config.get_api_url()
    r = requests.post(f'{url}/allocate', json=data)
    assert r.status_code == 400
    assert r.json()['message'] == f'Invalid sku {unknown_sku}'
1

En la primera prueba, intentamos asignar más unidades de las que tenemos en stock.

2

En el segundo, el SKU simplemente no existe (porque nunca hemos llamado a add_stock), por lo que no es válido en lo que respecta a nuestra aplicación.

Y claro, también podríamos implementarlo en la aplicación Flask:

La aplicación Flask empieza a tener fallos (flask_app.py)

def is_valid_sku(sku, batches):
    return sku in {b.sku for b in batches}

@app.route("/allocate", methods=['POST'])
def allocate_endpoint():
    session = get_session()
    batches = repository.SqlAlchemyRepository(session).list()
    line = model.OrderLine(
        request.json['orderid'],
        request.json['sku'],
        request.json['qty'],
    )

    if not is_valid_sku(line.sku, batches):
        return jsonify({'message': f'Invalid sku {line.sku}'}), 400

    try:
        batchref = model.allocate(line, batches)
    except model.OutOfStock as e:
        return jsonify({'message': str(e)}), 400

    session.commit()
    return jsonify({'batchref': batchref}), 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 descontrolarse, y pronto acabaremos con una pirámide de pruebas invertida (o "modelo de cono de helado", como le gusta llamarlo a Bob).

Introducir una capa de servicio y utilizar FakeRepository para probarla unitariamente

Si nos fijamos en lo que hace nuestra aplicación Flask, hay bastante de lo que podríamos llamar orquestación: obtenercosas de nuestro repositorio, validar nuestra entrada con el estado de la base de datos, gestionar errores y confirmar en la ruta feliz. La mayoría de estas cosas no tienen nada que ver con tener un punto final de API web (las necesitarías si estuvieras construyendo una CLI, por ejemplo; véaseel Apéndice C), y no son realmente cosas que deban comprobarse mediante pruebas de extremo a extremo.

A menudo tiene sentido dividir una capa de servicios, a veces llamadacapa de orquestación o capa de casos de uso.

¿Recuerdas el FakeRepository que preparamos en el Capítulo 3?

Nuestro falso repositorio, una colección de lotes en memoria (test_services.py)

class FakeRepository(repository.AbstractRepository):

    def __init__(self, batches):
        self._batches = set(batches)

    def add(self, batch):
        self._batches.add(batch)

    def get(self, reference):
        return next(b for b in self._batches if b.reference == reference)

    def list(self):
        return list(self._batches)

Aquí es donde resultará útil; nos permite probar nuestra capa de servicios con pruebas unitarias bonitas y rápidas :

Pruebas unitarias con falsificaciones en la capa de servicios (test_services.py)

def test_returns_allocation():
    line = model.OrderLine("o1", "COMPLICATED-LAMP", 10)
    batch = model.Batch("b1", "COMPLICATED-LAMP", 100, eta=None)
    repo = FakeRepository([batch])  1

    result = services.allocate(line, repo, FakeSession())  23
    assert result == "b1"


def test_error_for_invalid_sku():
    line = model.OrderLine("o1", "NONEXISTENTSKU", 10)
    batch = model.Batch("b1", "AREALSKU", 100, eta=None)
    repo = FakeRepository([batch])  1

    with pytest.raises(services.InvalidSku, match="Invalid sku NONEXISTENTSKU"):
        services.allocate(line, repo, FakeSession())  23
1

FakeRepository contiene los objetos Batch que utilizará nuestra prueba.

2

Nuestro módulo de servicios(services.py) definirá una función de la capa de servicios allocate(). Se situará entre nuestra función allocate_endpoint()de la capa API y la función de servicio de dominio allocate() de nuestro modelo de dominio.1

3

También necesitamos un FakeSession para falsear la sesión de la base de datos, como se muestra en el siguiente fragmento de código.

Una sesión de base de datos falsa (test_services.py)

class FakeSession():
    committed = False

    def commit(self):
        self.committed = True

Esta sesión falsa es sólo una solución temporal. Nos desharemos de ella y haremos las cosas aún mejor pronto, en el Capítulo 6. Pero mientras tanto, la falsa .commit() nos permite migrar una tercera prueba de la capa E2E:

Una segunda prueba en la capa de servicios (prueba_servicios.py)

def test_commits():
    line = model.OrderLine('o1', 'OMINOUS-MIRROR', 10)
    batch = model.Batch('b1', 'OMINOUS-MIRROR', 100, eta=None)
    repo = FakeRepository([batch])
    session = FakeSession()

    services.allocate(line, repo, session)
    assert session.committed is True

Una función de servicio típica

Escribiremos una función de servicio parecida a ésta:

Servicio de asignación básica (services.py)

class InvalidSku(Exception):
    pass


def is_valid_sku(sku, batches):
    return sku in {b.sku for b in batches}

def allocate(line: OrderLine, repo: AbstractRepository, session) -> str:
    batches = repo.list()  1
    if not is_valid_sku(line.sku, batches):  2
        raise InvalidSku(f'Invalid sku {line.sku}')
    batchref = model.allocate(line, batches)  3
    session.commit()  4
    return batchref

Las funciones típicas de la capa de servicio tienen pasos similares:

1

Obtenemos algunos objetos del repositorio.

2

Hacemos algunas comprobaciones o afirmaciones sobre la petición comparándola con el estado actual del mundo.

3

Llamamos servicio a un dominio.

4

Si todo va bien, guardamos/actualizamos cualquier estado que hayamos cambiado.

Este último paso es un poco insatisfactorio por el momento, ya que nuestra capa de servicio está estrechamente acoplada a nuestra capa de base de datos. Lo mejoraremos en el Capítulo 6 con el patrón Unidad de Trabajo.

Pero lo esencial de la capa de servicio está ahí, y nuestra aplicación Flask tiene ahora un aspecto mucho más limpio en :

Aplicación Flask que delega en la capa de servicio (flask_app.py)

@app.route("/allocate", methods=['POST'])
def allocate_endpoint():
    session = get_session()  1
    repo = repository.SqlAlchemyRepository(session)  1
    line = model.OrderLine(
        request.json['orderid'],  2
        request.json['sku'],  2
        request.json['qty'],  2
    )
    try:
        batchref = services.allocate(line, repo, session)  2
    except (model.OutOfStock, services.InvalidSku) as e:
        return jsonify({'message': str(e)}), 400  3

    return jsonify({'batchref': batchref}), 201  3
1

Instanciamos una sesión de base de datos y algunos objetos de repositorio.

2

Extraemos los comandos del usuario de la petición web y los pasamos a un servicio de dominio.

3

Devolvemos unas respuestas JSON con los códigos de estado adecuados.

Las responsabilidades de la aplicación Flask son cosas estándar de la web: gestión de sesiones por petición, 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 casos de uso/servicios, y la lógica de dominio permanece en el dominio.

Por último, podemos reducir con confianza nuestras pruebas E2E a sólo dos, una para el camino feliz y otra para el camino infeliz:

E2E prueba sólo las rutas felices e infelices (test_api.py)

@pytest.mark.usefixtures('restart_api')
def test_happy_path_returns_201_and_allocated_batch(add_stock):
    sku, othersku = random_sku(), random_sku('other')
    earlybatch = random_batchref(1)
    laterbatch = random_batchref(2)
    otherbatch = random_batchref(3)
    add_stock([
        (laterbatch, sku, 100, '2011-01-02'),
        (earlybatch, sku, 100, '2011-01-01'),
        (otherbatch, othersku, 100, None),
    ])
    data = {'orderid': random_orderid(), 'sku': sku, 'qty': 3}
    url = config.get_api_url()
    r = requests.post(f'{url}/allocate', json=data)
    assert r.status_code == 201
    assert r.json()['batchref'] == earlybatch


@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 = requests.post(f'{url}/allocate', json=data)
    assert r.status_code == 400
    assert r.json()['message'] == f'Invalid sku {unknown_sku}'

Hemos dividido con éxito nuestras pruebas en dos grandes categorías: pruebas sobre cosas de la web, que implementamos de extremo a extremo; y pruebas sobre cosas de orquestación, que podemos probar contra la capa de servicio en memoria.

¿Por qué todo se llama servicio?

Algunos de vosotros probablemente os estéis rascando la cabeza en este momento intentando averiguar cuál es exactamente la diferencia entre un servicio de dominio y una capa de servicio.

Lo sentimos; no elegimos los nombres, o tendríamos formas mucho más guays y amistosas de hablar de estas cosas.

En este capítulo vamos a utilizar dos cosas llamadas servicio. La primera es unservicio de aplicación (nuestra capa de servicio). Su trabajo consiste en gestionar las peticiones del mundo exterior y orquestar una operación. Lo que queremos decir es que la capa de servicio dirige la aplicación siguiendo un montón de pasos sencillos:

  • Obtener algunos datos de la base de datos

  • Actualizar el modelo de dominio

  • Persiste en los cambios

Este es el tipo de trabajo aburrido que tiene que ocurrir en cada operación de tu sistema, y mantenerlo separado de la lógica empresarial ayuda a mantener las cosas ordenadas.

El segundo tipo de servicio es un servicio de dominio. Es el nombre que se da a una parte de la lógica que pertenece al modelo de dominio, pero que no se encuentra de forma natural dentro de una entidad con estado o un objeto de valor. Por ejemplo, si estuvieras creando una aplicación de carrito de la compra, podrías elegir crear reglas de impuestos como un servicio de dominio. Calcular los impuestos es un trabajo independiente 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 calculate_tax pueden hacer el trabajo.

Poner las cosas en carpetas para ver dónde está cada cosa

A medida que nuestra aplicación vaya creciendo, tendremos que ir ordenando nuestra estructura de directorios. La disposición de nuestro proyecto nos da pistas útiles sobre qué tipo de objetos encontraremos en cada archivo.

Ésta es una forma de organizar las cosas:

Algunas subcarpetas

.
├── config.py
├── domain  1
│   ├── __init__.py
│   └── model.py
├── service_layer  2
│   ├── __init__.py
│   └── services.py
├── adapters  3
│   ├── __init__.py
│   ├── orm.py
│   └── repository.py
├── entrypoints  4
│   ├── __init__.py
│   └── flask_app.py
└── tests
    ├── __init__.py
    ├── conftest.py
    ├── unit
    │   ├── test_allocate.py
    │   ├── test_batches.py
    │   └── test_services.py
    ├── integration
    │   ├── test_orm.py
    │   └── test_repository.py
    └── e2e
        └── test_api.py
1

Tengamos una carpeta para nuestro modelo de dominio. Actualmente es sólo un archivo, pero para una aplicación más compleja, podrías tener un archivo por clase; podrías tener clases ayudantes padre para Entity, ValueObject, yAggregate, y podrías añadir un exceptions.py para las excepciones de la capa de dominio y, como verás en la Parte II, commands.py y events.py.

2

Distinguiremos la capa de servicios. Actualmente es sólo un archivo llamado servicios. py para nuestras funciones de la capa de servicios. Podrías añadir aquí las excepciones de la capa de servicio y, como verás en el capítulo 5, añadiremos unit_of_work.py.

3

Adaptadores es un guiño a la terminología de puertos y adaptadores. Esto se llenará con cualquier otra abstracción en torno a la E/S externa (por ejemplo, un redis_client.py). En sentido estricto, los llamarías adaptadores secundarios o adaptadores dirigidos, o a veces adaptadores dirigidos hacia dentro.

4

Los puntos de entrada son los lugares desde los que dirigimos nuestra aplicación. En la terminología oficial de puertos y adaptadores, éstos también son adaptadores, y se denominan adaptadores primarios, de conducción o de salida.

¿Qué pasa con los puertos? Como recordarás, son las interfaces abstractas que implementan los adaptadores. Solemos guardarlos en el mismo archivo que los adaptadores que los implementan.

Recapitulación

Añadir la capa de servicio nos ha aportado mucho:

  • Nuestros puntos finales de la API de Flask se vuelven muy finos y fáciles de escribir: su única responsabilidad es hacer "cosas de la web", como analizar JSON y producir los códigos HTTP adecuados para los casos felices o infelices.

  • Hemos definido una API clara para nuestro dominio, un conjunto de casos de uso o puntos de entrada que puede utilizar cualquier adaptador sin necesidad de saber nada sobre las clases de nuestro modelo de dominio, ya sea una API, una CLI (véaseel Apéndice C) o las pruebas. También son un adaptador para nuestro dominio.

  • Podemos escribir pruebas a "marchas forzadas" utilizando la capa de servicio, lo que nos deja libertad para refactorizar el modelo de dominio de la forma que creamos conveniente. Mientras podamos seguir ofreciendo los mismos casos de uso, podemos experimentar con nuevos diseños sin necesidad de reescribir un montón de pruebas.

  • Y nuestra pirámide de pruebas tiene buen aspecto: el grueso de nuestras pruebas son pruebas unitarias rápidas, con un mínimo de pruebas E2E y 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 (el puerto, en terminología de puertos y adaptadores).

Cuando ejecutamos las pruebas, la Figura 4-4 muestra cómo implementamos las dependencias abstractas utilizando FakeRepository (el adaptador).

Y cuando ejecutemos realmente nuestra aplicación, intercambiaremos la dependencia "real" que se muestra enla Figura 4-5.

apwp 0403
Figura 4-3. Dependencias abstractas de la capa de servicio
[ditaa, apwp_0403]
        +-----------------------------+
        |         Service Layer       |
        +-----------------------------+
           |                   |
           |                   | depends on abstraction
           V                   V
+------------------+     +--------------------+
|   Domain Model   |     | AbstractRepository |
|                  |     |       (Port)       |
+------------------+     +--------------------+
apwp 0404
Figura 4-4. Las pruebas proporcionan una implementación de la dependencia abstracta
[ditaa, apwp_0404]
        +-----------------------------+
        |           Tests             |-------------\
        +-----------------------------+             |
                       |                            |
                       V                            |
        +-----------------------------+             |
        |         Service Layer       |    provides |
        +-----------------------------+             |
           |                     |                  |
           V                     V                  |
+------------------+     +--------------------+     |
|   Domain Model   |     | AbstractRepository |     |
+------------------+     +--------------------+     |
                                    ^               |
                         implements |               |
                                    |               |
                         +----------------------+   |
                         |    FakeRepository    |<--/
                         |      (in-memory)     |
                         +----------------------+
apwp 0405
Figura 4-5. Dependencias en tiempo de ejecución
[ditaa, apwp_0405]
       +--------------------------------+
       | Flask API (Presentation Layer) |-----------\
       +--------------------------------+           |
                       |                            |
                       V                            |
        +-----------------------------+             |
        |         Service Layer       |             |
        +-----------------------------+             |
           |                     |                  |
           V                     V                  |
+------------------+     +--------------------+     |
|   Domain Model   |     | AbstractRepository |     |
+------------------+     +--------------------+     |
              ^                     ^               |
              |                     |               |
       gets   |          +----------------------+   |
       model  |          | SqlAlchemyRepository |<--/
   definitions|          +----------------------+
       from   |                | uses
              |                V
           +-----------------------+
           |          ORM          |
           | (another abstraction) |
           +-----------------------+
                       |
                       | talks to
                       V
           +------------------------+
           |       Database         |
           +------------------------+

Maravilloso.

Detengámonos en la Tabla 4-1, en la que consideramos en los pros y los contras de tener una capa de servicios.

Tabla 4-1. Capa de servicio: las compensaciones
Pros Contras
  • Tenemos un único lugar para capturar todos los casos de uso de nuestra aplicación.

  • Hemos colocado nuestra lógica de dominio inteligente detrás de una API, lo que nos deja libertad para refactorizar.

  • Hemos separado limpiamente "las cosas que hablan HTTP" de "las cosas que hablan de asignación".

  • Cuando se combina con el patrón Repositorio y FakeRepository, tenemos una buena forma de escribir pruebas a un nivel superior al de la capa de dominio; podemos probar más partes de nuestro flujo de trabajo sin necesidad de utilizar pruebas de integración (lee el Capítulo 5 para profundizar en esto).

  • Si tu aplicación es puramente una aplicación web, tus controladores/funciones de vista pueden ser el único lugar para capturar todos los casos de uso.

  • Es una capa más de abstracción.

  • Poner demasiada lógica en la capa de servicio puede conducir al antipatrón del Dominio Anémico. Es mejor introducir esta capa después de detectar que la lógica de orquestación se cuela en tus controladores.

  • Puedes obtener muchas de las ventajas de tener modelos de dominio ricos simplemente sacando la lógica de tus controladores y bajándola a la capa del modelo, sin necesidad de añadir una capa adicional entre medias (también conocido como "modelos gordos, controladores finos").

Pero aún quedan algunas torpezas por arreglar:

  • La capa de servicio sigue estando estrechamente acoplada al dominio, porque su API se expresa en términos de objetos OrderLine. Enel Capítulo 5, arreglaremos esto y hablaremos de la forma en que la capa de servicio permite un TDD más productivo.

  • La capa de servicio está estrechamente acoplada a un objeto session. En el Capítulo 6, introduciremos un patrón más que trabaja estrechamente con los patrones Repositorio y Capa de Servicios, el patrón Unidad de Trabajo, y todo será absolutamente encantador ¡Ya verás!

1 Los servicios de capa de servicio y los servicios de dominio tienen nombres confusamente similares. Abordaremos este tema más adelante en "¿Por qué todo se llama servicio?

Get Patrones de Arquitectura con Python now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.