Zum Inhalt
wannacode

Alte und fremde Systeme anbinden

Kurz gesagt: Ein Anti-Corruption Layer übersetzt das Modell eines alten oder fremden Systems in deine Domäne, und ein Hub mit kanonischem Format bringt die Verbindungen zwischen n Systemen auf einen Adapter pro System.

Problem

Ein Lager von 2009 und die Buchhaltung in XML

Der Shop hat ein altes Lager: ein Programm von 2009, das sich niemand anzufassen traut. Es spricht CSV, mit ; und Codes: A heißt verfügbar, B gesperrt, und die Menge -1 heißt „wird gerade gezählt“. Die Buchhaltung will XML, mit der Summe samt Punkt geschrieben. Die Lieferung will JSON.

So sagen die Bestellungen allen dreien Bescheid:

class Orders(Endpoint warehouse, Endpoint accounting,
Endpoint delivery)
{
public void Place(int id, string sku, int qty,
long cents)
{
// das Lager von 2009: CSV und der Code OUT
warehouse.Send($"{id};{sku};{qty};OUT");
// Buchhaltung: XML, die Summe mit Punkt
var total = Euros(cents);
accounting.Send(
$"""<invoice no="{id}" total="{total}"/>""");
// Lieferung: JSON
var json = $$"""{"order":{{id}},"sku":"{{sku}}"}""";
delivery.Send(json);
}
}
var orders = new Orders(new Endpoint("Lager"),
new Endpoint("Buchhaltung"), new Endpoint("Lieferung"));
orders.Place(1042, "HP-01", 1, 5900);
Ausgabe
Lager ← 1042;HP-01;1;OUT
Buchhaltung ← <invoice no="1042" total="59.00"/>
Lieferung ← {"order":1042,"sku":"HP-01"}

Es funktioniert. Aber die Bestellungen kennen jetzt drei fremde Formate, und die Codes des Lagers haben sich bis in den Checkout ausgebreitet: Irgendwo steht if (status == "B"). Auch der Katalog spricht mit dem Lager, die Verkäufer mit der Buchhaltung. Wenn das Lager einen neuen Status bekommt, weiß niemand, an wie vielen Stellen er suchen muss.

Die Idee

Ein Übersetzer an der Grenze

Ein fremdes System hat sein eigenes Modell: eigene Codes, eigene Einheiten, eigene seltsame Werte. Gelangt dieses Modell in die Domäne, fängt die Domäne an, in der Sprache des Lagers von 2009 zu denken, und ändert sich jedes Mal, wenn es das Lager tut.

Darum steht an der Grenze ein Übersetzer. Er liest das fremde Format und lässt nur die Begriffe der Domäne durch: Stock(sku, available). Nur der Übersetzer weiß, was A, B und -1 bedeuten. Das nennt man einen Anti-Corruption Layer.

Innen ist das ein Adapter, aber mit einer größeren Aufgabe: Ein Adapter ändert die Form einer Schnittstelle, ein Anti-Corruption Layer verhindert, dass ein ganzes Modell durchsickert. Er entscheidet, was jeder fremde Wert bedeutet und was mit dem Unbekannten geschieht. Die Regel: Das Unbekannte wird laut abgelehnt, nie geraten.

Sieh es dir an

Der Übersetzer im Code

record Stock(string Sku, int Available);
// die Grenze: nur hier weiß man, was A, B und -1 bedeuten
class WarehouseAcl
{
public List<string> Rejected { get; } = [];
public List<Stock> Read(IEnumerable<string> lines)
{
var stock = new List<Stock>();
foreach (var line in lines)
{
var f = line.Split(';');
var sku = f[0];
var qty = int.Parse(f[1]);
switch (f[2])
{
case "A" when qty >= 0:
stock.Add(new Stock(sku, qty));
break;
case "A":
Rejected.Add($"{sku}: unbekannte Menge");
break;
case "B":
stock.Add(new Stock(sku, 0));
break;
default:
Rejected.Add($"{sku}: unbekannter Status {f[2]}");
break;
}
}
return stock;
}
}
case "A" when qty >= 0 ist ein Fall mit einer Bedingung: "A" mit negativer Menge geht in den nächsten Fall weiter.

Die Datei von heute Morgen:

// die Datei, die das alte Lager jede Stunde per FTP hochlädt
string[] file = ["HP-01;3;A", "CH-07;0;A", "KB-12;5;B", "MS-03;-1;A", "SP-02;2;R"];
var acl = new WarehouseAcl();
foreach (var s in acl.Read(file))
Console.WriteLine($"{s.Sku}: {s.Available}");
foreach (var r in acl.Rejected)
Console.WriteLine($"✗ {r}");
Ausgabe
HP-01: 3
CH-07: 0
KB-12: 0
✗ MS-03: unbekannte Menge
✗ SP-02: unbekannter Status R

KB-12 ist gesperrt, also sind für die Domäne 0 verfügbar. MS-03 wird gezählt: Der Übersetzer weiß nicht, wie viele es sind, und erfindet keine Zahl. SP-02 hat den Status R, den niemand beschrieben hat. Die Zeile wird abgelehnt, und jemand fragt den Besitzer des Lagers, was er bedeutet.

Probier es aus

Finde den Fehler

Eine Kollegin schreibt einen kürzeren Übersetzer: „Alles, was nicht gesperrt ist, ist verkäuflich.“ Einen Monat später fängt das Lager an, den Status R zu schicken (reserviert für einen externen Verkäufer), und der Shop verkauft denselben Lautsprecher an zwei Kunden. Außerdem steht MS-03 mit -1 auf Lager auf der Seite. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Das Geflecht aus Verbindungen

Sechs Systeme: Bestellungen, Katalog, Lager, Buchhaltung, Lieferung und externe Verkäufer. Wenn jedes mit jedem spricht, sind es n(n-1)/2 = 15 Verbindungen, jede mit ihrer eigenen Übersetzung. Eine Änderung des Lagerformats betrifft 5 davon, in 5 Systemen, die zusammen ausgeliefert werden müssen.

Der andere Weg ist ein Hub mit einer kanonischen Form: eine gemeinsame Form der Nachricht (OrderPlaced), die alle verstehen. Jedes System verbindet sich einmal, mit einem Adapter zwischen seinem eigenen Format und dem kanonischen. Sechs Systeme, sechs Adapter. Eine Änderung des Lagerformats betrifft nur seinen Adapter: Dieser Adapter ist der Anti-Corruption Layer des Lagers.

record OrderPlaced(int Id, string Sku, int Qty,
long Cents);
delegate string Adapter(OrderPlaced e);
class Hub
{
readonly Dictionary<Endpoint, Adapter> adapters = [];
public void Connect(Endpoint system, Adapter adapter) =>
adapters[system] = adapter;
public void Publish(OrderPlaced e)
{
foreach (var (system, adapter) in adapters)
system.Send(adapter(e));
}
public int Size => adapters.Count;
}
Schritt für Schritt, in Worten
  1. Die Bestellungen senden an drei Systeme, jedes in seiner eigenen Sprache: CSV ans Lager von 2009, XML an die Buchhaltung, JSON an die Lieferung. Die Bestellungen kennen alle drei Formate.
  2. So sprechen auch die anderen: der Katalog mit dem Lager, die Verkäufer mit der Buchhaltung. 6 Systeme, die alle miteinander sprechen, sind 15 Verbindungen, jede mit ihrer eigenen Übersetzung.
  3. Das Lager ändert sein Format. Jede Verbindung dorthin ändert sich: 5 Stellen in 5 Systemen, zusammen ausgeliefert.
  4. Ein Hub: Jedes System verbindet sich einmal, mit einem Adapter zwischen seinem Format und einer kanonischen Form. 6 Systeme, 6 Adapter.
  5. Die Bestellungen veröffentlichen OrderPlaced einmal und kennen weder CSV noch XML. Der Hub gibt jedem System seine eigene Übersetzung.
  6. Das Lager ändert sein Format erneut: Nur sein Adapter ändert sich. Er hält das alte Modell aus der Domäne heraus: ein Anti-Corruption Layer.
  7. Punkt-zu-Punkt: 15 Verbindungen, 5 Änderungen bei einem neuen Format. Hub: 6 Adapter, 1 Änderung. Der Preis: Der Hub ist ein weiteres System, das nicht ausfallen darf.

Sieh es dir an

Der Hub im Code

Der Adapter ist ein Delegate: Jedes Lambda, das ein OrderPlaced nimmt und Text zurückgibt, passt. Die Bestellungen veröffentlichen einmal, und die Adapter sitzen neben jedem System:

var hub = new Hub();
hub.Connect(new Endpoint("Lager"),
e => $"{e.Id};{e.Sku};{e.Qty};OUT");
hub.Connect(new Endpoint("Buchhaltung"),
e => $"""<invoice no="{e.Id}" total="{Euros(e.Cents)}"/>""");
hub.Connect(new Endpoint("Lieferung"),
e => $$"""{"order":{{e.Id}},"sku":"{{e.Sku}}"}""");
// die Bestellungen veröffentlichen einmal, in der kanonischen Form
hub.Publish(new OrderPlaced(1042, "HP-01", 1, 5900));
Console.WriteLine($"Adapter: {hub.Size}");
Ausgabe
Lager ← 1042;HP-01;1;OUT
Buchhaltung ← <invoice no="1042" total="59.00"/>
Lieferung ← {"order":1042,"sku":"HP-01"}
Adapter: 3

Die Nachrichten sind dieselben wie am Anfang. Der Unterschied ist, wer das Format kennt: Die Bestellungen kennen CSV und XML nicht mehr, und für ein neues System kommt ein weiteres connect dazu, ohne dass die Bestellungen angefasst werden. Euros macht aus Cent einen Text mit Punkt: 5900 wird zu 59.00.

Probier es aus

Sag die Ausgabe voraus

foreach (var n in new[] { 3, 6, 10 })
{
var links = n * (n - 1) / 2;
Console.WriteLine($"{n} Systeme: {links} Verbindungen oder {n} Adapter");
}
Antwort

Die Idee

Synchron oder asynchron, und der Vertrag

Nicht jede Verbindung ist gleich:

Synchron (Anfrage und Antwort) Asynchron (Warteschlange, Datei)
Wann die Antwort wird jetzt gebraucht: „Ist es auf Lager?“ vor dem Verkauf es kann warten: die Rechnungen bis zum Morgen, die Lagerdatei jede Stunde
Wenn die andere Seite langsam ist du wartest auch, daher Timeout und Retry die Nachricht wartet in der Warteschlange
Wenn die andere Seite ausfällt du lehnst ab oder hast eine Ersatzantwort sie holt auf, sobald sie wieder da ist

Die kanonische Form ist ein Vertrag: Alle hängen von ihr ab. Sie ändert sich wie ein API-Vertrag: Ein neues Feld wird hinzugefügt, ohne die alten Adapter zu brechen, und ein Feld zu entfernen ist eine neue Version. Wenn der Vertrag Felder für das Lager und Felder nur für die Buchhaltung bekommt, gehört er allen und niemandem.

Die Idee

Wann der Hub schadet

  • Bei zwei oder drei Systemen. Ein Hub ist ein weiteres System: Er wird ausgeliefert, überwacht, und er fällt aus. Bei wenigen Verbindungen ist eine direkte Verbindung mit einem Übersetzer billiger.
  • Wenn Logik in ihn hineinkommt. Ein Hub, der Rabatte entscheidet oder nach Geschäftsregeln leitet, wird zu einem Ort, den kein Team besitzt und auf den jedes Team wartet. Der Hub transportiert und übersetzt; die Regeln bleiben in den Systemen.
  • Wenn die kanonische Form alles will. Ein Modell, das die Bestellung für alle Systeme auf einmal beschreibt, braucht Monate bis zur Einigung, und dann traut sich niemand mehr, es anzufassen. Mehrere kleine Nachrichten (OrderPlaced, StockChanged), jede mit einem klaren Besitzer, funktionieren besser.
  • Ein Anti-Corruption Layer ist nicht immer nötig. Ist das fremde Modell gut und stabil (zum Beispiel der Vertrag des Zahlungsanbieters) oder ein Modul deines eigenen Teams, ist der Übersetzer nur eine weitere Schicht, die du pflegen musst.
Genauer, für Neugierige

Echte Hubs und Broker: Apache Kafka, RabbitMQ, die Cloud-Warteschlangen (Amazon SQS, Azure Service Bus); für die Integration: Apache Camel, Spring Integration, MuleSoft, Kafka Connect. Der ältere Name ist ESB (Enterprise Service Bus). Sein schlechter Ruf kommt von den Bussen, in denen am Ende die gesamte Geschäftslogik lag.

Die Muster haben Namen. Das Buch „Enterprise Integration Patterns“ (Gregor Hohpe und Bobby Woolf, 2003) beschreibt den Message Translator, das Canonical Data Model, den Router und Dutzende weitere. Der Anti-Corruption Layer stammt aus „Domain-Driven Design“ von Eric Evans (2003).

Alte Systeme in der Praxis: Der Übersetzer bewahrt auch die Originaldatei auf, damit eine Zeile erneut abgespielt werden kann, sobald jemand herausfindet, was R bedeutet. Eine Datei über FTP wird erst gelesen, wenn sie vollständig hochgeladen ist (zum Beispiel signalisiert durch eine leere .done-Datei daneben). Die FTP-Server selbst, die Broker und das Netz dazwischen sind Infrastruktur und werden im DevOps-Kurs ZeroToOps gelehrt.

Synchron über einen Hub gibt es auch (ein API-Gateway vor mehreren Systemen), aber dann ist der Hub Teil jeder Anfrage: Ist er langsam, ist alles langsam.

Übung

Zurückgegebene Ware

Löse die Aufgabe in deinem Editor.

Das alte Lager lädt eine zweite Datei hoch: die zurückgegebene Ware. Eine Zeile lautet RET;Bestellung;Artikel;Anzahl;Datum, und das Datum hat die Form dd.MM.yyyy. Manchmal stehen in der Datei auch Zeilen aus der Inventur (INV;…), die keine Rückgaben sind. Schreibe einen Übersetzer, der der Domäne Rückgaben mit dem Datum im ISO-Format (2026-10-15) liefert und jede Zeile, die keine Rückgabe ist, laut ablehnt.

Die Eingabe ist RET;1042;HP-01;1;15.10.2026, RET;1043;CH-07;2;16.10.2026 und INV;HP-01;3.

Ausgabe
Rückgabe 1042: HP-01 × 1 am 2026-10-15
Rückgabe 1043: CH-07 × 2 am 2026-10-16
✗ keine Rückgabe: INV;HP-01;3

Weitere Aufgaben

  • Externe Verkäufer. Verbinde auch die Verkäufer mit dem Hub: Sie wollen die Zeile ORDER 1042 HP-01. Wie viele Zeilen ändern sich in den Bestellungen?
  • Die Buchhaltung antwortet. Die Buchhaltung schickt XML <paid no="1042" cents="5900"/> zurück. Schreibe den Übersetzer nach innen: Die Domäne will eine Zahlung in Euro.

Test

Teste dich

1. Was macht ein Anti-Corruption Layer?
2. Das alte Lager schickt eine Zeile mit einem Status, den der Übersetzer nicht kennt. Was ist richtig?
3. Acht Systeme sprechen alle miteinander. Wie viele Verbindungen sind das, und wie viele Adapter braucht ein Hub mit kanonischer Form?
4. Der Checkout muss wissen, ob ein Artikel auf Lager ist, bevor er die Bestellung annimmt. Die Buchhaltung will die Rechnungen bis zum Morgen. Wie verbindest du sie?
5. Wann braucht man keinen Hub?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·