Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über <http://dnb.d-nb.de> abrufbar.
ISBN 978-3-95845-903-8
1. Auflage 2011
www.mitp.de
E-Mail: mitp-verlag@sigloch.de
Telefon: +49 7953 / 7189 - 079
Telefax: +49 7953 / 7189 - 082
© 2011 mitp-Verlags GmbH & Co. KG, Frechen
Dieses Werk, einschließlich aller seiner Teile, ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlages unzulässig und strafbar. Dies gilt insbesondere für Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann benutzt werden dürften.
Übersetzung: Reinhard Engel
Lektorat: Sabine Schulz
Sprachkorrektorat: Petra Heubach-Erdmann
Coverbild: © Lux2008 – fotolia.de
Datenkonvertierung: CPI books GmbH, Leck
Dieses Ebook verwendet das EPUB-Format und ist optimiert für die Nutzung mit dem iBooks-Reader auf dem iPad von Apple. Bei der Verwendung von anderen Readern kann es zu Darstellungsproblemen kommen.
Der Verlag räumt Ihnen mit dem Kauf des E-Books das Recht ein, die Inhalte im Rahmen des geltenden Urheberrechts zu nutzen. Dieses Werk einschließlich seiner Teile ist urheberrechtlich geschützt. Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist ohne Zustimmung des Verlags unzulässig und strafbar. Dies gilt insbesondere für Vervielfältigungen, Mikroverfilmungen und Einspeicherung und Verarbeitung in elektronischen Systemen.
Der Verlag schützt seine E-Books vor Missbrauch des Urheberrechts durch ein digitales Rechtemanagement. Bei Kauf im Webshop des Verlages werden die E-Books mit einem nicht sichtbaren digitalen Wasserzeichen individuell pro Nutzer signiert. Bei Kauf in anderen Ebook-Webshops erfolgt die Signatur durch die Shopbetreiber. Angaben zu diesem DRM finden Sie auf den Seiten der jeweiligen Anbieter.
Für Ann, Deborah und Ryan,
die strahlenden Zentren meines Lebens.
– Michael
Vorwort
Geleitwort
Danksagungen
Einführung – Wie man dieses Buch lesen sollte
Teil I Wie Wandel funktioniert
1 Software ändern
1.1 Vier Gründe, Software zu ändern
1.2 Riskante Änderungen
2 Mit Feedback arbeiten
2.1 Was sind Unit-Tests?
2.2 Higher-Level-Tests
2.3 Testabdeckung
2.4 Der Algorithmus zur Änderung von Legacy Code
3 Überwachung und Trennung
3.1 Kollaborateure simulieren
4 Das Seam-Modell
4.1 Ein riesiges Blatt mit Text
4.2 Seams
4.3 Seam-Arten
5 Tools
5.1 Automatisierte Refactoring-Tools
5.2 Mock-Objekte
5.3 Unit-Test-Harnische
5.4 Allgemeine Test-Harnische
Teil II Software ändern
6 Ich habe nicht viel Zeit und ich muss den Code ändern
6.1 Sprout Method
6.2 Sprout Class
6.3 Wrap Method
6.4 Wrap Class
6.5 Zusammenfassung
7 Änderungen brauchen eine Ewigkeit
7.1 Verständlichkeit
7.2 Verzögerungszeit
7.3 Dependencies aufheben
7.4 Zusammenfassung
8 Wie füge ich eine Funktion hinzu?
8.1 Test-Driven Development (TDD)
8.2 Programming by Difference
8.3 Zusammenfassung
9 Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
9.1 Der Fall des irritierenden Parameters
9.2 Der Fall der verborgenen Dependency
9.3 Der Fall der verketteten Konstruktionen
9.4 Der Fall der irritierenden globalen Dependency
9.5 Der Fall der schrecklichen Include-Dependencies
9.6 Der Fall der Zwiebel-Parameter
9.7 Der Fall des Alias-Parameters
10 Ich kann diese Methode nicht in einem Test-Harnisch ausführen
10.1 Der Fall der verborgenen Methode
10.2 Der Fall der »hilfreichen« Sprachfunktion
10.3 Der Fall des nicht erkennbaren Nebeneffekts
11 Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
11.1 Effekte analysieren
11.2 Vorwärtsanalyse (Reasoning Forward)
11.3 Effektfortpflanzung (Effect Propagation)
11.4 Tools für Effektanalysen
11.5 Von der Effektanalyse lernen
11.6 Effektskizzen vereinfachen
12 Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?
12.1 Abfangpunkte
12.2 Ein Design mit Einschnürpunkten beurteilen
12.3 Fallen bei Einschnürpunkten
13 Ich muss etwas ändern, weiß aber nicht, welche Tests ich schreiben soll
13.1 Charakterisierungs-Tests
13.2 Klassen charakterisieren
13.3 Gezielt testen
13.4 Eine Heuristik für das Schreiben von Charakterisierungs-Tests
14 Dependencies von Bibliotheken bringen mich um
15 Meine Anwendung besteht nur aus API-Aufrufen
16 Ich verstehe den Code nicht gut genug, um ihn zu ändern
16.1 Notizen/Skizzen
16.2 Listing Markup
16.3 Scratch Refactoring
16.4 Ungenutzten Code löschen
17 Meine Anwendung hat keine Struktur
17.1 Die Geschichte des Systems erzählen
17.2 Naked CRC
17.3 Conversation Scrutiny
18 Der Test-Code ist im Weg
18.1 Konventionen für Klassennamen
18.2 Der Speicherort für Tests
19 Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
19.1 Ein einfacher Fall
19.2 Ein schwieriger Fall
19.3 Neues Verhalten hinzufügen
19.4 Die Objektorientierung nutzen
19.5 Es ist alles objektorientiert
20 Diese Klasse ist zu groß und soll nicht noch größer werden
20.1 Aufgaben erkennen
20.2 Andere Techniken
20.3 Die nächsten Schritte
20.4 Nach dem Extrahieren von Klassen
21 Ich ändere im ganzen System denselben Code
21.1 Erste Schritte
22 Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
22.1 Spielarten von Monstern
22.2 Monster mit automatischer Refactoring-Unterstützung zähmen
22.3 Die Herausforderung des manuellen Refactorings
22.4 Strategie
23 Wie erkenne ich, dass ich nichts kaputtmache?
23.1 Hyperaware Editing
23.2 Single-Goal Editing
23.3 Preserve Signatures
23.4 Lean on the Compiler
24 Wir fühlen uns überwältigt. Es wird nicht besser
Teil III Techniken zur Aufhebung von Dependencies
25 Techniken zur Aufhebung von Dependencies
25.1 Adapt Parameter
25.2 Break Out Method Object
25.3 Definition Completion
25.4 Encapsulate Global References
25.5 Expose Static Method
25.6 Extract and Override Call
25.7 Extract and Override Factory Method
25.8 Extract and Override Getter
25.9 Extract Implementer
25.10 Extract Interface
25.11 Introduce Instance Delegator
25.12 Introduce Static Setter
25.13 Link Substitution
25.14 Parameterize Constructor
25.15 Parameterize Method
25.16 Primitivize Parameter
25.17 Pull Up Feature
25.18 Push Down Dependency
25.19 Replace Function with Function Pointer
25.20 Replace Global Reference with Getter
25.21 Subclass and Override Method
25.22 Supersede Instance Variable
25.23 Template Redefinition
25.24 Text Redefinition
A Refactoring
A.1 Extract Method
B Glossar
»… damit fing es an …«
In der Einführung zu diesem Buch verwendet Michael Feathers diesen Ausdruck, um den Beginn seiner Leidenschaft für Software zu beschreiben.
»… damit fing es an …«
Kennen Sie dieses Gefühl? Erinnern Sie sich an den einen Moment in Ihrem Leben, über den Sie sagen könnten: »… damit fing es an …«? Gab es einen einzigen Moment, der den Lauf Ihres Lebens änderte und schließlich dazu führte, dass Sie zu diesem Buch gegriffen haben und begannen, dieses Vorwort zu lesen?
Ich war in der sechsten Klasse, als ich einen solchen Moment erlebte. Ich interessierte mich für Naturwissenschaften, den Weltraum und alles Technische. Meine Mutter hatte in einem Katalog einen Plastikcomputer entdeckt und für mich bestellt. Er hieß Digi-Comp I. Vierzig Jahre später hat dieser kleine Plastikcomputer auf meinem Bücherregal einen Ehrenplatz. Er war der Katalysator, an dem sich meine lebenslange Leidenschaft für Software entzündete. Er vermittelte mir eine erste Ahnung davon, welche Freude das Schreiben von Programmen machen kann, die für Menschen Probleme lösen. Er bestand nur aus drei S-R-Flip-Flops und sechs Und-Gates aus Plastik, aber das reichte aus – er erfüllte seinen Zweck. Damit begann es … – für mich.
Aber meine Freude wurde bald getrübt, als ich erkannte, dass Softwaresysteme fast immer in einem Chaos enden. Was als kristallklares Design im Geist der Programmierer entstanden war, verrottete im Laufe der Zeit wie ein Stück verdorbenes Fleisch. Das hübsche kleine System, das wir im letzten Jahr erstellt haben, entwickelte sich im nächsten Jahr in einen schrecklichen Morast aus verschlungenen Funktionen und Variablen.
Warum passiert das? Warum verrotten Systeme? Warum können sie nicht sauber bleiben? Manchmal schieben wir die Schuld auf unsere Kunden. Manchmal beschuldigen wir sie, die Anforderungen zu ändern. Wir trösten uns mit dem Glauben, das Design wäre schon in Ordnung gewesen, wären die Kunden nur mit dem zufrieden gewesen, was sie ihrer Aussage nach brauchten. Der Kunde ist selber schuld, wenn er seine Anforderungen an uns ändert.
Na ja, falls Sie es noch nicht wissen: Anforderungen ändern sich. Designs, die nicht flexibel auf Änderungen der Anforderungen reagieren können, sind per se schlechte Designs. Kompetente Software-Entwickler wollen Designs erstellen, die Änderungen tolerieren.
Dieses Problem scheint unlösbar schwierig zu sein. Tatsächlich ist es so schwierig, dass fast jedes jemals produzierte System langsam und kräftezehrend verrottet. Diese Verrottung ist so weit verbreitet, dass wir für verrottete Programme einen besonderen Begriff geprägt haben: Legacy Code.
Legacy Code – ein Begriff, der Programmierer abstößt. Er ruft Bilder von einem unergründlichen Sumpf mit verschlungenem Wurzelwerk, Blutegeln in trüben Gewässern und sirrenden Stechmücken hervor. Es riecht nach Verfall, Moder, Schleim und Verwesung. Auch wenn unsere erste Freude am Programmieren überschäumend gewesen sein mag, reicht das Elend beim Umgang mit Legacy Code oft aus, um diese Begeisterung zu ersticken.
Viele haben versucht, Methoden zu entwickeln, um zu verhindern, dass aus Code überhaupt Legacy Code werden kann. Wir haben Bücher über Prinzipien, Patterns und Verfahren geschrieben, die Programmierern helfen können, ihre Systeme sauber zu halten. Aber Michael Feathers hat etwas erkannt, das vielen anderen entgangen ist. Vorbeugung ist nicht perfekt. Selbst das disziplinierteste Entwicklungsteam, das die besten Prinzipien und Patterns beherrscht und die besten Verfahren anwendet, produziert immer wieder einmal chaotische Systeme. Der Morast wächst immer noch. Es reicht nicht aus, Verrottung zu verhindern – Sie müssen diesen Prozess umkehren können.
Darum geht es in diesem Buch: die Umkehrung der Verrottung. Wie kann man aus einem verschlungenen, undurchschaubaren, verworrenen System langsam und allmählich Schritt für Schritt ein einfaches, sauber strukturiertes System mit einem makellosen Design machen? Wie kann man die Entropie umkehren?
Bevor Sie sich von Ihrer Begeisterung fortreißen lassen, möchte ich Sie warnen: Verrottung umzukehren, ist nicht leicht und braucht Zeit. Die Techniken, Patterns und Tools, die Feathers in diesem Buch präsentiert, sind wirksam, aber sie erfordern Arbeit, Zeit, Ausdauer und Sorgfalt. Dieses Buch ist keine Silberkugel. Es sagt Ihnen nicht, wie Sie den ganzen Mist, der sich in Ihren Systemen angesammelt hat, über Nacht beseitigen können, sondern beschreibt einen Satz von Disziplinen, Konzepten und Einstellungen, den Sie für den Rest Ihrer Karriere mit sich tragen werden und der Ihnen helfen wird, aus Systemen, die sich im Laufe der Zeit verschlechtern, Systeme zu machen, die sich im Laufe der Zeit verbessern.
Robert C. Martin
29. Juni 2004
Erinnern Sie sich noch an das erste Programm, das Sie geschrieben haben? Ich erinnere mich an meins. Es war ein kleines Grafikprogramm, das ich auf einem frühen PC geschrieben habe. Ich begann später als die meisten meiner Freunde mit dem Programmieren. Sicher, Computer waren mir von klein auf bekannt; und ich erinnere mich daran, wie nachhaltig mich ein Minicomputer beeindruckt hat, den ich in einem Büro sah. Aber jahrelang hatte ich keine Gelegenheit, mich an einen Computer zu setzen. Später in meiner Teenagerzeit kaufte einer meiner Freunde einige der ersten TRS-80s. Ich war interessiert, aber auch ein wenig besorgt. Ich wusste, dass ich dem Computer verfallen würde, sollte ich anfangen, mit ihm zu spielen. Er sah einfach zu verlockend aus. Ich weiß nicht, woher ich mich so gut kannte, aber ich hielt mich zurück. Später im College hatte ein Zimmergenosse einen Computer; und ich kaufte mir einen C-Compiler, um mir das Programmieren beizubringen. Damit fing es an. Ich blieb jede Nacht auf, um dieses und jenes auszuprobieren und den Quellcode des Emacs-Editors zu studieren, der dem Compiler beilag. Es machte süchtig, es war anspruchsvoll, und ich liebte es.
Ich hoffe, Sie haben ähnliche Erfahrungen gemacht und haben die reine Freude erlebt, Dinge auf einem Computer zum Laufen zu bringen. Fast jeder Programmierer, den ich frage, kennt dieses Gefühl. Diese Freude gehört zu den Motiven, die uns diese Arbeit haben wählen lassen; aber wohin ist sie im Alltag verschwunden?
Vor einigen Jahren rief ich abends nach erledigter Arbeit meinen Freund Erik Meade an. Ich wusste, dass Erik gerade einen Beratungsauftrag mit einem neuen Team angenommen hatte, und fragte ihn deshalb: »Wie läuft’s?« Er sagte: »Nicht zu glauben, die schreiben Legacy Code.« Dies war einer der wenigen Male in meinem Leben, bei denen mich eine Äußerung eines Kollegen wie ein unerwarteter Schlag erwischte. Ich fühlte ihn direkt in der Magengrube. Erik hatte das Gefühl punktgenau ausgedrückt, das mich oft beschleicht, wenn ich zum ersten Mal mit einem fremden Team zu tun habe. Seine Mitglieder bemühen sich redlich, aber letztlich schreiben viele Menschen einfach nur Legacy Code. Die Gründe? Vielleicht ist der Termindruck zu stark. Vielleicht wiegt die Last des überkommenden Codes zu schwer. Vielleicht ist einfach nur kein besserer Code vorhanden, mit dem sie ihre Anstrengungen vergleichen könnten.
Was ist Legacy Code? Ich habe diesen Terminus bis jetzt undefiniert verwendet. Betrachten wir die strenge Definition: Legacy Code ist Code, den wir von jemand anderem übernommen haben. Vielleicht hat unser Unternehmen Code von einem anderen Unternehmen übernommen; vielleicht sind die Mitarbeiter des ursprünglichen Teams zu anderen Projekten abgewandert. Legacy Code ist Code eines anderen. Aber im Programmiererjargon bedeutet der Terminus viel mehr als das. Der Terminus Legacy Code hat im Laufe der Zeit zusätzliche Bedeutungen und mehr Gewicht angenommen.
Was denken Sie, wenn Sie den Terminus Legacy Code hören? Denken Sie wie ich an eine verworrene, unverständliche Struktur, an Code, den Sie ändern müssen, den Sie aber nicht wirklich verstehen? Denken Sie an schlaflose Nächte, in denen Sie versuchen, Funktionen hinzuzufügen, die leicht hinzuzufügen sein sollten? Fühlen Sie sich entmutigt? Haben Sie den Eindruck, der Code sei den Mitgliedern Ihres Teams so über, dass ihnen alles egal ist und sie den Code am liebsten sterben sähen. Ein Teil von Ihnen fühlt sich sogar schlecht bei dem Gedanken, den Code zu verbessern. Er scheint Ihre Anstrengungen nicht zu verdienen. Diese Definition von Legacy Code hat nichts damit zu tun, wer ihn geschrieben hat. Die Qualität von Code kann durch viele Faktoren verschlechtert werden; und viele haben nichts damit zu tun, ob der Code von einem anderen Team geschrieben wurde.
In der Branche wird Legacy Code oft salopp zur Bezeichnung von Code verwendet, den man nicht versteht und der schwer zu ändern ist. Aber im Laufe der Jahre, in denen ich verschiedenen Teams geholfen habe, ernste Code-Probleme zu beseitigen, habe ich eine andere Definition entwickelt.
Für mich ist Legacy Code ganz einfach Code ohne Tests. Mit dieser Definition habe ich mir einigen Kummer eingehandelt. Was haben Tests damit zu tun, ob Code schlecht ist? Darauf hab ich eine unkomplizierte Antwort, die ich in diesem Buch immer wieder aus verschiedenen Blickwinkeln darstelle:
Code ohne Tests ist schlechter Code. Es spielt keine Rolle, wie gut er geschrieben ist; es spielt keine Rolle, wie schön oder objektorientiert oder gut eingekapselt er ist. Mit Tests können wir das Verhalten unseres Codes schnell und verifizierbar ändern. Ohne Tests wissen wir nicht wirklich, ob unser Code besser oder schlechter wird.
Vielleicht halten Sie das für streng. Was ist mit sauberem Code? Reicht es nicht aus, wenn eine Code-Basis sehr sauber und gut strukturiert ist? Bitte verstehen Sie mich nicht falsch. Ich liebe sauberen Code. Ich liebe ihn mehr als die meisten Menschen, die ich kenne; doch sauberer Code ist zwar gut, aber allein nicht gut genug. Teams gehen erhebliche Risiken ein, wenn sie große Änderungen ohne Tests durchführen wollen. Es ähnelt der Hochseilartistik ohne Sicherheitsnetz. Es erfordert unglaubliches Können und ein klares Verständnis, was bei jedem Schritt passieren wird. Die Auswirkung der Änderung einiger Variablen genau zu überschauen, ist oft mit der Gewissheit vergleichbar, dass Sie nach einem Salto von einem anderen Artisten an den Armen aufgefangen werden. Wenn Sie in einem Team an Code arbeiten, der derartig übersichtlich ist, sind Sie in einer besseren Position als die meisten Programmierer. Bei meiner Arbeit sind mir Teams mit einem solchen Code selten begegnet. Sie scheinen eine statistische Anomalie zu sein. Und wissen Sie was? Wenn sie nicht mit Testunterstützung arbeiten, brauchen sie für Code-Änderungen immer noch länger als Teams, die systematisch testen.
Es stimmt: Teams werden besser und schreiben von Anfang an klareren Code; aber es dauert sehr lange, bis älterer Code klarer wird. In vielen Fällen wird dieses Ziel nie ganz erreicht. Deshalb habe ich kein Problem damit, Legacy Code als Code ohne Tests zu definieren. Es ist eine brauchbare Arbeitsdefinition, die auf eine Lösung verweist.
Obwohl ich bis jetzt ausführlich auf Tests eingegangen bin, handelt dieses Buch nicht vom Testen. In diesem Buch geht es darum, eine beliebige Code-Basis erfolgssicher zu ändern. In den folgenden Kapiteln beschreibe ich Techniken, mit denen Sie Code verstehen, in eine Testumgebung integrieren, refaktorisieren und funktional erweitern können.
Sie werden beim Lesen dieses Buches bemerken, dass es hier nicht um »schönen« Code geht. Die Beispiele in diesem Buch sind konstruiert, weil ich mit Kunden unter einer Geheimhaltungsvereinbarung arbeite. Aber in vielen Beispielen bemühe ich mich, den »Geist« des Codes wiederzugeben, der mir im Feld begegnet ist. Ich will nicht behaupten, alle Beispiele wären repräsentativ. Sicher gibt es im Feld Oasen mit großartigem Code, aber, ganz ehrlich, es gibt dort auch Code-Basen, die viel schlechter als alles sind, was ich in diesem Buch als Beispiel verwenden kann. Abgesehen von der Vertraulichkeit konnte ich einfach keinen derartigen Code in dieses Buch einfügen, ohne Sie zu Tode zu langweilen und wichtige Punkte in einem Morast von Details zu versenken. Folglich sind viele Beispiele relativ kurz. Wenn Sie ein solches Beispiel sehen und denken: »Der hat ja keine Ahnung – meine Methoden sind viel länger und viel schlechter«, nehmen Sie bitte meinen zugehörigen Ratschlag für bare Münze und prüfen Sie seine Anwendbarkeit, auch wenn das Beispiel einfacher zu sein scheint.
Die hier vorgestellten Techniken sind mit erheblich umfangreicherem Code getestet worden. Die Beispiele sind nur wegen des Buchformats kürzer. Insbesondere können Sie Ellipsen (…) in einem Code-Fragment wie folgt interpretieren: »Fügen Sie hier 500 Zeilen mit hässlichem Code ein.« Ein Beispiel:
In diesem Buch geht es nicht nur nicht um »schönen« Code, sondern noch weniger um »schönes« Design. Gutes Design sollte zu den Zielen jedes Programmierers gehören; aber bei Legacy Code nähern wir uns diesem Ziel schrittweise. In einigen Kapiteln beschreibe ich Methoden, wie man eine vorhandene Code-Basis mit neuem Code erweitern und dabei gute Designprinzipien berücksichtigen kann. Sie können in eine Legacy-Code-Basis Bereiche mit qualitativ hochwertigem Code einführen, sollten aber nicht überrascht sein, wenn bei einigen Änderungen andere Teile des Codes etwas hässlicher werden. Diese Arbeit gleicht einem chirurgischen Eingriff. Wir müssen Einschnitte vornehmen, und wir müssen durch die Eingeweide gehen und gewisse ästhetische Überlegungen beiseitelassen. Könnten die Hauptorgane und Eingeweide dieses Patienten in besserer Verfassung sein? Ja. Vergessen wir deshalb das anstehende Problem, nähen ihn wieder zu und raten ihm zu einer besseren Ernährung und regelmäßigem Training? Dies könnten wir tun; doch hier und jetzt müssen wir den Patienten nehmen, wie er ist, die Mängel beseitigen und ihn gesünder machen. Vielleicht wird er nie um olympische Medaillen kämpfen, aber wir dürfen das »Beste« nicht zum Feind des »Besseren« machen. Die Code-Basis kann gesünder und leichter handhabbar werden. Wenn sich ein Patient ein wenig besser fühlt, ist oft der geeignete Zeitpunkt, ihn zu einem gesünderen Lebensstil zu führen. Genau dies möchten wir mit Legacy Code erreichen. Wir versuchen, die dringenden Probleme zu beheben und dann den Code schrittweise zu verbessern, indem wir Änderungen erleichtern. Wenn es uns gelingt, diese Vorgehensweise fest in einem Team zu etablieren, wird auch das Design besser.
Die hier beschriebenen Techniken habe ich im Laufe der Jahre entdeckt oder von Kollegen gelernt, als ich bei meiner Arbeit mit Kunden versuchte, die Kontrolle über widerspenstige Code-Basen zu gewinnen. Dieser Legacy-Code-Schwerpunkt bildete sich zufällig heraus. Meine Arbeit bei Object Mentor bestand anfangs hauptsächlich darin, Teams mit ernsten Problemen bei der Entwicklung ihrer Fähigkeiten und der Verbesserung ihrer Interaktionen so weit zu unterstützen, dass sie regelmäßig qualitativ hochwertigen Code abliefern konnten. Wir verwendeten oft Extreme-Programming-Verfahren, um den Teams zu helfen, ihre Arbeit zu kontrollieren, intensiv zusammenzuarbeiten und Ergebnisse zu liefern. Oft glaube ich, Extreme Programming (XP) ist weniger eine Methode der Software-Entwicklung, sondern eher eine Methode zur Bildung funktionierender Teams, die nebenbei auch noch im Abstand von zwei Wochen großartige Software abliefern.
Doch von Anfang an gab es ein Problem. Viele der ersten XP-Projekte waren »Greenfield«-Projekte. Meine Kunden verfügten über umfangreiche Code-Basen, und sie hatten Probleme. Sie brauchten eine Methode, um ihre Arbeit in den Griff zu bekommen und termingerecht abzuliefern. Im Laufe der Zeit stellte ich fest, dass ich mit meinen Kunden immer wieder dieselben Probleme behandelte. Dieser Eindruck verdichtete sich bei einer Arbeit mit einem Team der Finanzbranche.
Bevor ich dazukam, hatte man erkannt, dass Unit-Testing eine beeindruckende Sache war, aber die Tests, die man ausführte, testeten das komplette Szenarium, griffen wiederholt auf eine Datenbank zu und führten umfangreiche Code-Fragmente aus. Die Tests waren schwer zu schreiben, und das Team führte sie nicht oft aus, weil sie so lange liefen. Als ich mich mit dem Team zusammensetzte, um die Dependencies (Abhängigkeiten) aufzulösen und den Code in kleineren Einheiten zu testen, hatte ich ein schreckliches Déjà-vu-Gefühl. Es schien, dass ich diese Art von Arbeit mit jedem Team, das ich traf, erneut leisten musste, und es war eine Art von Arbeit, über die niemand wirklich gerne nachdenkt. Es handelte sich um eine Drecksarbeit, die man erledigt, wenn man die Kontrolle über seinen Code gewinnen will und weiß, was man tun muss. Damals beschloss ich, dass es sich wirklich lohnen würde, über die Methoden zur Lösung dieser Probleme nachzudenken und sie aufzuschreiben, um Teams bei der Verbesserung ihrer Code-Basis zu helfen.
Eine Anmerkung zu den Beispielen: Ich habe Beispiele in mehreren verschiedenen Programmiersprachen verwendet. Die meisten Beispiele sind in Java, C++ und C geschrieben. Ich habe Java ausgewählt, weil diese Sprache weit verbreitet ist, und ich habe C++ eingeschlossen, weil diese Sprache in einer Legacy-Umgebung einige besondere Herausforderungen präsentiert. Ich habe C ausgewählt, weil es viele Probleme in prozeduralem Legacy Code hervorhebt. Zusammen decken diese Sprachen einen großen Teil des Spektrums der Legacy-Code-Probleme ab. Doch auch wenn Sie mit anderen Sprachen arbeiten, sollten Sie sich die Beispiele anschauen. Viele der behandelten Techniken können auch in anderen Sprachen, wie etwa Delphi, Visual Basic, COBOL oder FORTRAN, verwendet werden.
Ich hoffe, dass Ihnen die Techniken in diesem Buch bei Ihrer Arbeit helfen und dazu beitragen, die Freude am Programmieren wiederzufinden. Programmieren kann eine sehr lohnenswerte und erfreuliche Arbeit sein. Wenn Sie dieses Gefühl bei Ihrer Alltagsarbeit nicht haben, hoffe ich, dass Ihnen die Techniken in diesem Buch helfen werden, dieses Gefühl zu entdecken und in Ihrem Team zu kultivieren.
Vor allem schulde ich meiner Frau, Ann, und meinen Kindern, Deborah und Ryan, einen tief empfundenen Dank. Ihre Liebe und ihre Unterstützung machten dieses Buch und die vorhergehende Zeit des Lernens möglich. Außerdem möchte ich »Uncle Bob« Martin, dem Chef und Gründer von Object Mentor, danken. Sein strenger pragmatischer Ansatz zu Entwicklung und Design und seine Trennung des Kritischen vom Belanglosen gaben mir vor etwa einem Jahrzehnt Halt, als ich in einer Woge unrealistischer Ratschläge zu ertrinken schien. Und danke, Bob, dass du mir die Gelegenheit verschafft hast, in den vergangenen fünf Jahren mehr Code zu sehen und mit mehr Menschen zu arbeiten, als ich jemals für möglich gehalten hätte.
Ich muss auch Kent Beck, Martin Fowler, Ron Jeffries und Ward Cunningham für ihre gelegentlichen Ratschläge und ihre Lehren über Teamarbeit, Design und Programmieren danken. Mein besonderer Dank richtet sich an alle Menschen, die die Entwürfe lasen. Die offiziellen Gutachter waren Sven Gorts, Robert C. Martin, Erik Meade und Bill Wake; die inoffiziellen Gutachter waren Dr. Robert Koss, James Grenning, Lowell Lindstrom, Micah Martin, Russ Rufer und die Silicon Valley Patterns Group sowie James Newkirk.
Dank auch an die Gutachter der allerersten Entwürfe, die ich ins Internet stellte. Ihr Feedback hat die Richtung dieses Buches erheblich beeinflusst, nachdem ich sein Format umstrukturiert hatte. Ich entschuldige mich im Voraus bei allen, die ich vielleicht ausgelassen haben. Die ersten Gutachter waren: Darren Hobbs, Martin Lippert, Keith Nicholas, Phlip Plumlee, C. Keith Ray, Robert Blum, Bill Burris, William Caputo, Brian Marick, Steve Freeman, David Putman, Emily Bache, Dave Astels, Russel Hill, Christian Sepulveda und Brian Christopher Robinson.
Dank auch an Joshua Kerievsky, der wesentliche Anmerkungen zu einem der ersten Entwürfe beitrug, und Jeff Langr, der meine ganzen Schreibprozesse mit seinen Ratschlägen und zeitnahen Kritiken begleitete.
Die Gutachter halfen mir, meinen Entwurf erheblich zu glätten; doch sollte das Buch noch Fehler enthalten, bin ich dafür verantwortlich.
Dank an Martin Fowler, Ralph Johnson, Bill Opdyke, Don Roberts und John Brant für ihre Arbeit über das Refactoring. Sie war mir eine Inspiration.
Besonderen Dank schulde ich auch Jay Packlick, Jacques Morel und Kelly Mower von Sabre Holdings und Graham Wright von Workshare Technology für Unterstützung und Feedback.
Besonderen Dank schulde ich auch Paul Petralia, Michelle Vincenti, Lori Lyons, Krista Hansing und dem Rest des Teams bei Prentice-Hall. Danke, Paul, für die Hilfe und Ermutigung, die dieser Erstautor brauchte.
Mein besonderer Dank gilt auch Gary und Joan Feathers, April Roberts, Dr. Raimund Ege, David Lopez de Quintana, Carlos Perez, Carlos M. Rodriguez und dem verstorbenen Dr. John C. Comfort für ihre Hilfe und Ermutigung im Laufe der vergangenen Jahre. Ich muss auch Brian Button für das Beispiel in Kapitel 21, Ich ändere im ganzen System denselben Code, danken. Er schrieb diesen Code in etwa einer Stunde, als wir zusammen einen Refactoring-Kursus entwickelten. Dieser Code ist heute eines meiner Lieblingsbeispiele in meinen Programmierkursen.
Besondere danke ich auch Jannick Top, dessen Instrumentalstück De Futura mich als Soundtrack während meiner letzten Wochen bei der Arbeit an diesem Buch begleitete.
Schließlich möchte ich allen danken, mit denen ich im Laufe der letzten Jahre zusammengearbeitet habe und deren Einsichten und Herausforderungen das Material in diesem Buch verbessert haben.
Michael Feathers
mfeathers@objectmentor.com
www.objectmentor.com
www.michaelfeathers.com
Ich habe verschiedene Formate ausprobiert, bevor ich mich für das gegenwärtige Format dieses Buches entschied. Viele der verschiedenen Techniken und Verfahren, die beim Arbeiten mit Legacy Code nützlich sind, lassen sich isoliert nur schwer erklären. Die einfachsten Änderungen sind oft einfacher, wenn Sie Andockpunkte finden, Objekte simulieren und Dependencies mit einschlägigen Techniken aufheben. Schließlich kam ich zu dem Schluss, der Hauptinhalt des Buches (Teil II, Software ändern) ließe sich am besten durch die FAQ-Methode im FAQ-Format (FAQ = Frequently Asked Questions; häufig gestellte Fragen) erschließen. Weil besondere Techniken oft den Einsatz anderer Techniken erfordern, sind die FAQ-Kapitel stark verknüpft. Fast jedes Kapitel referenziert andere Kapitel und Abschnitte, in denen besondere Techniken und Refactorings beschrieben werden. Ich möchte mich entschuldigen, wenn Sie deshalb wild in diesem Buch hin und her blättern müssen, um Antworten auf Ihre Fragen zu finden; aber ich bin davon ausgegangen, dass Sie lieber blättern als das Buch von Deckel zu Deckel durchlesen würden, um die Arbeitsweise aller Techniken zu verstehen.
In Software ändern habe ich versucht, häufig gestellte Fragen zu beantworten, die bei der Legacy-Code-Arbeit auftauchen. Jedes Kapitel ist nach einem besonderen Problem benannt. Dadurch werden die Kapitelüberschriften ziemlich lang; aber hoffentlich können Sie so schnell einen Abschnitt finden, der Ihnen hilft, Ihr besonderes Problem zu lösen.
Software ändern wird von einem Satz einführender Kapitel (Teil I, Wie Wandel funktioniert) und einem Katalog von Refactorings eingerahmt, die bei Legacy-Code-Arbeit sehr nützlich sind (Teil III, Techniken zur Aufhebung von Dependencies). Bitte lesen Sie das einführende Kapitel, insbesondere Kapitel 4, Das Seam-Model. Diese Kapitel liefern den Kontext und die Nomenklatur für alle folgenden Techniken. Zusätzlich sollten Sie Termini, die nicht im Kontext beschrieben werden, im Glossar nachschlagen.
Die Refactorings in Techniken zur Aufhebung von Dependencies sind etwas Besonderes, da sie ohne Tests angewendet werden sollen. Sie dienen der Einrichtung von Tests. Ich rate Ihnen, jede einzelne Technik durchzulesen, damit Sie mehr Möglichkeiten kennen lernen, um Ihren Legacy Code zu zähmen.
In diesem Teil:
Kapitel 1
Software ändern
Kapitel 2
Mit Feedback arbeiten
Kapitel 3
Überwachung und Trennung
Kapitel 4
Das Seam-Modell
Kapitel 5
Tools
Code zu ändern, ist etwas Großartiges. Wir verdienen damit unseren Lebensunterhalt. Aber es gibt Methoden, Code zu ändern, die das Leben erschweren, und es gibt Methoden, die es erheblich erleichtern. In der Branche wurde nicht viel darüber geredet. Der Sache am nächsten kommt noch die Literatur über Refactoring. Ich glaube, wir sollten die Diskussion etwas breiter anlegen und überlegen, wie wir in den schlimmsten Situationen mit Code umgehen sollten. Zu diesem Zweck müssen wir uns zunächst näher mit der Mechanik von Änderungen befassen.
Der Einfachheit halber möchte ich vier Hauptgründe unterscheiden, Software zu ändern:
1. Eine Funktion hinzufügen
2. Einen Fehler beseitigen
3. Das Design verbessern
4. Die Nutzung von Ressourcen optimieren
Eine Funktion hinzuzufügen, scheint mir die unkomplizierteste Art von Änderungen zu sein. Die Software zeigt ein Verhalten, und der Anwender erwartet von dem System auch noch ein anderes Verhalten.
Angenommen, wir arbeiteten an einer Webanwendung und ein Manager teilte uns mit, das Unternehmenslogo solle nicht auf der linken, sondern auf der rechten Seite stehen. Wir sprechen mit ihm darüber und stellen fest, dass dies nicht ganz so einfach ist. Er möchte das Logo verschieben, aber erwartet auch andere Änderungen. Es soll beim nächsten Release animiert werden. Gehört dies in die Kategorie »Fehler beseitigen« oder »Neue Funktion hinzufügen«? Das hängt von Ihrem Standpunkt ab. Aus der Sicht des Kunden handelt es sich definitiv um die Beseitigung eines Problems. Vielleicht hat er die Webseite gesehen und ein Meeting mit Mitarbeitern seiner Abteilung veranstaltet; und sie haben beschlossen, das Logo an eine andere Stelle zu setzen und ein wenig mehr Funktionalität zu fordern. Auf der Sicht eines Entwicklers kann die Änderung als vollkommen neue Funktion eingestuft werden. »Wenn die Abteilung einfach aufhören würde, ständig ihre Meinung zu ändern, wären wir jetzt fertig.« Aber in einigen Unternehmen wird eine Verschiebung eines Logos einfach als Beseitigung eines Fehlers gesehen, ungeachtet der Tatsache, dass das Team dafür umfangreiche neue Arbeit leisten muss.
Man könnte dies leicht als subjektive Einschätzung abtun. Für Sie ist dies ein zu beseitigender Fehler, für mich eine neue Funktion, also was? Leider müssen in vielen Unternehmen Korrekturen von Fehlern und Erweiterungen um neue Funktionen unterschiedlich überwacht und abgerechnet werden, weil es auch noch Verträge, Garantien oder Qualitätsinitiativen gibt. Zwar können wir endlos darüber diskutieren, ob wir Funktionen hinzufügen oder Fehler beheben, aber letztlich müssen Code und andere Artefakte geändert werden. Dieser Streit auf semantischer Ebene, was die Tätigkeit denn nun letztlich sei, maskiert etwas für uns technisch viel Wichtigeres: die Verhaltensänderung eines Systems. Und dabei bedeutet es einen großen Unterschied, ob neues Verhalten hinzugefügt oder altes geändert wird.
Verhalten ist der wichtigste Aspekt von Software. Es ist der Grund, warum Anwender Software verwenden. Anwender lieben es, wenn wir Verhalten hinzufügen (vorausgesetzt, es leistet, was sie wirklich wollten), aber wenn wir Verhalten ändern oder entfernen, das sie benötigen (Fehler einführen), verlieren wir ihr Vertrauen.
Fügen wir in unserem Unternehmenslogo-Beispiel Verhalten hinzu? Ja; denn nach der Änderung wird das System ein Logo auf der rechten Seite anzeigen. Beseitigen wir Verhalten? Ja; denn es gibt kein Logo mehr auf der linken Seite.
Betrachten wir einen schwierigeren Fall. Angenommen, ein Kunde wolle ein Logo rechts auf einer Webseite anzeigen, aber es gäbe kein Logo auf der linken Seite, mit dem wir anfangen könnten. Ja; wir fügen Verhalten hinzu; aber entfernen wir auch Verhalten? Wurde an der Stelle, an der das Logo erscheinen soll, irgendetwas anderes angezeigt?
Ändern wir Verhalten, fügen wir Verhalten hinzu, oder beides?
Wir können eine Unterscheidung treffen, die für uns als Programmierer nützlicher ist. Wenn wir Code modifizieren müssen (und HTML zählt in diesem Fall als Code), könnten wir Verhalten ändern. Wenn wir nur Code hinzufügen und ihn aufrufen, fügen wir oft Verhalten hinzu. Betrachten wir ein anderes Beispiel, eine Methode einer Java-Klasse:
Die Klasse enthält eine Methode, mit der wir Track-Listings (etwa mit den Songs einer CD) hinzufügen können. Fügen wir eine Methode hinzu, mit der wir Track-Listings ersetzen können:
Haben wir mit dieser Methode neues Verhalten zu unserer Anwendung hinzugefügt oder haben wir Verhalten geändert? Weder noch. Eine Methode hinzuzufügen, ändert Verhalten erst, wenn die Methode irgendwie aufgerufen wird.
Ändern wir den Code erneut. Wir wollen einen neuen Button in die Benutzerschnittstelle des CD-Players einfügen und ihn mit der replaceTrackListing-Methode verknüpfen. Damit fügen wir das Verhalten hinzu, das wir in der replaceTrackListing-Methode spezifiziert haben; aber wir ändern auch Verhalten auf subtile Weise; denn die Benutzerschnittstelle wird mit diesem neuen Button etwas anders dargestellt. Möglicherweise dauert es eine Mikrosekunde länger, bis es komplett angezeigt wird. Es scheint fast unmöglich zu sein, Verhalten hinzuzufügen, ohne zugleich vorhandenes Verhalten bis zu einem gewissen Grad zu ändern.
Das Design zu verbessern, ist eine weitere Art von Software-Änderung. Wir wollen die Software umstrukturieren, etwa damit sie wartungsfreundlicher wird, wobei ihr Verhalten im Allgemeinen bewahrt werden soll. Wird dabei Verhalten, vielleicht aus Versehen, entfernt, bezeichnen wir dies oft als Bug (Fehler). Einer der Hauptgründe, warum viele Programmierer nicht versuchen, das Design zu verbessern, liegt darin, dass dabei leicht Verhalten verloren gehen, beschädigt oder unerwünscht verändert werden kann.
Der Prozess, Design zu verbessern, ohne Verhalten zu ändern, wird als Refactoring bezeichnet. Es basiert auf der Idee, dass wir Software wartungsfreundlicher machen können, ohne ihr Verhalten zu ändern, wenn wir Tests schreiben, mit denen wir kontrollieren, dass das vorhandene Verhalten nicht geändert wird, und in kleinen Schritten vorgehen, um dies nach jedem Schritt zu verifizieren. Entwickler säubern schon seit Jahren den Code vorhandener Systeme; aber erst in den letzten Jahren hat sich das Refactoring verbreitet. Es unterscheidet sich von allgemeinen Säuberungen darin, dass wir nicht einfach risikoarme Änderungen wie etwa eine Umformatierung von Quellcode oder invasive und riskante Dinge wie etwa das Umschreiben ganzer Code-Fragmente vornehmen, sondern dass wir eine Reihe kleiner struktureller Änderungen vornehmen und dabei von Tests unterstützt werden, die das Ändern des Codes erleichtern. Die Essenz des Refactorings besteht darin, Verhalten zu bewahren, während funktionale Änderungen Verhalten modifizieren.
Optimierung ähnelt dem Refactoring, verfolgt aber ein anderes Ziel. Bei beiden wird die Funktionalität nicht geändert, aber beim Refactoring wird die Programm-Struktur geändert, während bei der Optimierung die Nutzung von Ressourcen (Zeit, Speicherplatz usw.) verbessert wird.
Doch ähnelt das Refactoring der Optimierung tatsächlich viel stärker als dem Hinzufügen von Funktionen oder der Beseitigung von Fehlern? Refactoring und Optimierung haben gemeinsam, dass die Funktionalität invariant bleibt, während etwas anderes geändert wird.
Im Allgemeinen können wir bei der Arbeit an einem System drei verschiedene Aspekte ändern: Struktur, Funktionalität und Ressourcenverbrauch.
Was ändert sich normalerweise und was bleibt im Wesentlichen konstant, wenn wir vier unserer verschiedenen Arten von Änderungen vornehmen (ja, oft ändern sich alle drei Aspekte, aber wir wollen das Typische betrachten):
Oberflächlich sehen sich Refactoring und Optimierung sehr ähnlich. Sie halten die Funktionalität invariant. Aber was passiert, wenn wir neue Funktionalität separat betrachten? Wenn wir eine Funktion hinzufügen, führen wir eine neue Funktionalität ein, aber ohne vorhandene Funktionalität zu ändern.
Beim Hinzufügen von Funktionen, beim Refactoring und bei der Optimierung bleibt die vorhandene Funktionalität konstant. Und wenn wir das Beseitigen von Fehlern genauer betrachten, stellen wir zwar fest, dass wir damit Funktionalität ändern; aber die Änderungen sind oft sehr klein, verglichen mit der insgesamt vorhandenen Funktionalität, die nicht geändert wird.
Das Hinzufügen von Funktionen und das Beseitigen von Fehlern ähneln stark dem Refactoring und der Optimierung. In allen vier Fällen wollen wir Funktionalität oder Verhalten ändern, aber gleichzeitig viel mehr bewahren (siehe Abbildung 1.1).
Was bedeutet diese detaillierte Analyse der möglichen Änderungen für unsere praktische Arbeit? Positiv betrachtet scheint sie uns zu sagen, worauf wir uns konzentrieren müssen. Wir müssen dafür sorgen, dass die kleine Anzahl der Dinge, die wir ändern, korrekt geändert werden. Negativ betrachtet lernen wir, dass dies nicht das Einzige ist, auf das wir uns konzentrieren müssen. Wir müssen herausfinden, wie wir den Rest des Verhaltens bewahren können. Dazu gehört leider mehr, als einfach den Code in Ruhe zu lassen. Wir müssen Gewissheit haben, dass sich das Verhalten nicht ändert, und das kann sehr schwierig sein. Das Verhalten, das wir bewahren müssen, ist normalerweise sehr umfangreich, aber das ist nicht das Problem. Das Problem ist, dass wir oft nicht wissen, in welchem Umfang Verhalten durch unsere Änderungen gefährdet ist. Andernfalls könnten wir uns auf dieses Verhalten konzentrieren und den Rest ignorieren.
Um Risiken zu verändern, müssen wir drei Fragen stellen:
1. Welche Änderungen müssen wir vornehmen?
2. Wie erfahren wir, dass wir sie korrekt vorgenommen haben?
3. Wie können wir sicher sein, dass wir nichts beschädigt haben?
Wie viele Änderungen können Sie sich leisten, wenn Änderungen riskant sind?
Die meisten Teams, mit denen ich gearbeitet habe, arbeiten mit einem sehr konservativen Risikomanagement. Sie minimierten die Anzahl der Änderungen ihrer Code-Basis. Manchmal handeln sie nach der Maxime: »Wenn es nicht kaputt ist, fass es nicht an.« In anderen Teams werden Änderungen »kleingeredet«. Die Entwickler sind einfach sehr vorsichtig, wenn sie Änderungen vornehmen: »Was? Sie erstellen dafür eine andere Methode?« Antwort: »Nein, ich füge nur die Code-Zeilen direkt hier in die Methode ein, wo ich gleichzeitig den Rest des Codes sehen kann. Ich muss weniger editieren, und es ist sicherer.«
Es ist verlockend zu denken, wir können Software-Probleme minimieren, indem wir sie ignorieren; aber leider holt uns die Wirklichkeit immer ein. Wenn wir vermeiden, neue Klassen und Methoden zu erstellen, werden die vorhandenen immer größer und unübersichtlicher. Wenn Sie ein umfangreiches System ändern, müssen Sie damit rechnen, dass es eine Weile dauert, mit dem Arbeitskontext vertraut zu werden. Gute und schlechte Systeme unterscheiden sich auch dadurch, dass Sie bei guten nach dieser Lernphase ein Gefühl der Sicherheit haben und sich zutrauen, die Änderungen erfolgreich vorzunehmen. Wenn Sie dagegen schlecht strukturierten Code nach der Lernphase ändern wollen, haben Sie eher das Gefühl, von einer Klippe zu springen, um einem Tiger zu entkommen. Sie zögern diesen Schritt immer weiter hinaus: »Bin ich bereit dafür? Nun, mir bleibt wohl nichts anderes übrig.«
Änderungen zu vermeiden, hat auch andere negative Konsequenzen. Wer Code nicht ändert, verliert oft die Fähigkeit dafür. Eine große Klasse in Teile zu zerlegen, kann ziemlich anstrengend sein, wenn Sie es nicht mehrfach pro Woche tun. Andernfalls wird es zur Routine. Sie erkennen immer besser, was kaputtgehen kann und was nicht, und die Arbeit geht viel leichter von der Hand.
Die letzte Konsequenz, Änderungen zu vermeiden, ist Angst. Leider haben viele Teams eine unglaubliche Angst vor Änderungen; und jeden Tag wird es schlimmer. Oft merken die Mitglieder gar nicht, wie viel Angst sie haben, bis sie bessere Techniken kennen lernen und die Angst langsam nachlässt.
Jetzt haben Sie erfahren, dass es schlecht ist, Änderungen zu vermeiden; aber welche Alternativen gibt es? Eine Alternative besteht einfach darin, sich mehr anzustrengen. Vielleicht können wir mehr Entwickler einstellen, damit alle genügend Zeit für Studium und Analyse des Codes haben und die Änderungen »richtig« durchgeführt werden. Sicher, mehr Zeit und bessere Analysen machen Änderungen sicherer. Oder etwa nicht? Wie kann ein Team nach allen Analysen sicher sein, ob es alles richtig verstanden hat?
Es gibt zwei grundsätzliche Methoden, ein System zu ändern: Edit and Pray (Bearbeiten und Beten) und Cover and Modify (Abdecken und Modifizieren). Leider ist Edit and Pray wohl eher der Branchenstandard. Bei Edit and Pray studieren Sie den Code gründlich, den Sie ändern wollen, planen die Änderungen sorgfältig und fangen dann an, den Code zu ändern. Wenn Sie fertig sind, führen Sie das System aus, um zu prüfen, ob die Änderungen wirksam waren, und probieren dann dieses und jenes aus, um festzustellen, ob Sie nichts beschädigt haben. Dieses Herumprobieren ist wichtig. Wenn Sie Ihre Änderungen vornehmen, hoffen und beten Sie, nichts falsch zu machen; danach nehmen Sie sich zusätzlich Zeit, um dies zu überprüfen.
Oberflächlich scheint Edit and Pray dasselbe wie »sorgfältig arbeiten« zu sein, sehr professionelles Verhalten also. Ihre »Sorgfalt« ist geradezu greifbar; und bei sehr invasiven Änderungen gehen Sie besonders sorgfältig vor, weil viel mehr schiefgehen kann. Aber Sicherheit hängt nicht nur von der Sorgfalt ab. Ich glaube, niemand würde zu einem Chirurgen gehen, der mit einem Buttermesser operiert, nur weil er sorgfältig arbeitet. Eine wirksame Änderung von Software erfordert ähnlich wie ein wirksamer chirurgischer Eingriff umfassendere Fähigkeiten. Sorgfältig zu arbeiten, bringt nicht viel, wenn man nicht die richtigen Tools und Techniken anwendet.
Cover and Modify ist eine andere Methode, Systeme zu ändern. Sie basiert auf der Idee, dass wir mit einem Sicherheitsnetz arbeiten können, wenn wir Software ändern. Natürlich handelt es sich nicht um ein normales Sicherheitsnetz, mit dem wir uns beim Stürzen vor Schaden bewahren, sondern um eine Art Schutzmantel, in den wir unseren Code einhüllen, um schädliche Änderungen einzudämmen und den Rest unserer Software nicht zu infizieren. Software zu bedecken bedeutet, sie mit Tests abzudecken. Wenn wir ein Code-Fragment mit einem brauchbaren Satz von Tests umgeben haben, können wir Änderungen vornehmen und sehr schnell herausfinden, ob sie positive oder negative Auswirkungen haben. Wir gehen immer noch mit derselben Sorgfalt vor; doch mit dem Feedback, das wir bekommen, können wir den Code präziser ändern.
Wenn Sie mit dieser Anwendung von Tests nicht vertraut sind, hört sich all dies wahrscheinlich etwas seltsam an. Traditionell werden Tests nach der Entwicklung geschrieben und ausgeführt. Eine Gruppe von Programmierern schreibt Code und ein Team von Testern prüft dann mit diversen Tests, ob der Code die Spezifikationen erfüllt. In einigen sehr traditionellen IT-Abteilungen wird Software so und nicht anders entwickelt. Das Team kann Feedback bekommen, aber die Feedback-Schleife ist lang. Das Team arbeitet einige Wochen oder Monate, und dann sagen Tester in einer anderen Gruppe, ob alles richtig ist oder nicht.
Solche Tests versuchen eigentlich, die »Korrektheit zu demonstrieren«. Obwohl dies ein erstrebenswertes Ziel ist, können Tests auch ganz anders eingesetzt werden, nämlich »um Änderungen aufzudecken«.
Solche Tests werden üblicherweise als Regressionstests bezeichnet. Wir führen periodisch Tests aus, die bekanntermaßen richtiges Verhalten prüfen, um festzustellen, ob unsere Software immer noch so funktioniert wie vor den Änderungen.
Tests, die Code-Fragmente einschließen, die Sie ändern wollen, sind eine Art Software-Zwinge. Sie können das meiste Verhalten konstant halten und sicher sein, nur das zu ändern, was Sie ändern wollen.
Software-Zwinge
Eine Zwinge (Schraubstock, engl. vise) ist ein Gerät, in das man ein Werkstück einspannen kann, um es für die Dauer der Bearbeitung in einer bestimmten Position zu fixieren.
Tests, die Änderungen entdecken, verhalten sich wie eine Zwinge, in die unser Code eingespannt ist. Sie fixieren das Verhalten des Codes. Bei Änderungen können wir sicher sein, dass wir immer nur einen Teil des Verhaltens gleichzeitig ändern. Kurz gesagt: Wir kontrollieren unsere Arbeit.
Regressionstests sind eine hervorragende Errungenschaft. Warum werden sie von Entwicklern nicht öfter eingesetzt? Bei Regressionstests gibt es ein kleines Problem: Sie werden häufig auf die Anwendungsschnittstelle angewendet, egal ob es sich um eine Webanwendung, eine Befehlszeilenanwendung oder eine GUI-basierte Anwendung handelt. Regressionstests wurden traditionell der Anwendungsebene zugeordnet. Leider! Denn sie können ein sehr nützliches Feedback liefern. Deshalb lohnt es sich, sie auf einer feinkörnigeren Ebene einzusetzen.