Ir al contenido
wannacode

Integrar sistemas antiguos y ajenos

En resumen: Un anti-corruption layer traduce el modelo de un sistema antiguo o ajeno a tu dominio, y un hub con formato canónico reduce las conexiones entre n sistemas a un adaptador por sistema.

Problema

Un almacén de 2009 y contabilidad en XML

La tienda tiene un almacén antiguo: un programa de 2009 que nadie se atreve a tocar. Habla CSV, con ; y códigos: A es disponible, B es bloqueado, y la cantidad -1 significa «contando ahora mismo». Contabilidad quiere XML, con el total escrito con punto. Entrega quiere JSON.

Así avisan los pedidos a los tres:

class Orders:
def __init__(self, warehouse, accounting, delivery):
self.warehouse = warehouse
self.accounting = accounting
self.delivery = delivery
def place(self, order, sku, qty, cents):
# el almacén de 2009: CSV y el código OUT
self.warehouse.send(f"{order};{sku};{qty};OUT")
# contabilidad: XML, el total con punto
total = euros(cents)
self.accounting.send(
f'<invoice no="{order}" total="{total}"/>')
# entrega: JSON
self.delivery.send(
json.dumps({"order": order, "sku": sku},
separators=(",", ":")))
orders = Orders(Endpoint("almacén"), Endpoint("contabilidad"), Endpoint("entrega"))
orders.place(1042, "HP-01", 1, 5900)
Salida
almacén ← 1042;HP-01;1;OUT
contabilidad ← <invoice no="1042" total="59.00"/>
entrega ← {"order":1042,"sku":"HP-01"}

Funciona. Pero ahora los pedidos conocen tres formatos ajenos, y los códigos del almacén se han extendido hasta el cobro: en algún sitio hay un if (status == "B"). El catálogo también habla con el almacén, y los vendedores con contabilidad. Cuando el almacén añade un estado nuevo, nadie sabe en cuántos lugares hay que buscar.

La idea

Un traductor en la frontera

Un sistema ajeno tiene su propio modelo: sus propios códigos, sus propias unidades, sus propios valores raros. Si ese modelo entra en el dominio, el dominio empieza a pensar en el idioma del almacén de 2009 y cambia cada vez que cambia el almacén.

Por eso en la frontera hay un traductor. Lee el formato ajeno y deja pasar solo los conceptos del dominio: Stock(sku, available). Solo el traductor sabe qué significan A, B y -1. A esto se le llama anti-corruption layer.

Por dentro es un adaptador, pero con una tarea mayor: un adaptador cambia la forma de una interfaz, mientras que un anti-corruption layer impide que se filtre un modelo entero. Decide qué significa cada valor ajeno y qué hacer con lo desconocido. La regla: lo desconocido se rechaza en voz alta, nunca se adivina.

Míralo

El traductor en código

@dataclass(frozen=True)
class Stock:
sku: str
available: int
# la frontera: solo aquí se sabe qué significan A, B y -1
class WarehouseAcl:
def __init__(self):
self.rejected = []
def read(self, lines):
stock = []
for line in lines:
sku, qty, status = line.split(";")
qty = int(qty)
if status == "A" and qty >= 0:
stock.append(Stock(sku, qty))
elif status == "A":
self.rejected.append(f"{sku}: cantidad desconocida")
elif status == "B":
stock.append(Stock(sku, 0))
else:
self.rejected.append(f"{sku}: estado desconocido {status}")
return stock
Stock es una dataclass: el dominio recibe un objeto con nombres, no una línea del archivo.

El archivo de esta mañana:

# el archivo que el almacén antiguo sube por FTP cada hora
file = ["HP-01;3;A", "CH-07;0;A", "KB-12;5;B", "MS-03;-1;A", "SP-02;2;R"]
acl = WarehouseAcl()
for s in acl.read(file):
print(f"{s.sku}: {s.available}")
for r in acl.rejected:
print("✗", r)
Salida
HP-01: 3
CH-07: 0
KB-12: 0
✗ MS-03: cantidad desconocida
✗ SP-02: estado desconocido R

KB-12 está bloqueado, así que para el dominio hay 0 disponibles. MS-03 se está contando: el traductor no sabe cuántos hay y no inventa un número. SP-02 tiene el estado R, que nadie ha descrito. Se rechaza, y alguien preguntará al dueño del almacén qué significa.

Pruébalo

Encuentra el error

Una colega escribe un traductor más corto: «todo lo que no está bloqueado se puede vender». Un mes después el almacén empieza a enviar el estado R (reservado para un vendedor externo), y la tienda vende el mismo altavoz a dos clientes. Además, MS-03 aparece en el sitio con -1 en stock. ¿En qué línea está el error?

Elige la línea con el error.

La idea

La telaraña de conexiones

Seis sistemas: pedidos, catálogo, almacén, contabilidad, entrega y vendedores externos. Si cada uno habla con todos los demás, las conexiones son n(n-1)/2 = 15, cada una con su propia traducción. Un cambio del formato del almacén toca 5 de ellas, en 5 sistemas que hay que publicar juntos.

La otra vía es un hub con una forma canónica: una forma común del mensaje (OrderPlaced) que todos entienden. Cada sistema se conecta una vez, con un adaptador entre su propio formato y el canónico. Seis sistemas, seis adaptadores. Un cambio del formato del almacén toca solo su adaptador: ese adaptador es el anti-corruption layer del almacén.

@dataclass(frozen=True)
class OrderPlaced:
id: int
sku: str
qty: int
cents: int
class Hub:
def __init__(self):
self.adapters = {}
def connect(self, system, adapter):
self.adapters[system] = adapter
def publish(self, event):
for system, adapter in self.adapters.items():
system.send(adapter(event))
def size(self):
return len(self.adapters)
Paso a paso, en palabras
  1. Los pedidos envían a tres sistemas, cada uno en su propio idioma: CSV al almacén de 2009, XML a contabilidad, JSON a entrega. Los pedidos conocen los tres formatos.
  2. Así hablan también los demás: el catálogo con el almacén, los vendedores con contabilidad. 6 sistemas que hablan todos con todos son 15 conexiones, cada una con su propia traducción.
  3. El almacén cambia su formato. Cambia cada conexión hacia él: 5 lugares en 5 sistemas, publicados juntos.
  4. Un hub: cada sistema se conecta una vez, con un adaptador entre su formato y una forma canónica. 6 sistemas, 6 adaptadores.
  5. Los pedidos publican OrderPlaced una vez y no conocen ni CSV ni XML. El hub da a cada sistema su propia traducción.
  6. El almacén cambia otra vez su formato: solo cambia su adaptador. Mantiene el modelo antiguo fuera del dominio: un anti-corruption layer.
  7. Punto a punto: 15 conexiones, 5 cambios con un formato nuevo. Hub: 6 adaptadores, 1 cambio. El precio: el hub es un sistema más que no debe caer.

Míralo

El hub en código

El adaptador es una función: Python no pide una interfaz, siempre que se llame con un OrderPlaced y devuelva texto. Los pedidos publican una vez, y los adaptadores están junto a cada sistema:

hub = Hub()
hub.connect(Endpoint("almacén"),
lambda e: f"{e.id};{e.sku};{e.qty};OUT")
hub.connect(Endpoint("contabilidad"),
lambda e: f'<invoice no="{e.id}" total="{euros(e.cents)}"/>')
hub.connect(Endpoint("entrega"),
lambda e: json.dumps({"order": e.id, "sku": e.sku}, separators=(",", ":")))
# los pedidos publican una vez, en la forma canónica
hub.publish(OrderPlaced(1042, "HP-01", 1, 5900))
print(f"adaptadores: {hub.size()}")
Salida
almacén ← 1042;HP-01;1;OUT
contabilidad ← <invoice no="1042" total="59.00"/>
entrega ← {"order":1042,"sku":"HP-01"}
adaptadores: 3

Los mensajes son los mismos que al principio. La diferencia es quién conoce el formato: los pedidos ya no conocen CSV ni XML, y para un sistema nuevo se añade un connect más, sin tocar los pedidos. euros convierte los centavos en texto con punto: 5900 pasa a ser 59.00.

Pruébalo

Predice la salida

for n in [3, 6, 10]:
links = n * (n - 1) // 2
print(f"{n} sistemas: {links} conexiones o {n} adaptadores")
Respuesta

La idea

Síncrono o asíncrono, y el contrato

No todas las conexiones son iguales:

Síncrona (solicitud y respuesta) Asíncrona (cola, archivo)
Cuándo la respuesta se necesita ahora: «¿hay en stock?» antes de la venta puede esperar: las facturas para la mañana, el archivo del almacén cada hora
Si el otro lado es lento esperas tú también, de ahí timeout y retry el mensaje espera en la cola
Si el otro lado está caído rechazas o tienes una respuesta de reserva se pone al día cuando vuelve

La forma canónica es un contrato: todos dependen de ella. Cambia como un contrato de API: un campo nuevo se añade sin romper los adaptadores antiguos, y quitar un campo es una versión nueva. Si el contrato recibe campos para el almacén y campos solo para contabilidad, es de todos y de nadie.

La idea

Cuándo el hub perjudica

  • Con dos o tres sistemas. Un hub es un sistema más: se despliega, se monitorea y falla. Con pocas conexiones, una conexión directa con un traductor es más barata.
  • Cuando se le mete lógica. Un hub que decide descuentos o enruta según reglas de negocio se convierte en un lugar que ningún equipo posee y que todos los equipos esperan. El hub transporta y traduce; las reglas se quedan en los sistemas.
  • Cuando la forma canónica lo quiere todo. Un modelo que describe el pedido para todos los sistemas a la vez tarda meses en acordarse, y luego nadie se atreve a tocarlo. Varios mensajes pequeños (OrderPlaced, StockChanged), cada uno con un dueño claro, funcionan mejor.
  • Un anti-corruption layer no siempre hace falta. Si el modelo ajeno es bueno y estable (por ejemplo, el contrato del proveedor de pagos), o es un módulo de tu propio equipo, el traductor es solo una capa más que mantener.
Más precisamente, para curiosos

Hubs y brokers reales: Apache Kafka, RabbitMQ, las colas de la nube (Amazon SQS, Azure Service Bus); para la integración: Apache Camel, Spring Integration, MuleSoft, Kafka Connect. El nombre más antiguo es ESB (enterprise service bus). Su mala fama viene de los buses que acabaron cargando con toda la lógica de negocio.

Los patrones tienen nombre. El libro «Enterprise Integration Patterns» (Gregor Hohpe y Bobby Woolf, 2003) describe el message translator, el canonical data model, el router y decenas más. El anti-corruption layer viene de «Domain-Driven Design» de Eric Evans (2003).

Sistemas antiguos en la práctica: el traductor guarda también el archivo original, para poder reproducir una línea cuando alguien averigüe qué significa R. Un archivo por FTP se lee solo cuando se ha subido completo (por ejemplo, señalado con un archivo .done vacío a su lado). Los servidores FTP, los brokers y la red entre ellos son infraestructura y se enseñan en el curso de DevOps ZeroToOps.

Síncrono a través de un hub también se hace (un API gateway delante de varios sistemas), pero entonces el hub es parte de cada solicitud: si es lento, todo es lento.

Ejercicio

Mercancía devuelta

Resuelve la tarea en tu editor.

El almacén antiguo sube un segundo archivo: la mercancía devuelta. Una línea es RET;pedido;artículo;cantidad;fecha, y la fecha es dd.MM.yyyy. A veces el archivo trae también líneas del inventario (INV;…), que no son devoluciones. Escribe un traductor que entregue al dominio devoluciones con la fecha en formato ISO (2026-10-15) y rechace en voz alta toda línea que no sea una devolución.

La entrada es RET;1042;HP-01;1;15.10.2026, RET;1043;CH-07;2;16.10.2026 e INV;HP-01;3.

Salida
devolución 1042: HP-01 × 1 el 2026-10-15
devolución 1043: CH-07 × 2 el 2026-10-16
✗ no es una devolución: INV;HP-01;3

Más tareas

  • Vendedores externos. Conecta también los vendedores al hub: quieren la línea ORDER 1042 HP-01. ¿Cuántas líneas cambian en los pedidos?
  • Contabilidad responde. Contabilidad devuelve XML <paid no="1042" cents="5900"/>. Escribe el traductor hacia dentro: el dominio quiere un pago en euros.

Test

Ponte a prueba

1. ¿Qué hace un anti-corruption layer?
2. El almacén antiguo envía una línea con un estado que el traductor no conoce. ¿Qué es correcto?
3. Ocho sistemas hablan todos con todos. ¿Cuántas conexiones son, y cuántos adaptadores necesita un hub con forma canónica?
4. El cobro debe saber si un artículo hay en stock antes de aceptar el pedido. Contabilidad quiere las facturas para la mañana. ¿Cómo los conectas?
5. ¿Cuándo no hace falta un hub?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·