Refactoring: Strategien zum langfristigen Verbessern von Quelltextstrukturen

Seite 4: Systemtest

Inhaltsverzeichnis

Die hier erstellten Tests enthalten Details, die nicht unbedingt für sie notwendig sind. Der Charakter von Systemtests ist leider, dass sie viele Details berücksichtigen müssen und nicht unbedingt dazu geeignet sind, eine große Masse des Funktionsumfangs abzudecken. Systemtestsammlungen können schnell unüberschaubar werden, sollte ein Team versuchen, jegliches Verhalten auf dieser Abstraktionsebene abzusichern. Die Wartung der Tests kann sich so frustrierend gestalten, dass manche Teams es ganz aufgeben. Eine bessere Alternative ist oft eine Kombination aus wenigen Systemtests und einer großen Anzahl von Unit-Tests (Tests für isolierte Einheiten von Code).

Zu Beginn des Refactorings ist es schwer möglich, Letztere einzuführen, da die Struktur des Codes es nicht oder nur unter unverhältnismäßiger Anstrengung erlaubt. Umso wichtiger ist es, mit diesen Systemtests eine solide Testgrundlage auf einer höheren Ebene zu erstellen. Am Ende der Umstrukturierung empfiehlt es sich jedoch, sie durch neue Unit-Tests auf niedriger Ebene oder durch Tests aus testgetriebener Entwicklung (TDD) zu ersetzen.

Während des Refactorings lässt sich jederzeit unerwartetes Verhalten entdecken. Da es sich jedoch nur auf die Veränderung der Struktur konzentriert, ist es wichtig, es nicht mit dem Beseitigen von Bugs zu vermischen. Da das System wahrscheinlich schon seit einer Weile erfolgreich im Einsatz ist, kann jede Änderung unerwartete Nebeneffekte haben. Vermeintlich fehlerbehaftetes Verhalten kann das Gesamtkonstrukt stabil halten, wenn sich Nutzer und andere Systeme darauf verlassen. In jedem Fall sollten Finder die Abweichung (idealerweise mit einem Test) dokumentieren und sie mit den Produktverantwortlichen besprechen.

In fast jeder unkontrolliert gewachsenen Codebasis findet sich mindestens eine komplex verzweigte Struktur aus if- und else-Zweigen (oder einem Äquivalent). Manch neues Verhalten kommt in das System, indem Entwickler einfach einen weiteren Zweig hinzufügen. Die Gesamtstruktur verändern sie dabei nicht – sei es, weil die Änderung nicht eingeplant wurde oder die Voraussetzungen (Fähigkeiten, Sicherung durch automatisierte Tests, Kenntnis bestehender Funktionen) nicht erfüllt waren.

Es kommt zum sogenannten Broken-Window-Effekt: Das übermäßig vorhandene Chaos desensibilisiert, sodass es normal scheint, es weiter voran zu treiben statt die Unordnung zu beseitigen. Falls das Team keine gemeinsame Codeverantwortung hat, fühlt sich eventuell niemand zuständig, aufzuräumen. Um die Arbeitsabläufe dahingehend zu verbessern, ist zu überlegen, komplex verzweigte Strukturen zunächst aufzulösen. Dafür lässt sich jeder Codezweig durch eine gut benannte Hilfsmethode ersetzen.

Im folgenden Listing kann das Verhalten der Methode durch Extraktion von erklärenden Variablen und Methoden übersichtlicher gestaltet werden:

public void executeMove(int roll) {
print(players.get(currentPlayer) + " is the current player");
print("They have rolled a " + roll);

if (inPenaltyBox[currentPlayer]) {
boolean rollIsOdd = roll % 2 != 0;

if (rollIsOdd) {
playerGetsOutOfPenaltyBox();

moveTheCurrentPlayer(roll);

askTheNextQuestion();
} else {
playerDoesNotGetOutOfPenaltyBox();
}
} else {
moveTheCurrentPlayer(roll);

askTheNextQuestion();
}
}

Das Ziel sollte sein, in jeder Methode auf dem gleichen Abstraktionslevel zu bleiben. Im obigen Beispiel hat public etwa die Aufgabe, die verschiedenen AusfĂĽhrungszweige zu koordinieren und zu entscheiden, welchen Weg die ProgrammausfĂĽhrung nimmt. Die privaten Methoden fĂĽhren dann die Teilschritte der Logik aus, wobei jede jeweils nur fĂĽr einen Schritt verantwortlich ist.

Die bisher beschriebenen Aufräumarbeiten sind jedoch erst der Anfang. Sie sollen dazu dienen, das Verhalten besser zu verstehen und temporär zu dokumentieren. Kleine Verschönerungen können viel Zeit verschlingen aber kaum Verbesserungen bringen. Weitere Techniken sollten dazu dienen, das Design des Codes grundlegend zu verbessern. Dafür sind Kenntnisse guter Gestaltung (wie die SOLID-Prinzipien) und wie sie sich erreichen lässt, notwendig.

Sobald das grundlegende Verhalten klar ist, lassen sich Alternativen für das bisherige Design entwickeln. Es empfiehlt sich, die Verantwortlichkeiten des Codes aufzulisten und auf neue Klassen zu verteilen. Sie lassen sich dann mit testgetriebener Entwicklung neu erstellen. Sobald sie funktionieren, können die Entwickler den bisherigen Code ersetzen. Quelltexte mit klaren Zuständigkeiten folgen oft dem "Tell, don't ask"-Prinzip: Ein Teil des Programms schickt ein Kommando an eine Klasse und vertraut darauf, dass sie tut was sie verspricht. Code, der nach diesem Prinzip erstellt wurde, ist einfacher zu ändern und zu testen. Gegenüber komplizierten Vererbungshierarchien erlaubt die Komposition aus kleineren Einheiten den Vorteil, Verhalten einfacher zu kombinieren und das Wissen über die Einzelheiten an einer Stelle zu bündeln.

Ein guter Anfang ist, primitive Variablen und Collections (z.B. Listen) in eigenen Klassen zu kapseln. Sie bündeln dann Verhalten, das mit diesen Werten verknüpft ist, an einer zentralen Stelle. Es ist so leichter zu finden und wiederholt sich nicht überall im Code. Außerdem lässt es sich auf die Weise unabhängig vom Rest testen.

Nach demselben Prinzip ist die Geschäftsdomäne im Code sichtbar zu machen. Treffend benannte Klassen mit gut definiertem Verhalten erleichtern es, Veränderungen vorzunehmen, die durch einen Geschäftswert getrieben sind. Es erlaubt dem Team auch, die Domäne besser zu verstehen und Missverständnisse beim Implementieren von Anforderungen zu reduzieren. Es empfiehlt sich daher, die Prinzipien des "Domain Driven Designs" zu kennen und zu versuchen, sie umzusetzen.

Die Fähigkeit, Verhalten des Systems aus kleinen unabhängigen Einheiten zusammenzusetzen, lässt sich erlernen. Bewusstes Üben an Beispielen hilft, sie zu verbessern und die direkte Zusammenarbeit in der Gruppe oder beim Paarprogrammieren ermöglicht das Überprüfen eigener Annahmen. Disziplin beim Einsatz neu erlernter Techniken hilft, sie zu verinnerlichen: was als bewusste Anstrengung beginnt, kann durch Routine zum intuitiven Werkzeug werden.