Към съдържанието
wannacode

Интеграция със стари и чужди системи

Накратко: Anti-corruption layer превежда модела на стара или чужда система към твоя домейн, а шина с канонична форма свежда връзките между n системи до по един адаптер на система.

Проблем

Склад от 2009 г. и счетоводство на XML

Магазинът има стар склад: програма от 2009 г., която никой не смее да пипа. Тя говори CSV, с ; и кодове: A е налично, B е блокирано, а количество -1 значи „броим в момента“. Счетоводството иска XML със сумата с точка. Доставката иска JSON.

Ето как поръчките казват на трите:

class Orders:
def __init__(self, warehouse, accounting, delivery):
self.warehouse = warehouse
self.accounting = accounting
self.delivery = delivery
def place(self, order, sku, qty, cents):
# складът от 2009: CSV и код OUT
self.warehouse.send(f"{order};{sku};{qty};OUT")
# счетоводството: XML, сумата с точка
total = euros(cents)
self.accounting.send(
f'<invoice no="{order}" total="{total}"/>')
# доставката: JSON
self.delivery.send(
json.dumps({"order": order, "sku": sku},
separators=(",", ":")))
orders = Orders(Endpoint("склад"), Endpoint("счетоводство"), Endpoint("доставка"))
orders.place(1042, "HP-01", 1, 5900)
Изход
склад ← 1042;HP-01;1;OUT
счетоводство ← <invoice no="1042" total="59.00"/>
доставка ← {"order":1042,"sku":"HP-01"}

Работи. Но поръчките вече знаят три чужди формата, а кодовете на склада са плъзнали и в касата: някъде пише if (status == "B"). Каталогът също говори със склада, търговците със счетоводството. Когато складът добави нов статус, никой не знае на колко места да търси.

Идеята

Преводач на границата

Чуждата система има свой модел: свои кодове, свои мерни единици, свои странни стойности. Ако той влезе в домейна, домейнът започва да мисли на езика на склада от 2009 г. и се сменя всеки път, когато складът се смени.

Затова на границата стои преводач. Той чете чуждия формат и пуска навътре само понятията на домейна: Stock(sku, available). Какво значат A, B и -1, знае само той. Това се казва anti-corruption layer (слой срещу заразата).

Отвътре това е адаптер, но с по-голяма задача: адаптерът сменя формата на един интерфейс, а anti-corruption layer пази цял модел да не изтече. Той решава какво значи всяка чужда стойност и какво прави с непознатото. Правилото: непознатото се отказва на глас, не се познава.

Виж го

Преводачът в код

@dataclass(frozen=True)
class Stock:
sku: str
available: int
# границата: само тук се знае какво значат A, B и -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}: неизвестно количество")
elif status == "B":
stock.append(Stock(sku, 0))
else:
self.rejected.append(f"{sku}: непознат статус {status}")
return stock
Stock е dataclass: домейнът получава обект с имена, не ред от файла.

Файлът от тази сутрин:

# файлът, който старият склад качва по FTP всеки час
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)
Изход
HP-01: 3
CH-07: 0
KB-12: 0
✗ MS-03: неизвестно количество
✗ SP-02: непознат статус R

KB-12 е блокиран, затова за домейна е 0 налични. MS-03 се брои в момента: преводачът не знае колко е и не измисля число. SP-02 е със статус R, който никой не е описал. Отказан е и някой ще попита собственика на склада какво значи.

Пробвай

Намери грешката

Колега пише по-кратък преводач: „всичко, което не е блокирано, е за продан“. Месец по-късно складът пуска статус R (запазено за външен търговец) и магазинът продава една и съща колонка на двама купувачи. Освен това MS-03 се появява на сайта с -1 бройки. На кой ред е грешката?

Избери реда с грешката.

Идеята

Паяжината от връзки

Шест системи: поръчки, каталог, склад, счетоводство, доставка и външни търговци. Ако всяка говори с всяка, връзките са n(n-1)/2 = 15, и всяка има свой превод. Смяна на формата на склада пипа 5 от тях, в 5 системи, които трябва да се пуснат заедно.

Другият начин е шина с канонична форма: една обща форма на съобщението (OrderPlaced), която всички разбират. Всяка система се свързва веднъж, с адаптер между своя формат и каноничния. Шест системи, шест адаптера. Смяната на формата на склада пипа само неговия адаптер: той е anti-corruption layer-ът на склада.

@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)
Стъпка по стъпка, с думи
  1. Поръчките пращат на три системи, всяка на нейния език: CSV на склада от 2009 г., XML на счетоводството, JSON на доставката. Поръчките знаят и трите формата.
  2. Така говорят и другите: каталогът със склада, търговците със счетоводството. 6 системи, които си говорят всяка с всяка, са 15 връзки, всяка със свой превод.
  3. Складът сменя формата си. Сменя се всяка връзка към него: 5 места в 5 системи, пуснати заедно.
  4. Шина: всяка система се свързва веднъж, с адаптер между своя формат и една канонична форма. 6 системи, 6 адаптера.
  5. Поръчките публикуват OrderPlaced веднъж и не знаят нито CSV, нито XML. Шината дава на всяка система нейния превод.
  6. Складът пак сменя формата: сменя се само неговият адаптер. Той пази стария модел извън домейна: anti-corruption layer.
  7. Точка-до-точка: 15 връзки, 5 промени при нов формат. Шина: 6 адаптера, 1 промяна. Цената: шината е още една система, която не бива да пада.

Виж го

Шината в код

Адаптерът е функция: Python не иска интерфейс, стига да се вика с OrderPlaced и да връща текст. Поръчките публикуват веднъж, а адаптерите са до всяка система:

hub = Hub()
hub.connect(Endpoint("склад"),
lambda e: f"{e.id};{e.sku};{e.qty};OUT")
hub.connect(Endpoint("счетоводство"),
lambda e: f'<invoice no="{e.id}" total="{euros(e.cents)}"/>')
hub.connect(Endpoint("доставка"),
lambda e: json.dumps({"order": e.id, "sku": e.sku}, separators=(",", ":")))
# поръчките публикуват веднъж, в каноничната форма
hub.publish(OrderPlaced(1042, "HP-01", 1, 5900))
print(f"адаптери: {hub.size()}")
Изход
склад ← 1042;HP-01;1;OUT
счетоводство ← <invoice no="1042" total="59.00"/>
доставка ← {"order":1042,"sku":"HP-01"}
адаптери: 3

Съобщенията са същите като в началото. Разликата е кой знае формата: поръчките вече не знаят CSV и XML, а за нова система се добавя един connect, без да се пипат поръчките. euros прави центовете на текст с точка: 5900 става 59.00.

Пробвай

Предвиди изхода

for n in [3, 6, 10]:
links = n * (n - 1) // 2
print(f"{n} системи: {links} връзки или {n} адаптера")
Отговор

Идеята

Синхронно или асинхронно, и договорът

Не всяка връзка е еднаква:

Синхронно (заявка и отговор) Асинхронно (опашка, файл)
Кога отговорът трябва сега: „има ли стока?“ преди продажбата може да почака: фактурите до сутринта, файлът на склада всеки час
Ако другата страна е бавна чакаш и ти, затова timeout и retry съобщението чака в опашката
Ако другата страна е паднала отказваш или имаш резервен отговор наваксва, когато се върне

Каноничната форма е договор: всички зависят от нея. Сменя се като договора на API: ново поле се добавя, без да чупи старите адаптери, а махането на поле е нова версия. Ако в договора влезе стока за склада и поле само за счетоводството, той става на всички и не е на никого.

Идеята

Кога шината вреди

  • При две-три системи. Шината е още една система: пуска се, наблюдава се и пада. При малко връзки една права връзка с преводач е по-евтина.
  • Когато в нея влезе логика. Шина, която решава отстъпки или маршрути по бизнес правила, става място, което никой екип не притежава, а всички чакат. Шината пренася и превежда, правилата остават в системите.
  • Когато каноничната форма иска всичко. Модел, който описва поръчката за всички системи наведнъж, се договаря месеци и после никой не смее да го пипа. По-добре са няколко малки съобщения (OrderPlaced, StockChanged), всяко с ясен собственик.
  • Anti-corruption layer не винаги трябва. Ако чуждият модел е добър и стабилен (например договорът на доставчика на плащания) или ако е модул на твоя екип, преводачът е само още един слой за поддръжка.
По-точно, за любопитните

Истинските шини и брокери: Apache Kafka, RabbitMQ, облачните опашки (Amazon SQS, Azure Service Bus); за интеграция: Apache Camel, Spring Integration, MuleSoft, Kafka Connect. По-старото име е ESB (enterprise service bus). Лошата му слава е от шините, в които е влязла цялата бизнес логика.

Шаблоните имат имена. Книгата „Enterprise Integration Patterns“ (Gregor Hohpe и Bobby Woolf, 2003) описва преводача (message translator), каноничния модел на данните, маршрутизатора и още десетки. Anti-corruption layer е понятие от „Domain-Driven Design“ на Eric Evans (2003).

Стари системи на практика: преводачът пази и оригиналния файл, за да може редът да се пусне наново, след като се изясни какво значи R. Файл по FTP се чете чак когато е качен целият (например по празен файл .done до него). Самите FTP сървъри, брокерите и мрежата между тях са инфраструктура и се учат в курса по DevOps ZeroToOps.

Синхронно през шина също се прави (API gateway пред няколко системи), но шината тогава е част от всяка заявка: ако тя е бавна, бавно е всичко.

Упражнение

Върнатите стоки

Реши задачата в редактора си.

Старият склад качва и втори файл: върнатите стоки. Редът е RET;поръчка;артикул;брой;дата, а датата е dd.MM.yyyy. Във файла понякога има и редове от инвентаризацията (INV;…), които не са връщания. Напиши преводач, който дава на домейна връщания с дата във формат ISO (2026-10-15) и отказва на глас всеки ред, който не е връщане.

Входът е RET;1042;HP-01;1;15.10.2026, RET;1043;CH-07;2;16.10.2026 и INV;HP-01;3.

Изход
връщане 1042: HP-01 × 1 на 2026-10-15
връщане 1043: CH-07 × 2 на 2026-10-16
✗ не е връщане: INV;HP-01;3

Още задачи

  • Външните търговци. Свържи към шината и търговците: те искат ред ORDER 1042 HP-01. Колко реда в поръчките се сменят?
  • Счетоводството отговаря. Счетоводството връща XML <paid no="1042" cents="5900"/>. Напиши преводача навътре: домейнът иска плащане в евро.

Тест

Провери се

1. Какво прави anti-corruption layer?
2. Старият склад праща ред със статус, който преводачът не познава. Какво е правилно?
3. Осем системи си говорят всяка с всяка. Колко връзки са, и колко адаптера иска шина с канонична форма?
4. Касата трябва да знае дали има стока, преди да приеме поръчката. Счетоводството иска фактурите до сутринта. Как ги свързваш?
5. Кога шината е излишна?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·