Abhängigkeiten in Legacy-Systemen verwaltbar machen

Ob Bibliotheken, Frameworks oder einfach nur andere Klassen – mit wachsendem Funktionsumfang steigt meist die Anzahl der Codeabhängigkeiten. Um alles aktuell zu halten, lassen sich auch in älteren Systemen Maßnahmen ergreifen.

In Pocket speichern vorlesen Druckansicht
Abhängigkeiten in Legacy-Systemen verwaltbar machen
Lesezeit: 18 Min.
Von
  • Jörg Basedow
Inhaltsverzeichnis

In jedem Umfeld, in dem Individualsoftware zum Einsatz kommt, treffen Entwickler früher oder später auf sogenannte Legacy-Systeme. Dabei muss es sich nicht immer um Software handeln, die seit Jahrzehnten existiert – manche Systeme haben bereits nach ein paar Monaten Legacy-Status.

Beispielsweise ist gerade in der Start-up-Welt häufig schnell etwas zu programmieren, um das Geschäft zum Laufen zu bekommen und Geldgeber zufrieden zu stellen. Dabei häufen die Verantwortlichen schnell technische Schulden an und verzichten oft auf das Schreiben von Tests. Grund für Letzteres ist die Annahme, dass das System irgendwann neu implementiert wird. Wenn sie dann noch ein veraltetes Framework nutzen und es eine hohe Entwicklerfluktuation gibt, stehen Projektneulinge vor den gleichen Herausforderungen, die auch beim Umgang mit klassischen Legacy-Systemen verbreitet sind: Die ursprünglichen Anforderungen sind im Code verborgen, Entwickler scheuen große Änderungen aus Angst, Funktionen unbenutzbar zu machen, und die Implementierung ist meist stark vom eingesetzten Framework abhängig.

Eine Neuentwicklung des Systems ist in der Regel zeitaufwendig und geht gegebenenfalls sogar mit Umsatzeinbußen einher. Um das "Alt"-System wieder in den Griff zu bekommen, sind daher zunächst automatische Tests nötig, die helfen, Sicherheit für größere Änderungen zu schaffen und den Funktionsumfang zu dokumentieren. Dabei kann man in den meisten Fällen vorerst nur aufwendig zu schreibende, langsam laufende, funktionale Tests verfassen, da der Code noch zu schlecht strukturiert ist. Um nach und nach Unit-Tests einführen zu können, müssen die Klassen ihre Abhängigkeiten explizit gereicht bekommen, damit sie sich für den Test durch Mock-Objekte ersetzen lassen.

Wie man Code entsprechend überarbeitet und die wachsende Menge an Abhängigkeiten managen kann, zeigt der erste Teil dieses Artikels an einem PHP-Beispiel. Im zweiten geht es darum, die starke Verflechtung mit dem eingesetzten Framework und anderem Drittanbieter-Code zu minimieren. Dadurch sollte sich die Software einfacher aktualisieren lassen, damit sie nicht auf alten Versionen hängen bleibt, die eventuell sicherheitskritische Fehler enthalten.

Software entsteht häufig unter Zeitdruck, wodurch das Überarbeiten und Verbessern des Codes auf der Strecke bleibt. Es entstehen lange Methoden und große Klassen und insbesondere Controller und Models, die meist eine starke Abhängigkeit zum Framework haben, sind zu umfangreich.

Ein typischer Controller könnte wie im folgenden Beispiel gezeigt aussehen:

class UserController extends FrameworkController
{
public function actionView($id)
{
$user = $this->getDb("SELECT * FROM user WHERE id = :id", ↵
['id'] => $id);

$photoPath = APP_PATH.'/data/users'.user[photo_name];

$this->render('view', ['user' => $user, 'photoPath' => ↵
$photoPath]);
}
}

Um den Code zu prüfen, ist eine Datenbank mit passenden Inhalten zu füllen, eventuell noch eine Datei ins Dateisystem zu legen und nach Testabschluss aufzuräumen.

Um den Code zu entkoppeln, könnten Entwickler beispielsweise zwei Services einführen:

class UserController extends FrameworkController
{
public function actionView($id)
{
$user = $this->getUserRepository()->find($id);

$photoPath = $this->getUserService()->getPhotoForUser($user);

$this->render('view', ['user' => $user, 'photoPath' => ↵
$photoPath]);
}
}

Auf die Art müssen sie nicht mehr den Controller prüfen, da er keine Logik enthält, sondern widmen sich stattdessen den Service-Methoden mit zwei entsprechend kleineren Tests. Die Services sollten nach dem Single-Responsibility-Prinzip angelegt werden. Dadurch entstehen allerdings Abhängigkeiten zwischen ihnen.

Ältere Frameworks arbeiten oft mit einer tiefen Vererbungshierarchie, die das Einfügen ergänzender Funktionen an der richtigen Stelle erschwert. Es empfiehlt sich daher, lieber auf die Komposition von Klassen zu setzen, obwohl auch sie die Anzahl der Anhängigkeiten erhöhen. Dependency-Injection- (DI) oder Service-Container können helfen, damit besser umzugehen. In Java enthält etwa das Spring-Framework einen DI-Container, in PHP steht beispielsweise Pimple zur Verfügung. Das Laravel-Framework bringt einen eigenen Service-Container mit, der sich über sogenannte Provider konfigurieren lässt.

Mehr Infos

Sonderheft "Altlasten im Griff"

Mehr Artikel zum Thema Legacy-Code sind im Sonderheft iX Developer 01/2017 zu finden, dass sich unter anderem im heise Shop erwerben lässt.

Im vorliegenden Fall kommt die Symfony-Dependency-Injection-Komponente zum Einsatz, die Programme unabhängig vom Symfony-Framework nutzen können. Beispielhaft wird ein Legacy-Projekt betrachtet, das mit dem Yii-Framework umgesetzt ist.