Legacy-Software wieder testbar machen
In einer idealen Welt wird Software von Beginn an durchdacht entwickelt und bei jeder Komponente auf eine gute Testbarkeit geachtet. Der Alltag eines Softwareentwicklers sieht freilich anders aus: monolithischer, schwer verständlicher, unzureichend dokumentierter und fehleranfälliger Code aus vergangenen Tagen muss gefixt oder um neue Funktionalitäten erweitert werden – und das am besten gestern. Gleichzeitig darf bestehende Funktionalität jedoch unter gar keinen Umständen angefasst werden, was zu einer Art "Fear Driven Development" führt, die jegliche Bemühungen zur Verbesserung der Code-Qualität im Keim erstickt.
Um die Wartbarkeit von Altanwendungen langfristig zu erhalten und gleichzeitig sicherzustellen, dass sich gewünschtes Verhalten nicht unbeabsichtigt verändert, werden strukturelle oder logische Refactorings zur Verbesserung des automatisierten Testens aber unabdingbar. Im Rahmen dieses Artikels wollen wir uns dabei nicht nur geeignete Refactoring-Techniken aus dem praktischen Alltag anschauen, sondern uns auch der Organisation des Refactorings selbst zuwenden.
Leben mit dem Vermächtnis
Zunächst gilt es zu hinterfragen, um was es sich bei "Legacy-Software" denn überhaupt handelt. Verbreitete Übersetzungen des Begriffes "Legacy" sprechen von Kostbarkeiten, welche man von möglicherweise längst vergangenen Vorfahren geerbt hat. Obwohl diese Definition auf den ersten Blick nicht so recht auf Software passen mag, ist sie im Kern doch zutreffend. So hat man es üblicherweise mit einer gewachsenen Codebasis zu tun, zu der andere Entwickler in der Vergangenheit bereits unzählige Manntage beigetragen haben.
Wertvoll wird die Software für uns aber spätestens durch den Umstand, dass sie sich weiterhin im Produktivbetrieb befindet und nicht einfach mit einem Fingerschnippen abgelöst werden kann. Stattdessen bedarf sie kontinuierlicher Wartung und Anpassung an heutige Gegebenheiten. Weil die ursprünglich beteiligten Personen jedoch in aller Regel nicht mehr zur Verfügung stehen, verschiebt sich die Verantwortlichkeit zur Ausführung dieser Wartungsarbeiten – und zwar auf uns als gegenwärtig beauftragte Entwickler und Architekten. Im wahren Leben bietet ein Nachlass aber nicht immer Anlass zu grenzenloser Freude, sondern stellt uns zuweilen auch vor ernste Probleme.
Vergangenheit: Code & Fix
Mögen wir nicht alle gerne Spaghetti? Bei den meisten Entwicklern löst dieser Begriff vermutlich eher Übelkeit als Appetit aus, denn aus leidvoller Erfahrung ist der Source Code so mancher Altanwendung fragwürdig konzipiert, schwer verständlich sowie kaum dokumentiert und getestet – falls überhaupt. Im Produktionsbetrieb treten derweil scheinbar immer wiederkehrende Bugs auf, die gleichzeitig nur mit Mühe reproduziert werden können – etwa bedingt durch Fehler im Umgang mit Multithreading. Aufgrund komplexer Abhängigkeiten und Wechselwirkungen auf fachlicher und technischer Ebene gestaltet sich ihre Analyse zumeist aufwändig, bedarf aber einer zeitnahen Umsetzung.
Gegenwart: Fear Driven Development
Nach einer Weile ist schließlich der Punkt erreicht, an dem sich die Anwendung guten Gewissens als nicht mehr wartbar bezeichnen lässt. Modifikationen ziehen stets unbeabsichtigte Schmetterlingseffekte nach sich, welche unter Umständen erst Wochen oder Monate nach einem Release auffallen und rückblickend schwerlich einem bestimmten Ticket zuzuordnen sind.
Derweil verlangt der Kunde die Realisierung immer neuer Features und die Behebung im Produktionsbetrieb wiederholt aufgeschlagener Bugs, zeigt gleichzeitig aber wenig Mut zu grundlegender Veränderung (oder: "Machen Sie aber bloß nichts kaputt!"). Allgemein steigende Qualitätsanforderungen (etwa hinsichtlich Performance, Skalierbarkeit und Usability) tragen weiterhin nicht unbedingt zu einer Entspannung der Lage bei. Auf den Punkt gebracht lässt sich dieses für alle Parteien nicht sonderlich angenehme Arbeitsumfeld als "Fear Driven Development" bezeichnen.
Zukunft: Continuous Refactoring
Wenn die Vergangenheit von Code & Fix geprägt war und die Gegenwart von Angst bestimmt wird, stellt sich unweigerlich die Frage, wie es um die Zukunft derartiger Legacy-Software bestellt ist. Haben solche Altanwendungen überhaupt eine Perspektive oder sollte ich mir als verantwortlicher Entwickler vielleicht doch besser einen neuen Job suchen?
Verständnis erlangen
Ein möglicher Ausweg aus dieser verfahrenen Situation ist kontinuierliches Refactoring. Wie wollen wir aber guten Gewissens möglicherweise tiefgreifende Modifikationen vornehmen, wenn wir noch nicht einmal sicher sagen können, wie die betroffenen Komponenten denn überhaupt funktionieren? Im ersten Schritt sollten wir daher anstreben, den bereits existierenden Code zumindest fachlich zu verstehen. Bereits einfache Maßnahmen, wie etwa die Vergabe aussagekräftigerer Bezeichner für Klassen, Methoden und Variablen sowie die logische Strukturierung des Codes durch Blöcke tragen sukzessive zu einem verbesserten fachlichen und technischen Know-how bei. Ist dieses zu einem gewissen Grad aufgebaut, sollten wir es pragmatisch und dauerhaft für andere Kollegen (und uns selbst!) festhalten, beispielsweise unter Verwendung einfacher Bordmittel wie JavaDoc [1].
Qualität des Codes verbessern
Auf dieser Basis können wir im nächsten Schritt nun eine Verbesserung der Code-Qualität in Angriff nehmen. Nach der Entfernung auskommentierten oder nicht mehr eingebundenen Codes, welcher sich im doch eher unwahrscheinlichen Bedarfsfall leicht aus der Versionskontrolle wiederherstellen lässt, sollten die strengsten Code Smells beseitigt und stattdessen sinnvoll anwendbare Entwurfsmuster eingebracht werden.
Zur weiteren Verbesserung des allgemeinen Verständnisses bietet es sich an, bei dieser Gelegenheit auch gleich eine Anpassung des Programmierstils an die aktuellen Konventionen des Projektes vorzunehmen. Nicht weniger Beachtung sollte allerdings auch ein durchdachtes Exception Handling finden. Wenig aussagekräftige Fehlermeldungen wie "Bearbeitung wurde aufgrund eines unbekannten Fehlers abgebrochen" ohne jeglichen Stack Trace haben sich gerade bei der Analyse von Produktionsproblemen bekanntlich noch nie bewährt.
Testbarkeit erreichen
Haben wir den Code verstanden und weist dieser ein Mindestmaß an technischer Qualität auf, können wir uns dem eigentlichen Ziel widmen und unsere Anwendung wieder testbar machen. Die zuvor angewandte Strukturierung erleichtert uns jetzt die Extraktion einzelner Quelltextpassagen, welche sich dann leicht in Isolation testen lassen. Wählen wir für die entstandenen Methoden sprechende und wohl definierte Signaturen, können wir unsere Tests leicht gegen diese Schnittstellen laufen lassen und tragen gleichzeitig zu einem verbesserten Verständnis der Programmstruktur bei.
Zuvor fest verdrahtete Abhängigkeiten können wir nun bei Bedarf durch Mocks ersetzen und auf diesem Wege nicht nur den zu testenden Code weiter isolieren, sondern auch die Performance der Tests (etwa auf dem CI Server) erhöhen. Ausgehend von einer eher mäßigen oder erst gar nicht vorhandenen Testbasis sollten wir dabei alle Bereiche des Programms einbeziehen, welche wir im Rahmen unseres Refactorings angepasst haben. Sich nur auf jenen Code zu beschränken, welchen wir selbst eingeführt haben, erscheint bei einer gewachsenen Altanwendung nicht sinnvoll. Unser Ziel sollte schließlich darin bestehen, mit dem kontinuierlich angewandten Refactoring auch eine sukzessive Vergrößerung der Testbasis zu erzielen. Wie so oft ist auch hier eine Orientierung an der Testpyramide zu empfehlen, d. h. verstärkt auf Unit-Tests zurückzugreifen und sich bei Integration-Tests auf fachlich wichtige und wohl definierte Abläufe zu konzentrieren.
Neue Funktionen umsetzen
Mit der neu erlangten Kontrolle über den ehemaligen Spaghetti-Code und unserer neuen Testbasis im Rücken, können wir uns nun vom Fear Driven Development verabschieden und mit der Umsetzung der so dringlich benötigten Features und Bugfixes beginnen. Angst vor Veränderung des Codes kann endlich der Vergangenheit angehören, weil sich das korrekte Verhalten des Systems mit den zuvor geschriebenen Tests (zumindest bis zu einem gewissen Grad) jederzeit überprüfen lässt.
Die Verantwortlichen überzeugen
Freilich sollte im Team zunächst ein Konsens darüber entstehen, dass Refactorings nicht nur erlaubt, sondern sinnvoll und gerne gesehen sind. In der Praxis muss zuweilen allerdings mit Widerstand von oben gerechnet werden, den es zunächst zu überwinden gilt. Als Totschlagargument schlechthin dient gerne der Hinweis des Projektmanagements, dass der Kunde schließlich nicht für Refactoring bezahle, sondern ausschließlich für neue Funktionen und die Beseitigung produktionsrelevanter Bugs. Falls dem tatsächlich so sein sollte, müssen die zur Implementierung neuer Funktionen veranschlagten Kosten eben um jene Aufwände ergänzt werden, welche sich aus den zur sauberen Umsetzung benötigten Refactorings ergeben.
Wenn der Dom fertig ist, geht die Welt unter!
Spätestens jetzt sollte auch der Kunde ein Interesse daran zeigen, dass sich diese mittelfristig reduzieren. Ein weiteres, oft angebrachtes Gegenargument sind Verweise auf ein vermeintlich nahes Projektende. Denken wir jedoch an die zahlreichen Baustellen auf unseren Straßen, so wissen wir aus eigener Erfahrung nur zu gut, dass initial festgelegte Termine meist nicht mehr als ein frommer Wunsch bleiben. Ferner endet die Entwicklung bekanntlich in den seltensten Fällen mit der Übergabe paketierter Software. Vielmehr schließt sich zumeist eine Wartungsphase an, welche die Entwicklungsdauer um ein Vielfaches übersteigt.
Wer die Sinnhaftigkeit von Refactorings also aufgrund einer vermeintlich beschränkten Projektdauer in Frage stellt, dem sei folgende Kölsche Weisheit an die Hand gegeben: "Wenn der Dom fertig ist, geht die Welt unter!". Mit Hinblick auf die nervliche Gesundheit nachfolgender Entwickler ist es daher wünschenswert, nicht bereits vor dem Release an der Qualität des Codes zu sparen.
Ebenfalls gerne angebracht wird der Verweis auf Test Driven Development. In Anbetracht dessen, dass unser bestehender Code faktisch nicht mehr testbar ist, dürfte es sich hierbei allerdings um ein schwieriges Unterfangen handeln. Auch Blackbox-Tests auf oberster Abstraktionsebene können nicht als ernst gemeinte Alternative durchgehen. Aufgrund ihres weitreichenden Kontextes sind sie nämlich nicht nur technisch, sondern je nach Domäne vor allem auch fachlich sehr komplex. An alle Abhängigkeiten, Implikationen und Seiteneffekte zu denken gestaltet sich schwierig bis unmöglich. Nicht umsonst hat sich in der Informatik das Prinzip "Teile und herrsche" bewährt.
Voraussetzungen schaffen
Um kontinuierliches Refactoring sinnvoll praktizieren zu können, sollten ein paar Rahmenbedingungen erfüllt werden. Wichtig ist in diesem Zusammenhang vor allem Collective Code Ownership, um der Entstehung geistigen Privatbesitzes, isolierter Wissensinseln und selbsternannter Wächter entgegenzuwirken. Wie wollen wir auch die Qualität des Codes einer Altanwendung verbessern, wenn es uns nicht gestattet ist, Veränderungen an offensichtlich fehlerhaften Werken vergangener Programmierkünstler vorzunehmen, ohne formal um Erlaubnis gebeten zu haben oder nach getaner Arbeit Rechenschaft ablegen zu müssen?
Wird durch fest eingeplante Pair Reviews vor der Fertigstellung eines Tasks sichergestellt, dass Code Conventions eingehalten und aussagekräftige Tests geschrieben werden, ist die Entstehung ungewollter Insellösungen nicht zu befürchten.
Ferner sollte von den Funktionen am Markt befindlicher Entwicklungsumgebungen und Refactoring-Tools Gebrauch gemacht werden. Auf diese Weise ist sichergestellt, dass zumindest rein strukturelle Modifikationen keine unbeabsichtigten Veränderungen der Geschäftslogik mit sich bringen und auf diese Weise den Skeptikern in die Karten spielen. Gleichzeitig bleibt der von Entwicklern selbst zu erbringende Aufwand möglichst gering.
Refactorings ins Vorgehensmodell integrieren
Grundlegende Eingriffe wie Refactorings werden für den Entwicklungsprozess nicht erst in dem Moment ihrer Anwendung relevant. Um in der Implementierungsphase nicht mit knapp werdenden Ressourcen kämpfen zu müssen, ist es vielmehr ratsam, bereits während der Planung eine Vorstellung vom Umfang beabsichtigter Aufräumarbeiten zu gewinnen und diese ebenso wie Tests in die initiale Schätzung eines Tasks einfließen zu lassen.
Vor der Implementierung einer neuen Funktion bleibt nun ausreichend Zeit, um zunächst rein strukturelle Refactorings (z. B. Extraktion von Code) durchzuführen und darauf aufbauend geeignete Regressionstests zu schreiben. Mit diesen im Rücken können wir uns im nächsten Schritt auch an logische Veränderungen (z. B. Anpassung einer Schleife) heranwagen und deren Auswirkungen auf den Programmablauf nachvollziehen. Auch wenn uns grüne Balken signalisieren wollen, dass weiterhin alles zum Besten steht, sind wir bei solchen Modifikationen dennoch gut beraten, einen Kollegen auf einen Kaffee (oder eine Mate) zu einem Code Review einzuladen. Bringt dieser keine Einwände vor, können wir schließlich mit der Umsetzung unserer neuen Funktion beginnen. Gegen Ende der Implementierungsphase kommen uns die zuvor geschriebenen Regressionstests zu Gute, weil sie uns eine zusätzliche Sicherheit darüber geben, dass der neue Code keine unliebsamen Schmetterlinge freigelassen hat.
Refactorings anwenden
In der Praxis hat sich unlängst die Erkenntnis durchgesetzt, dass Menschen nicht perfekt sind und Softwareentwicklung folglich als iterativer Prozess zu verstehen ist. Auch bei Refactoring handelt es sich nicht um ein neues Konzept, sondern vielmehr um eine bewährte Methode zur Überarbeitung bestehenden Codes bei gleichzeitiger Erhaltung des äußeren Verhaltens. Dementsprechend viele Erfahrungen haben andere Entwickler im Laufe der Jahre bereits gesammelt und zu umfangreichen Werken kondensiert. Nachfolgend werden bewusst nur einige ausgewählte Maßnahmen vorgestellt, welche sich zur Erreichung unseres Ziels, nämlich Legacy-Software wieder testbar zu machen, aus eigener Erfahrung in besonderem Maße eignen.
Code auslagern
Das Prinzip des Teilens und Herrschens gilt natürlich nicht nur für unsere Tests, sondern erst recht auch für den Code selbst. Schon aus Gründen der besseren Lesbarkeit sollten wir daher in einem ersten Schritt monolithische Blöcke in kleinere Portionen zerteilen und diese in eigene Klassen und Methoden auslagern. Die bewusste Auswahl fachlich stimmiger Bezeichner bis hin zur Variablenebene verbessert das Verständnis in verschiedener Hinsicht. Zum einen zwingen wir uns selbst zu einer genaueren Auseinandersetzung mit dem Code und zum anderen ermöglichen wir anderen Entwicklern später einen leichteren Einstieg.
Vorteilhaft ist eine Zerlegung aber auch im Hinblick auf die Testbarkeit, versetzt sie uns doch in die Lage, einzelne Bereiche unserer Anwendung in Isolation zu überprüfen. Auf der anderen Seite können wir den ausgelagerten und für sich getesteten Code nun einfacher durch eine Mock-Implementierung (partielles Mocking) ersetzen, wodurch sich auch der aufrufende Code leichter automatisierten Tests unterziehen lässt.
Seiteneffekte vermeiden
Denken wir einmal an die letzte Situation zurück, welche uns bei der Analyse bestehenden Codes ein "Was zum …?" entlockte, so war mit hoher Wahrscheinlichkeit ein unerwarteter Seiteneffekt im Spiel. Durch eine Umstellung auf funktional geprägtes Methodendesign lassen sich diese jedoch erfreulicherweise deutlich reduzieren. Zugriffe auf Eingabeparameter sollten ausschließlich lesend erfolgen, neu erzeugte bzw. veränderte Daten nach der Ausführung (ggf. in einem Transferobjekt) zurückgegeben werden. Fernab der Vermeidung unliebsamer Seiteneffekte entsteht so eine Schnittstelle mit einem klar definierten Vertrag, dessen Einhaltung wir leicht mit einem Test überprüfen können. Einen Schritt weiter gedacht, können wir derart ausgestaltete Methoden bereits als Blackbox ansehen, deren konkrete Implementierung nicht nur leicht austauschbar, sondern insbesondere auch ohne umfangreiches Wissen über interne Vorgänge und Abhängigkeiten testbar ist.
Kopplung reduzieren
In diesem Zusammenhang sollten wir auch bestrebt sein, die Kopplung zu externen Abhängigkeiten (insbesondere Objekten und deren Methoden) zu reduzieren. Optimalerweise übergeben wir diese als zusätzliche Eingabeparameter, was uns erneut die Möglichkeit eröffnet, sie in unseren Testfällen durch Mocks zu ersetzen. Konfigurieren wir das Verhalten der Mocks individuell, können wir darüber hinaus mehrere Testszenarien für verschiedene Anwendungsfälle erstellen. Erscheint dies im konkreten Fall nicht praktikabel, etwa aufgrund des gewählten oder vorgegebenen Software-Designs, kann durch die punktuelle Einführung von Interfaces eine zusätzliche, leichtgewichtige Abstraktionsschicht geschaffen werden, welche uns das Einklinken eigener Mocks für unsere Tests erlaubt.
Sichtbarkeit überdenken
Ein wesentliches Konzept der Objektorientierung ist die Kapselung (Information Hiding) von Variablen und Methoden, welches in aktuellen Programmiersprachen wie Java unter anderem mit Hilfe von Sichtbarkeitsmodifikatoren realisiert wird.
Verbreitet sind etwa Konventionen wie diese: Alles, was von außen zugänglich sein soll, definieren wir als public, der Rest ist je nach Verwendung in Klassenhierarchie und Packages entweder protected oder private. Einfach und unproblematisch, oder etwa nicht? Spätestens bei der Implementierung neuer Tests, welche Methoden mit den Modifiern protected oder private auf einem Objekt aufrufen möchten, welches nicht im gleichen Paket beheimatet ist, entsteht plötzlich ein Sichtbarkeitsproblem. Mögliche Lösungen: Sichtbarkeit dauerhaft erhöhen, Hacks per Reflection in der Testklasse oder der Einsatz einer Bibliothek, welche dann wiederum Hacks per Reflection ausführt.
Wenn wir ehrlich sind, ist uns keine dieser Lösungen sonderlich sympathisch. Glücklicherweise stellen Sichtbarkeitsmodifikatoren aber nicht die einzige Möglichkeit zur Realisierung des Information Hidings dar. Zumindest auf der Ebene von Methoden können wir die Sichtbarkeit nämlich auch über wohldefinierte Interfaces steuern, welche sich für jeden Kontext individuell zuschneiden lassen. Gerade im Zusammenspiel mit Dependency Injection werden Implementierungsdetails wie zusätzlich vorhandene Hilfsmethoden so gezielt vor externen Clients verborgen, sind in unseren Tests hingegen weiterhin uneingeschränkt sichtbar. An dieser Stelle können wir aber noch einen Schritt weitergehen. Nicht nur mit Hinblick auf die Testbarkeit sollte es doch eigentlich unser Ziel sein, Methoden in sich geschlossen zu definieren, sodass wir sie ohne umfangreiche Kontextinformationen oder die Inkaufnahme von Seiteneffekten wiederverwenden können. Erfüllen unsere Methoden diese Voraussetzung, sollten wir uns als Entwickler einmal selbst fragen, warum wir unseren Code nach außen denn eigentlich überhaupt noch verstecken wollen…
Prinzipien haben
Viele bewährte Techniken und Prinzipien des strukturierten Software-Designs zielen zwar nicht primär auf Refactorings ab, tragen aber dennoch zu einer verbesserten Testbarkeit bei. Nach dem Integration Operation Segregation Principle sollten Methoden entweder selbst Fachlogik implementieren oder bereits bestehende Methoden zu einem breiter gefassten Use Case aggregieren, nicht jedoch beides gleichzeitig leisten. Halten wir uns an dieses Prinzip, können wir unsere Tests leicht in zwei Kategorien aufteilen. Für isolierte Fachlogik sind klassische Unit-Tests geradezu prädestiniert, während komplexe Use Cases wahlweise durch Unit-Tests mit Mocks und Verifikation oder vollständige Integrationstests abgedeckt werden können.
Architektur anpassen
Obwohl sich die zuvor beschriebenen Refactorings in erster Linie auf das Design einer Software fokussieren, ist es der Testbarkeit sicher nicht abträglich, langfristig auch eine stringent umgesetzte Architektur anzustreben. Ohne an dieser Stelle näher auf bewährte oder aktuelle Architekturstile eingehen zu wollen, wirkt sich der Zugewinn an Struktur durch eine Unterteilung der Gesamtanwendung in beherrschbare Subsysteme nicht nur auf den ehemaligen Spaghetti-Code aus, sondern gleichermaßen auch auf seine Tests. Ob wir die entstehenden Komponenten hierbei Services nennen oder sie einem Layer oder Tier zuordnen, soll uns an dieser Stelle egal sein. Achten wir auf eine lose Kopplung durch klar definierte Schnittstellen und beschränken wir uns nach Möglichkeit auf ein Abstraktionslevel (z. B. Zugriff auf die Datenbank), so erleichtert uns dies das Testen in Isolation, weil sich eingebundene Komponenten durch entsprechend konfigurierte Mocks ersetzen lassen.
In einer klassischen Schichtenarchitektur könnten wir DAOs etwa durch Integrationstests mit einer In-Memory-Datenbank überprüfen, während wir für die aufbauenden Services losgelöste Unit-Tests mit gemockten DAOs vorsehen. Diese Maßnahme bewirkt eine stärkere Isolation der Tests voneinander und erhöht gleichzeitig deren Performance, weil kostspielige Datenbankzugriffe auf ein Mindestmaß reduziert werden.
Ein Beispiel aus der Praxis
Abschließend wollen wir uns einmal folgende Methode anschauen. Es wird nur ganz kurz wehtun – versprochen!
private int getMyCustomersForTheCalculations (int key2, String key1, List<Customer> list, int mode) { // 1. Build SQL // ... // 2. Execute database call // ... // 3. Validate input parameters // ... if (error == 78) { return 4711; } else if (error != 321 && mode != 445 && Date.isToday("MO")) { return -7; } list.addAll(…); return 1337; }
Nach Ausspruch des obligatorischen "Was zum…?" drängen sich sofort einige konkrete Fragen auf:
- Rückgabetyp int für MyCustomers?!
- Welche Calculations sind hier wohl gemeint?
- Welche Semantik besitzen die Schlüsselparameter?
- Übergabe einer Liste von Customers an die getter-Methode?!
- Was soll denn eigentlich dieser mode sein und welche Varianten gibt es?
- Validierung der Eingabedaten nach Ausführung der DB-Operation?!
- Welche Fachlogik steckt hinter dem Rückgabewert?
- Warum darf die Methode nicht auch von außen aufgerufen werden?
Was an dieser Stelle vermutlich ein wenig konstruiert wirken mag, gehört bei vielen gewachsenen Projekten zum Alltag. Einige sorgsame Refactorings später sieht die Welt aber schon wieder ein wenig sauberer aus:
public List<Customer> getRetiredCustomers (String name, int zipCode) { // 1. Validate input parameters validateName (name); validateZipCode(zipCode); // 2. Build SQL StringBuilder sql = getCustomersSelectSql(); sql.append(…); … // 3. Execute database call List<Customer> customers = database.query(sql, asList(name, zipCode, true)); return customers; }
Auch ohne ausführliche Dokumentation sehen wir der Methode anhand ihrer klar definierten Signatur bereits auf den ersten Blick an, welche Fachlichkeit sie realisiert und wie sie aufzurufen ist. Aufgrund ihrer funktional ausgestalteten Implementierung gehören unliebsame Überraschungen durch Seiteneffekte nun endlich der Vergangenheit an. Wir haben auf diese Weise nicht nur die Testbarkeit deutlich erhöht, sondern auch zu einem besseren Verständnis und einer erhöhten Wiederverwendbarkeit beigetragen.
Zusammenfassung und Ausblick
Altanwendungen anzupassen und um neue Funktionen zu ergänzen, kann in der Praxis schnell zur schmerzhaften Herkulesaufgabe werden, weil solche Erbstücke häufig mit Seiteneffekten und anderen unvorhersehbaren Wechselwirkungen zu kämpfen haben. Um nicht dem "Fear Driven Development" zu verfallen, sondern Wartbarkeit der Software und Produktivität der Entwickler gleichermaßen wiederherzustellen, sind wiederholt durchgeführte Refactorings zur Verbesserung der Testautomatisierung erforderlich. Um diese wirkungsvoll umsetzen zu können, sollte allerdings jeder Entwickler zur Anpassung des Codes berechtigt sein und sich von geeigneten Tools unterstützen lassen.
Sowohl in der Planungs- als auch Implementierungsphase berücksichtigt, kann kontinuierliches Refactoring ein selbstverständlicher Teil der Projektkultur werden. Die beschriebenen Techniken aus eigener, praktischer Erfahrung liefern hierzu das Handwerkszeug. Nichts desto trotz handelt es sich freilich nicht um ein Allheilmittel für alle technischen Beschwerden. Einer nicht mehr zu den Anforderungen passenden Architektur oder Technologie lässt sich durch einfaches Refactoring des Anwendungsdesigns auch nicht mehr zu neuem Glanz verhelfen. In solchen Fällen werden tiefergehende Modifikationen und Maßnahmen erforderlich, welche gesondert zu beleuchten sind.
Weiterführende Literatur
- Fowler, et. al.: Refactoring: Improving the Design of Existing Code, Addison-Wesley Professional
- Robert C. Martin: Clean Code: A Handbook of Agile Software Craftsmanship, Prentice Hall