Интеграция със стари и чужди системи
Накратко: Anti-corruption layer превежда модела на стара или чужда система към твоя домейн, а шина с канонична форма свежда връзките между n системи до по един адаптер на система.
Проблем
Склад от 2009 г. и счетоводство на XML
Магазинът има стар склад: програма от 2009 г., която никой не смее да пипа. Тя говори CSV, с ; и кодове: A е налично, B е блокирано, а количество -1 значи „броим в момента“. Счетоводството иска XML със сумата с точка. Доставката иска JSON.
Ето как поръчките казват на трите:
record Orders(Endpoint warehouse, Endpoint accounting, Endpoint delivery) {
void place(int id, String sku, int qty, long cents) { // складът от 2009: CSV и код OUT String csv = id + ";" + sku + ";" + qty + ";OUT"; warehouse.send(csv); // счетоводството: XML, сумата с точка String xml = "<invoice no=\"%d\" total=\"%s\"/>"; accounting.send(xml.formatted(id, euros(cents))); // доставката: JSON String json = "{\"order\":%d,\"sku\":\"%s\"}"; delivery.send(json.formatted(id, sku)); }}var orders = new Orders(new Endpoint("склад"), new Endpoint("счетоводство"), new 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 пази цял модел да не изтече. Той решава какво значи всяка чужда стойност и какво прави с непознатото. Правилото: непознатото се отказва на глас, не се познава.
Виж го
Преводачът в код
record Stock(String sku, int available) {}
// границата: само тук се знае какво значат A, B и -1class WarehouseAcl { final List<String> rejected = new ArrayList<>();
List<Stock> read(List<String> lines) { List<Stock> stock = new ArrayList<>(); for (String line : lines) { String[] f = line.split(";"); String sku = f[0]; int qty = Integer.parseInt(f[1]); switch (f[2]) { case "A" -> { if (qty >= 0) stock.add(new Stock(sku, qty)); else rejected.add(sku + ": неизвестно количество"); } case "B" -> stock.add(new Stock(sku, 0)); default -> rejected.add(sku + ": непознат статус " + f[2]); } } return stock; }}switch със стрелки (->) не пропада към следващия случай, затова няма break.Файлът от тази сутрин:
// файлът, който старият склад качва по FTP всеки часvar file = List.of("HP-01;3;A", "CH-07;0;A", "KB-12;5;B", "MS-03;-1;A", "SP-02;2;R");var acl = new WarehouseAcl();for (Stock s : acl.read(file)) System.out.println(s.sku() + ": " + s.available());for (String r : acl.rejected) System.out.println("✗ " + r);HP-01: 3CH-07: 0KB-12: 0✗ MS-03: неизвестно количество✗ SP-02: непознат статус RKB-12 е блокиран, затова за домейна е 0 налични. MS-03 се брои в момента: преводачът не знае колко е и не измисля число. SP-02 е със статус R, който никой не е описал. Отказан е и някой ще попита собственика на склада какво значи.
Пробвай
Намери грешката
Колега пише по-кратък преводач: „всичко, което не е блокирано, е за продан“. Месец по-късно складът пуска статус R (запазено за външен търговец) и магазинът продава една и съща колонка на двама купувачи. Освен това MS-03 се появява на сайта с -1 бройки. На кой ред е грешката?
Избери реда с грешката.
Преводачът познава вместо да превежда: всеки статус, който не е B, става „налично“, и всяко количество, дори -1, отива в домейна. Непознатото трябва да се отказва на глас, както в WarehouseAcl по-горе: изброени познати случаи и отказ за всичко друго.
Идеята
Паяжината от връзки
Шест системи: поръчки, каталог, склад, счетоводство, доставка и външни търговци. Ако всяка говори с всяка, връзките са n(n-1)/2 = 15, и всяка има свой превод. Смяна на формата на склада пипа 5 от тях, в 5 системи, които трябва да се пуснат заедно.
Другият начин е шина с канонична форма: една обща форма на съобщението (OrderPlaced), която всички разбират. Всяка система се свързва веднъж, с адаптер между своя формат и каноничния. Шест системи, шест адаптера. Смяната на формата на склада пипа само неговия адаптер: той е anti-corruption layer-ът на склада.
record OrderPlaced(int id, String sku, int qty, long cents) {}
interface Adapter { String translate(OrderPlaced event);}
class Hub { private final Map<Endpoint, Adapter> adapters = new LinkedHashMap<>();
void connect(Endpoint system, Adapter adapter) { adapters.put(system, adapter); }
void publish(OrderPlaced event) { adapters.forEach((system, adapter) -> system.send(adapter.translate(event))); }
int size() { return adapters.size(); }}record Orders(Endpoint warehouse, Endpoint accounting, Endpoint delivery) {
void place(int id, String sku, int qty, long cents) { // складът от 2009: CSV и код OUT String csv = id + ";" + sku + ";" + qty + ";OUT"; warehouse.send(csv); // счетоводството: XML, сумата с точка String xml = "<invoice no=\"%d\" total=\"%s\"/>"; accounting.send(xml.formatted(id, euros(cents))); // доставката: JSON String json = "{\"order\":%d,\"sku\":\"%s\"}"; delivery.send(json.formatted(id, sku)); }}Стъпка по стъпка, с думи
- Поръчките пращат на три системи, всяка на нейния език: CSV на склада от 2009 г., XML на счетоводството, JSON на доставката. Поръчките знаят и трите формата.
- Така говорят и другите: каталогът със склада, търговците със счетоводството. 6 системи, които си говорят всяка с всяка, са 15 връзки, всяка със свой превод.
- Складът сменя формата си. Сменя се всяка връзка към него: 5 места в 5 системи, пуснати заедно.
- Шина: всяка система се свързва веднъж, с адаптер между своя формат и една канонична форма. 6 системи, 6 адаптера.
- Поръчките публикуват OrderPlaced веднъж и не знаят нито CSV, нито XML. Шината дава на всяка система нейния превод.
- Складът пак сменя формата: сменя се само неговият адаптер. Той пази стария модел извън домейна: anti-corruption layer.
- Точка-до-точка: 15 връзки, 5 промени при нов формат. Шина: 6 адаптера, 1 промяна. Цената: шината е още една система, която не бива да пада.
Виж го
Шината в код
Адаптерът е функционален интерфейс: всяка ламбда с OrderPlaced на входа и текст на изхода става. Поръчките публикуват веднъж, а адаптерите са до всяка система:
var hub = new Hub();hub.connect(new Endpoint("склад"), e -> e.id() + ";" + e.sku() + ";" + e.qty() + ";OUT");hub.connect(new Endpoint("счетоводство"), e -> "<invoice no=\"%d\" total=\"%s\"/>".formatted(e.id(), euros(e.cents())));hub.connect(new Endpoint("доставка"), e -> "{\"order\":%d,\"sku\":\"%s\"}".formatted(e.id(), e.sku()));// поръчките публикуват веднъж, в каноничната формаhub.publish(new OrderPlaced(1042, "HP-01", 1, 5900));System.out.println("адаптери: " + 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 (int n : new int[] {3, 6, 10}) { int links = n * (n - 1) / 2; System.out.println(n + " системи: " + links + " връзки или " + n + " адаптера");}Връзките са двойките системи: всяка от n с всяка от другите n - 1, делено на 2, защото връзката A–B е същата като B–A. Растат почти като n², а адаптерите растат като n. Това е най-лошият случай: в истински магазин не всяка система говори с всяка. Но и 8 връзки с 8 различни превода са много.
Идеята
Синхронно или асинхронно, и договорът
Не всяка връзка е еднаква:
| Синхронно (заявка и отговор) | Асинхронно (опашка, файл) | |
|---|---|---|
| Кога | отговорът трябва сега: „има ли стока?“ преди продажбата | може да почака: фактурите до сутринта, файлът на склада всеки час |
| Ако другата страна е бавна | чакаш и ти, затова 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"/>. Напиши преводача навътре: домейнът иска плащане в евро.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: Събития и опашки · Дизайн на API: договор, версии, повторени заявки · Когато съседът е бавен: timeout, retry, circuit breaker · Транзакция през няколко услуги (saga)
- Продължава в: Смяна на жива система (strangler fig, expand–contract) · Enterprise ландшафт и капабилитита · Твоята първа архитектурна задача
- Задачи по темата: Архитектът в работа