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.
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
.
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:
-
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. -
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 utilizar
FakeRepository
. -
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
'
)
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
random_sku()
,random_batchref()
, etc. son pequeñas funciones de ayuda que generan caracteres aleatorios utilizando el módulouuid
. Como ahora estamos ejecutando contra una base de datos real, ésta es una forma de evitar que varias pruebas y ejecuciones interfieran entre sí.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.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
)
:
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
(
)
:
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}
'
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 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
]
)
result
=
services
.
allocate
(
line
,
repo
,
FakeSession
(
)
)
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
]
)
with
pytest
.
raises
(
services
.
InvalidSku
,
match
=
"
Invalid sku NONEXISTENTSKU
"
)
:
services
.
allocate
(
line
,
repo
,
FakeSession
(
)
)
FakeRepository
contiene los objetosBatch
que utilizará nuestra prueba.Nuestro módulo de servicios(services.py) definirá una función de la capa de servicios
allocate()
. Se situará entre nuestra funciónallocate_endpoint()
de la capa API y la función de servicio de dominioallocate()
de nuestro modelo de dominio.1Tambié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
(
)
if
not
is_valid_sku
(
line
.
sku
,
batches
)
:
raise
InvalidSku
(
f
'
Invalid sku {line.sku}
'
)
batchref
=
model
.
allocate
(
line
,
batches
)
session
.
commit
(
)
return
batchref
Las funciones típicas de la capa de servicio tienen pasos similares:
Obtenemos algunos objetos del repositorio.
Hacemos algunas comprobaciones o afirmaciones sobre la petición comparándola con el estado actual del mundo.
Llamamos servicio a un dominio.
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
(
)
repo
=
repository
.
SqlAlchemyRepository
(
session
)
line
=
model
.
OrderLine
(
request
.
json
[
'
orderid
'
]
,
request
.
json
[
'
sku
'
]
,
request
.
json
[
'
qty
'
]
,
)
try
:
batchref
=
services
.
allocate
(
line
,
repo
,
session
)
except
(
model
.
OutOfStock
,
services
.
InvalidSku
)
as
e
:
return
jsonify
(
{
'
message
'
:
str
(
e
)
}
)
,
400
return
jsonify
(
{
'
batchref
'
:
batchref
}
)
,
201
Instanciamos una sesión de base de datos y algunos objetos de repositorio.
Extraemos los comandos del usuario de la petición web y los pasamos a un servicio de dominio.
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
│ ├── __init__.py │ └── model.py ├── service_layer
│ ├── __init__.py │ └── services.py ├── adapters
│ ├── __init__.py │ ├── orm.py │ └── repository.py ├── entrypoints
│ ├── __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
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.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.
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.
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.
[ditaa, apwp_0403] +-----------------------------+ | Service Layer | +-----------------------------+ | | | | depends on abstraction V V +------------------+ +--------------------+ | Domain Model | | AbstractRepository | | | | (Port) | +------------------+ +--------------------+
[ditaa, apwp_0404] +-----------------------------+ | Tests |-------------\ +-----------------------------+ | | | V | +-----------------------------+ | | Service Layer | provides | +-----------------------------+ | | | | V V | +------------------+ +--------------------+ | | Domain Model | | AbstractRepository | | +------------------+ +--------------------+ | ^ | implements | | | | +----------------------+ | | FakeRepository |<--/ | (in-memory) | +----------------------+
[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.
Pros | Contras |
---|---|
|
|
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.