Case-Study: Erfolgreiche Neu­strukturierung einer Logistik-Anwendung

Ein Container wird entladen
Irgendwas mit Containern – der “klassische Use-Case” im Domain-Driven Design...

Einer der führenden Lebensmittel-Einzelhändler veranstaltet wöchentlich wechselnde Aktionen mit Non-Food-Artikeln. Dabei kommt es regelmäßig zu gravierenden Verzögerungen in der Produktion oder beim Transport. Hierauf muss man frühzeitig reagieren können, etwa zur Planung der Werbemittel. Deshalb wird die komplette Prozesskette – vom Abschluss der Produktion bis zur Entladung in den europäischen Logistikzentren – mittels einer Web-gestützten Anwendung gesteuert und überwacht.

Die Anwendung startete ursprünglich mit einem deutlich reduzierten Funktionsumfang, wurde aber aufgrund des Erfolgs stetig erweitert. Diese Entwicklung wird sich auf absehbare Zeit fortsetzen, um die Prozesskette zu verlängern, weitere Lieferanten aufschalten zu können und die bestehenden Prozessschritte mit Tracking-Daten externer Dienstleister zu vertiefen.

Das Projekt wurde ursprünglich mit einem Rapid-Development- / CRUD-Ansatz entwickelt. Die Entwicklung war jedoch ins Stocken geraten. Außerdem hatte die Applikation ernsthafte Performance-Probleme, weil immer mehr Aktionsartikel im System gepflegt wurden.

Der Kunde war unschlüssig, ob das Projekt sinnvoll weiter ausgebaut werden kann oder ob auf eine Software-Lösung von der Stange zurückgegriffen werden muss. Aufgrund der spezifischen Anforderungen und der kritischen Relevanz, gab der Kunde der Anwendung mit einem neuen Entwickler-Team noch eine Chance.

Situationsanalyse

Ein Spaghetti-Gericht
Spaghetti-Code – nicht so schön wie dieses Gericht

Recht schnell war klar, dass es zwar eine gewisse fachliche (also notwendige) Komplexität gibt, die konkrete Implementierung das Projekt allerdings technisch (und somit unnötig) verkomplizierte.

  • Das verwendete Framework ohne saubere Modelle war den fachlichen Anforderungen nicht mehr gewachsen. Der Zugriff auf die Datenbank war sehr kleinteilig.

    Ein leistungsfähiges Framework, sauber strukturierte Modelle und eine solide Datenbank-Abstraktion hätten viel duplizierten Code vermieden und Verantwortlichkeiten klar getrennt.

  • Fachlich getrennte Prozessschritte waren sowohl im Code als auch in der Datenbank eng miteinander verbunden, weil die Anwendung implizit aus einem großen, globalen Domain-Modell bestand. Dadurch war beispielsweise unklar, welche Teilprozesse für welche Daten zuständig waren.

    Sauber getrennte Module hätten Verantwortlichkeiten klarer gemacht und sowohl zu einem besseren Datenbank­modell als auch verständ­licherem Code geführt.

  • Neben der fehlenden Aufteilung in überschaubare Teilmodule waren zudem fachliche Anforderungen und Aspekte der technischen Infrastruktur miteinander verzahnt; neben dem kleinteiligen Zugriff auf die Datenbank etwa auch beim Excel-Import und -Export, Zugriff aufs Dateisystem oder Session-Management.

    Wenn man die Infrastruktur­belange abstrahiert, wird die Fachlogik übersichtlicher. Außerdem hätten wir beispielsweise die Excel-Bibliothek einfacher gegen eine alternative Implemen­tierung austauschen können, die mit den gestiegenen Datenmengen besser zurecht kommt.

  • Die ursprünglichen Modelle konnten nicht mehr ent­sprechend neuer Anforderungen und Erkenntnisse restrukturiert werden. Somit waren schwer nachvollziehbare Über­setzungs­schritte zwischen dem technisch-inhärenten Datenmodell und dem fachlichen dargestellten Domain-Modell in der Nutzeroberfläche notwendig.

    Mit einer guten Test­abdeckung hätte man diese Modelle bei jeder not­wendigen Anpassung im Laufe der Zeit in überschaubaren Schritten refakturieren und entsprechend der Realität weiterentwickeln können.

  • Eine saubere Objekt-Orientierung ließ sich nicht erkennen. Methoden umfassten mehrere hundert Zeilen Code; einzelne Klassen kamen auf über 5000 Zeilen. Es gab keine automatisierten Tests, Datenbank-Transaktionen fehlten, bei zahlreichen Bug-Reports konnte die Ursache nicht geklärt werden.

    Durch entsprechende Fortbildung der Entwickler wäre ein grundlegendes Level bei der Code-Qualität möglich gewesen.

Details zu den technischen Schwierigkeiten

Trotz der erheblichen Misstände wollten wir die Applikation modernisieren und hielten einen Rewrite aus wirtschaftlicher Sicht für keine Alternative. Wir erarbeiteten mit dem Kunden neue Perspektiven und ergriffen zahlreiche Maßnahmen, um das Projekt für zukünftige Anforderungen wieder fit zu machen.

Im folgenden eine Auswahl wesentlicher Maßnahmen.

Moderne Architektur & Modularisierung

Spielsteine
Geteilte Verantwortlichkeit – höhere Autonomie

Wir entschieden uns für einen modularisierten Monolithen als Grundarchitektur. Und die Applikation sollte auf Symfony mit Doctrine migriert werden. Weiterhin wurden die Domain-Komponenten anfangs grob definiert.

  • Die Entscheidung für einen Monolithen erschien mit Blick auf die nächsten Jahre als ausreichend bzw. kann recht einfach weiterentwickelt werden. Außerdem ist das Refactoring einfacher, wenn man nur in einer Anwendung arbeitet.

  • Neue Features können situationsabhängig in Symfony oder in der alten Applikation umgesetzt werden.

    Für den schrittweisen Über­gang musste die bestehende Applikation in Symfony integriert werden: Als Fallback werden Requests an die alte Applikation durchgereicht. Umgekehrt können neue, in Symfony implementierte Services im Legacy-Code genutzt werden. Das Session-Management des alten Frameworks und von Symfony wurden miteinander verheiratet.

  • Mögliche Domain-Komponenten mit ihrer jeweiligen Verantwortlichkeit wurden in einer Prozess­analyse mittels Event-Storming identifiziert.

    Die Verzeichnisstruktur von Symfony wurde entsprechend der gewählten Zielarchitektur so konfiguriert, dass die Module weitestgehend unabhängig voneinander entwickelt werden können.

Hintergründe zur Entscheidung

Diese Grundstruktur erlaubte uns, die Teilprozesse in überschaubare Module mit klar definierten Ver­antwortlich­keiten auszulagern.

Qualitätssicherung

Grüne Ampel
Freie Fahrt für sauberen Code!

Es stand außer Frage, dass wir umfangreiche, automatisierte Tests schreiben wollten. (Das war auch der Wunsch des Kunden.)

Hierfür implementierten wir einige projekt-spezifische Erweiterungen für PHPUnit. Verschiedene QA-Tools unterstützten eine gleichbleibende Code-Qualität.

  • Bei Infrastruktur- und End-To-End-Tests muss die Datenbank nach jedem Test wieder zurück gesetzt werden. Nur dann laufen diese Tests dauerhaft verlässlich und unabhängig voneinander.

    Hierfür implementierten wir ein intelligentes Datenbank-Tear-Down: Datensätze, die während des Test-Setups oder der Ausführung erzeugt wurden, werden anschließend automatisch aus der Datenbank gelöscht.

  • Entitäten werden mittels Builder-Pattern gebaut. Somit sind die Tests leicht verständlich. Und bei Änderungen am Modell ist der Wartungsaufwand für die Tests minimal.

  • Die einzelnen Tests strukturierten wir (ähnlich dem Behaviour-Driven Development) in die Phasen “given” (setup), “when” (execution) und “then” (verification).

  • Eine einheitliche Code-Qualität wurde mittels Test-Coverage sowie statischer Code-Analyse (PHP CodeSniffer, PHP Mess Detector, Psalm) sichergestellt.

Einzelheiten der Umsetzung

Die Geschäftslogik kann nun komplett mit schnellen Unit-Tests ohne Infrastruktur (Datenbank, Dateisystem, Excel-Bibliotheken, Framework, Sessions, ...) getestet werden.

Mit diesem Sicherheitsnetz und in Verbindung mit Test-Driven Development konnten wir mit minimalem Aufwand Tests schreiben, die 1. gut verständlich waren, 2. stabil und schnell liefen und 3. im Projektverlauf pflegeleicht waren.

Die Überwachung der Code-Qualität gab den Entwicklern immer wieder die notwendigen Impulse, mit den neuen Regeln vertraut zu werden und lieferte Metriken für den Projektfortschritt.

Refactoring & Schulung

Oldtimer
Reifes Alter – aber gut in Schuss und von hohem Wert

Nach den Vorarbeiten implementierten wir erste Features im Kontext der Domain-Module mit Symfony. Wir schulten das Team regelmäßig über die neuen Projektstrukturen und die neuen Technologien. Die ersten Domain-Module dienten auch als praktische Beispiele für die Kollegen.

  • Neue Funktionalität in den Domain-Komponenten implementierten wir konsequent test-getrieben mit einer hohen Testabdeckung. Die Erstellung der Tests gelang mit minimalem Zusatz­aufwand. Die Tests gaben auch die notwendige Sicherheit, wenn die Symfony-Implementierung aufgrund neuer Erkenntnisse refakturiert werden sollte.

  • Bei anstehenden Änderungen wurde (sofern sinnvoll) Bestands-Code der alten Applikation refakturiert und Schritt für Schritt in die zugehörigen Domain-Module migriert.

  • Wir schulten die Team-Kollegen umfangreich zu Themen wie Symfony, Test-Driven Development mit PHPUnit, der gewählten Ziel-Architektur und Domain-Driven Design. Vermutlich hätten wir früher Pair- oder Mob-Programming einführen sollen, um diese neuen Themen noch praktischer zu verankern.

Fortschritte bei der Umstellung

Nun konnten wir die An­wendung schrittweise aufräumen – immer unter Berücksichtigung der betriebs­wirtschaftlichen und fachlichen Rahmen­bedingungen.

Performance

Rally-Auto beim Rennen
Keine Schönheit – aber robust und schnell

Die ärgsten Performance-Engpässe konnten zu Projekt­beginn schnell behoben werden.

Hier gab es mit überschaubaren Anpassungen erstaunliche Quick-Wins, die sowohl die Ausführungs­zeit als auch den Speicherbedarf enorm reduzierten.

  • Die problematischen Requests wurden mittels Call-Graph analysiert. So konnten die problematischen Stellen schnell identifiziert und iterativ optimiert werden.

  • Langsame Datenbank-Anfragen wurden in der Call-Graph-Analyse ebenfalls entdeckt und deren Ausführungs­plan in MySQL analysiert. Durch passende Indexe oder optimierte Anfragen gab es auch hier schnelle Ergebnisse.

  • Obwohl bei den Optimierungen noch Luft nach oben gewesen wäre, waren die Ergebnisse für den Kunden für den Moment akzeptabel.

Systematisches Vorgehen bei der Performance-Analyse

Später konnten aufgrund der zwischen­zeitlich erfolgten Vorarbeiten weitere Performance-Gewinne erzielt werden. Einige zentrale Seiten mit vielfältigen Filter-Möglichkeiten sollten auch noch auf ElasticSearch umgestellt werden, um die Seitenladezeit weiter zu beschleunigen.

Umfangreichere Erweiterungen und Umbauten

Älteres Haus mit modernem Aufbau
Harmonisches Zusammenspiel von Alt und Neu

Im Projektverlauf ließen sich einige tiefgreifende Änderungen durchführen, die anfangs schwer vorstellbar waren.

  • Das klassische HTML-Frontend wurde auf ein JavaScript-basiertes Frontend umgestellt, das mit dem Backend via REST kommunizierte.

  • Wir entwickelten ein neues rollen- und gruppenbasiertes Rechtekonzept, weil das alte Konzept unübersichtlich war, Rechte an vielen Stellen mit zusätzlichen Prüfungen ergänzt wurden und der Kunde die Zugriffsrechte dynamisch vergeben wollte.

  • Wir setzten einen Micro-Service als API-Endpoint für eingehende Nachrichten externer Partner auf, weil hier erhöhte Anforderungen an Performance und Erreichbarkeit bestanden. Dieser Micro-Service kommuniziert mit der Anwendung per Message-Bus. Auch einige ausgehende Nachrichten an externe Partner wurden bereits auf dieses System umgestellt.

  • Die Anwendung wurde in die Cloud verlagert. Soweit bereits möglich wurden Services (Datenbank, Message-Bus, ...) genutzt, die der Cloud-Anbieter betreut.

Einige Beispiele

Fazit

Wir bereiteten die Migration auf ein modernes Framework vor und strukturierten die Anwendung in überschaubare, fachlich abgegrenzte Module. Mit einer intelligenten Test-Strategie und verschiedenen Qualitäts­metriken etablierten wir einen praktikablen Qualitätsstandard. Außerdem schulten wir die Kollegen in den neuen Strukturen, Technologien und Prozessen.

Mit diesen Konzepten überzeugten wir den Kunden sowie das Team und schufen die Basis für eine erfolgreiche Weiterentwicklung der Anwendung. Der Bestands-Code wurde schrittweise in die neue Applikation migriert; und tiefgreifende neue Anforderungen konnten gut umgesetzt werden.

Die ärgsten Performance-Sorgen konnten bereits zu Beginn auf ein akzeptables Maß reduziert werden. Im Projekt­verlauf waren weitere Performance-Optimierungen möglich.

Die Altanwendung wird nicht von heute auf morgen verschwinden, aber das Projekt kann nun sauber und deutlich dynamischer weiterentwickelt werden. Mit neuem Vertrauen in den Erfolg stockte der Kunde schließlich das Budget auf, um neue Funktionen schneller umsetzen zu können.

Referenz des Team-Leads

Das umfangreiche Statement des Team-Leads finden Sie auf der Startseite.