»Refactoring ist, wenn einem auffällt, daß der Funktionsname >foobar< ziemlich bescheuert ist, und man die Funktion in >Sinus< umbenennt.«
Andreas Bogk, in: Lutz Donnerhacke: »Fachbegriffe der Informatik«
In Kapitel 14 ging es um Symptome zweifelhaften Codes, die schon bei flüchtiger Betrachtung auffallen. Aber auch Code, der auf den ersten und zweiten Blick ganz aufgeräumt wirkt, kann von Überarbeitungen profitieren. Als Anfänger sind Sie zunächst froh, ein lauffähiges Programm entwickelt zu haben, das ungefähr das macht, was es soll. Dabei wird es aber auf Dauer nicht bleiben. Sie stellen fest, dass Ihr Programm auch für andere Bereiche erweitert werden könnte. Ihr Chef - falls Sie beruflich programmieren -erwartet Erweiterungen. Aus einem Prototyp soll doch die spätere Produktivversion werden (Sie haben hoffentlich versucht, sich dagegen zu wehren). Sie haben erkannt, dass Sie von Anfang an mit einer anderen Bibliothek besser gefahren wären.
Außerdem altert jeder Code, auch wenn das bei digitaler Information seltsam klingt. Zwar ist er nicht eines Tages abgenutzt und kaputt wie ein durchgerostetes Fahrrad, aber er wird im Laufe der Zeit weniger nützlich. Zum einen ändern sich die Umweltbedingungen, zum anderen verwischen Änderungen das ursprüngliche Konzept. Was gestern noch richtig und sinnvoll war, ist es heute nur noch teilweise. Neuere Bereiche passen weniger gut zu älteren, der rote Faden, der das Programm hoffentlich einmal ausgezeichnet hat, kann im Laufe der Zeit verloren gegangen sein. Der letzte und für die Leser dieses Buchs wohl wichtigste Faktor: Als weniger guter und geübter Programmierer macht man relativ schnell Fortschritte. Daher steht man häufig vor Code, der gestern noch ganz solide schien und einem heute die Schamesröte ins Gesicht treibt.
Das alles sind gute Gründe für eine Überarbeitung des vorhandenen Codes. Diese Wartung und Pflege des Codes nennt sich Refactoring, und ist nicht das Gleiche wie Debugging. Während Debugging das Ausmerzen konkreter Fehler bedeutet, soll Refactoring einen vor zukünftigen bewahren oder zumindest die Entwicklung erleichtern. Weil Refactoring selbst neue Bugs nach sich ziehen kann, sollten Sie keinen Code überarbeiten, der bekannte Bugs und Probleme hat. Beim Refactoring können Sie keine vorher vorhandenen Bugs gebrauchen, weil dann nicht mehr zu unterscheiden ist, ob ein Bug erst durch das Refactoring in den Code eingeschleppt wurde oder vorher schon vorhanden war.
Es geht beim Refactoring nicht primär darum, den Code nach persönlichen Vorlieben zu verschönern. Refakturieren Sie Ihren Code nur, wenn Sie damit konkrete Dinge verbessern wollen. Der Code sollte nach der Refakturierung
• übersichtlicher,
• allgemeiner und
• möglichst kürzer
sein als vorher. Der neue und bessere Code ist leichter zu verstehen, weil er übersichtlicher ist. Er ist besser wiederverwendbar, weil er allgemeiner ist. Und er hat weniger Fehler, weil er kürzer ist.1
In vielen Situationen steht man vor der Wahl, entweder Tabula Rasa zu machen und den Code von Grund auf neu zu schreiben oder ihn Stück für Stück zu verbessern. Die Versuchung ist groß, das bisher Geschriebene zu verwerfen und ganz neu anzufangen - gar nicht erst an den Symptomen herumdoktern, sondern gleich eine neue, herrliche Welt erschaffen, Code von der Brillanz eines lupenreinen Diamanten! Ein naheliegender Impuls, dem sowohl Einzelkämpfer als auch ganze Firmen schon erlegen sind. Leider führt er häufig ins Debakel. Der alte Code war nicht nur schwer zu durchschauen, weil sein Autor damals noch schlechter programmierte als heute, sondern auch, weil er viele Sonderfälle und Nebenbedingungen abdecken und sich in eine vorhandene Umgebung einfügen musste. Die Hyäne, der Nacktmull und die Vogelspinne gelten gemeinhin nicht als gutaussehende Tiere, aber es gibt Gründe für ihr Aussehen. Sie alle haben sich an bestimmte Anforderungen und Umweltbedingungen angepasst. Die alten Rahmenbedingungen werden sich genauso auf das neue Projekt auswirken und einige der alten Hässlichkeiten in die neue Version hinüberretten.
Die Meinungen darüber, ob man gelegentlich einen völligen Neustart versuchen sollte, sind bei professionellen Programmierern und Autoren kluger Artikel durchaus geteilt. Während die einen schlechten Code einfach löschen und besseren Code schreiben, warnen andere genau davor. Joel Spolsky, einer der bekanntesten Blogger im Bereich der Softwareentwicklung, schreibt: »Dass neuer Code besser sein soll als alter, ist eine absurde Vorstellung. Den alten Code hat man verwendet. Man hat ihn getestet. Man hat jede Menge Bugs darin gefunden und behoben.«1 2 Es gibt laut Spolsky überhaupt keinen Grund
zu der Annahme, dass man im zweiten Anlaufbesseren Code schreiben wird als im ersten. Allerdings redet der Autor hier von dem mindestens mittelguten Code, den professionelle Programmierer her'orbringen, und nicht von dem unansehnlichen Gestrüpp, das Thema dieses Buchs ist.
Wer noch neu im Programmiergeschäft ist, darf zwar davon ausgehen, dass er im zweiten Versuch besseren Code produzieren wird, aber trotzdem spielt viel Wunschdenken in die Vorstellung vom ganz neuen, viel besseren Code hinein. Wir raten deshalb dazu, bestehenden Code nur dann von Grund auf neu zu schreiben, wenn er Fehler enthält und Sie auch nach gründlicher Suche nicht dahinterkommen, wo diese Fehler sitzen. In allen anderen Fällen investieren Sie Ihre Zeit besser in graduelle Verbesserungen, denn Sie kommen damit ebenfalls ans Ziel, bloß ist das Risiko geringer, dass hinterher gar nichts mehr funktioniert.
Anders als beim Debugging gibt es für Refactoring keinen konkreten, unausweichlichen Anlass. Schieben Sie das Refakturieren auf, wird der bisherige Code weiter funktionieren und Sie können ihn auch durch neue Features ergänzen. Das macht es schwieriger, einfache Faustregeln dafür aufzustellen, wann Sie eine existierende Codebasis überarbeiten sollten.
Grundsätzlich empfehlen Ratgeber für Softwareentwicklung, dass man immer dann refakturieren sollte, wenn man auf schlechten Code stößt. Für Anfänger kann das ein Problem sein, weil sie ständig aufVerbesserungswürdiges stoßen. Refactoring gibt einem zwar das gute Gefühl, aufgeräumt und geputzt zu haben, nötig ist aber ein sinnvolles Gleichgewicht zwischen dem Schreiben von neuem Code und dem Polieren des vorhandenen. Ein günstiger Zeitpunkt für Refactoring liegt relativ nah, aber nicht zu nah an der Entstehung des Codes: Man erinnert sich zwar noch einigermaßen an die nötigen Details, hält aber nicht mehr unbedingt an schlechten Lösungen fest, nur weil es viel Arbeit war, sie sich auszudenken.
Für Messies gibt es einen zweiteiligen Ratschlag, der das Betreten von Zimmern der eigenen Wohnung betrifft:
1. Mach es nicht noch schlimmer.
2. Mach jedes Mal, wenn du einen Raum betrittst, irgendeine Kleinigkeit besser.
Diese Regeln lassen sich auch auf Refactoring anwenden. Manchen Codeteilen werden Sie dabei nie begegnen, weil Sie diesen Raum einfach nie betreten. Das ist okay, denn wenn kein Grund auftaucht, jemals in diese entlegenen Keller hinabzusteigen, dann heißt das, dass der betreffende Code zwar vielleicht hässlich sein mag, aber gut genug funktioniert. Sie können Ihre Zeit also wichtigeren Dingen widmen.
Wenn Sie auf Code treffen, den Sie für neue Aufgaben erweitern und anpassen wollen, dann tun Sie das auf eine Weise, die zumindest die Qualität nicht verschlechtert. Der Code sollte nachher mindestens so gut strukturiert und lesbar sein wie vorher.
Bauen Sie daher keine Sonderfälle für Ihre neuen Anforderungen ein, sondern machen Sie eine Lösung allgemeiner, wo immer es möglich ist. Wenn Sie beim Lesen eines Codeblocks auf Abschnitte stoßen, von denen Sie denken »das könnte klarer sein« - und wenn Sie womöglich auch schon wissen, wodurch diese Klarheit herzustellen wäre -, dann zögern Sie nicht, sondern schreiben Sie sie gleich um. Ihr zukünftiges Selbst wird es Ihnen danken. Und ziehen Sie Codekommentare nach; wenn keine existieren, dann schreiben Sie welche.
Passen Sie sich an Namenskonventionen und Formatierung des Abschnitts an. Wenn die Ihrem jetzigen Stil widersprechen, dann ändern Sie den ganzen Abschnitt. Zwischen diesen beiden Möglichkeiten liegt die Flickschusterei: Sie fügen schnell ein paar neue Zeilen in ihrem aktuell bevorzugten Stil ein und lassen den Rest der Funktion unverändert. Vermeiden Sie diesen dritten Weg.
Schreiben Sie möglichst ein Änderungsdatum an die Funktion, das erleichtert es Ihnen, refakturierten von älterem Code zu unterscheiden, wenn Sie die Datei wieder lesen. Wenn Sie ein Versionskontrollsystem diszipliniert nutzen, können Sie auf das Notieren von Änderungsdaten auch verzichten und stattdessen in seiner History nachsehen. Das funktioniert aber nur, wenn Sie häufig Ihre Änderungen in das VCS einchecken - wenn Sie das nur einmal im Monat tun, können Sie Änderungen schlecht nachvollziehen.
Die neue Lösung wird nicht unbedingt gleich attraktiver als die alte sein. Lassen Sie sich davon nicht abschrecken. Wenn die Richtung stimmt, in die Sie sich mit Ihrem Code bewegen, aber das Ergebnis Ihrer Umbauarbeiten Sie noch nicht zufriedenstellt, macht der Code nur eine notwendige Zwischenphase durch, eine Art Larvenstadium.
Anzeichen dafür, dass Überarbeitungen jetzt kein Fehler wären
Schwer erklärbarer Code
Stellen Sie sich gelegentlich folgende Frage: »Könnte ich diese Funktion einem anderen Programmierer in einer Minute erklären? In fünf Minuten? Wenigstens in zehn? Gar nicht?« Neigen Sie zu den beiden letzten Antworten, sollten Sie die betreffende Funktion vereinfachen.
Schlechte Zeichen
Der Code enthält offensichtliche Probleme, die in Kapitel 14 beschrieben sind.
Sich häufende Ärgernisse
Von einigen Autoren wird eine Dreierregel propagiert, die mit der im Kapitel »Schlechte Zeichen« beschriebenen »Rule of Three« verwandt ist: Wenn Sie sich zum dritten Mal über eine suboptimale Lösung ärgern, ist die Wahrscheinlichkeit groß, dass Sie sich noch viele weitere Male darüber ärgern werden. Eine gute Gelegenheit, das Ärgernis abzustellen.
Falsche Orte
Sie suchen eine Datei oder Codestelle an einem bestimmten Ort, finden sie dort aber nicht. Offenbar ist das der naheliegendste Ort und es wäre günstig, das Gesuchte dorthin umzuziehen. Zum einen findet man es dann beim nächsten Mal schneller, zum anderen sind die Schubladen, in denen das Gehirn als Erstes sucht, oft auch die logischsten und besten.
Verständnisprobleme
Sie verstehen den eigenen Code beim Wiederlesen zunächst nicht, nach einigem Nachdenken aber schließlich doch: Nutzen Sie das neue Verständnis und schreiben Sie den Code so um, dass Sie beim nächsten Wiederlesen eine Chance haben, ihn direkt zu verstehen. Das kann bedeuten, dass Sie eine weniger elegante, aber leichter verständliche Lösung vorziehen müssen. Vielleicht haben Sie schon beim ersten Hinschreiben vor dieser Wahl gestanden und sich stolz für die elegante, komplizierte Variante entschieden. Aber komplexen Code schreiben können viele. Die schwierigere Aufgabe ist es, Code zu schreiben, den man beim Wiederlesen selbst noch versteht. Hier ist Ihr Ehrgeiz besser aufgehoben.
Zu viele Speziallösungen
Sie wissen schon beim Implementieren neuer Features, dass Sie spätere Änderungen immer an mehreren Stellen vornehmen müssen, und sind vielleicht sogar vorausschauend genug, einen Kommentar zu schreiben, in dem Sie Ihr zukünftiges Ich daran erinnern. In diesem Fall sollten Sie alle betroffenen Stellen zu einer Funktion zusammenfassen (siehe den Abschnitt >>Code zusammenfassen« weiter unten). Wenn Ihr vergangenes Ich den letzten Punkt nicht beachtet hat und Sie jetzt ähnliche Änderungen und Reparaturen an verschiedenen Stellen Ihrer Software vornehmen müssen, dann ergreifen Sie die Gelegenheit und fassen Sie diese Stellen zusammen.
Fehler sind entstanden, weil Sie irgendetwas vergessen haben, woran Sie »einfach immer
denken« wollten.
Beseitigen Sie den Anlass für diesen Knoten im Taschentuch. Menschen denken nicht einmal dann immer zuverlässig an das, woran sie unbedingt denken sollten, wenn ihr Leben auf dem Spiel steht: Eine wiederkehrende Unfallursache auch bei den erfahrensten Kletterern sind unvollständig geknüpfte Anseilknoten. Deshalb rät man Kletterern, an »Partnerchecks« zu denken. Und auch der Partnercheck wird immer wieder vergessen. Es gibt nicht für alles, aber für sehr vieles im Leben eine bessere Lösung als »da muss ich halt einfach jedes Mal aufmerksam dran denken«.
Schlechte Nachbarschaft
Schlechter Code wird meist auch von schlechtem Code aufgerufen, einfach weil er normalerweise vom selben Autor zur selben Zeit geschrieben wurde. Wenn Sie eine faule Stelle gefunden haben, notieren Sie, welche Funktionen den refakturierten Code aufrufen, und werfen Sie einen kritischen Blick darauf - möglicherweise verbergen sich hier Bereiche, die ebenfalls refakturiert gehören.
Wann sollte man refakturieren? I 185
Bevor Sie jetzt nicken und loslegen, gehen Sie bitte noch einmal kurz in sich. Haben Sie verstanden, was der Code bewirkt, den Sie refakturieren wollen? Wenn nicht, kann es peinlich werden (siehe den Kasten »Ist das Code oder kann das weg?« weiter unten in diesem Kapitel). Und haben Sie genug Zeit, das funktionierende System nicht nur zu zerlegen, sondern auch wieder in Gang zu bringen? Das Leben nach einem unvollendeten Refactoring kann schwieriger und hässlicher sein als vorher, genau wie nach einer halbfertigen Wohnungsrenovierung. Die beiden Punkte klingen trivial, sind aber gar nicht so leicht zu beachten. Wenn Sie schon wissen, dass Sie überhaupt keine Lust auf das Umschreiben von Code haben, ständig durch Meetings und andere Projekte unterbrochen werden oder die Zeit knapp ist, müssen Sie priorisieren, ob Sie jetzt Code refakturieren oder neue Features entwickeln wollen.
Über die Frage, ob man unter Zeitdruck refakturieren sollte, gehen die Meinungen auseinander:
»Das ist der völlig falsche Zeitpunkt, weil die Wahrscheinlichkeit für Fehler steigt und man sich in einem Wirrwarr aus halb refakturiertem Code mit neuen und interessanten Fehlern verlieren kann, während man eigentlich an anderen Stellen alle Hände voll zu tun hätte.«
Johannes Jander
»Ich muss generell bei allen Argumenten widersprechen, die darauf hinauslaufen, das Richtige gerade dann nicht zu tun, wenn Zeitdruck herrscht. Dann gerade! Denn best practices sparen Zeit. Man muss aber die Nerven dazu haben und die Fokussiertheit, das ist das Problem. Wenn man beides nicht hat, fasst man den Code am besten gar nicht erst an.«
Jan Bölschc
Eine brauchbare Kompromissposition ist vermutlich, etwas mehr Zeit in Refactoring zu stecken, als man unter Zeitdruck eigentlich will, aber auch mehr liegen zu lassen, als eigentlich schön wäre.
Wenn Sie sich für gezieltes Refactoring eines prinzipiell funktionierenden Stücks Software entschieden haben, dann lohnt es sich, zunächst eine Bestandsaufnahme zu machen. Schreiben Sie die Probleme samt dem Funktionsnamen auf. Schreiben Sie auch auf, was Sie besser machen wollen und warum. Seien Sie sich darüber im Klaren, was Sie aus welchen Gründen besser machen wollen - kratzen Sie sich nur da, wo es juckt. Wenn Sie anfangen, Ihr Programm an allen Enden gleichzeitig zu refakturieren, dann verlieren Sie schnell den Fokus und machen nichts besser.
Die Bestandsaufnahme ist eine Übersicht und sollte nicht übermäßig detailliert sein, denn der Vorgang des Aufschreibens ist wichtiger als das konkrete Ergebnis. Das Niederschreiben des Problems hilft Ihnen, es konkreter zu fassen und zu verstehen, und es wirkt gleichzeitig als Erinnerung und Selbstverpflichtung. Und falls alle unsere Ermahnungen vergeblich waren und Sie mitten im Refactoring überraschend für sechs Monate nach Australien verreisen, haben Sie mithilfe dieser Notizen danach zumindest eine Chance, den Code wieder zum Laufen zu bringen.
Ein gescheiterter Plan
Die Beiträge der Riesenmaschine waren aus Dummheitsgründen in einem Format angelegt, das zwar so ähnlich wie XML aussah, aber keines war. Überliefert sind einige Zeilen guter Änderungsvorsätze in einem separaten Textdokument mir dieser Überschrift:
Fahrplan für den Umbau der Beitragsdateien zu richtigem XML und dann für den Einsatz von SimpleXML [1]
• erst das bisherige Tool so umschreiben, dass es Dateien in der alten UND der neuen Form verdauen und schreiben kann (done) [2]
• dannalle Dateien ändern (und zwar so, dass sie nicht versehentlich 2x geändert werden können) [3]
- <?xml version="i.O" encoding="iso8859-1" ?> einbauen [4]
- um den Beitrag herum so was wie <beitrag> </beitrag>
- & statt & [5]
• dann Umstellung auf SimpleXML
• alles Ausgelesene muss durch utf8_decode wegen der Umlaute
[1] Die Überschrift diente der Erinnerung daran, wozu dieser Umbau eigentlich gut war, und damit der Motivation. SimpleXML ist eine PHP-Extension, die große Vereinfachungen im Umgang mit den Beitragsdateien versprach.
[2] ln der ursprünglichen Version musste jedes Element des Pseudo-XML in einer ganz bestimmten Zeile der Datei stehen, um erkannt zu werden. Man muss nicht unbedingt erwähnen, dass das keine gute Idee war. Wir erwähnen es vorsichtshalber trotzdem: Es ist keine gute Idee. Don'r try this at home.
[3] Dass hinter dieser Zeile kein >>done<< mehr steht, deutet darauf hin, dass es sich um eine immer noch viel zu komplexe Suchen-und-Ersetzen-Maßnahme mit zahlreichen Feh-lermöglichkeiren handelte. Weniger schlechte Programmierer hätten gewusst, dass man so etwas nicht mit Suchen und Ersetzen löst, sondern ein kleines Programm schreibt, das Beiträge im alten Format einliest und im neuen zurückschreibt. Man hält die Maschine an, lässt den Konverter testhalber auf ein paar Beiträge los, und bearbeitet dann den gesamten Datenbestand damit.
[4] Eine mäßig gute Idee, UTF-8 wäre besser gewesen (siehe Kapitel 17, Abschnitt »Zeichenkodierung«).
[5] Das & ist in XML ein für den internen Gebrauch re>semertes Zeichen und darf daher nicht einfach so im Text vorkommen.
Dieser Refactoringversuch ist also einerseits gescheitert, andererseits ist immerhin der Plan erhalten geblieben, aus dem bei einem neuen Anlauf hervorginge, dass es jetzt zwei Sorten von Beiträgen gibt: neue in korrekterem XML und alte, unkorrigierre. Ohne dieses Dokument würde man vermutlich nur einen der neueren Beiträge betrachten und leichtfertig davon ausgehen, dass alle älteren dasselbe Format haben — ein Ausgangspunkt für neues Unglück.
Bevor Sie den Codeeditor öffnen, um mit dem Refactoring anzufangen, checken Sie bitte den Stand der Dinge in ein Versionskontrollsystem ein oder machen Sie wenigstens ein Backup. Das Unangenehmste, was Ihnen beim Refactoring passieren kann, ist, dass Sie überraschend den Stand von gestern wiederherstellen müssten, um einen plötzlich aufgetauchten, datenbankzerstörenden Fehler zu suchen, der bisher nicht bekannt war. Um diesen Fehler zu finden, müssten Sie auf Ihrem Rechner den Zustand des beim Kunden oder auf Ihrem Webserver laufenden Systems herstellen. Wenn Sie jetzt mit Ihren durch das Refactoring bedingten Änderungen noch nicht fertig sind und kein Versionskontroll-system haben, müssen Sie alle Ihre Änderungen verwerfen. Eine andere Gefahr besteht darin, sich beim Refactoring zu vergaloppieren und nicht mehr zurück zu können oder Fehler einzubauen. In all diesen Fällen ist es besser, die Refactoring-Änderungen zu verwerfen und auf den bewährten, wenn auch hässlichen Stand zurückzugehen. Als Saftwareentwickler befinden Sie sich - anders als z. B. auf einer Everest-Expedition - in einer beneidenswerten Lage, denn wenn Sie ein Versionskontrollsystem verwenden, dann gibt es keinen point of no return.
Drucken Sie den problematischen Code aus und legen Sie ihn mit Anmerkungen neben die Bestandsaufnahme der Probleme. Dieser Ausdruck ist eine Detailsicht des bestehenden Codes und ergänzt die Übersicht. Den Quellcodestand vor dem Refactoring neben der Tastatur liegen zu haben, kann als Gedächtnisstütze sehr hilfreich sein. Außerdem hilft es gegen Prokrastinationsanfälle, die damit beginnen, dass man aus dem Versionskontrollsystem die alte Version ziehen will und nach einer Stunde das Reddit-Tab wieder schließt. (Und wenn Sie alle unsere guten Backupratschläge ignoriert haben, werden Sie möglicherweise schon in wenigen Stunden dankbar dafür sein, dass wenigstens noch ein Papierausdruck Ihres ursprünglichen Codes existiert.)
Prüfen Sie dann, ob Sie wirklich verstanden haben, warum der Code existiert und was er macht. Wenn Sie sich nicht sicher sind, ob Sie Zweck und Funktionsweise des Codes verstanden haben, fragen Sie jemanden, wenn es möglich ist. Erklären Sie ihm, was der Code Ihrer Meinung nach macht und was daran verbesserungswürdig wäre. Ist gerade niemand zur Hand, erklären Sie den Code Ihrem Gummientchen (siehe dazu den Abschnitt »Wenn sonst nichts hilft« in Kapitel 13).
Refakturieren sollten Sie immer von oben nach unten: erst dieÄnderungen an der groben Architektur und dann die Feinheiten, sonst verlieren Sie sich in Details, erreichen aber nicht viel. Wenn Sie also beispielsweise feststellen, dass Ihre Benutzerverwaltung inzwischen auch Aufgaben wie Bestellungen und Zahlungsabwicklung übernimmt, die in einem eigenen Modul besser aufgehoben wären, dann sollten Sie zunächst diese Funktionen abspalten und sich erst dann Gedanken machen, ob das neue Finanzmodul jetzt eigene Fenster, Menüs und Datenbankinstanzen bekommt. Aber Vorsicht, »Änderung an der Architektur« bedeutet nicht, dass Sie das ganze Softwaregebilde mit der Planierraupe einebnen. Wenn Sie sich bei solchen Plänen ertappen, entfernen Sie sich vom Rechner, atmen Sie tief durch und denken Sie an Ameisen beim Herumtragen einzelner Fichtenna-dein. Es gibt für alle Probleme eine Refactoring-Lösung, die aus kleinen, in sich abgeschlossenen Schritten besteht. Auch für Ihres.
Refactoring sollte möglichst nur die interne Struktur eines Codemoduls betreffen. Nach außen, also in Richtung des Codes, der das zu refakturierende Modul verwendet, sollte sich möglichst wenig ändern. Es gibt natürlich die Ausnahme, dass die Schnittstelle eines Moduls unglücklich spezifiziert wurde oder erweitert werden muss. Ein Beispiel dafür wäre eine Schnittstelle, die einen Benutzernamen als Parameter übergibt und erwartet -und dann merkt man, dass man eigentlich lieber eine Benutzer-ID übergeben will, weil nur die eindeutig sind. In diesem Fall umfasst das Refactoring auch den Code, der das Modul verwendet.
Obwohl solche Änderungen im Code im Idealfall nur klein sind, ziehen sie nicht weniger Fehler nach sich als große. Die Wahrscheinlichkeit, dass nach einer Änderung ein Fehler auftritt, steigt zwischen »eine Zeile geändert« und »fünf Zeilen geändert« steil auf 80% an und sinkt dann bei 20 Zeilen wieder auf 40%.3 Das liegt vor allem daran, dass kleine Änderungen banal wirken und deshalb nicht ernst genug genommen werden.
Testen Sie deshalb nach jedem Schritt, ob alles noch geht. Ohne Unit Tests (siehe Kapitel 16) werden Sie beim Ausprobieren vermutlich irgendwelche Sonderfälle übersehen. Die beim Refactoring erzeugten Fehler machen sich dann erst im Laufe der nächsten Tage oder Wochen bemerkbar.
Wenn Sie kein Versionskontrollsystem einsetzen, die Folgen der so entstehenden neuen Probleme aber trotzdem beherrschbar halten möchten, können Sie die alte Version einer Funktion stehenlassen, ihren Inhalt auskommentieren und die alte Funktion nur noch dazu benutzen, die neue aufzurufen. Falls es Ärger gibt, ist der Weg zurück zur alten Lösung nicht ganz verstellt. Der Nachteil: Mit einer gewissen Wahrscheinlichkeit wird die alte Funktion nach dem Eintreten der Projektdeadline oder mit dem Nachlassen Ihres Interesses für immer und ewig stehenbleiben und künftige Leser verwirren.
Genau dieser Logik verdanken wir folgendes Codebeispiel in C, das Johannes in den frühen 90ern schrieb, Mitte der 90er für eine neue Programmierplattform erweiterte und in den späten 90ern durch den Aufruf einer einzigen Library-Funktion ersetzte. Es ist der etwas stümperhafte Vergleich zweier Strings. Weil es ihm unheimlich war, den alten Code zu löschen, kommentierte er ihn zunächst mal aus - und 15 Jahre später ist dieser fossilisierte Zustand in Bernstein hervorragend erhalten. Man kann - bei starker Vergrößerung - sogar noch die Code-Unreinheiten erkennen.
int compare_strings (long firstData, long secondData) {
I* #if TARGET_API_MAC_CARBON // die Erweiterung für die neue Programmierplattform p2cstrcpy ((char*) &strl, ((MWNickPtr)firstData)->nick); p2cstrcpy ((char*) &str2, ((MWNickPtr)secondData)->nick);
#else II der Urzustand vom Beginn der 90er
BlockMove (((MWNickPtr)firstData)->nick , &strl, 255);
p2cstr (str); BlockMove (((MWNickPtr)secondData)->nick , &str2, 255);
p2cstr (str);
#endif
result = strcmp((char*)&str1, (char*)&str2);
*I
result = RelString (((MWNickPtr)firstData)->nick, ((MWNickPtr)secondData)->nick, false, false); II die aktuelle Version return result;
Prinzipiell können Sie mit einem Texteditor und Suchen und Ersetzen refakturieren. Wir empfehlen Ihnen aber, sich Unterstützung durch Software zu holen, wo immer es möglich ist. Entwicklungsumgebungen wie Eclipse erlauben es, einige der unten beschriebenen Arbeitsgänge teilweise zu automatisieren. Die Software kann Ihnen zwar nicht sagen, was Sie verbessern sollen, greift Ihnen aber beim Wie unter die Arme: Sie können mit einem Klick bestimmen, dass eine bestimmte Funktion in eine andere Datei oder Klasse verlegt werden soll, die Software erledigt dann den Umzug und die nötigen Umbenennungen (siehe Abbildung 15-1).
2i DesanitiaeNamoServle about_dialog.xmi J BrowsePicture.java
pockoge nct. krautchan.android.activity;
• i-ro'lirt )ovo l.o.Buffer'
oubltc dass Eisenhex public stotic ftn privote Alert.Diol
Wvert d i public void onCre super.onCreot: sctContentVie
Button tesl8u testButton. se
Jr do
Reven Fi le Sm
Open Oeclaration Open Type Hierarchy Open Call Hierarchy Show in Breadcrumb Quick Outline Quick Type Hierarchy Show In
F3
\:XB
XO
XT
•st); ><« {
publi
Copi
Copy Qualified Name Paste
intent.s oct.ion PlCIC_OlR[CTORY");
XV io'-d’));
•o TITLE", "Please se.eet a foldcn"); Xl *o.BUTTON_TEXP, "Use this folder“);
Quick Fix Source
References
Declarations
});
Button goKC8u ■YgoKCButton* goKCButton se public vo Eisen
}
}).
'7 sbcwBooktnae Button sho..Ab
/7s.ht>«About8iJ
showAboutButt
I2_ iPätgfeftK-JS ■ @ üvadoc Ded Android
Add to Snippets..
Find Bugs Run As Debug As Profile As Validate Team
Compare With Replace With Coogle WtkiText
Extract Interface...
Extract Superclass...
Use Supertype Where Possible...
Pull Up...
Push Down...
Extract Class...
lntroduce Parameter Object... lntroduce lndirection... lnfer Generic Type Arguments...
Abbildung 15-1: Das Refactoring-Menü von Eclipse
Ähnliche Refactoring-Hilfen gibt es für diverse Sprachen und Entwicklungsumgebungen. Im Wikipedia-Eintrag »Code Refactoring<< (en.wikipedia.org/wiki/Code_refactoring) sind die aktuell verfügbaren Tools aufgelistet.
Ein guter erster Schritt ist es, die Furcht vor dem großen Klumpen überarbeitungswürdigen Codes zu lindern. Lagern Sie abschreckende Codeteile, die Ihnen schon immer Unglück beschert haben, in eigene Dateien aus. So lindern Sie Demotivation und Angst davor, den eigenen Code anzufassen.
In Objective-C und vielen anderen Sprachen gehört es zum allgemein anerkannten Standard, jede Klasse in eine Datei zu packen, in Java ist es sogar technisch schwierig, etwas anderes zu tun. In jeder anderen Sprache ist es eine gute Idee. Versuchen Sie, logische Trennlinien zwischen den Teilen des Codes zu finden. Es macht erst mal nichts, wenn diese Teile immer noch ziemlich groß sind, in diesem Stadium hilft jede Reduzierung.
Den Code auf einzelne Dateien zu verteilen, hat auch folgenden Vorteil: Solange alles in einer einzigen Datei steckt, ist man versucht, ganze Arbeitsgänge wie ein Kochrezept in einer einzigen Funktion abzuhandeln. Zergliedert man komplexe Workflows in einzelne Dateien, die jeweils nur noch für einen Arbeitsschritt zuständig sind, dann tut man sich leichter, diese Arbeitsschritte in kleine Funktionen aufzuteilen. Das ist der nächste Schritt.
In der Theorie ist alles ganz einfach: Egal, ob Sie objektorientiert, funktional oder proze-dural programmieren, jedes Softwaremodul4 sollte für eine Aufgabe zuständig sein - und umgekehrt sollte eine Aufgabe nicht über verschiedene Module verstreut werden. In einer wissenschaftlichen Auswertungssoftware zum Beispiel sollten Experimentdaten in einem Modul gelesen und in einem zweiten ausgewertet werden, und in einem dritten sollen die Ergebnisse in Dateien geschrieben werden.
Im Zuge der Programmierung wird es jedoch häufig komplizierter: So brauchen Sie für die Auswertung nicht nur die Experimentdaten aus Ihrer Datenbank, sondern auch noch Kalibrierungsparameter, die aus einer Datei stammen, also müssen Sie auch Funktionen schreiben, die diese Parametrisierungsdateien lesen. Die Funktionen, die die Ergebnisse in Dateien schreiben, müssen diese aber noch nach XML übersetzen, damit andere Software etwas mit den Ergebnissen anfangen kann - und nach Ende der Auswertung soll auch noch eine Mail an den Auswerter verschickt werden. Was zunächst einfach erscheint, kann sich daher schnell zu einem System entwickeln, in dem Datenbankabfragen und Dateioperationen nebeneinander stehen und in dem ein Modul für Formatierung, Ausgabe und das Senden der Erfolgsmail zuständig ist. Das ist zwar tolerierbar, macht das System aber unübersichtlich - und sorgt dafür, dass Sie mit etwas zeitlichem Abstand ständig suchen müssen, wo was genau implementiert wurde.
Daher ist es sinnvoll, aus den drei bisherigen Modulen fünf zu machen.
Vorher:
• Lesemodul
• Auswertungsmodul
• Schreibmodul
Nachher:
• Datenbankmodul
• Dateimodul (hierhin wandern die Leseoperationen aus dem ersten und die Schreiboperationen aus dem letzten Modul)
• Auswertungsmodul
• XML-Formatierer
• Mailmodul
Nach diesem Umbau ist jedes Modul kleiner und fokussierter. Schon der Name kann jetzt ziemlich präzise darüber Auskunft geben, was das Modul macht. Das Hauptprogramm ist nur noch für die Orchestrierung zuständig, indem es aus den einzelnen Modulen in der richtigen Reihenfolge Funktionen aufruft.
Allgemeiner formuliert: Zerlegen Sie zu große Codemodale oder Objekte in handlichere Einheiten, die nur noch einen Aspekt abdecken. Schreiben Sie zu diesem Zweck auf, wofür jedes Modul da ist, und überlegen Sie, ob es sich dabei um eine Aufgabe handelt oder um mehrere. Legen Sie dann neue Module an, auf die Sie den Code verteilen, damit ähnliche Funktionen zusammenkommen und Dinge, die Unverwandtes erledigen, getrennt sind.
Wasist »zu groß<<?
Es gibt keine einfache Faustregel, die besagt, wie viele Zeilen man braucht, um eine Idee auszudrücken. Als Leser dieses Buchs können Sie provisorisch davon ausgehen, dass Ihre Funktionen, Objekte oder Klassen eher zu groß als zu klein sind.
Fast immer, wenn man lange Codeblöcke schreibt, finden sich darin Bereiche, die mehrfach vorkommen - vielleicht nicht unbedingt alle auf einem Fleck, aber doch über das
Programm verstreut. Solche Wiederholungen kann man vermeiden, indem man den Code in Funktionen auslagert und nur noch diese Funktionen aufruft.
Wenn Sie Code vorfinden, der durch zahlreiche überschriftenartige Kommentare in einzelne Schritte gegliedert ist, ist ein Teil Ihrer Arbeit bereits getan. Die Überschriften sagen Ihnen wahrscheinlich sogar, wie die neuen Funktionen heißen könnten.
Übersetzt die Funktion, die Daten aus der Datenbank liest, diese Daten gleich nach HTML? Dann ist es meist sinnvoll, die HTML-Transformation in eine Funktion auszulagern. Mehr dazu im nächsten Abschnitt, »Nebenwirkungen entfernen«.
Funktionen dienen der Strukturierung des Codes, und Strukturierung ist mindestens genauso wichtig wie Wiederverwendbarkeit - einfach, weil sie das Lesen so viel leichter macht. Führen Sie Funktionen auch dort ein, wo etwas ganz Spezielles, nirgendwo Wiederverwendbares geschieht. Später wird sich außerdem oft herausstellen, dass das, was man beim Schreiben für garantiert nicht wiederverwendbar hielt, doch auch an anderen Stellen nützlich sein kann.
Eine Entwicklungsumgebung (siehe Kapitel 20) kann diese Aufgabe erleichtern. Viele Entwicklungsumgebungen bringen eine »Extract Method«-Funktion mit, die einen markierten Codebereich automatisch mitsamt allen benötigten Parametern in eine separate Methode befördert und vielleicht sogar Vorschläge dazu macht, welche anderen Codebereiche sich ebenfalls durch diese Methode ersetzen lassen würden.
Dieses Isolieren von einzelnen Aufgaben in kurze, abgeschlossene Einheiten ist ein wichtiger Schritt, der die Grundlage für weiteres Refactoring darstellt, denn Sie können Ihren Code nur dann sinnvoll neu zusammenstellen, wenn er nicht in endlos langen Funktionen aufbewahrt wird. Auch Codeduplikationen lassen sich leichter finden, wenn der Code nicht irgendwo in einer tiefen Einrückungsebene versteckt ist.
Machen Sie sich jedoch bewusst, dass die schiere Größe eines Moduls noch nichts darüber aussagt, ob es hinreichend fokussiert ist. Einfache Aufgaben sind normalerweise mit wenig Code zu erledigen, komplexe Aufgaben benötigen hingegen viele einzelne Anweisungen. Insbesondere Programme, die ein grafisches Benutzerinterface mitbringen, haben häufig recht große Module für diese GUI-Aufgaben. Der Code für die interne Datenverarbeitung mag viel anspruchsvoller sein, beansprucht letztendlich jedoch nur einen Bruchteil der Codezeilen des grafischen Frontends.
Schrecken Sie daher nicht davor zurück, Module zu schreiben, die deutlich größer sind als andere. Ein gut durchdachtes Modul verkörpert eine ganz bestimmte ldee - nicht mehr, aber auch nicht weniger.
Es kostet etwas Überwindung, Code so zu strukturieren, dass er aus kurzen Funktionen besteht, von denen jede nur eine Sache erledigt. Typischerweise schreibt man gerade als Anfänger Code so runter, wie man den Programmablauf plant, und führt eher beiläufig hier und da mal eine Funktion ein, in die man Teile auslagert.
Wenn eine Funktion analyzeExperimentData() Experimentdaten auswertet und auch gleich noch in eine Datei chreibt, dann ist das Speichern ein Nebeneffekt. Nebeneffekte sind problematisch, weil sie das Verständnis des Codes erschweren und ihn weniger gut wartbar machen. Man erkennt solche Funktionen mit Nebeneffekten gelegentlich an Kommentaren wie »Achtung, diese Funktion macht AUSSERDEM noch ...<< Eine Funktion ohne Nebeneffekte kann man so oft aufrufen, wie man will, und man kann den Aufruf der Funktion bei Bedarf an eine andere Stelle verschieben. Insbesondere Funktionen, die einen Wert zurückliefern, sollten das nebenwirkungsfrei erledigen.
Aufgrund des Funktionsnamens weiß man in unserem Beispiel, was die eigentliche Aufgabe der Funktion sein soll. Erledigt die Funktion scheinbar bequem das Speichern gleich mit, dann muss man sie umbauen, sobald man die Ergebnisse lieber in einer Datenbank hätte. Oder man kopiert sie in eine zweite Funktion und ersetzt den Teil, der die Ergebnisse in eine Datei schreibt, durch einen, der sie in einer Datenbank speichert. Jetzt hat man zwei sehr ähnliche Funktionen mit hoher Verwechslungsgefahr.
Zerlegen Sie den Code in kurze Funktionen, die wirklich nur die notwendigen Berechnungen erledigen und die Ergebnisse zurückliefern. Wenn Sie eine Datenauswertungsfunktion schreiben wollen, sollte diese die Daten an den Code zurückliefern, der sie aufgerufen hat. Dieser Code ruft dann eine Funktion auf, die die Ergebnisse in eine Datei (oder eine Datenbank) speichert. Es genügt nicht, das Speichern in eine Funktion auszulagern, die Sie direkt aus der Auswertefunktion aufrufen. Das verschiebt das Problem nur, und der Nebeneffekt passiert dann eben indirekt.
Generell sollte jedes Modul des Codes, egal ob Klasse oder Funktion, nur eine einzige Aufgabe haben. Ein Beispiel für das Unglück, das aus einer Überladung von Beitrags-lDs mit Zusatzfunktionen resultiert, tauchte beim Schreiben dieses Buchs auf: Im zu diesem Zeitpunkt noch recht neuen Editor Stypi, einem Onlinetool zum gemeinsamen und gleichzeitigen Bearbeiten von Texten, werden die Beiträge unterschiedlicher Autoren in unterschiedlichen Farben markiert, jedenfalls in der Theorie. In der Praxis blieb der Text verwirrenderweise einfarbig. Nach einigem Kopfkratzen und mit den Entwicklern gewechselten Mails stellte sich Folgendes heraus: Jeder Nutzer bekommt automatisch eine von nur 16 Farben zugewiesen. Diese Farbe leitet sich durch eine Modulo-16-Divi-sion aus der Nutzer-ID ab und lässt sich demzufolge auch nicht ändern. Die einzige Lösung bestand darin, so lange immer neue Accounts anzulegen, bis schließlich alle Beteiligten unterschiedlich eingefärbt waren.
Kürzerer Code enthält weniger Fehler als längerer, und das nichi nur, weil es darin einfach weniger Platz gibt; siehe blog. vivekhaUla r.com/post/1 06696782 92/size-is-the-best-predictor-of-code-qualUy.
Wannsollte man refakturieren? | 183
Diese Zahlen entnehmen wir dem Standardwerk »Code Complete« von Steve McConnell (Microsoft Press 2004, deutsche Ausgabe bei Microsoft Press Deutschland 2005).
Mit »Modul« meinen wir hier etwa eine Klasse in der objektorientierten Programmierung oder eine Funktion in der prozeduralen.