Beruflich Dokumente
Kultur Dokumente
Michael Feathers
mitp
Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der
Deutschen Nationalbibliografie; detaillierte bibliografische
Daten sind i m Internet über <http://dnb.d-nb.de> abrufbar.
ISBN 978-3-8266-9021-1
1. Auflage 2 0 1 1
E-Mail: kundenbetreuung@hjr-verlag.de
www.mitp.de
Authorized translation f r o m the English language edition, entitled Working Effectively with
Legacy Code by Feathers, Michael; published by Pearson Education, Inc., Publishing as Pren-
tice Hall PTR, Copyright © 2 0 0 5 . Pearson Education, Inc. Tenth Printing, May 2 0 0 9
All rights reserved. No part of this book may be reproduced or transmitted in any f o r m or by
any m e a n s , electronic or mechanical, including photocopying, recording or by any Informa-
tion storage retrieval system, without permission f r o m Pearson Education, Inc. G E R M A N
language edition published by mitp, eine Marke der Verlagsgruppe Hüthig Jehle R e h m
G m b H , Copyright © 2 0 1 1 .
Vorwort 13
Geleitwort 15
Danksagungen 21
1 Software ändern 27
1.1 Vier Gründe, Software zu ändern 27
1.2 Riskante Änderungen 31
4 Das Seam-Modell 53
4.1 Ein riesiges Blatt mit Text 53
4.2 Seams 54
4.3 Seam-Arten 57
5 Tools 69
5.1 Automatisierte Refactoring-Tools 69
5.2 Mock-Objekte 71
5.3 Unit-Test-Harnische 72
5.4 Allgemeine Test-Harnische 77
7
Inhaltsverzeichnis
6 Ich habe nicht viel Zeit und ich muss den Code ändern 81
6.1 SproutMethod 83
6.2 Sprout Class 87
6.3 Wrap Method 91
6.4 Wrap Class 95
6.5 Zusammenfassung 100
8
Inhaltsverzeichnis
13 Ich muss etwas ändern, weiß aber nicht, welche Tests ich
schreiben soll 205
13.1 Charakterisierungs-Tests 206
13.2 Klassen charakterisieren 209
13.3 Gezielt testen 210
13.4 Eine Heuristik für das Schreiben von Charakterisierungs-Tests 215
16 Ich verstehe den Code nicht gut genug, um ihn zu ändern 227
16.1 Notizen/Skizzen 228
16.2 Listing Markup 229
16.3 Scratch Refactoring 230
16.4 Ungenutzten Code löschen 231
20 Diese Klasse ist zu groß und soll nicht noch größer werden 261
20.1 Aufgaben erkennen 264
20.2 Andere Techniken 278
Inhaltsverzeichnis
TO
Inhaltsverzeichnis
A Refactoring 415
B Glossar 421
Stichwortverzeichnis 423
n
Vorwort
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 Pro-
grammierer entstanden war, verrottete im Laufe der Zeit wie ein Stück verdorbe-
nes Fleisch. Das hübsche kleine System, das wir im letzten Jahr erstellt haben,
entwickelte sich im nächsten Jahr in einen schrecklichen Morast aus verschlunge-
nen 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 sel-
ber schuld, wenn er seine Anforderungen an uns ändert.
Vorwort
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.
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.
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 erfor-
dern 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 Diszipli-
nen, 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 ver-
schlechtern, Systeme zu machen, die sich im Laufe der Zeit verbessern.
Robert C. Martin
29. Juni 2004
14
Geleitwort
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-8os. 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 Zim-
mergenosse einen Computer; und ich kaufte mir einen C-Compiler, um mir das
Programmieren beizubringen. Damit fing es an. Ich blieb jede Nacht auf, um die-
ses 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 Program-
mierer, 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 verschwun-
den?
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 mei-
nem 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? Viel-
leicht 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.
Geleitwort
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 Defini-
tion von Legacy Code hat nichts damit zu tun, wer ihn geschrieben hat. Die Quali-
tä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 beseiti-
gen, 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 geschrie-
ben ist; es spielt keine Rolle, wie schön oder objektorientiert oder gut eingekap-
selt 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
16
Geleitwort
passieren wird. Die Auswirkung der Änderung einiger Variablen genau zu über-
schauen, 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, brau-
chen 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 begeg-
net 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 ver-
wenden kann. Abgesehen von der Vertraulichkeit konnte ich einfach keinen derar-
tigen 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 Anwendbar-
keit, auch wenn das Beispiel einfacher zu sein scheint.
Die hier vorgestellten Techniken sind mit erheblich umfangreicherem Code getes-
tet 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:
m_nMargi ns++;
17
Geleitwort
In diesem Buch geht es nicht nur nicht um »schönen« Code, sondern noch weni-
ger um »schönes« Design. Gutes Design sollte zu den Zielen jedes Programmie-
rers 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 chir-
urgischen 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 Verfas-
sung 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 olym-
pische Medaillen kämpfen, aber wir dürfen das »Beste« nicht zum Feind des »Bes-
seren« 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 verwen-
deten 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 ablie-
fern.
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. Die-
ser Eindruck verdichtete sich bei einer Arbeit mit einem Team der Finanzbranche.
18
Geleitwort
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-Frag-
mente 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 Dejä-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 nachzu-
denken und sie aufzuschreiben, um Teams bei der Verbesserung ihrer Code-Basis
zu helfen.
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 kultivie-
ren.
19
Danksagungen
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 Pro-
grammieren 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 Pat-
terns 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, Mar-
tin Lippert, Keith Nicholas, Phlip Plumlee, C. Keith Ray, Robert Blum, Bill Burris,
William Caputo, Brian Marick, Steve Freeman, David Putman, Emily Bache, Dave
Asteis, Rüssel Hill, Christian Sepulveda und Brian Christopher Robinson.
Dank auch an Joshua Kerievsky, der wesentliche Anmerkungen zu einem der ers-
ten Entwürfe beitrug, und Jeff Langr, der meine ganzen Schreibprozesse mit sei-
nen 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.
Danksagungen
Besonderen Dank schulde ich auch Jay Packlick, Jacques Morel und Kelly Mower
von Sabre Holdings und Graham Wright von Workshare Technology für Unter-
stü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. Rai-
mund 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 ver-
gangenen 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.mi chaelfeathe rs.com
Einführung-Wie man dieses Buch
lesen sollte
Ich habe verschiedene Formate ausprobiert, bevor ich mich für das gegenwärtige
Format dieses Buches entschied. Viele der verschiedenen Techniken und Verfah-
ren, 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 erfor-
dern, 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 funk-
tioniert) 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 Glos-
sar nachschlagen.
Die Refactorings in Techniken zur Aufhebung von Dependencies sind etwas Besonde-
res, da sie ohne Tests angewendet werden sollen. Sie dienen der Einrichtung von
Tests. Ich rate Ihnen, jede einzelne Technik durchzulesen, damit Sie mehr Mög-
lichkeiten kennen lernen, um Ihren Legacy Code zu zähmen.
Teil I
Wie Wandel funktioniert
In diesem Teil:
• Kapitel 1
Was ist Seam? 21
• Kapitel 2
Mit Feedback arbeiten 33
• Kapitel 3
Überwachung und Trennung 45
• Kapitel 4
Das Seam-Modell 53
• Kapitel 5
Tools 69
25
Kapitel i
Software ändern
Code zu ändern, ist etwas Großartiges. Wir verdienen damit unseren Lebensunter-
halt. 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.
27
Kapitel 1
Software ändern
Funktion eingestuft werden. »Wenn die Abteilung einfach aufhören würde, stän-
dig ihre Meinung zu ändern, wären wir jetzt fertig.« Aber in einigen Unterneh-
men 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 vie-
len Unternehmen Korrekturen von Fehlern und Erweiterungen um neue Funktio-
nen 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 semanti-
scher Ebene, was die Tätigkeit denn nun letztlich sei, maskiert etwas für uns tech-
nisch 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 hinzu-
fügen (vorausgesetzt, es leistet, was sie wirklich wollten), aber wenn wir Verhal-
ten ändern oder entfernen, das sie benötigen (Fehler einführen), verlieren wir
ihr Vertrauen.
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?
28
1.1
V i e r G r ü n d e , Software zu ändern
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 hinzuge-
fü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 Benutzer-
schnittstelle des CD-Players einfügen und ihn mit der replaceTrackListing-
Methode verknüpfen. Damit fügen wir das Verhalten hinzu, das wir in der
repl aceTrackLi sti ng-Methode spezifiziert haben; aber wir ändern auch Verhal-
ten auf subtile Weise; denn die Benutzerschnittstelle wird mit diesem neuen But-
ton etwas anders dargestellt. Möglicherweise dauert es eine Mikrosekunde länger,
bis es komplett angezeigt wird. Es scheint fast unmöglich zu sein, Verhalten hin-
zuzufü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, viel-
leicht aus Versehen, entfernt, bezeichnen wir dies oft als Bug (Fehler). Einer der
Hauptgründe, warum viele Programmierer nicht versuchen, das Design zu ver-
bessern, 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
29
Kapitel 1
Software ändern
denen wir kontrollieren, dass das vorhandene Verhalten nicht geändert wird, und
in kleinen Schritten vorgehen, um dies nach jedem Schritt zu verifizieren. Ent-
wickler säubern schon seit Jahren den Code vorhandener Systeme; aber erst in den
letzten Jahren hat sich das Refactoring verbreitet. Es unterscheidet sich von allge-
meinen 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 unter-
stützt werden, die das Ändern des Codes erleichtern. Die Essenz des Refactorings
besteht darin, Verhalten zu bewahren, während funktionale Änderungen Verhal-
ten modifizieren.
1.1.3 Optimierung
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.
Oberflächlich sehen sich Refactoring und Optimierung sehr ähnlich. Sie halten
die Funktionalität invariant. Aber was passiert, wenn wir neue Funktionalität sepa-
rat betrachten? Wenn wir eine Funktion hinzufügen, führen wir eine neue Funk-
tionalität ein, aber ohne vorhandene Funktionalität zu ändern.
3°
1.2
Riskante Änderungen
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 i.i).
A |
Was bedeutet diese detaillierte Analyse der möglichen Änderungen für unsere
praktische Arbeit? Positiv betrachtet scheint sie uns zu sagen, worauf wir uns kon-
zentrieren 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 heraus-
finden, 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 Ver-
halten durch unsere Änderungen gefährdet ist. Andernfalls könnten wir uns auf
dieses Verhalten konzentrieren und den Rest ignorieren.
Die letzte Konsequenz, Änderungen zu vermeiden, ist Angst. Leider haben viele
Teams eine unglaubliche Angst vor Änderungen; und jeden Tag wird es schlim-
mer. 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 wel-
che Alternativen gibt es? Eine Alternative besteht einfach darin, sich mehr anzu-
strengen. 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 Änderun-
gen sicherer. Oder etwa nicht? Wie kann ein Team nach allen Analysen sicher sein,
ob es alles richtig verstanden hat?
32
Kapitel 2
Es gibt zwei grundsätzliche Methoden, ein System zu ändern: Edit and Pray (Bear-
beiten 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 die-
ses und jenes aus, um festzustellen, ob Sie nichts beschädigt haben. Dieses Her-
umprobieren 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 schiefge-
hen kann. Aber Sicherheit hängt nicht nur von der Sorgfalt ab. Ich glaube, nie-
mand 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. Sorg-
fältig zu arbeiten, bringt nicht viel, wenn man nicht die richtigen Tools und Tech-
niken 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 Spezifi-
33
Kapitel 2
Mit Feedback arbeiten
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 gleichzei-
tig ändern. Kurz gesagt: Wir kontrollieren unsere Arbeit.
34
Mit Feedback arbeiten
weise keine Zeit und kein Rechner für uns verfügbar gewesen. Wir atmen erleich-
tert durch und gehen zurück an die Arbeit. Wir müssen noch etwa fünf weitere
Änderungen vornehmen, die ähnlich kompliziert wie die letzte sind. Und wir sind
nicht allein. Wir wissen, dass mehrere andere Entwickler ebenfalls Änderungen
vornehmen.
Am nächsten Morgen bekommen wir einen Anruf. Daiva aus dem Test-Team teilt
uns mit, die Tests AE1021 und AE1029 wären über Nacht gescheitert. Sie sei nicht
sicher, ob dies unsere Änderungen wären, rufe aber uns an, weil sie wisse, dass
wir uns für sie darum kümmern würden. Wir debuggen den Code und prüfen, ob
die Fehler auf unsere Änderungen oder die eines anderen Entwicklers zurückzu-
führen sind.
Leider kommt diese Situation allzu häufig vor. Betrachten wir ein anderes Szena-
rium.
Wir müssen eine ziemlich lange, komplizierte Funktion ändern. Glücklicherweise
finden wir einen Satz von Unit-Tests dafür vor. Die Entwickler, die zuletzt mit dem
Code gearbeitet haben, haben einen Satz von über 20 Unit-Tests geschrieben, die
die Funktion gründlich durchleuchten. Wir führen die Tests aus und stellen fest,
dass alle bestanden werden. Als Nächstes schauen wir uns die Tests an, um zu ver-
stehen, wie sich der Code verhält.
Als wir den Code ändern wollen, wird uns klar, dass wir nicht genau wissen, wie
wir vorgehen sollen. Der Code ist unklar, und wir würden ihn wirklich gerne bes-
ser verstehen, bevor wir ihn ändern. Da die Tests nicht alles erfassen, wollen wir
möglichst klaren Code schreiben, damit wir mehr auf unsere Änderungen ver-
trauen können. Abgesehen davon sollte niemand noch einmal dieselbe Arbeit leis-
ten müssen, um den Code zu verstehen. Was für eine Zeitverschwendung!
Wir beginnen, den Code zu refaktorisieren. Wir extrahieren einige Methoden und
verschieben einige Bedingungen. Nach jeder kleinen Änderung führen wir die
Suite von Unit-Tests aus. Sie werden fast bei jeder Ausführung bestanden. Vor
einigen Minuten haben wir einen Fehler gemacht und die Logik einer Bedingung
umgekehrt. Doch da ein Test scheiterte, konnten wir die Situation in einer Minute
bereinigen. Nach dem Refactoring ist der Code viel übersichtlicher. Wir führen
unsere geplante Änderung durch und wir sind sicher, dass sie korrekt ist. Wir
fügen einige Tests hinzu, um das neue Verhalten zu verifizieren. Die nächsten
Programmierer, die dieses Code-Fragment bearbeiten, werden es leichter haben
und auf Tests zugreifen können, die seine Funktionalität abdecken.
Wollen Sie Ihr Feedback in einer Minute oder über Nacht bekommen? Welches
Szenarium ist effizienter?
35
Kapitel 2
Mit Feedback arbeiten
Tests sind unschätzbar. Sie können Ihnen bei der Entwicklung Feedback liefern
und helfen, Code viel sicherer zu refaktorisieren.
Test-Harnisch
In diesem Buch benutze ich den Terminus Test-Harnisch (engl, test harness) als
generische Bezeichnung für den Test-Code, den wir schreiben, um ein Software-
Fragment zu prüfen, und den Code, den wir benötigen, um die Tests auszufüh-
ren. Beim Arbeiten mit unserem Code können wir viele verschiedene Arten von
Test-Harnischen verwenden. In Kapitel 5, Tools, beschreibe ich das xUnit-Test-
Framework und das FIT-Framework. Beide können für die Tests benutzt wer-
den, die ich in diesem Buch beschreibe.
Kann man überhaupt nur eine Funktion oder eine Klasse testen? In prozeduralen
Systemen ist es oft schwierig, Funktionen isoliert zu testen. Funktionen auf obers-
ter Ebene rufen andere Funktionen auf, die wiederum andere Funktionen aktivie-
ren usw. bis hinunter auf die Maschinenebene. In objektorientierten Systemen ist
es ein wenig einfacher, Klassen isoliert zu testen; doch Tatsache ist, dass Klassen
im Allgemeinen nicht isoliert existieren. Wie viele Klassen haben Sie geschrieben,
die nicht auf andere Klassen zugreifen? Ziemlich wenige, nicht wahr? Normaler-
weise handelt es sich um kleine Daten-Klassen oder Datenstruktur-Klassen wie
etwa Stacks oder Queues (und selbst diese nutzen vielleicht andere Klassen).
Isoliertes Testen ist ein wichtiger Aspekt der Definition eines Unit-Tests, aber
warum ist er wichtig? Schließlich können viele Fehler auftreten, wenn Software-
Komponenten integriert werden. Sollten umfangreiche Tests, die breite funktio-
nale Code-Fragmente abdecken, nicht wichtiger sein? Nun ja, sie sind zweifellos
wichtig; aber es gibt bei umfangreichen Tests einige Probleme:
• Fehlerlokalisierung - Je weiter Tests von ihrem Testgegenstand entfernt sind,
desto schwieriger wird es, ein Scheitern eines Tests richtig zu deuten. Oft ist es
schwierig, die Ursache für das Scheitern zu lokalisieren. Man muss die Test-In-
puts, seine Outputs und den Fehler analysieren, um genau die Stelle zu finden,
36
2.1
W a s sind Unit-Tests?
an der der Fehler aufgetreten ist. Natürlich ist dies auch bei Unit-Tests erforder-
lich; dort ist es aber oft trivial, weil die Tests so klein sind.
• Ausführungsdauer - Die Ausführung größerer Tests dauert normalerweise
länger. Deshalb sind Testläufe oft ziemlich lästig. Tests, deren Ausführung zu
lange dauert, werden letztlich ganz ausgelassen.
• Abdeckung - Der Zusammenhang zwischen einem Code-Fragment und den
Werten, mit denen es ausgeführt wird, kann schwer zu erkennen sein. Mit Co-
verage-Tools (Abdeckungs-Tools) können wir normalerweise feststellen, ob ein
Code-Fragment von einem Test geprüft wird; doch wenn wir neuen Code hin-
zufügen, müssen wir möglicherweise eine aufwendige Mehrarbeit leisten, um
High-Level-Tests zu erstellen, die den neuen Code prüfen.
Einer der frustrierendsten Aspekte umfangreicherer Tests liegt darin, dass sie
auf Grund ihres Umfangs das Lokalisieren von Fehlern erschweren. Das scheint
nicht offensichtlich zu sein. Denn wenn wir unsere Tests ausführen und sie
bestanden werden und wir dann eine kleine Änderung machen und die Tests
danach scheitern, wissen wir doch genau, wo das Problem ausgelöst wird. Es
muss an unserer letzten kleinen Änderung liegen. Wir können die Änderung
rückgängig machen und es noch einmal versuchen. Doch bei umfangreichen
Tests kann die Ausführung zu lange dauern; deshalb sind wir oft versucht, die
Tests nicht oft genug auszuführen, um Fehler wirklich zu lokalisieren.
Unit-Tests schließen Lücken, die von größeren Tests nicht abgedeckt werden kön-
nen. Wir können Code-Fragmente unabhängig testen; wir können Tests so grup-
pieren, dass wir einige unter bestimmten Bedingungen und andere unter anderen
Bedingungen ausführen. So können wir Fehler schnell lokalisieren. Wenn wir den
Ort eines Fehlers in einem bestimmten Code-Fragment vermuten, können wir in
einem Test-Harnisch normalerweise schnell einen Test schreiben, um zu prüfen,
ob der Fehler wirklich an der vermuteten Stelle ausgelöst wird.
37
Kapitel 2
Mit Feedback arbeiten
größer. Wenn Sie sich nicht die Zeit genommen haben, eine Klasse in einem Test-
Harnisch separat instanziierbar zu machen, wie leicht wird es sein, wenn Sie
mehr Code hinzufügen? Einfacher wird es nie. Man schiebt es vor sich her. Mit der
Zeit braucht der Test dann i/io Sekunde zur Ausführung.
Ein Unit-Test, der i/io Sekunde zur Ausführung braucht, ist ein langsamer
Unit-Test.
Das meine ich ernst. Als ich dies schrieb, war I/IO Sekunde zur Ausführung eines
Unit-Tests unglaublich lang. Rechnen wir nach: Ein Projekt mit 3.000 Klassen
und zehn Tests pro Klasse enthält 30.000 Tests. Wie lange dauert die Ausführung
aller Tests, wenn jeder 1/10 Sekunde braucht? Fast eine Stunde! Dies ist eine lange
Zeit, wenn man auf Feedback wartet. Ihr Projekt umfasst keine 3.000 Klassen?
Halbieren Sie die Anzahl. Dann warten Sie immer noch eine halbe Stunde.
Bräuchten die Tests dagegen jeweils nur 1/100 Sekunde, sprächen wir über fünf
bis zehn Minuten. Wenn es so lange dauert, arbeite ich möglichst nur mit einer
Teilmenge der Tests; aber ich habe nichts dagegen, alle paar Stunden alle Tests lau-
fen zu lassen.
Mit Hilfe von Moores Gesetz hoffe ich, in meinem Leben noch Test-Feedback zu
erleben, das selbst für die größten Systeme fast in Echtzeit erfolgt. Ich vermute,
das Arbeiten mit solchen Systemen wird dem Arbeiten mit Code ähneln, der
zurückbeißen kann. Er wird uns mitteilen können, wenn er durch Änderungen
beschädigt wird.
Unit-Tests laufen schnell. Wenn sie nicht schnell laufen, sind sie keine Unit-
Tests.
Andere Arten von Tests verkleiden sich oft als Unit-Tests. Ein Test ist kein Unit-
Test, ...
1. ... wenn er mit einer Datenbank kommuniziert;
2. ... wenn er über ein Netzwerk kommuniziert;
3. ... wenn er auf das Dateisystem zugreift.
4. ... wenn Sie Ihre Umgebung verändern müssen (etwa indem Sie Konfigura-
tionsdateien bearbeiten), um ihn auszuführen.
Solche Tests sind nicht schlecht. Oft lohnt es sich, sie zu schreiben; und im All-
gemeinen werden sie in Unit-Test-Harnischen geschrieben. Doch es ist wichtig,
sie von echten Unit-Tests zu unterscheiden, damit Sie mit einem Satz von Tests
arbeiten können, die schnell ausgeführt werden, wenn Sie Änderungen vorneh-
men.
38
2.2
Higher-Level-Tests
2.2 Higher-Level-Tests
Unit-Tests sind hervorragende Instrumente, aber auch höher angesiedelte Tests,
die Szenarien und Interaktionen in einer Anwendung abdecken, haben eine Exis-
tenzberechtigung. Sie können damit das Verhalten eines Satzes von Klassen
gleichzeitig fixieren. Oft können Sie dann Tests für einzelne Klassen leichter
schreiben.
2.3 Testabdeckung
Wie geht man die Änderung eines Legacy-Projekts am besten an? Zunächst müs-
sen Sie akzeptieren, dass es im Zweifelsfall immer sicherer ist, Änderungen mit
Tests zu ummanteln. Wenn wir Code ändern, können wir Fehler machen; schließ-
lich sind wir alle nur Menschen. Doch wenn wir unseren Code vorher mit Tests
abdecken, wächst unsere Chance, Fehler abzufangen.
Abbildung 2.1 zeigt einen kleinen Satz von Klassen. Wir wollen die getResponse-
Text-Methode von Invoi cellpdateResponder und die getVal ue-Methode von
Invoice ändern. Sie sind unsere Änderungspunkte, die wir mit Tests für ihre
Klassen abdecken wollen.
39
Kapitel 2
Mit Feedback arbeiten
Also, wie gehen wir am besten vor? Wie können wir Tests einrichten, ohne den
Code zu ändern? Die traurige Wahrheit ist, dass dies in vielen Fällen nicht gut
machbar ist. In einigen Fällen kann es sogar unmöglich sein. In diesem Beispiel
könnten wir versuchen, das DBConnection-Problem mit einer echten Datenbank
zu umgehen; aber was ist mit dem Servlet-Problem? Müssen wir ein komplettes
Servlet erstellen und an den Konstruktor von Invoi ceUpdateResponder überge-
ben? Können wir es in den richtigen Zustand versetzen? Es könnte möglich sein.
Was würden wir bei einer GUI-Desktop-Anwendung tun? Vielleicht hätten wir
dann gar keine Programm-Schnittstelle. Die Logik könnte direkt in die GUI-Klas-
sen eingebunden sein. Was tun wir dann?
D a s Legacy-Code-Dilemma
Wollen wir Code ändern, sollten wir vorher Tests einrichten. Um Tests einzu-
richten, müssen wir oft Code ändern.
40
2.3
Testabdeckung
In dem Invoi ce-Beispiel können wir versuchen, auf einer höheren Ebene zu
testen. Wenn es schwierig ist, Tests zu schreiben, ohne eine bestimmte Klasse
zu ändern, ist es manchmal einfacher, eine Klasse zu testen, die diese bestimmte
Klasse verwendet; davon abgesehen müssen wir normalerweise Dependencies
zwischen Klassen an irgendeiner Stelle aufheben. In diesem Fall können wir die
Dependency von Invoi ceUpdateServl et aufheben, indem wir genau die Daten
übergeben, die Invoi cellpdateResponder wirklich braucht: eine Collection von
Invoice-IDs (Rechnungsnummern), die in dem Invoi ceUpdateServl et gespei-
chert sind. Wir können auch die InvoiceUpdateResponder-Dependency von
DBConnection aufheben, indem wir eine Schnittstelle (IDBConnection) einfüh-
ren und Invoi cellpdateResponder so ändern, dass er stattdessen die Schnitt-
stelle verwendet. Abbildung 2.2 zeigt den Zustand dieser Klassen nach den
Änderungen.
Sind diese Refactorings ohne Tests sicher? Vielleicht. Diese Refactorings werden
als Primitivize Parameter (25.16) bzw. Extract Interface (25.10) bezeichnet. Sie wer-
den in dem Katalog der Techniken zur Aufhebung von Dependencies am Ende des
Buches beschrieben. Wenn wir Dependencies aufheben, können wir oft Tests
schreiben, die invasive Änderungen sicherer machen. Der Trick ist, diese anfängli-
chen Refactorings sehr konservativ vorzunehmen.
Konservativ vorzugehen, ist immer richtig, wenn wir Fehler einführen könnten.
Doch um Code abzudecken, lassen sich Dependencies manchmal nicht so sau-
Kapitel 2
Mit Feedback arbeiten
Wenn Sie Dependencies in Legacy Code aufheben, müssen Sie oft Zugeständ-
nisse an Ihr ästhetisches Empfinden machen. Einige Dependencies lassen sich
sauber aufheben; andere sehen aus Designer-Sicht weniger ideal aus. Sie sind
mit den Einschnitten eines Chirurgen vergleichbar: Vielleicht bleibt hinterher in
Ihrem Code eine Narbe zurück, aber alles darunter kann besser werden.
Wenn Sie den Code später um den Punkt herum abdecken können, an dem Sie
Dependencies aufgehoben haben, können Sie auch diese Narbe heilen.
Änderungen vorzunehmen, ist das tägliche Ziel beim Arbeiten mit Legacy Code;
allerdings nicht beliebige Änderungen. Wir wollen funktionale Änderungen vor-
nehmen, die einen Wert haben und zugleich das System mehr unter Testkon-
trolle bringen. Am Ende jeder Programmierepisode sollten wir nicht nur Code
vorweisen können, der einige neue Funktionen erfüllt, sondern auch die zuge-
hörigen Tests. Im Laufe der Zeit bilden sich Bereiche in der Code-Basis heraus,
die wie Inseln aus dem Ozean auftauchen. Das Arbeiten auf diesen Inseln wird
viel einfacher. Im Laufe der Zeit entwickeln sich die Inseln zu umfangreichen
Landmassen. Schließlich können Sie auf Kontinenten von Code arbeiten, der
komplett durch Tests abgedeckt ist.
Betrachten wir die einzelnen Schritte und wie dieses Buch Ihnen dabei hilft.
42
2.4.1 Identifizieren Sie Änderungspunkte
Die Stellen, an denen Sie Änderungen vornehmen müssen, hängen von der Archi-
tektur des Systems ab. Wenn Sie das Design nicht genug kennen, um sicher zu
sein, Änderungen an den richtigen Stellen vorzunehmen, helfen Ihnen Kapitel 16,
Ich verstehe den Code nicht gut genug, um ihn zu ändern, und Kapitel 17, Meine
Anwendung hat keine Struktur, weiter.
Dependency-Probleme treten auch auf, wenn wir eine Idee für einen Test haben,
diese aber nicht leicht umsetzen können. Wenn Sie aufgrund von Dependencies
für umfangreiche Methoden keine Tests schreiben können, sollten Sie Kapitel 22,
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben, lesen.
Wenn Sie feststellen, dass Sie Dependencies aufheben können, es aber zu lange
dauert, Ihre Tests zu erstellen, hilft Ihnen Kapitel 7, Änderungen brauchen eine
Ewigkeit, weiter. Es beschreibt zusätzliche Maßnahmen zur Aufhebung von
Dependencies, mit denen Sie Ihre durchschnittliche Build-Zeit verkürzen können.
43
Kapitel 2
Mit Feedback arbeiten
44
Kapitel 3
Idealerweise müssten wir nichts Besonderes tun, um mit einer Klasse zu arbeiten.
In einem idealen System sollten wir sofort Objekte einer beliebigen Klasse in
einem Test-Harnisch erstellen und mit ihnen arbeiten können. Wir müssten Tests
für sie schreiben und uns dann anderen Dingen zuwenden können. Wäre dies so
leicht, müsste nichts darüber geschrieben werden; aber leider ist dies oft nicht der
Fall. Dependencies zwischen Belassen können es sehr schwierig machen,
bestimmte Gruppen von Objekten unter Testkontrolle zu bringen. Vielleicht wol-
len wir ein Objekt einer Klasse erstellen und es befragen; doch um es zu erstellen,
brauchen wir Objekte einer anderen Klasse, und diese Objekte brauchen wie-
derum Objekte einer anderen Klasse usw. Schließlich schließen wir fast das
gesamte System in einen Harnisch ein. In einigen Sprachen ist dies keine große
Sache. In anderen, insbesondere in C++, kann allein die Link-Zeit einen schnellen
Turnaround unmöglich machen, wenn wir Dependencies nicht aufheben.
Im Allgemeinen gibt es bei der Einrichtung von Tests zwei Gründe, Dependencies
aufzuheben: Überwachung (engl, sensing) und Trennung (engl. Separation):
Hier ist ein Beispiel. Wir haben eine Belasse namens NetworkBri dge in einer Net-
work-Management* Anwendung:
45
Kapitel 3
Überwachung und Trennung
NetworkBri dge akzeptiert ein Array von EndPoints und verwaltet ihre Konfigu-
ration mit der lokalen Hardware. Nutzer von NetworkBri dge können mit den
Methoden dieser Klasse Traffic von einem Endpunkt zu einem anderen dirigieren.
NetworkBri dge leistet dies, indem sie die Einstellungen der EndPoi nt-Klasse
setzt. Jedes EndPoi nt-Objekt öffnet ein Socket und kommuniziert über das Netz-
werk mit einem bestimmten Gerät.
Dies ist nur eine kurze Beschreibung von NetworkBri dge. Wir könnten mehr
ins Detail gehen, aber aus der Testperspektive sind bereits einige Probleme evi-
dent. Wie gehen wir vor, um Tests für NetworkBri dge zu schreiben? Die Klasse
könnte schon bei ihrer Konstruktion die echte Hardware aufrufen. Müssen wir
die Hardware zur Verfügung stellen, um eine Instanz der Klasse zu erstellen?
Und noch schlimmer: Woher wissen wir, was die Bridge mit dieser Hardware
oder den Endpunkten anstellt? Aus unserer Perspektive ist die Klasse eine Black-
box.
Doch vielleicht könnten wir Code schreiben, um Paketen über das Netzwerk nach-
zuschnüffeln. Vielleicht können wir eine Hardware-Komponente überreden, mit
NetworkBri dge so zu kommunizieren, dass sie wenigstens nicht »einfriert«,
wenn wir sie instanziieren. Vielleicht können wir die Komponenten zu einem
lokalen Cluster von Endpunkten zusammenschalten und diese unter Testkontrolle
bringen. Diese Lösungen könnten funktionieren, bedeuten aber sehr viel Arbeit.
Vielleicht brauchen wir für die Logik, die wir in NetworkBri dge ändern wollen,
diese Dinge gar nicht. Doch das Problem ist, dass wir kein Objekt dieser Klasse
instanziieren können, um direkt auszuprobieren, wie sie funktioniert.
Dieses Beispiel illustriert die Probleme sowohl der Überwachung als auch der
Trennung. Wir können die Auswirkungen unserer Aufrufe von Methoden dieser
Klasse nicht beobachten, und wir können sie nicht separat, getrennt vom Rest der
Anwendung ausführen.
Welches Problem ist schwieriger? Überwachung oder Trennung? Die Antwort ist
nicht klar. Üblicherweise brauchen wir beide. Doch eins ist klar: Es gibt viele Mög-
lichkeiten, Software zu zerlegen. Am Ende des Buches finden Sie einen ganzen
Katalog einschlägiger Techniken. Es gibt jedoch ein Hauptinstrument für die
Überwachung: simulierte Kollaborateure.
46
3-1
Kollaborateure simulieren
3.1.1 Fake-Objekte
Ein Fake-Objekt ist ein Objekt, das beim Testen einen Kollaborateur Ihrer Klasse
verkörpert. Das folgende Beispiel eines Point-of-sale-Systems enthält eine Klasse
namens S a l e (siehe Abbildung 3.1). Sie verfügt über eine Methode namens
s c a n ( ) , die einen Strichcode für Artikel akzeptiert, den ein Kunde kaufen möchte.
Wenn s c a n ( ) aufgerufen wird, muss das Sale-Objekt den Namen und den Preis
des eingescannten Artikels auf einem Kassen-Display anzeigen.
Sale
+ scan(barcode : String)
Wie können wir testen, ob der richtige Text auf dem Display angezeigt wird? Wenn
die Aufrufe des Display-APIs der Kasse tief in der Sal e-Klasse verborgen sind,
wird dies schwer sein. Es könnte nicht leicht sein, die Auswirkung auf das Display
zu beobachten. Doch wenn wir die Stelle in dem Code finden können, an der das
Display aktualisiert wird, können wir das Design wie in Abbildung 3.2 gezeigt
ändern.
Sale ArtR56Display
+ scan(barcode : String) + showLinefline : String)
Hier haben wir die neue Klasse ArtR56Display eingeführt. Diese Klasse enthält
den gesamten Code, der für die Kommunikation mit einem bestimmten Display-
Gerät verwendet wird. Wir müssen ihm nur mitteilen, was angezeigt werden soll.
Wir können den gesamten Display-Code aus Sal e nach ArtR56Di spl ay verschie-
ben. Danach leistet das System genau dasselbe wie vorher. Gewinnen wir damit
Kapitel 3
Überwachung und Trennung
irgendetwas? Nun, nach dieser Änderung können wir das Design wie in Abbil-
dung 3.3 gezeigt ändern.
«interface»
Sale Display
ArtR56Display FakeDisplay
• showLine(line : String) - lastLine : String
- getLastüne() : String
• showLine(line : String)
Die Sale-Klasse kann jetzt entweder auf ein ArtR56Display-Objekt oder ein
FakeDi spl ay-Objekt zugreifen. Mit diesem können wir jetzt Sal e separat testen.
Wie funktioniert dies? Nun, S a l e akzeptiert ein Display; und ein Display ist ein
Objekt einer Klasse, die die Di spl ay-Schnittstelle implementiert.
Sowohl ArtR56Di spl ay als auch FakeDi spl ay implementieren Di spl ay.
Ein Sal e-Objekt kann ein Display über den Konstruktor übernehmen und intern
speichern:
}
}
48
3-i
Kollaborateure simulieren
In der scan-Methode ruft der Code die showLi ne-Methode des display-Objekts
auf. Was dann passiert, hängt von der Art des an das Sal e-Objekt übergebenen
Displays ab. Bei einem ArtR56Di spl ay versucht es, die Daten auf echter Kassen-
Hardware anzuzeigen; bei einem FakeDi spl ay versucht es das nicht, aber wir
können erkennen, was angezeigt werden würde. Dafür könnten wir folgenden
Test schreiben:
import junit.framework.*;
sale.scan("l");
assertEquals("Mi1k $3.99", display.getLastLineO) ;
}
}
Die showLi ne-Methode akzeptiert eine Textzeile und weist sie der Variablen 1 a s t -
Li ne zu. Die getLastLi ne-Methode gibt diese Zeile zurück, wenn sie aufgerufen
wird. Dies ist ein ziemlich schlankes Verhalten, hilft uns aber erheblich weiter;
denn wir können mit diesem Test feststellen, ob der richtige Text an das Display
gesendet wird, wenn die Sal e-Klasse verwendet wird.
49
Kapitel 3
Überwachung und Trennung
Das stimmt zwar; doch deswegen ist dieser Test nicht unecht. Selbst wenn wir
einen Test konzipieren könnten, der uns genau zeigen könnte, welche Pixel wo
auf einem echten Kassen-Display angezeigt werden würden, wäre dies keine
Garantie, dass die Software mit der gesamten Hardware funktionierte. Wir kön-
nen Tests nur selektiv schreiben. Dieser Test teilt uns mit, wie Sal e-Objekte Dis-
plays beeinflussen, mehr nicht. Aber das ist nicht trivial. Falls wir einen Fehler
entdecken, kann uns dieser Test helfen zu erkennen, ob das Problem in Sal e
liegt oder nicht. Eine solche Information hilft uns, Fehler zu lokalisieren und so
Zeit zu sparen.
Wenn wir Tests für einzelne Units schreiben, erhalten wir kleine, wohl-verstan-
dene Teile - Sie können uns helfen, uns ein klareres Bild unseres Codes zu
machen.
3.1.2 D i e beiden A s p e k t e e i n e s F a k e - O b j e k t s
Fake-Objekte können auf den ersten Blick verwirrend sein. Besonders seltsam ist
etwa ihre Eigenschaft, dass ihre Methoden zwei »Seiten« haben. Betrachten wir
noch einmal die FakeDi spl ay-Klasse (siehe Abbildung 3.4).
Die showLi ne-Methode wird in FakeDi spl ay benötigt, weil FakeDi spl ay die
Display-Schnittstelle implementiert. Sie ist die einzige Methode von Display
und die einzige, die ein Sale-Objekt sieht. Die andere Methode, getLastLine,
dient dem Testen. Deshalb deklarieren wir display als FakeDi spl ay, nicht als
Di spl ay:
import junit.framework.*;
5°
3-i
Kollaborateure simulieren
sale.scan("l");
assertEquals("Mi 1k $3.99", display.getLastLineQ);
}
}
Die Sal e-Klasse sieht ein FakeDi spl ay-Objekt als Di spl ay, aber im Test müssen
wir uns an FakeDi spl ay halten; sonst können wir getLastLi ne() nicht aufru-
fen, um festzustellen, was tatsächlich angezeigt wird.
Das Beispiel in diesem Abschnitt ist sehr einfach, zeigt aber das zentrale Konzept
der Fake-Objekte. Sie können auf verschiedene Weisen implementiert werden. In
OO-Sprachen werden sie oft als einfache Klassen wie die FakeDi spl ay-Klasse in
dem vorhergehenden Beispiel implementiert. In Nicht-OO-Sprachen können wir
Fake-Objekte durch Definition einer alternativen Funktion implementieren, die
Werte in einer globalen Datenstruktur speichert und auf die wir bei Tests zugrei-
fen können. Details finden Sie in Kapitel 19, Mein Projekt ist nicht objektorientiert.
Wie kann ich es sicher ändern?
3.1.4 Mock-Objekte
Fake-Objekte lassen sich leicht schreiben und sind ein sehr wertvolles Tool für die
Überwachung. Wenn Sie viele solcher Objekte schreiben müssen, können Sie
auch einen fortgeschritteneren Typ von Fakes verwenden: Mock-Objekte (Scheinob-
jekt). Mock-Objekte sind Fakes, die intern Zusicherungen (engl, assertions) ausfüh-
ren. Hier ist ein Beispiel für einen Test, der ein Mock-Objekt verwendet:
import junit.framework.*;
Bei diesem Test erstellen wir ein Mock-Display-Objekt. Mock-Objekte haben einen
Vorteil: Wir können ihnen sagen, welche Aufrufe zu erwarten sind und was sie
prüfen sollen, wenn sie diese Aufrufe erhalten. Dies ist genau das, was bei diesem
Testfall passiert. Wir teilen der Sal eTest-Klasse mit, dass sie einen Aufruf der
showLi ne-Methode mit dem Argument "Mi 1 k $ 3 . 9 9 " erwarten soll. Nachdem die
Kapitel 3
Überwachung und Trennung
Erwartung gesetzt worden ist, verwenden wir das Objekt einfach im Test. In die-
sem Fall rufen wir die Methode scan() auf. Danach prüfen wir mit der veri f y O -
Methode, ob alle Erwartungen erfüllt worden sind. Falls nicht, scheitert der Test.
52
Kapitel 4
Das Seam-Modell
Wenn man versucht, Tests für vorhandenen Code zu schreiben, erkennt man
schnell, wie schlecht sich der Code zum Testen eignet. Es sind nicht nur
bestimmte Programme oder Sprachen. Ganz allgemein scheinen Programmier-
sprachen einfach das Testen nicht sehr gut zu unterstützen. Leicht testbare Pro-
gramme zu schreiben, scheint nur möglich zu sein, wenn man (erstens) die Tests
parallel zur Entwicklung eines Programms schreibt oder (zweitens) sich von vorn-
herein um ein »testbares Design« (engl, »design for testability«) bemüht. Der
erste Ansatz gibt Grund zur Hoffnung; aber wenn man den Code in der Praxis
zum Maßstab nimmt, ist der zweite nicht sehr erfolgreich.
Eine Sache ist mir aufgefallen: Seit meinen ersten Versuchen, Code unter Testkon-
trolle zu bringen, denke ich grundsätzlich anders über Code. Ich könnte dies ein-
fach als persönliche Marotte abtun, aber ich habe festgestellt, dass diese andere
Methode mir auch hilft, wenn ich mit einer neuen und unvertrauten Program-
miersprache arbeite. Weil ich in diesem Buch nicht auf jede Programmiersprache
eingehen kann, lege ich meine Sichtweise hier dar und hoffe, dass sie Ihnen eben-
falls hilft.
Damals war ein Programm einfach ein Listing. Alle paar Stunden ging ich aus
dem Labor in den Druckerraum und holte mir einen Ausdruck meiner Pro-
gramme ab, um das Ergebnis des letzten Compiler-Laufs zu studieren und Ursa-
chen für Programmfehler zu finden. Über Modularität wusste ich nicht genug; sie
war mir egal. Wir sollten modularen Code schreiben, um zu zeigen, dass wir dies
konnten, aber wirklich wichtig war für mich, ob der Code die richtigen Antworten
lieferte. Als ich mich zum ersten Mal mit objektorientiertem Code beschäftigte,
Kapitel 4
Das Seam-Modell
Oberflächlich betrachtet ist das alles wahr; aber was ist mit der Modularität? Wir
hören oft, es sei besser, Programme zu schreiben, die aus kleinen wiederverwend-
baren Teilen bestehen. Aber wie oft sind die kleinen wiederverwendbaren Teile
unabhängig voneinander? Nicht sehr oft. Wiederverwendbarkeit ist schwierig zu
realisieren. Selbst unabhängig aussehende Teile von Software hängen oft auf sub-
tile Weise voneinander ab.
4.2 Seams
Wenn Sie Ihre ersten Klassen für Unit-Tests isolieren, müssen Sie oft zahlreiche
Dependencies aufheben. Interessanterweise ist diese Arbeit oft aufwendig, unab-
hängig davon, wie »gut« das Design ist. Klassen vorhandener Projekte zum Testen
zu isolieren, ändert Ihr Konzept von einem »guten« Design. Sie werden anfangen,
Software ganz anders zu betrachten. Die Vorstellung, ein Programm sei einfach
ein großes Listing, funktioniert nicht mehr. Doch wie sollten wir es betrachten?
Betrachten wir ein Beispiel, eine Funktion in C++:
bool CAsyncSslRec::Init()
{
if (m_bSslInitialized) {
return true;
}
m_smutex.Uniock();
m_nSslRefCount++;
m_bSslInitialized = true;
FreeLibrary(m_hSslDlll);
m_hSslDlll=0;
FreeLi brary(m_hSslDl 12);
m_hSslD112=0;
54
if (!m_bFailureSent) {
m_bFailureSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSI FAILURE);
}
CreateLibrary(m_hSslDl11,"syncesel1.dl 1 " ) ;
CreateLi brary(m_hSslDll2,"syncesel2.dl 1 " ) ;
m_hSslDlll->Init() ;
m_hSslDl 12->Ini t() ;
return true;
}
Sieht wie ein Listing aus, oder? Angenommen, wir wollten die ganze Methode
außer der folgenden Zeile ausführen:
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
Kein Problem, nicht wahr? Wir müssen nur diese Zeile aus dem Code löschen.
Okay, machen wir das Problem etwas schwieriger. Wir wollen die Ausführung die-
ser Codezeile verhindern, weil PostRecei veError eine globale Funktion ist, die
mit einem anderen Subsystem kommuniziert und dieses Subsystem sperrig und
nur schwer unter Testkontrolle zu bringen ist. Jetzt besteht das Problem darin, wie
wir die Methode bei einem Test ausführen, ohne PostRecei veError aufzurufen.
Wie können wir dies tun und dennoch bei der Produktion den Aufruf von
PostRecei veError zulassen?
Die vielen möglichen Antworten führen uns zu einem neuen Konzept: dem Seam.
Seam
Ein Seam ist eine Stelle, an der Sie Verhalten in Ihrem Programm modifizieren
können, ohne es an dieser Stelle zu ändern.
Der Aufruf von PostRecei veError enthält einen Seam. Wir können das Ver-
halten an dieser Stelle auf mehrere Weisen ändern. Hier ist eine der unkom-
pliziertesten: PostRecei veError ist eine globale Funktion, die nicht zu der
CasynchSsl Rec-Klasse gehört. Was passiert, wenn wir eine Methode genau der-
selben Signatur in die CasynchSsl Rec-Klasse einfügen?
class CAsyncSslRec
{
Kapitel 4
Das Seam-Modell
};
In der Implementierungsdatei können wir einen Body für die Methode einfügen:
Jetzt können wir eine Unterklasse der CasyncSsl Rec-Klasse bilden und die
PostRecei veError-Methode überschreiben:
}
};
Wenn wir dann an der Stelle, an der CAsyncSsl Ree erstellt wird, stattdessen eine
TestingAsyncSslRec erstellen, beseitigen wir im folgenden Code das Verhalten
des Aufrufs von PostRecei veError:
bool CAsyncSslRec::Init()
{
if (m_bSslInitialized) {
return true;
}
m_smutex.UnlockO;
m_nSslRefCount++;
m_bSslInitialized = true;
FreeLibrary(m_hSslDill);
m_hSslDlll=0;
FreeLibrary(m_hSslDl 12);
m_hSslD112=0;
if (!m_bFailureSent) {
m_bFai1u reSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}
CreateLibrary(m_hSslDlll,"syncesel1.dll");
CreateLibrary(m_hSslD112,"syncesel2.dl!");
56
4-3
Seam-Arten
m_hSslDni->Init() ;
m_hSslDl12->Ini t();
return true;
}
Jetzt können wir Tests für diesen Code schreiben, der keine unerwünschten
Nebeneffekte hat.
Einen solchen Seam bezeichne ich als Objekt-Seam. Wir konnten die aufgerufene
Methode ändern, ohne die Methode zu modifizieren, von der sie aufgerufen wird.
Objekt-Seams stehen in objektorientierten Sprachen zur Verfügung. Sie sind nur
eine von vielen verschiedene Arten von Seams.
Warum Seams? Wozu ist dieses Konzept gut?
Dependencies aufzuheben, ist eins der größten Probleme, wenn man Legacy Code
unter Testkontrolle bringen will. Wenn wir Glück haben, sind die Dependencies
klein und lokalisiert; doch in pathologischen Fällen sind sie zahlreich und durch-
dringen die gesamte Code-Basis. Mit dem Seam-Konzept der Software können wir
bereits in der Code-Basis vorhandene Gelegenheiten erkennen. Wenn wir Verhal-
ten an Seams ersetzen können, können wir in unseren Tests selektiv Dependen-
cies aufheben. An den Stellen, an denen sich diese Dependencies befanden,
können wir auch anderen Code ausführen, wenn wir bestimmte Bedingungen in
dem Code überwachen und Tests dafür schreiben wollen. Oft können wir so genü-
gend Tests einrichten, um ein aggressiveres Vorgehen zu unterstützen.
4.3 Seam-Arten
Die verfügbaren Seam-Arten sind je nach Programmiersprache verschieden. Man
lernt sie am besten kennen, indem man alle Schritte studiert, um den Text eines
Programms in ausführbaren Code umzuwandeln. Jeder identifizierbare Schritt
enthüllt andere Arten von Seams.
4.3.1 Präprozessor-Seams
In den meisten Programmierumgebungen wird Programmtext von einem Compi-
ler gelesen. Der Compiler generiert Objekt-Code oder Bytecode. Je nach Sprache
kann es danach weitere Verarbeitungsschritte geben, aber was ist mit früheren
Schritten?
Nur wenige Sprachen führen vor dem Kompilieren einen Verarbeitungsschritt
aus; am bekanntesten sind C und C++.
In C und C++ wird vor dem Compiler ein Makro-Präprozessor ausgeführt, der im
Laufe der Jahre immer wieder heftig kritisiert worden ist. Mit ihm können wir
etwa aus einigen unverfänglich aussehenden Textzeilen wie
Kapitel 4
Das Seam-Modell
TEST(getBalance,Account)
{
Account account;
L0NGS_EQUAL(0, account. getBalanceü) ;
}
m_pRtg->Adj(2.0);
#ifdef DEBUC
#ifndef WINDOWS
{ FILE *fp = fopen(TGLOGNAME,"w");
if (fp) { fprintf(fp,"%s", m_pRtg->pszState); fclose(fp); }}
#endif
m_pTSRTable->p_nFlush |= GF_FL0T;
#endif
Doch abgesehen davon schätze ich die Präprozessoren von C und C++, weil sie uns
mehr Seams zur Verfügung stellen. Das folgende Beispiel-C-Programm enthält
58
4-3
Seam-Arten
#include <DFHLItem.h>
#include <DHLSRecord.h>
Mit Präprozessor-Seams können wir die Aufrufe von db_update ersetzen. Zu die-
sem Zweck führen wir eine Header-Datei namens 1 ocal def s . h ein.
#include <DFHLItem.h>
#include <DHLSRecord.h>
#include "1ocaldefs.h"
#i fdef TESTINC
59
Kapitel 4
Das Seam-Modell
#defi ne db_update(account_no,item)\
{last_item = (item); last_account_no = (account_no);}
#endif
Mit dieser Ersetzung von db_update können wir Tests schreiben, um zu verifizie-
ren, ob db_update mit dem richtigen Parameter aufgerufen wurde, weil uns die
#i ncl ude-Direktive des C-Präprozessors einen Seam zur Verfügung stellt, mit
dem wir Text ersetzen können, bevor er kompiliert wird.
Seam
Ein Seam ist eine Stelle, an der Sie Verhalten in Ihrem Programm modifizieren
können, ohne es an dieser Stelle zu ändern.
Ein Seam ist eine Stelle, an der sich Verhalten ändern kann. Doch wir können den
Code an dieser Stelle nicht ändern, nur um ihn zu testen. Der Quellcode sollte
sowohl bei der Produktion und beim Testen gleich sein. In dem vorhergehenden
Beispiel wollten wir das Verhalten an der Stelle des db_update-Aufrufs ändern.
Um diesen Seam zu nutzen, müssen Sie an einer anderen Stelle eine Änderung
vornehmen. In diesem Fall ist der Aktivierungspunkt eine Präprozessor-#i fdef-
Direktive namens TESTING. Wenn TESTING definiert ist, dann definiert die Datei
1 ocal def s. h Makros, die Aufrufe von db_update in der Quelldatei ersetzen.
4.3.2 Link-Seams
In vielen Sprachen ist das Kompilieren nicht der letzte Schritt des Build-Prozesses.
Die Compiler generiert eine intermediäre Repräsentation des Codes, die Aufrufe
von Code in anderen Dateien enthält. Linker kombinieren diese Repräsentationen.
Sie lösen alle Aufrufe so auf, dass zur Laufzeit ein komplettes Programm vorliegt.
Sprachen wie etwa C und C++ verfügen tatsächlich über einen separaten Linker,
der die eben beschriebene Funktion erfüllt. In Java und ähnlichen Sprachen ver-
60
4-3
Seam-Arten
Unabhängig davon, wie Ihre Sprache Referenzen auflöst, können Sie mit diesem
Mechanismus normalerweise Teile eines Programms ersetzen. Betrachten wir
etwa eine kleine Java-Klasse namens Fi tFi 1 ter:
package fitnesse;
import fit.Parse;
import fit.Fixture;
import java.io.*;
import java.util.Date;
import java.io.*;
import java.util.*;
public void p r o c e s s O {
try {
tables = new Parse(input) ;
fixture.doTables(tables);
} catch (Exception e) {
exception(e);
}
tables.print(output);
}
In dieser Datei importieren wir f i t . Parse und fi t. Fi xture. Wie finden Compi-
ler und JVM diese Klassen? In Java können Sie den Pfad zu diesen Klassen mit
einer Umgebungsvariablen namens C l a s s p a t h setzen. Sie können gleichnamige
61
Kapitel 4
Das Seam-Modell
Angenommen, wir wollten beim Testen eine andere Version der Parse-Klasse
zur Verfügung stellen. Wo wäre der Seam?
Diese Art der dynamischen Verlinkung ist in vielen Sprachen möglich. Die meis-
ten enthalten Methoden, mit denen man Link-Seams nutzen kann. Aber nicht jede
Verlinkung erfolgt dynamisch. In vielen älteren Sprachen erfolgt fast die gesamte
Verlinkung statisch, und zwar einmal nach dem Kompilieren.
void CrossPlaneFigure::rerenderO
{
// Label zeichnen
drawText(m_nX, m_nY, m_pchLabel, getClipLenO);
drawLine(m_nX, m_nY, m_nX + getClipLenO , m_nY);
drawLine(m_nX, m_nY, m_nX, m_nY + getDropLenO);
if (!m_bShadowBox) {
drawLine(m_nX + getClipLenO, m_nY, m_nX + getClipLenO,
m_nY + getDropLenO);
drawLine(m_nX, m_nY + getDropLenO, m_nX + getClipLenO,
m_nY + getDropLenO);
}
// Figur zeichnen
for (int n = 0; n < edges.sizeO; n++) {
}
Dieser Code enthält viele direkte Aufrufe einer Grafik-Bibliothek. Leider gibt es
nur eine Methode, um wirklich zu verifizieren, dass dieser Code tut, was er tun
62
4-3
Seam-Arten
soll: Sie müssen die Darstellung der Figuren auf dem Bildschirm kontrollieren.
Bei kompliziertem Code ist dies ziemlich fehleranfällig und auch mühsam. Alter-
nativ können Sie Link-Seams verwenden. Wenn alle Zeichenfunktionen zu einer
bestimmten Bibliothek gehören, können Sie Stub-Versionen erstellen, die mit
dem Rest der Anwendung verlinkt werden. Wollen Sie nur die Dependencies iso-
lieren, können diese Stub-Versionen einfach leere Funktionen sein:
Wenn die Funktionen Werte zurückgeben, müssen Sie ebenfalls etwas zurückge-
ben. Oft eignet sich Code, der den Erfolg oder den Standardwert eines Typs
zurückgibt:
int getStatusO
{
return FLAG_OKAY;
}
Der Fall einer Grafik-Bibliothek ist etwas atypisch. Ein Grund, warum sie kein
guter Kandidat für diese Technik ist, liegt darin, dass dies fast eine reine »Anwei-
sungs«-Schnittstelle ist. Sie rufen Funktionen auf, um ihnen Anweisungen zu
geben, nicht um umfangreiche Informationen abzurufen. Solche Informationen
abzurufen, ist schwierig, weil sich die Standardwerte oft nicht als Rückgabewerte
eignen, wenn Sie Ihren Code testen wollen.
Trennung ist häufig ein Grund, einen Link-Seam zu verwenden. Sie können ihn
auch zur Überwachung einsetzen; es ist nur etwas aufwendiger. Im Beispiel der
Grafik-Bibliothek könnten wir etwa Aufrufe mit zusätzlichen Datenstrukturen auf-
zeichnen:
std::queue<GraphicsAction> actions;
TESTCsimpleRender, Figure)
{
std: :string text = "simple";
63
Kapitel 4
Das Seam-Modell
•
Anwendungstipp
I
Wenn Sie Link-Seams verwenden, sollten Sie den Unterschied zwischen Test-
und Produktionsumgebungen deutlich erkennbar machen.
4.3.3 Objekt-Seams
Objekt-Seams sind in objektorientierten Programmiersprachen wohl die nütz-
lichsten Seams. Aufrufe in einem objektorientierten Programm haben eine grund-
legende Eigenschaft: Sie definieren nicht, welche Methode tatsächlich ausgeführt
werden wird. Betrachten wir ein Java-Beispiel:
cell .RecalculateO;
Dieser Code lässt uns vermuten, bei diesem Aufruf werde eine Methode namens
Recalculate ausgeführt. Wenn das Programm lauffähig sein soll, muss es eine
Methode mit diesem Namen enthalten; doch tatsächlich können es mehrere sein.
64
4-3
Seam-Arten
cell.Recalculate();
Ohne das Objekt zu kennen, auf das cel 1 verweist, wissen wir dies einfach nicht.
Es könnten die Recal cul ate-Methode von Val ueCel 1 oder die von Formul aCel 1
sein. Es könnte sogar die Recal cul ate-Methode einer anderen Klasse sein, die
nicht von Cel 1 abgeleitet ist. (In diesem Fall wäre cel 1 ein besonders grausamer
Name für diese Variable!) Wenn wir in dieser Code-Zeile ändern können, welche
Recal cul ate-Methode aufgerufen wird, ohne den umgebenden Code zu ändern,
ist dieser Aufruf ein Seam.
cell.Recalculate();
In diesem Code erstellen wir ein Cell-Objekt und verwenden es dann in dersel-
ben Methode. Ist der Aufruf von Recal cul ate ein Objekt-Seam? Nein. Es gibt kei-
nen Aktivierungspunkt. Wir können nicht ändern, welche Recal cul ate-Methode
aufgerufen wird, weil die Auswahl von der Klasse des Cel 1 -Objekts abhängt. Die
Klasse des Cel 1 -Objekts wird festgelegt, wenn das Objekt erstellt wird; und das
können wir nicht ändern, ohne die Methode zu modifizieren.
cell.RecalculateO ;
Ist der Aufruf von cel 1 . Recal cul ate in bui 1 dMartSheet jetzt ein Seam? Ja. Wir
können in einem Test ein CustomSpreadsheet erstellen und bui 1 dMartSheet
mit einer Cel 1 unserer Wahl aufrufen. Wir haben es geschafft zu variieren, was
65
Kapitel 4
Das Seam-Modell
der Aufruf der cel 1. Recal cul ate-Methode bewirkt, ohne die Methode zu
ändern, von der sie aufgerufen wird.
Okay, die meisten Objekt-Seams sind ziemlich unkompliziert. Doch der folgende
Fall ist etwas komplizierter. Liegt beim Aufruf von Recal cul ate in dieser Version
von bui 1 dMartSheet ein Objekt-Seam vor?
Die Recal cul ate-Methode ist eine statische Methode. Ist der Aufruf von Recal -
cul ate in bui 1 dMartSheet ein Seam? Ja. Wir müssen bui 1 dMartSheet nicht
ändern, um bei diesem Aufruf Verhalten zu modifizieren. Wenn wir das Keyword
static von Recal cul ate löschen und sie nicht als private, sondern als pro-
tected Methode deklarieren, können wir sie beim Testen in Unterklassen über-
schreiben:
Recalculate(cell);
}
protected void RecalculateCCell cell) {
}
}
public class TestingCustomSpreadsheet extends CustomSpreadsheet {
protected void RecalculateCCell cell) {
}
}
66
4-3
Seam-Arten
Ist dies nicht alles ziemlich indirekt? Wenn uns eine Dependency nicht mehr
gefällt, warum ändern wir nicht einfach den Code an der entsprechenden Stelle?
Manchmal funktioniert das; aber bei besonders widerspenstigem Legacy Code ist
es oft am besten, den Code so wenig wie möglich zu ändern, wenn Sie Tests ein-
richten. Wenn Sie die möglichen Seams Ihrer Sprache kennen und nutzen kön-
nen, können Sie Tests oft sicherer als auf andere Weise einrichten.
Die beschriebenen Seams-Typen sind die Haupttypen. Sie sind in vielen Program-
miersprachen zu finden. Betrachten wir noch einmal das Beispiel vom Anfang
dieses Kapitels und welche Seams es enthält:
m_bSslInitialized = true;
FreeLibrary(m_hSslDlll);
m_hSslDl11=0;
FreeLi brary(m_hSslDl12);
m_hSslDl 12=0;
if (!m_bFailureSent) {
m_bFai1u reSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}
CreateLibrary(m_hSslDl 11,"syncesell.dll");
CreateLibrary(m_hSslD112,"syncese!2.dl 1");
m_hSslDl1l->Ini t();
m_hSslD112->Init() ;
return true;
67
Kapitel 4
Das Seam-Modell
2. Wir könnten eine #i ncl ude-Direktive für den Präprozessor in den Code einfü-
gen, um ein Makro namens PostRecei veError für unsere Tests zu definieren.
Es wäre also ein Präprozessor-Seam. Die Makro-Definition könnten wir mit
einem Präprozessor aktivieren oder deaktivieren.
3. Wir könnten auch eine virtuelle Funktion für PostRecei veError deklarieren,
wie wir dies am Anfang dieses Kapitels getan haben. Es liegt hier also auch ein
Objekt-Seam vor. In diesem Fall ist der Aktivierungspunkt die Stelle, an der wir
ein Objekt erstellen wollen. Wir könnten entweder ein CAsyncSsl Ree-Objekt
oder ein Objekt einer Test-Unterklasse erstellen, die PostRecei veError über-
schreibt.
Eigentlich erstaunlich, dass es so viele Möglichkeiten gibt, das Verhalten bei die-
sem Aufruf zu ersetzen, ohne die Methode zu bearbeiten:
bool CAsyncSslRec::Init()
{
if (!m_bFailureSent) {
m_bFai1u reSent=TRUE;
PostReceiveError(SOCKETCALLBACK, SSL_FAILURE);
}
return true;
}
Wollen Sie Teile des Codes unter Testkontrolle bringen, müssen Sie den richtigen
Seam-Typ auswählen. In objektorientierten Sprachen sind im Allgemeinen Objekt-
Seams die beste Wahl. Präprozessor-Seams und Link-Seams können manchmal nütz-
lich sein, aber sie sind nicht so explizit wie Objekt-Seams. Außerdem kann es
schwierig sein, Tests zu warten, die von ihnen abhängen. Ich behalte mir Präpro-
zessor-Seams und Link-Seams am liebsten für Fälle vor, in denen Dependencies den
gesamten Code durchdringen und es keine besseren Alternativen gibt.
Wenn Sie sich daran gewöhnt haben, Code unter dem Aspekt von Seams zu
betrachten, können Sie leichter erkennen, wie Sie ihn testen können und wie Sie
den neuen Code strukturieren sollten, um das Testen zu vereinfachen.
68
Tools
Welche Tools brauchen Sie für Ihre Arbeit mit Legacy Code? Sie brauchen einen
Editor (oder eine IDE) und Ihr Build-System sowie ein Test-Framework. Etwaige
Refactoring-Tools für Ihre Sprache können ebenfalls sehr hilfreich sein.
In diesem Kapitel beschreibe ich einige der Tools, die gegenwärtig verfügbar sind,
und die Rolle, die sie bei Ihrer Arbeit mit Legacy Code spielen können.
Bei diesen vielen Tools sollte Refactoring eigentlich viel einfacher sein. In einigen
Umgebungen ist dies auch der Fall. Leider unterscheiden sich viele dieser Tools
im Umfang ihrer Refactoring-Unterstützung. Erinnern wir uns noch einmal
daran, wie Martin Fowler in seinem Buch Refactoring: Improving the Design of Exis-
ting Code (Addison-Wesley 1999) Refactoring definiert:
Refactoring (Subst.). Eine Änderung der internen Struktur von Software, um sie
verständlicher und änderungsfreundlicher zu machen, ohne ihr vorhandenes
Verhalten zu ändern.
Eine Änderung ist nur dann ein Refactoring, wenn sie kein Verhalten ändert. Re-
factoring-Tools sollten verifizieren, dass eine Änderung Verhalten nicht modifi-
69
Kapitel 5
Tools
ziert; und viele tun dies auch. Dies war eine Hauptregel im Smalltalk
Refactoring Browser, in der Arbeit von Bill Opdyke und in vielen der ersten Java-
Refactoring-Tools. Doch an den Rändern führen einige Tools keine echten
Prüfungen durch - und wenn diese nicht prüfen, könnten Sie beim Refactoring
subtile Bugs einführen.
Deswegen sollten Sie Ihre Refactoring-Tools sorgfältig auswählen. Stellen Sie fest,
was die Tool-Entwickler über die Sicherheit ihrer Tools sagen. Führen Sie eigene
Tests durch. Wenn ich ein neues Refactoring-Tool kennen lerne, führe ich oft
kleine Plausibilitätsprüfungen durch. Wenn Sie versuchen, eine Methode zu extra-
hieren, und ihr den Namen einer bereits in dieser Klasse existierenden Methode
geben, wird dies als Fehler angezeigt? Was passiert, wenn Sie den Namen einer
Methode in einer Basisklasse verwenden? Entdeckt das Tool diesen Fall? Falls
nicht, könnten Sie aus Versehen eine Methode überschreiben und den Code
beschädigen.
In diesem Buch beschreibe ich das Arbeiten mit und ohne automatische Refacto-
ring-Unterstützung. In den Beispielen erwähne ich, ob ich davon ausgehe, dass
ein Refactoring-Tool zur Verfügung steht.
In allen Fällen nehme ich an, dass die Refactorings durch das Tool Verhalten
bewahren. Stellen Sie fest, dass die automatischen Refactorings Ihres Tools dies
nicht leisten, nutzen Sie diese Refactorings nicht. Befolgen Sie dann der Sicher-
heit halber die Ratschläge für Fälle, in denen kein Refactoring-Tool zur Verfügung
steht.
70
5-2
Mock-Objekte
Es ist sinnvoll, Ihren Code mit Tests abzusichern, bevor Sie die automatischen
Refactorings ausführen. Sie können einige automatische Refactorings ohne
Tests durchführen, aber Sie müssen wissen, was das Tool prüft und was nicht.
Wenn ich zum ersten Mal ein neues Tool einsetze, prüfe ich zunächst, wie gut
es das Extrahieren von Methoden unterstützt. Wenn ich ihm so weit traue, dass
ich es ohne Tests einsetzen kann, kann ich den Code in einen viel besser testba-
ren Zustand bringen.
5.2 Mock-Objekte
Dependencies sind eines der großen Probleme beim Arbeiten mit Legacy Code.
Wenn wir ein Code-Fragment isoliert ausführen wollen, müssen wir oft Depen-
dencies von anderem Code aufheben. Aber das ist selten einfach. Wenn wir den
anderen Code entfernen, müssen wir an seiner Stelle Komponenten haben, die
uns beim Testen die richtigen Werte liefern, damit wir unsere Code-Fragmente
gründlich testen können. Bei objektorientiertem Code bezeichnen wir diese Kom-
ponenten oft als Mock-Objekte.
Kapitel 5
Tools
5.3 Unit-Test-Harnische
Test-Tools haben eine lange und bewegte Geschichte. In jedem Jahr lerne ich vier
oder fünf Teams kennen, die teure Lizenzen für ein Test-Tool erworben haben, das
seinen Preis nicht wert ist. Um fair zu den Tool-Anbietern zu sein: Testen ist ein
schwieriges Problem; und Entwickler lassen sich oft von dem Gedanken verfüh-
ren, sie könnten ein GUI oder eine Web-Schnittstelle testen, ohne mit ihrer
Anwendung etwas Besonderes zu machen. Möglich ist es; aber normalerweise
bedeutet es mehr Arbeit, als jemand in einem Team zugestehen möchte. Außer-
dem ist eine Benutzerschnittstelle oft nicht der Platz für Tests. UIs werden häufi-
ger verändert und sind zu weit von der zu testenden Funktionalität entfernt. Wenn
Ul-basierte Tests scheitern, kann es schwer sein, den Grund zu finden. Dennoch
geben Entwickler oft beträchtliche Summen für den Versuch aus, alle ihre Tests
mit dieser Art von Tools durchzuführen.
Die wirksamsten Test-Tools, die ich kennen gelernt habe, waren kostenlos. Das
erste ist das xUnit-Test-Framework. Es wurde ursprünglich von Kent Beck in
Smalltalk geschrieben und dann von Kent Beck und Erich Gamma nach Java por-
tiert. xUnit hat ein für ein Unit-Test-Framework kleines, leistungsstarkes Design.
Hier sind seine Schlüsselfunktionen:
Das xUnit-Framework ist für die meisten führenden und eine Reihe recht exoti-
scher Sprachen portiert worden.
Konzeptionell ist das Design von xUnit revolutionär; denn es wird von zwei Ideen
beherrscht: Einfachheit und Fokus. Sie können damit ohne Wenn und Aber Tests
schreiben. Obwohl xUnit ursprünglich für Unit-Tests konzipiert wurde, können
Sie damit auch größere Tests schreiben, weil der Umfang eines Tests in xUnit
keine Rolle spielt. Wenn der Test in der Sprache geschrieben werden kann, die Sie
verwenden, kann xUnit ihn ausführen.
Die meisten Beispiele in diesem Buch verwenden Java und C++. In Java ist JUnit
der bevorzugte xUnit-Harnisch. Er sieht den meisten anderen xUnits sehr ähnlich.
In C++ habe ich oft einen eigenen Test-Harnisch, CppUnitLite, verwendet. Er ist
anders aufgebaut und wird in diesem Kapitel ebenfalls beschrieben. Cpp UnitLite
ist eine einfachere Version von Cpp Unit, das ich vor Jahren geschrieben habe und
72
5-3
Unit-Test-Harnische
5.3.1 JUnit
In JUnit schreiben Sie Tests, indem Sie von einer Klasse namens TestCase Unter-
klassen ableiten:
import junit.framework.*;
Jede Methode in einer Testklasse definiert einen Test, wenn sie eine Signatur der
Form voi dtestXXXO hat, wobei XXX der Name ist, den Sie dem Test geben wol-
len. Jede Testmethode kann Code und Zusicherungen enthalten. Die obige
testEmpty-Methode enthält Code, um ein neues Formul a-Objekt zu erstellen und
seine value-Methode aufzurufen. Der Code der Zusicherung (assertEqual s)
prüft, ob der Rückgabewert den Wert 0 hat. Ist dies der Fall, wird der Test erfolg-
reich, andernfalls scheitert er.
Kurz ausgedrückt, passiert bei der Ausführung von JUnit-Tests Folgendes: Der
JUnit-Test-Runner lädt eine Testklasse wie die weiter vorne gezeigte und ermittelt
dann per Reflection alle ihre Test-Methoden. Seine nächste Aktion ist ziemlich raf-
finiert. Er erstellt für jede dieser Test-Methoden ein vollkommen separates Objekt.
Aus dem vorhergehenden Code erstellt er zwei Objekte: ein Objekt für die Ausfüh-
rung der testEmpty-Methode und ein Objekt für die Ausführung der testDi gi t-
Methode. In beiden Fällen ist die Klasse der Objekte dieselbe: Formul aTest. Jedes
Objekt wird so konfiguriert, dass es genau eine der Test-Methoden von Formul a-
Test ausführt. Der Schlüssel liegt darin, dass wir für jede Methode mit einem voll-
kommen separaten Objekt arbeiten. Es gibt keine Möglichkeit, dass sich die
Methoden gegenseitig beeinflussen. Hier ist ein Beispiel.
rotected void s e t U p O {
ployee = new Employee("Fred", 0, 10);
ate cardDate = new TDate(10, 10, 2000);
ployee.addTimeCard(new TimeCard(cardDate,40));
}
73
Kapitel 5
Tools
Wenn Sie zum ersten Mal einen xUnit-Harnisch sehen, wirkt er wahrscheinlich
etwas seltsam. Warum haben Testfall-Klassen überhaupt setUp- und tearDown-
Methoden? Warum können wir nicht einfach die Objekte erstellen, die wir in dem
Konstruktor benötigen? Nun ja, wir könnten dies tun; doch denken Sie daran, was
der Test-Runner mit Testfall-Klassen macht. Er erstellt für jede Testfall-Klasse
einen Satz von Objekten, eins pro Test-Methode. Dies sind zahlreiche Objekte;
doch dies ist nicht so schlimm, wenn diese Objekte noch nicht alloziert haben, was
sie brauchen. Indem wir den Code in setUp einfügen, um zu erstellen, was wir
brauchen, wenn wir es brauchen, sparen wir Ressourcen. Außerdem können wir
setUp auch so verzögern, dass es erst ausgeführt wird, wenn wir mögliche Pro-
bleme beim Einrichten erkennen und berichten können.
5.3.2 CppUnitLite
Als ich CppUnit zum ersten Mal portierte, wollte ich es so weit wie möglich JUnit
angleichen. Weil ich meinte, es wäre für Entwickler einfacher, die bereits die
xUnit-Architektur kannten, schien mir dies das Richtige zu sein. Doch fast sofort
74
5-3
Unit-Test-Harnische
stieß ich auf eine Reihe von Dingen, die man wegen der Unterschiede zwischen
den C++- und Java-Funktionen in C++ kaum oder gar nicht sauber implementie-
ren kann. Das Hauptproblem: In C++ gibt es keine Reflection. In Java können Sie
eine Referenz der Methoden einer abgeleiteten Klasse speichern, Methoden zur
Laufzeit finden usw. In C++ müssen Sie Code schreiben, um die Methoden zu
registrieren, auf die Sie zur Laufzeit zugreifen wollen. Folglich wurde es etwas
schwerer, CppUnit anzuwenden und zu verstehen. Man musste für eine Test-
klasse eine eigene Suite-Funktion schreiben, damit der Test-Runner Objekte für
einzelne Methoden ausführen konnte.
Test *EmployeeTest::suite()
{
TestSuite *suite = new TestSuite;
sui te. addTest(new TestCaner<EmployeeTest>("testNormal Pay", testNormal Pay));
suite.addTest(new TestCaner<EmployeeTest>("testOvertime", testOvertime));
return suite;
}
Natürlich wird dies ziemlich mühsam. Es ist schwer, den Schwung für das Schrei-
ben von Tests zu bewahren, wenn Sie Test-Methoden in einem Klassen-Header
deklarieren, sie in einer Quelldatei definieren und sie dann in einer Suite-Methode
registrieren müssen. Mit verschiedenen Makros können diese Probleme gelöst
werden; aber ich beschloss, neu anzufangen. Schließlich fand ich eine Lösung,
mit der man einen Test mit der folgenden einfachen Quelldatei schreiben kann:
#include "testharness.h"
#include "employee.h"
#include <memory>
Dieser Test prüft mit einem Makro namens L0NGS_EQUAL die Gleichheit zweier
langer Ganzzahlen. Das Makro verhält sich genau wie die Methode assertEqual s
in JUnit, ist aber auf long-Werte zugeschnitten.
Das TEST-Makro führt im Hintergrund einige hässliche Dinge aus. Es erstellt eine
Unterklasse einer Test-Klasse und gibt ihr einen Namen, der aus zwei Argumen-
ten zusammengesetzt wird (dem Namen des Tests und dem Namen der zu testen-
den Klasse). Dann erstellt es eine Instanz dieser Unterklasse, die so konfiguriert
wird, dass der Code in geschweiften Klammern ausgeführt wird. Die Instanz ist
statisch; wenn das Programm geladen wird, fügt es sich selbst in eine statische
75
Kapitel 5
Tools
Liste von Test-Objekten ein. Später kann ein Test-Runner die Liste abarbeiten und
jeden Test ausführen.
Nachdem ich dieses kleine Framework geschrieben hatte, beschloss ich, es nicht
zu veröffentlichen, weil der Code in dem Makro nicht besonders klar war und ich
viel Zeit damit verbringe, Entwickler davon zu überzeugen, klareren Code zu
schreiben. Einer meiner Freunde, Mike Hill, stieß auf einige derselben Probleme,
bevor wir uns kennen lernten, und erstellte ein Microsoft-spezifisches Test-Frame-
work namens TestKit, das die Registrierung auf gleiche Weise löste. Von Mike
ermutigt, begann ich, die Anzahl der neueren C++-Funktionen zu reduzieren, die
in meinem kleinen Framework verwendet wurden, und dann veröffentlichte ich
es. (Diese Probleme waren in CppUnit schwerwiegend. Fast jeden Tag erhielt ich
E-Mails von Entwicklern, die Templates, Ausnahmen oder die Standard-Bibliothek
mit ihrem C++-Compiler nicht verwenden konnten.)
Sowohl CppUnit und CppUnitLite eignen sich als Test-Harnische. Weil Tests in
CppUnitLite etwas kürzer sind, verwende ich sie in den C++-Beispielen in diesem
Buch.
5.3.3 NUnit
NUnit ist ein Test-Framework für .NET-Sprachen. Sie können Tests für C#,
VB.NET oder andere Sprachen schreiben, die auf der .NET-Plattform laufen. Die
Arbeitsweise von NUnit ähnelt der von JUnit sehr stark. Der eine wichtige Unter-
schied besteht darin, dass es Test-Methoden und Test-Klassen mit Attributen mar-
kiert. Die Syntax der Attribute hängt von der .NET-Sprache ab, in der die Tests
geschrieben sind.
Imports NUnit.Framework
End Class
76
5-4
Allgemeine Test-Harnische
Damit Sie Fit nutzen können, müssen Sie Code für die Handhabung der Tabellen
anpassen, damit er Ihre Code-Fragmente ausführen und Ergebnisse davon abru-
fen kann. Im Allgemeinen ist dies ziemlich leicht, weil das Framework Code zur
Verfügung stellt, der verschiedene Tabellentypen unterstützt.
Fit hat eine besonders leistungsstarke Eigenschaft: Es fördert die Kommunikation
zwischen Entwicklern und Designer. Designer spezifizieren, was die Software tun
soll, und können Dokumente schreiben und echte Tests darin einbetten. Die Tests
werden ausgeführt, aber nicht bestanden. Später können Entwickler die Funktio-
nen hinzufügen, damit die Tests bestanden werden. Sowohl Anwender als auch
Entwickler können sich regelmäßig ein aktuelles Bild von den Fähigkeiten des
Systems machen.
77
Kapitel 5
Tools
Aus Platzgründen kann ich hier leider nicht mehr über Fit schreiben. Näheres
erfahren Sie unter http: / / f i t. c2. com.
5.4.2 Fitnesse
Fitnesse ist im Wesentlichen Fit, das in einem Wiki gehostet wird. Es wurde haupt-
sächlich von Robert Martin und Micah Martin entwickelt. Ich habe ein klein wenig
damit gearbeitet, aber dann aufgehört, um mich auf dieses Buch zu konzentrie-
ren. Doch danach möchte ich mich wieder damit befassen.
78
Teil II
Software ändern
In diesem Teil:
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern 81
Kapitel 7
Änderungen brauchen eine Ewigkeit 101
Kapitel 8
Wie fuge ich eine Funktion hinzu? 109
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen . . . 127
Kapitel 10
Ich kann diese Methode nicht in einem Test-Harnisch ausfuhren 159
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden
sollte ich testen? 173
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die
Dependencies für alle beteiligten Klassen aufheben? 193
Kapitel 13
Ich muss etwas ändern, weiß aber nicht, welche Tests ich
schreiben soll 205
Kapitel 14
Dependencies von Bibliotheken bringen mich um 217
Kapitel 15
Meine Anwendung besteht nur aus API-Aufrufen 219
Kapitel 16
Ich verstehe den Code nicht gut genug, um ihn zu ändern 227
Kapitel 17
Meine Anwendung hat keine Struktur 233
Kapitel 18
Der Test-Code ist im Weg 243
Kapitel 1 9
Mein Projekt ist nicht objektorientiert. Wie kann ich es
sicher ändern? 247
Kapitel 20
Diese Klasse ist zu groß und soll nicht noch größer werden . . . . 261
Kapitel 21
Ich ändere im ganzen System denselben Code 283
Kapitel 2 2
Ich muss eine Monster-Methode ändern und kann keine
Tests dafür schreiben 301
Kapitel 2 3
Wie erkenne ich, dass ich nichts kaputtmache? 319
Kapitel 2 4
Wir fühlen uns überwältigt. Es wird nicht besser. 329
79
Kapitel 6
Stellen wir uns den Tatsachen: Dieses Buch, das Sie gerade lesen, beschreibt
zusätzliche Arbeit - Arbeit, die Sie im Moment wahrscheinlich nicht leisten und
die bedeuten könnte, dass die Änderungen an Ihrem Code noch länger dauern.
Lohnt es sich überhaupt, sich gerade jetzt mit diesen Dingen zu befassen?
Die Wahrheit ist: Die Arbeit, die Sie investieren, um Dependencies aufzuheben
und Tests für Ihre Änderungen zu schreiben, braucht Zeit; aber in meisten Fällen
sparen Sie damit letztlich Zeit - und Frustration. Wann? Nun, das hängt von dem
Projekt ab. In einigen Fällen können Sie Tests für Code schreiben, den Sie ändern
müssen; und das dauert vielleicht zwei Stunden. Die Änderung danach braucht
vielleicht 15 Minuten. Wenn Sie dann Ihre Erfahrung beurteilen, fragen Sie sich
vielleicht: »Ich habe gerade zwei Stunden verschwendet - hat es sich gelohnt?«
Hängt davon ab. Sie wissen nicht, wie lange diese Arbeit ohne Tests gedauert hätte.
Sie wissen auch nicht, wie lange das Debugging bei einem Fehler gedauert hätte,
Zeit, die Sie mit Tests hätten sparen können. Ich meine nicht nur die Zeit, die Sie
sparen würden, wenn die Tests den Fehler abfangen würden, sondern auch die
Zeit, die Sie mit Tests sparen würden, um einen Fehler zu lokalisieren. Ist der
Code von Tests umgeben, ist es oft einfacher, funktionale Probleme einzugrenzen.
Was ist mit den Kosten? Angenommen, wir machten eine einfache Änderung und
schrieben Tests zu unserer Unterstützung. Wenn wir jetzt alle unsere Änderun-
gen korrekt durchführen, haben sich dann die Tests gelohnt? Sie wissen nicht,
wann Sie in diesem Code-Fragment eine weitere Änderung vornehmen. Im besten
Fall gehen Sie bereits bei der nächsten Iteration zu diesem Code zurück; dann
zahlt sich Ihre Investition schnell aus. Im schlimmsten Fall wird dieser Code erst
wieder Jahre später modifiziert. Doch wahrscheinlich werden Sie den Code immer
wieder lesen, um herauszufinden, ob Sie eine Änderung dort oder an einer ande-
ren Stelle machen müssen. Wäre der Code nicht einfacher zu verstehen, wenn die
Klassen kleiner wären und es Unit-Tests gäbe? Wahrscheinlich. Aber dies ist nur
der Worst Case. Wie oft kommt er vor? Üblicherweise häufen sich Änderungen in
Systemen an bestimmten Stellen. Wenn Sie den Code heute an einer Stelle
ändern, müssen Sie ihn wahrscheinlich bald auch an einer benachbarten Stelle
modifizieren.
81
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
Ich beginne meine Arbeit mit neuen Teams oft mit einem Experiment. Eine Itera-
tion lang versuchen wir, den Code nicht ohne entsprechende Tests zu ändern.
Meint jemand, er könne keinen Test schreiben, muss er ein kurzes Meeting des
Teams einberufen, um dieses Problem mit der Gruppe zu diskutieren. Am Anfang
sind diese Iterationen schrecklich. Die Entwickler haben das Gefühl, mit ihrer
eigentlichen Arbeit nicht fertig zu werden. Aber langsam entdecken sie, dass sie
immer besseren Code vorfinden. Ihre Änderungen gehen leichter voran, und sie
spüren, dass dies die richtige Methode ist, um besser voranzukommen. Es dauert
etwas, bis ein Team diese Anfangshürde überwindet. Doch wenn es etwas gäbe,
was ich jedem Team auf der Welt mitgeben könnte, wäre es das gemeinsame
Erlebnis, das sich auf den Gesichtern widerspiegelt: »Toll, dahin gehen wir nicht
mehr zurück.«
Wenn Sie diese Erfahrung noch nicht gemacht haben, müssen Sie es nachholen.
Letztlich beschleunigen Tests Ihre Arbeit, und das ist fast in jedem Entwicklungs-
unternehmen wichtig. Aber offen gestanden, als Programmierer bin ich schon
zufrieden, dass die Arbeit viel weniger frustrierend ist.
Wenn Sie dieses Hindernis überwunden haben, ist das Leben zwar nicht komplett
rosig, aber es ist besser. Wenn Sie den Wert des Testens erfahren und den Unter-
schied zur alten Arbeitsweise erkannt haben, müssen Sie nur noch nüchtern ent-
scheiden, was Sie in jedem bestimmten Fall tun sollten.
Das Schwierigste an der Entscheidung, unter Druck Tests zu schreiben oder nicht,
ist die Tatsache, dass Sie vielleicht nicht wissen, wie lange es dauern wird, die
Funktion hinzuzufügen. Bei Legacy Code ist es besonders schwierig, sinnvolle
Schätzungen abzugeben. Einige Techniken können Ihnen helfen. Wenn Sie wirk-
lich nicht wissen, wie lange es dauern wird, sollten Sie einen Blick in Kapitel 16
werfen, Ich verstehe den Code nicht gut genug, um ihn zu ändern. Wenn Sie wirklich
82
6.1
Sprout Method
nicht wissen, wie lange es dauern wird, eine Funktion hinzuzufügen, und befürch-
ten, Ihre Zeit werde nicht ausreichen, ist es verlockend, die Funktion so schnell Sie
können zusammenzuschustern. Falls Sie dann genügend Zeit haben, können Sie
zurückgehen und einige Tests und Refactorings durchführen. Doch Letzteres ist
das eigentlich Schwierige. Bevor Entwickler das Anfangshindernis überwunden
haben, vermeiden sie oft diese Arbeit. Es kann ein moralisches Problem sein. In
Kapitel 24, Wir fühlen uns überwältigt. Es wird nicht besser, finden Sie einige kon-
struktive Methoden, um Fortschritte zu machen.
Was ich bis jetzt beschrieben habe, scheint ein echtes Dilemma zu sein: Zahlen
Sie jetzt oder zahlen Sie später mehr. Entweder schreiben Sie Tests, während Sie
Ihre Änderungen durchführen, oder Sie leben mit der Tatsache, dass es im Laufe
der Zeit immer schwieriger wird. So schwierig kann es sein; aber manchmal ist es
das nicht.
Wenn Sie jetzt sofort eine Klasse ändern müssen, versuchen Sie, die Klasse in
einem Test-Harnisch zu instanziieren. Können Sie dies nicht, sollten Sie zuerst
Kapitel 9, Ich kann diese Klasse nicht in einen Test-Harnisch einfügen, oder Kapitel 10,
Ich kann diese Methode nicht in einem Test-Harnisch ausführen, lesen. Den zu
ändernden Code in einem Test-Harnisch einzufügen, könnte einfacher sein, als
Sie denken. Wenn Sie danach zu dem Schluss kommen, dass Sie sich im Moment
wirklich nicht leisten können, Dependencies aufzuheben und Tests einzurichten,
studieren Sie die geforderten Änderungen. Könnten Sie dafür frischen Code
schreiben? In vielen Fällen ist dies möglich. Im Rest dieses Kapitels werden meh-
rere einschlägige Techniken dafür beschrieben.
Überlegen Sie, ob Sie diese Techniken einsetzen könnten, vergessen Sie aber
nicht, dass dies sorgfältig geschehen muss. Wenn Sie sie einsetzen, fügen Sie
getesteten Code in Ihr System ein; aber solange Sie den Code, der Ihren neuen
Code aufruft, nicht durch Tests abdecken, testen Sie die Anwendung des neuen
Codes nicht. Gehen Sie vorsichtig vor!
83
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
Wir müssen Code hinzufügen, um zu verifizieren, dass sich keiner der neuen Ein-
träge bereits in t r a n s a c t i o n B u n d l e befindet, bevor wir ihre Daten veröffentli-
chen und sie hinzufügen. Ein Blick auf den Code zeigt, dass dies am Anfang der
Methode vor der Schleife geschehen muss. Doch tatsächlich könnte es in der
Schleife passieren. Wir könnten den Code wie folgt ändern:
Diese Änderung scheint einfach zu sein, ist aber ziemlich invasiv. Wie können wir
erkennen, dass wir alles richtig machen? Unser neuer Code und der alte Code sind
nicht getrennt. Noch schlimmer: Der Code wird unübersichtlicher. Wir vermen-
gen hier zwei Operationen: die Veröffentlichung des Datums und die Entdeckung
doppelter Einträge. Diese Methode ist ziemlich kurz, aber bereits jetzt ist sie etwas
schwerer zu durchschauen; außerdem haben wir eine temporäre Variable einge-
führt. Solche Variablen sind nicht unbedingt schlecht, aber manchmal ziehen sie
neuen Code an. Sollten wir bei der nächsten erforderlichen Änderung alle nicht-
doppelten Einträge manipulieren müssen, bevor sie hinzugefügt werden, gibt es
nur eine Stelle in dem Code, an der eine entsprechende Variable existiert: direkt in
dieser Methode. Es wird verlockend sein, diesen Code ebenfalls hier einzufügen.
Hätten wir auch eine andere Methode verwenden können?
Ja. Wir können das Entfernen doppelter Einträge als vollkommen separate Opera-
tion behandeln. Mit dem Test-Driven Development (8.1) können wir eine neue
Methode namens uniqueEntries erstellen:
84
6.1
Sprout Method
Wir benutzen hier eine neue temporäre Variable, aber der Code ist viel übersichtli-
cher. Wenn wir weiteren Code hinzufügen müssen, der mit nicht-doppelten Ein-
trägen arbeitet, können wir für diesen Code ebenfalls eine Methode erstellen und
sie von hier aufrufen. Und wenn der Code für nicht-doppelte Einträge noch
umfangreicher wird, können wir eine Klasse für alle diese neuen Methoden ein-
führen. Netto sorgen wir dafür, dass diese Methode klein bleibt und dass insge-
samt alle Methoden kürzer, verständlicher und übersichtlicher sind.
Dies war ein Beispiel für Sprout Method. Hier sind die erforderlichen Schritte die-
ser Technik:
x. Identifizieren Sie die Stellen, an denen Sie Ihren Code ändern müssen.
2. Wenn Sie die Änderung als eine einzige Sequenz von Anweisungen an einer
Stelle in einer Methode schreiben können, fügen Sie einen Aufruf für eine neue
Methode ein, die die betreffende Arbeit leistet, und kommentieren Sie ihn dann
aus. (Ich ziehe es vor, diesen Aufruf zu schreiben, bevor ich die Methode selbst
schreibe, um ein Gefühl dafür zu bekommen, wie der Methodenaufruf im Kon-
text aussieht.)
3. Stellen Sie fest, welche lokalen Variablen Sie aus der Quellmethode brauchen,
und fügen Sie sie als Argumente des Aufrufs ein.
85
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
Nun zu den Vorteilen. Mit Sprout Method können Sie neuen und alten Code sauber
trennen. Selbst wenn Sie den alten Code nicht sofort unter Testkontrolle bringen,
86
6.2
Sprout Class
können Sie wenigstens Ihre Änderungen separat verwalten; und Sie haben eine
saubere Schnittstelle zwischen dem neuen und dem alten Code. Sie sehen alle
betroffenen Variablen, und dies kann es einfacher machen zu beurteilen, ob der
Code im richtigen Kontext steht.
In solchen Fällen können Sie eine andere Klasse für Ihre Änderungen erstellen
und sie von der Quellklasse aus nutzen. Betrachten wir ein vereinfachtes Beispiel.
Hier ist eine antike Methode aus einer C++-Klasse namens Quarterl yReportCe-
nerator:
std::string QuarterlyReportCenerator::generate()
{
std::vector<Result> results = database.queryResults(beginDate,
endDate);
std::string pageText;
87
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
} eise {
pageText += "No results for this period";
}
pageText += "</table>";
pageText += "</body>";
pageText += "</html>";
return pageText;
}
"<trxtd>Department</tdxtd>Manager</td><td>Profi t</tdxtd>Expenses</
tdx/tr>"
Nehmen wir weiter an, diese Klasse sei riesig; deshalb würde es über einen Tag
dauern, die Klasse in einen Test-Harnisch einzufügen, und so viel Zeit hätten wir
im Moment nicht.
class QuarterlyReportTableHeaderProducer
{
public:
string makeHeader() ;
};
string QuarterlyReportTableProducer::makeHeader()
{
return "<trxtd>Department</tdxtd>Manager</tdxtd>Profit</
tdxtd>Expenses</td>";
}
Danach können wir sie instanziieren und das Objekt direkt in Quarterl yReport-
Cenerator: :generate() aufrufen:
QuarterlyReportTableHeaderProducer producer;
pageText += producer.makeHeaderO ;
Doch mal ehrlich: Ist es nicht lächerlich, für eine solch winzige Änderung eine
neue Klasse zu erstellen, die das Design nicht verbessert, ein ganz neues Konzept
einführt und den Code unübersichtlicher macht? Nun, an diesem Punkt stimmt
das. Wir tun dies nur, um eine nachteilige Dependency aufzuheben. Doch
schauen wir uns die Sache näher an.
88
6.2
Sprout Class
class QuarterlyReportTableHeaderCenerator
{
public:
string generateO;
};
Jetzt gehört die Klasse zu einem Konzept, mit dem wir vertraut sind. Quarterl y-
ReportTabl eHeaderGenerator ist ein Generator, genau wie QuarterlyReport-
Generator. Beide verfügen über eine generateO-Methode, die einen String
zurückgibt. Wir können diese Gemeinsamkeit in dem Code fixieren, indem wir
eine Schnittstellenklasse erstellen und beide Generator-Klassen davon ableiten:
class HTMLCenerator
{
public:
Virtual -HTMLGeneratorO = 0;
V i r t u a l string generateO = 0;
};
class QuarterlyReportTableHeaderCenerator : public HTMLCenerator
{
public:
V i r t u a l string generateO ;
};
class QuarterlyReportCenerator : public HTMLCenerator
{
public:
In diesem Fall konnten wir die Klasse schnell in den Satz der vorhandenen Kon-
zepte der Anwendung einbetten. In vielen anderen Fällen ist dies nicht möglich,
aber das bedeutet nicht, dass wir uns zurückhalten sollten. Einige Sprout-Klassen
lassen sich nicht in die Hauptkonzepte der Anwendung einbetten, sondern führen
neue ein. Vielleicht erstellen Sie eine Sprout-Klasse und glauben, sie sei für Ihr
Design ziemlich unbedeutend, bis Sie an anderer Stelle etwas Ähnliches tun und
die Ähnlichkeit erkennen. Manchmal können Sie den duplizierten Code aus
89
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
neuen Klassen per Refactoring herauslösen, und oft müssen Sie sie umbenennen,
aber erwarten Sie nicht, dass alles auf einmal passiert.
Oft schätzen Sie eine Sprout-Klasse nach einigen Monaten ganz anders ein als bei
ihrer Erstellung. Die bloße Existenz dieser neuen Klasse in Ihrem System gibt viel
zum Nachdenken. Wenn Sie eine Änderung in ihrer Nachbarschaft machen müs-
sen, überlegen Sie vielleicht, ob die Änderung zu dem neuen Konzept gehört oder
ob das Konzept ein wenig geändert werden muss. Dies gehört alles zum laufenden
Prozess des Designs.
Im Wesentlichen führen uns zwei Fälle zu Sprout Class: Erstens wollen Sie eine
ganz neue Aufgabe zu einer Ihrer Klassen hinzufügen. Angenommen, eine Steu-
ererklärungs-Software solle bestimmte Abschreibungen ab einem bestimmten
Datum des Jahres nicht mehr zulassen. Sollten Sie eine Datumsprüfung in die
TaxCalcul ator-Klasse einfügen? Oder gehört diese eigentlich nicht zu der
Hauptaufgabe von TaxCal cul ator, nämlich Steuern zu berechnen? Sollte die
Prüfung vielleicht in einer neuen Klasse erfolgen? Zweitens wollen Sie die Funk-
tionalität ein wenig erweitern. Sie können die Erweiterung in eine vorhandene
Klasse einfügen, können die Klasse aber nicht in einen Test-Harnisch einfügen.
Wenn wir sie wenigstens in einem Harnisch kompilieren könnten, könnten wir
versuchen, Sprout Method anzuwenden, aber manchmal haben wir nicht einmal so
viel Glück.
Auch wenn das Motiv in diesen beiden Fällen verschieden ist, unterscheiden sie
sich vom Ergebnis nicht sehr. Ob eine neue Funktionalität umfangreich genug ist,
um als neue Aufgabe bezeichnet zu werden, ist eine Frage der Bewertung. Und
weil sich Code im Laufe der Zeit ändert, erweist sich die Entscheidung, eine
Sprout-Klasse zu erstellen, im Nachhinein oft als besser.
90
6.3
Wrap Method
5. Entwickeln Sie zuerst Tests der Sprout-Klasse unter Verwendung von Test-Dri-
ven Development (8.1).
6. Entfernen Sie den Kommentar in der Quellmethode, um die Erstellung des
Objekts und den Aufruf zu aktivieren.
Der Hauptnachteil von Sprout Class ist die konzeptionelle Komplexität. Wenn sich
Programmierer in eine neue Code-Basis einarbeiten, entwickeln sie ein Gefühl
dafür, wie die zentralen Klassen zusammenarbeiten. Mit Sprout Class fangen Sie
an, die Abstraktionen zu entkernen, und verlagern den Hauptteil der Arbeit in
andere Klassen. Gelegentlich ist dies genau richtig. Manchmal bleibt Ihnen nichts
anderes übrig. Dinge, die idealerweise in der einen Klasse geblieben wären, lan-
den in Sprout-Klassen, nur um eine sichere Änderung zu ermöglichen.
Sie können Verhalten auch ohne zeitliche Kopplung hinzufügen, etwa mit Sprout
Method; aber eine andere Technik ist gelegentlich auch sehr nützlich: Wrap
Method. Ein einfaches Beispiel.
91
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
92
6.3
Wrap Method
Mit dieser Technik können Sie also Verhalten zu vorhandenen Aufrufen der
ursprünglichen Methode hinzufügen. Wenn Sie bei jedem Client-Aufruf von
pay() einen Protokollsatz erstellen wollen, kann diese Technik sehr nützlich sein.
Mit einer anderen Form von Wrap Method können Sie einfach eine neue Methode
hinzufügen, die noch nicht aufgerufen wird. Wenn das Protokollieren in dem vor-
hergehenden Beispiel öffentlich (von außen zugänglich) sein soll, könnten wir
eine makeLoggedPayment-Methode zu Employee hinzufügen:
public void p a y O {
logPaymentO;
Money amount = calculatePayO;
dispatchPayment(amount);
}
93
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
Hier sind die Schritte für die erste Version von Wrap Method:
1. Identifizieren Sie eine Methode, die Sie ändern müssen.
2. Wenn Sie die Änderung als eine einzige Sequenz von Anweisungen an einer
Stelle schreiben können, geben Sie der Methode einen anderen Namen und
erstellen dann eine neue Methode mit demselben Namen und derselben Signa-
tur wie die alte Methode. Wenden Sie dabei Preserve Signatures (23.3) an.
3. Fügen Sie einen Aufruf der alten Methode in die neue Methode ein.
4. Entwickeln Sie eine Methode für die neue Funktion, testen Sie sie zuerst (siehe
Test-Driven Development (8.1)) und rufen Sie sie dann von der neuen Methode
auf.
Wenn wir in der zweiten Version nicht den Namen der alten verwenden wollen,
sehen die Schritte wie folgt aus:
1. Identifizieren Sie eine Methode, die Sie ändern müssen.
2. Wenn Sie die Änderung als eine einzige Sequenz von Anweisungen an einer
Stelle schreiben können, entwickeln Sie dafür per Test-Driven Development (8.1)
eine neue Methode.
3. Erstellen Sie eine andere Methode, die die neue Methode und die alte Methode
aufruft.
Ein weiterer Vorteil von Wrap Method liegt darin, dass sie die neue Funktionalität
explizit unabhängig von der vorhandenen macht. Code für verschiedene Aufgaben
wird nicht vermischt.
Der Hauptnachteil von Wrap Method liegt darin, dass sie zu ungeeigneten Namen
führen kann. Im vorhergehenden Beispiel benennen wir die pay-Methode in di s -
patchPayO um, weil wir für den Code in der ursprünglichen Methode einen
anderen Namen brauchten. Wenn unser Code nicht besonders brüchig oder kom-
plex ist oder wenn wir über ein Refactoring-Tool verfügen, mit dem Sie sicher
Extract Method (A.i) ausführen können, können wir einige weitere Extraktionen
durchführen und bessere Namen verwenden. Doch in vielen Fällen wenden wir
Wrap Method an, weil wir keine Tests haben, weil der Code brüchig ist und weil
diese Tools nicht zur Verfügung stehen.
94
6.4
Wrap Class
class Employee
{
public void pay() {
Money amount = new M o n e y O ;
for (Iterator it = timecards.iteratorO; it.hasNextO; ) {
Timecard card = (Timecard)it.next();
if (payPeriod.contains(date)) {
amount.add(card.getHoursO * payRate) ;
}
}
payDispatcher.pay(this, date, amount);
}
Wir wollen die Tatsache protokollieren, dass wir einen bestimmten Mitarbeiter
bezahlen. Wir können eine weitere Klasse mit einer pay-Methode erstellen.
Objekte dieser Klasse können einen Mitarbeiter speichern, den Vorgang in der
pay-Methode protokollieren und dann so an das Empl oyee-Objekt delegieren, dass
es die Bezahlung leisten kann. Wenn Sie die ursprüngliche Klasse nicht in einem
Test-Harnisch instanziieren können, ist es in einem solchen Fall oft am einfachs-
ten, Extract Implementer (25.g) oder Extract Interface (25.10) anzuwenden und
diese Schnittstelle mit dem Wrapper zu implementieren.
Im folgenden Code wandeln wir die Empl oyee-Klasse mit Extract Implementer in
eine Schnittstelle um. Jetzt implementiert eine neue Klasse, LoggingEmployee,
diese Klasse. Wir können ein beliebiges Empl oyee-Objekt an ein LoggingEmplo-
yee-Objekt übergeben, damit dieses sowohl protokolliert als auch bezahlt.
95
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
private void l o g P a y m e n t O {
}
Diese Technik wird als Decorator Pattern bezeichnet. Wir erstellen Objekte einer
Klasse, die eine andere Klasse einhüllt und sie weitergibt. Die einhüllende Klasse
sollte dieselbe Schnittstelle wie die eingehüllte Klasse haben, damit Clients nicht
wissen, dass sie mit einem Wrapper arbeiten. In dem Beispiel ist Loggi ngEmpl o-
yee ein Decorator für Employee. Er muss eine pay-Methode und die anderen
Methoden von Empl oyee enthalten, die von dem Client verwendet werden.
Das Decorator Pattern bietet für dieses Problem eine ideale Lösung an. Mit
einem Decorator erstellen Sie eine abstrakte Klasse, die den Satz von Operatio-
nen definiert, die Sie unterstützen müssen. Dann erstellen Sie eine Unterklasse,
die von dieser abstrakten Klasse erbt, eine Instanz der Klasse in ihrem Kon-
struktor akzeptiert und einen Body für jede dieser Methoden zur Verfügung
stellt. Hier ist diese Klasse für das ToolControl 1 er-Problem:
abstract class ToolControllerDecorator extends ToolController
{
protected ToolController Controller;
public ToolControllerDecoratorCToolController Controller) {
this.controller = Controller;
}
public void r a i s e O { Controller. r a i s e O ; }
public void l o w e r O { Controller.Tower() ; }
96
6.4
Wrap Class
public void s t e p O {
// alle Empfänger benachrichtigen
Controller.stepC);
}
}
Das wirklich Nützliche ist dabei, dass wir die Unterklassen von ToolControl-
lerDecorator verschachteln können:
ToolController Controller = new StepNotifyingControllerC
new AlarmingController
(new A C M E C o n t r o l l e r O ) , notifyees);
Decorator ist ein nützliches Pattern, sollte aber nicht zu häufig verwendet wer-
den. Durch Code zu navigieren, der Decorators enthält, die andere Decorators
dekorieren, ähnelt dem Schälen einer Zwiebel: Sie müssen Schicht um Schicht
abschälen; aber es treibt Ihnen Tränen in die Augen.
97
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
müssten Aufrufe von pay() nur an einer Stelle protokollieren. Anstatt die Funk-
tionalität als Decorator einzuhüllen, können wir sie in eine andere Klasse einfügen,
die ein Employee-Objekt akzeptiert, die Zahlung leistet und dann den Vorgang
protokolliert.
class LoggingPayDispatcher
{
private Employee e;
public LoggingPayDispatcher(Employee e) {
this.e = e;
}
Jetzt können wir LogPayDi s p a t c h e r an einer Stelle erstellen, an der wir Zahlun-
gen protokollieren müssen.
Der Schlüssel zu Wrap Class liegt darin, dass wir neues Verhalten zu einem Sys-
tem hinzufügen können, ohne es in eine vorhandene Klasse einzufügen. Wird der
Code, den Sie einhüllen wollen, von vielen Stellen aufgerufen, lohnt sich der Ein-
satz eines Decorators. Mit dem Decorator Pattern können wir neues Verhalten
transparent auf einmal zu vorhandenen Aufrufen wie pay() hinzufügen. Wenn
das neue Verhalten dagegen nur an wenigen Stellen benötigt wird, kann ein weni-
ger Decorator-artiger Wrapper sehr nützlich sein. Im Laufe der Zeit sollten Sie die
Aufgaben des Wrappers überprüfen; vielleicht könnte der Wrapper zu einem wei-
teren hoch angesiedelten Konzept in Ihrem System werden.
98
6.4
Wrap Class
3. Erstellen Sie per Test-Driven Development (8.1) in dieser Klasse eine Methode, die
die neue Arbeit leistet. Schreiben Sie eine weitere Methode, die die neue
Methode und die alte Methode in der eingehüllten Klasse aufruft.
4. Instanziieren Sie die Wrapper-Klasse in Ihrem Code an der Stelle, an der Sie das
neue Verhalten aktivieren müssen.
Der Unterschied zwischen Sprout Method und Wrap Method ist recht trivial. Mit
Sprout Method schreiben Sie eine neue Methode und rufen sie von einer vorhande-
nen Methode auf. Mit Wrap Method benennen Sie eine Methode um und ersetzen
Sie durch eine neue, die die neue Arbeit leistet und die alte Methode aufruft. Nor-
malerweise verwende ich Sprout Method, wenn der Code in der vorhandenen
Methode einen sauberen Algorithmus hat. Ich setze Wrap Method ein, wenn die
neue Funktion meines Erachtens genau so wichtig ist wie die ursprüngliche. In
diesem Fall erhalte ich nach dem Einhüllen oft einen neuen High-Level-Algorith-
mus, etwa wie folgt:
Der Einsatz von Wrap Class ist ein ganz anderes Problem. Die Einsatzschwelle für
dieses Pattern ist höher. Im Allgemeinen tendiere ich in zwei Fällen zu Wrap Class:
1. Das gewünschte Verhalten ist vollkommen unabhängig und ich will die vorhan-
dene Klasse nicht mit systemnahem oder fremdem Verhalten verunreinigen.
2. Die Klasse ist so umfangreich geworden, dass ich sie nicht noch weiter ver-
schlimmern möchte. Dann dient die einhüllende Klasse nur dazu, einen Pflock
einzuschlagen, um einen Wegweiser für spätere Änderungen zu setzen.
Der zweite Fall ist ziemlich schwierig und gewöhnungsbedürftig. Eine sehr
umfangreiche Klasse mit vielleicht 10 oder 15 verschiedenen Aufgaben einzuhül-
len, nur um sie um eine triviale Funktionalität zu erweitern, kann etwas seltsam
aussehen. Welche Argumente sprechen also dafür?
Das größte Hindernis zur Verbesserung einer umfangreichen Code-Basis ist der
vorhandene Code. Leser: »Ha, ha; Kalau lässt grüßen!« Aber es geht nicht darum,
wie schwierig die Arbeit mit schwierigem Code ist; es geht darum, zu welchen
Annahmen Sie durch diesen Code verleitet werden. Wenn Sie den größten Teil
Ihres Tages damit verbringen, durch hässlichen Code zu waten, ist es sehr leicht
zu glauben, dass er immer hässlich sein wird und dass die kleine Maßnahme, mit
der Sie ihn verbessern, den Aufwand einfach nicht lohnt. Vielleicht denken Sie:
»Welche Rolle spielt es schon, ob ich dieses kleine Fragment verbessere, wenn ich
in 90 Prozent meiner Zeit immer noch mit undefinierbarem Schleim arbeiten
99
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern
muss? Sicher, ich kann dieses Fragment verbessern, aber was bringt mir das heute
Nachmittag? Morgen?« Nun, wenn Sie die Sache so sehen, muss ich Ihnen
zustimmen. Nicht viel. Aber wenn Sie geduldig und laufend solche kleinen Ver-
besserungen vornehmen, wird Ihr System nach einigen Monaten erheblich anders
aussehen. Eines Morgens stellen Sie plötzlich fest: »Huch, das sieht ja gar nicht
mehr wie undefinierbarer Schleim aus; dieser Code ist ziemlich gut. Anscheinend
hat hier jemand vor Kurzem refaktorisiert.« An diesem Punkt, wenn Sie den
Unterschied zwischen gutem und schlechtem Code in Ihrem Bauch fühlen, sind
Sie ein anderer Mensch. Vielleicht verspüren Sie sogar den Wunsch, weit mehr
Code zu refaktorisieren, als für Ihre Aufgabe erforderlich ist, nur um sich das
Leben zu erleichtern. Wahrscheinlich hört sich das für Sie einfältig an, solange Sie
es nicht selbst erfahren haben; aber ich habe dies in Teams immer wieder erlebt.
Das Schwierige sind die ersten Schritte, weil sie manchmal einfältig aussehen.
»Was? Eine Klasse einhüllen, nur um diese kleine Funktion hinzuzufügen? Der
Code sieht schlechter aus als vorher. Er ist komplizierter.« Ja, das ist er, im
Moment. Aber wenn Sie wirklich anfangen, diese 10 oder 15 Aufgaben in dieser
eingehüllten Klasse herauszulösen, wird alles viel sachgerechter aussehen.
6.5 Zusammenfassung
In diesem Kapitel habe ich einen Satz von Techniken beschrieben, mit denen Sie
Änderungen vornehmen können, ohne vorhandene Klassen unter Testkontrolle
zu bringen. Aus der Sicht des Designs ist schwer abzuschätzen, was man davon
halten soll. In vielen Fällen können wir damit den Abstand zwischen definitiv
neuen Aufgaben und alten vergrößern. Anders ausgedrückt: Sie sind der Aus-
gangspunkt auf dem Weg zu einem besseren Design. Aber in anderen Fällen wis-
sen wir, dass wir eine Klasse nur deshalb erstellen, um neuen Code mit Tests zu
schreiben, und nicht bereit sind, die Zeit aufzuwenden, um die vorhandene Klasse
unter Testkontrolle zu bringen. Dies ist eine sehr reale Situation. Wenn Entwickler
in Projekten so vorgehen, stellen sie fest, dass immer neue Klassen und Methoden
rund um den Kadaver der alten großen Klassen hervorsprießen. Aber dann pas-
siert etwas Interessantes. Irgendwann sind es die Entwickler leid, den alten Kada-
ver zu umgehen, und beginnen, ihn unter Testkontrolle zu bringen. Ein Grund
dafür ist die wachsende Vertrautheit. Wenn sie sich diese große, ungetestete
Klasse wiederholt anschauen, um herauszufinden, wo sie Neues anpfropfen kön-
nen, lernen Sie sie immer besser kennen. Sie wirkt nicht mehr so bedrohlich. Ein
anderer Grund ist schiere Erschöpfung. Wenn Sie den ganzen Plunder in Ihrem
Wohnzimmer nicht mehr sehen können, wollen Sie ihn wegräumen. Gute Aus-
gangspunkte sind die Kapitel 9, Ich kann diese Klasse nicht in einen Test-Harnisch
einfügen, und 20, Diese Klasse ist zu groß und soll nicht noch größer werden.
100
Kapitel 7
Wie lange dauern Änderungen? Das kommt darauf an. Ist der Code schrecklich
unübersichtlich, dauert es sehr lange. Wir müssen den Code durchforsten, alle
Wirkungen und Nebenwirkungen einer Änderung verstehen und dann die Ände-
rung vornehmen. In übersichtlichen Code-Fragmenten kann dies sehr schnell
geschehen, aber in wirklich verschlungenen Bereichen kann es sehr lange dauern.
Einige Teams treffen auf viel schlimmere Bedingungen als andere. Für sie dauern
selbst die einfachsten Code-Änderungen sehr lange. Die Mitarbeiter dieser Teams
können feststellen, welche Funktion sie hinzufügen müssen, genau darstellen, wo
die Änderung erfolgen muss, sie dann in fünf Minuten durchführen und dennoch
den Code mit ihrer Änderung erst viele Stunden später freigeben.
7.1 Verständlichkeit
Wenn der Umfang des Codes in einem Projekt wächst, wird er von einem
bestimmten Punkt an zunehmend unverständlicher. Es dauert immer länger her-
ausfinden, was geändert werden muss.
Einiges davon ist unvermeidlich. Wenn wir Code zu einem System hinzufügen,
können wir ihn in vorhandene Klassen, Methoden oder Funktionen einfügen,
oder wir können neue hinzufügen. In jedem Fall dauert es eine Weile herauszufin-
den, wie wir eine Änderung machen müssen, wenn wir den Kontext nicht kennen.
Doch ein wohl gewartetes System und ein Legacy-System unterscheiden sich in
einem wesentlichen Aspekt: In einem wohl gewarteten System dauert es vielleicht
eine Weile, um die Stelle für eine Änderung herauszufinden, aber danach geht die
Änderung normalerweise leicht vonstatten; und Sie fühlen sich im Umgang mit
dem System sehr viel sicherer. Bei einem Legacy-System kann es lange dauern
herauszufinden, was Sie tun müssen, und auch die Änderung ist schwierig. Viel-
leicht haben Sie das Gefühl, dass Sie außer einem engen Verständnis des jeweili-
gen Änderungskontextes nicht viel gelernt haben. In den schlimmsten Fällen
scheint keine Zeit der Welt auszureichen, um alles für eine Änderung Erforderli-
che zu verstehen, und Sie müssen blind in den Code hineingehen und in der Hoff-
nung anfangen, alle Probleme lösen zu können, auf die Sie stoßen.
Systeme, die aus kleinen, gut benannten, verständlichen Komponenten aufgebaut
sind, ermöglichen ein schnelleres Arbeiten. Wenn Verständlichkeit bei Ihrem Pro-
101
Kapitel 7
Änderungen brauchen eine Ewigkeit
jekt ein großes Problem ist, finden Sie in Kapitel 16, Ich verstehe den Code nicht gut
genug, um ihn zu ändern, und Kapitel 17, Meine Anwendung hat keine Struktur, Anre-
gungen für Ihr weiteres Vorgehen.
7.2 Verzögerungszeit
Änderungen dauern oft aus einem anderen sehr häufigen Grund lange: Verzöge-
rungszeit (engl, lag time). Die Verzögerungszeit ist die Zeitspanne zwischen der
Durchführung einer Änderung und dem Zeitpunkt, an dem Sie ein wirkliches
Feedback über die Änderung bekommen. Als ich dies schrieb, rollte der Mars-
Rover Spirit über die Oberfläche des Mars und nahm Bilder auf. Die Übertragung
von Signalen zwischen Erde und Mars dauert über sieben Minuten. Glücklicher-
weise verfügte Spirit über Onboard-Steuerungs-Software, die dem Rover eine
gewisse eigenständige Beweglichkeit ermöglichte. Stellen Sie sich vor, was es
bedeutet hätte, das Gerät manuell von der Erde aus zu steuern. Sie würden einige
Steuerelemente betätigen und erhielten erst 14 Minuten später ein Feedback darü-
ber, wie weit sich der Rover bewegt hat. Dann würden Sie entscheiden, was Sie als
Nächstes tun wollen, und warteten wieder 14 Minuten, um das Ergebnis zu über-
prüfen. Es scheint lächerlich ineffizient zu sein, nicht wahr? Doch bei näherer
Betrachtung ist dies genau die Methode, wie die meisten von uns heute Software
entwickeln. Wir machen einige Änderungen, werfen einen Build an und stellen
dann später fest, was passiert ist. Leider haben wir keine Software, die von alleine
Hindernisse, etwa scheiternde Tests, beim Build umgehen kann. Stattdessen ver-
suchen wir, mehrere Änderungen zusammenzufassen und alle auf einmal durch-
zuführen, damit wir den Compiler nicht zu oft anwerfen müssen. Sind unsere
Änderungen erfolgreich, machen wir weiter, wenn auch etwas langsamer als der
Mars-Rover. Wenn wir auf ein Hindernis stoßen, geht es noch langsamer voran.
Dies ist wirklich traurig, da diese Art zu arbeiten in den meisten Sprachen völlig
überflüssig ist. Sie ist reine Zeitverschwendung. In den meisten Mainstream-
Sprachen können Sie immer Dependencies so aufheben, dass Sie den Code neu
kompilieren und den gerade bearbeiteten Code in weniger als zehn Sekunden tes-
ten können. Ein wirklich motiviertes Team kann diese Zeit in den meisten Fällen
auf unter fünf Sekunden drücken. Praktisch bedeutet dies: Sie sollten jede Klasse
oder jedes Modul in Ihrem System separat von den anderen und in einem eigenen
Test-Harnisch kompilieren können. Wenn Sie so arbeiten, erhalten Sie sehr
schnell Feedback und können Ihre Software entsprechend schneller entwickeln.
Der menschliche Geist hat einige interessante Eigenschaften. Wenn wir eine
kurze Aufgabe (fünf bis zehn Sekunden lang) ausführen müssen und nur einen
Schritt pro Minute machen können, führen wir ihn normalerweise aus und
machen dann eine Pause. Kennen wir den nächsten Schritt noch nicht, beginnen
wir zu planen. Nach der Planung wandert unser Geist herum, bis wir den nächs-
ten Schritt ausführen können. Können wir jedoch die Zeit zwischen Schritten von
102
7-3
Dependencies aufheben
einer Minute auf einige Sekunden komprimieren, ändert sich die Qualität der
mentalen Arbeit. Mit Feedback können wir schnell verschiedene Ansätze auspro-
bieren. Unsere Arbeit ähnelt mehr dem Autofahren als dem Warten an einer
Bushaltestelle. Wir konzentrieren uns stärker, weil wir nicht laufend auf die
nächste Gelegenheit zum Handeln warten. Und was am wichtigsten ist: Wir brau-
chen viel weniger Zeit, um Fehler zu erkennen und zu korrigieren.
Was hält uns davon ab, ständig so zu arbeiten? Einige Entwickler können dies.
Programmierer, die in interpretierten Sprachen entwickeln, können bei ihrer
Arbeit oft fast sofort Feedback bekommen. Doch die anderen Entwickler, die mit
kompilierten Sprachen arbeiten, werden hauptsächlich durch Dependencies
behindert. Sie müssen ständig etwas kompilieren, was ihnen eigentlich gleichgül-
tig ist, weil sie etwas anderes kompilieren wollen.
Ist die zu ändernde Klasse in einen Test-Harnisch eingehüllt, können Sie im Allge-
meinen sehr schnelle Editieren-Kompilieren-Linken-Testen-Zyklen nutzen.
Gewöhnlich sind die Ausführungskosten für die meisten Methoden relativ nied-
rig, verglichen mit den Kosten aufgerufener Methoden, insbesondere wenn Sie
externe Ressourcen wie etwa eine Datenbank, Hardware oder die Kommunika-
tionsinfrastruktur aufrufen. Wenn dies nicht passiert, sind Ihre Methoden norma-
lerweise sehr berechnungsintensiv. Die Techniken aus Kapitel 22, Ich muss eine
Monster-Methode ändern und kann keine Tests dafür schreiben, können helfen.
103
Kapitel 7
Änderungen brauchen eine Ewigkeit
Pinch Points (Einschub in 12.1) finden können, also Stellen, an denen es einfacher
ist, Tests zu schreiben.
Im Rest dieses Kapitels beschreibe ich, wie Sie die Struktur Ihres Codes ändern
können, um Builds zu vereinfachen.
7.3.1 Build D e p e n d e n c i e s
Wenn ein objektorientiertes System ein Cluster von Klassen enthält, die Sie
schneller erstellen wollen, müssen Sie zunächst herausfinden, welche Dependen-
cies Sie daran hindern. Im Allgemeinen ist dies ziemlich einfach: Sie versuchen
einfach, diese Klassen in einem Test-Harnisch zu verwenden. Fast jedes Problem,
auf das Sie stoßen, wird Folge einer Dependency sein, die Sie aufheben sollten.
Wenn die Klassen in einem Test-Harnisch laufen, gibt es immer noch einige
Dependencies, die die Kompilierzeit beeinflussen können. Es lohnt sich, alle Kom-
ponenten zu untersuchen, die von dem abhängen, was Sie instanziieren konnten.
Diese Dinge müssen jedes Mal neu kompiliert werden, wenn Sie das System neu
erstellen. Wie können Sie diesen Aufwand minimieren?
Zu diesem Zweck extrahieren Sie Schnittstellen der Klassen in Ihrem Cluster, die
von Klassen außerhalb des Clusters verwendet werden. In vielen IDEs können Sie
eine Schnittstelle extrahieren, indem Sie eine Klasse auswählen und mit einer
Menüauswahl eine Liste aller Methoden in der Klasse anzeigen, in der Sie diejeni-
gen auswählen können, die in die neue Schnittstelle aufgenommen werden sollen.
Danach können Sie einen Namen für die neue Schnittstelle festlegen. Optional
können Sie auch alle Referenzen der Klasse durch Referenzen der neuen Schnitt-
stelle in der gesamten Code-Basis ersetzen lassen. Dies ist eine unglaublich nütz-
liche Funktion. In C++ ist Extract Implementer (25.9) ein wenig einfacher als
Extract Interface (25.10). Sie müssen die Namen von Referenzen nicht überall
ändern, aber Sie müssen die Stellen ändern, an denen Instanzen der alten Klasse
erstellt werden (siehe Extract Implementer (25.9^ für Details).
Wenn Sie diese Cluster von Klassen unter Testkontrolle gebracht haben, können
Sie die physische Struktur Ihres Projekts ändern, um Builds zu vereinfachen,
indem Sie die Cluster in ein neues Package oder in eine Bibliothek verschieben.
Ihre Builds werden dadurch komplexer, entscheidend ist aber: Wenn Sie Depen-
dencies aufheben und Klassen in neue Packages oder Bibliotheken auslagern, stei-
gen die Kosten einer Neuerstellung des gesamten Systems leicht an, aber die
Durchschnittszeit für einen Build kann erheblich sinken.
Betrachten wir ein Beispiel. Abbildung 7.1 zeigt einen kleinen Satz zusammenar-
beitender Klassen, alle im selben Package.
Wir wollen die AddOpportunityFormHandler-Klasse ändern, es wäre aber schön,
wenn wir auch unseren Build beschleunigen könnten. Zunächst versuchen wir,
einen AddOpportunityFormHandl er zu instanziieren. Leider sind alle Klassen
konkret, von denen er abhängt. AddOpportuni tyFormHandl er braucht eine Con-
104
7-3
Dependencies aufheben
105
Kapitel 7
Änderungen brauchen eine Ewigkeit
Wenn wir wollen, können wir uns noch weiter von einer erzwungenen Neukompi-
lierung isolieren (siehe Abbildung 7.3). Dieses Design des Systems erhalten wir,
wenn wir Extract Implementer (25.g) auf die Opportuni tyltem-Klasse anwenden.
Jetzt ist das Package Opportuni tyProcessi ng wirklich von der Datenbank-Im-
plementierung unabhängig. Alle Tests, die wir in diesem Package schreiben, sollten
schnell kompiliert werden; und das Package selbst muss nicht neu kompiliert wer-
den, wenn wir Code in der Datenbank-Implementierungsklasse ändern.
7-3
Dependencies aufheben
Das Dependency-Inversion-Prinzip
(Prinzip der Abhängigkeitsumkehrung) Wenn Ihr Code von einer Schnittstelle
abhängt, ist diese Dependency normalerweise sehr gering und unaufdringlich.
Ihr Code muss sich nicht ändern, solange die Schnittstelle konstant bleibt; und
Schnittstellen ändern sich typischerweise viel seltener als der Code hinter ihnen.
Haben Sie eine Schnittstelle, können Sie Klassen bearbeiten oder neue Klassen
hinzufügen, die diese Schnittstelle implementieren, ohne dass der Code geän-
dert werden müsste, der die Schnittstelle verwendet.
Deshalb ist es besser, von Schnittstellen oder abstrakten Klassen abzuhängen als
von konkreten Belassen. Die Abhängigkeit von weniger volatilen Dingen mini-
miert die Gefahr, dass bestimmte Änderungen eine massive Neukompilierung
erforderlich machen.
Bis jetzt haben wir einiges getan, damit die Belasse AddOpportuni tyFormHandl er
nicht neu kompiliert wird, wenn wir Klassen ändern, die von ihr abhängen.
Dadurch werden Builds beschleunigt, aber es ist nur die Hälfte des Problems. Wir
können Builds auch für Code beschleunigen, der von der Klasse AddOpportuni -
tyFormHandl er abhängt. Betrachten wir noch einmal das Package-Design (siehe
Abbildung 7.5).
AddOpportuni tyFormHandl er ist die einzige public, nicht zum Test gehörige
Produktionsklasse in Opportuni tyProcessi ng. Klassen in anderen Packages, die
davon abhängen, müssen neu kompiliert werden, wenn wir sie ändern. Wir kön-
nen diese Dependency auch aufheben, indem wir Extract Interface (25.10) oder
Extract Implementer (25.g) auf AddOpportunityFormHandler anwenden. Dann
können Belassen in anderen Packages von den Schnittstellen abhängen. Wenn wir
dies tun, schirmen wir alle Nutzer dieses Packages bei den meisten Änderungen
wirksam von der Neukompilierung ab.
Wir können Dependencies aufheben und Belassen auf verschiedene Packages ver-
teilen, um Build-Zeiten zu verkürzen; und der Aufwand lohnt sich sehr. Wenn Sie
Ihre Tests sehr schnell neu kompilieren und ausführen können, können Sie beim
107
Kapitel 7
Änderungen brauchen eine Ewigkeit
Wenn Sie mehr Schnittstellen und Packages in Ihr Design einfügen, um Depen-
dencies aufzuheben, nimmt der Zeitaufwand für eine Neuerstellung des gesam-
ten Systems leicht zu. Es müssen mehr Dateien kompiliert werden. Aber der
durchschnittliche Zeitaufwand für einen Make-Lauf, also einen Build, bei dem
nur die erforderlichen Dateien kompiliert werden, kann erheblich sinken.
7.4 Zusammenfassung
Mit den Techniken in diesem Kapitel können Sie die Build-Zeit für kleine Cluster
von Klassen beschleunigen; doch dies ist nur ein kleiner Teil dessen, was Sie mit
Schnittstellen und Packages machen können, um Dependencies zu handhaben.
Im Buch Agile Software Development: Principles, Patterns, and Practices (Pearson
Education, 2002) von Robert C. Martin finden Sie andere ähnliche Techniken, die
jeder Software-Entwickler kennen sollte.
108
Kapitel 8
Dies muss die abstrakteste und problemspezifischste Frage in dem Buch sein.
Deshalb hätte ich sie fast ausgelassen. Doch tatsächlich gibt es unabhängig von
unserem Design-Ansatz oder den besonderen Einschränkungen, denen wir unter-
worfen sind, einige Techniken, mit denen wir uns die Arbeit erleichtern können.
Betrachten wir zunächst den Kontext. Einer der wichtigsten Aspekte von Legacy
Code ist das Fehlen von Tests für unseren Code. Noch schlimmer: Test hinzufügen
kann schwierig sein. Deswegen sind viele Teams versucht, auf die Techniken aus
Kapitel 6, Ich habe nicht viel Zeit und ich muss den Code ändern, zurückzugreifen.
Mit den dort beschriebenen Techniken (Sprouting und Wrapping) können wir
Code ohne Tests hinzufügen. Dies ist aber mit einigen mehr oder weniger offen-
sichtlichen Gefahren verbunden. Zum einen ändern wir den vorhandenen Code
durch Sprouting oder Wrapping nicht nennenswert; deshalb wird dieser für län-
gere Zeit nicht besser. Zum anderen ist die Duplizierung von Code ein Risiko.
Wenn wir Code aus ungetesteten Bereichen duplizieren, könnte er einfach vor
sich hin rotten. Noch schlimmer: Vielleicht erkennen wir die Duplizierung erst,
wenn wir bereits umfangreiche Änderungen vorgenommen haben. Schließlich
sind auch Angst und Resignation Risiken: Angst, dass wir einen bestimmten Teil
des Codes nicht ändern und die Arbeit mit ihm nicht vereinfachen können, und
Resignation, weil ganze Bereiche des Codes einfach nicht besser werden. Angst
beeinträchtigt unsere Entscheidungsfähigkeit. Die im Code verbliebenen Sprouts
und Wraps sind kleine Erinnerungen daran.
Im Allgemeinen ist es besser, sich dem Problem zu stellen, als davor davonzulau-
fen. Wenn wir den Code einer Testkontrolle unterwerfen können, können wir mit
den Techniken aus diesem Kapitel gute Fortschritte erzielen. Wenn Sie nicht wis-
sen, welche Tests Sie einfügen sollen, lesen Sie Kapitel 13, Ich muss eine Änderung
vornehmen, weiß aber nicht, welche Tests ich schreiben soll. Wenn Sie durch Depen-
dencies behindert werden, lesen Sie Kapitel 9, Ich kann diese Klasse nicht in einen
Test-Harnisch einfügen, und Kapitel 10, Ich kann diese Methode nicht in einem Test-
Harnisch ausfuhren.
Nachdem wir Tests eingerichtet haben, können wir leichter neue Funktionen hin-
zufügen. Wir haben eine solide Grundlage.
109
Kapitel g
Wie füge ich eine Funktion hinzu?
110
8.1
Test-Driven Development (TDD)
Dies ist ungewöhnlich viel Code für eine Antwort auf einen Test in TDD. Üblicher-
weise sind die Schritte viel kleiner, obwohl sie so groß werden können, wenn Sie
sicher sind, dass Ihr Algorithmus dies erfordert.
in
Kapitel g
Wie füge ich eine Funktion hinzu?
fail("expected InvalidBasisException");
}
catch (InvalidBasisException e) {
}
if (elements.sizeO == 0)
throw new InvalidBasisException("no elements");
112
8.1
Test-Driven Development (TDD)
ante des ersten. Hier ist ein Test, der uns diesem Code näher bringt. In diesem Fall
beträgt der erwartete Wert nicht - 0 . 5 , sondern 0.5. Wir schreiben einen neuen
Test für eine Methode, die noch nicht existiert: secondMomentAbout.
113
Kapitel g
Wie füge ich eine Funktion hinzu?
if (elements.sizeO == 0)
throw new InvalidBasisException("no elements");
if (elements.sizeO == 0)
throw new InvalidBasisException("no elements");
Vielleicht sind Sie schockiert, dass wir den Code so leichtfertig kopiert und einge-
fügt haben; aber wir werden den duplizierten Code gleich entfernen. Der Code,
den wir schreiben, ist frischer Code. Aber der Trick, benötigten Code einfach zu
kopieren und in einer neuen Methode zu modifizieren, ist beim Arbeiten mit
Legacy Code sehr arbeitssparend. Wenn wir Funktionen zu einem besonders
schrecklichen Code hinzufügen wollen, können wir unsere Änderungen oft leich-
ter verstehen, wenn wir sie an einer neuen Stelle einfügen und direkt mit dem
alten Code vergleichen können. Wir können den duplizierten Code später entfer-
nen, um den neuen Code sauberer in die Klasse zu integrieren, oder wir können
die Änderung einfach verwerfen und eine andere Lösung ausprobieren. Denn wir
wissen, dass wir immer noch den alten Code haben und studieren können.
114
8.1
Test-Driven Development (TDD)
if (elements.size() == 0)
throw new InvalidBasisException("no elements");
Dieser abschließende Schritt, duplizierten Code zu entfernen, ist sehr wichtig. Wir
können schnell und brutal Funktionen zu Code hinzufügen, indem wir ganze Blö-
cke von Code kopieren; doch wenn wir den duplizierten Code danach nicht entfer-
nen, halsen wir uns nur Probleme auf, indem wir die Wartung immer schwerer
machen. Haben wir dagegen Tests eingerichtet, können wir duplizierten Code
leicht entfernen. Dieses Beispiel hat dies gezeigt. Doch wir konnten nur deshalb
auf Tests zurückgreifen, weil wir von Anfang an konsequent TDD verwendet
haben. Bei Legacy Code sind die Tests sehr wichtig, die wir für den vorhandenen
Code schreiben, wenn wir TDD verwenden. Wenn wir die Tests eingerichtet
haben, können wir jeden erforderlichen Code schreiben, um Funktionen hinzuzu-
fügen; denn wir können sicher sein, dass wir ihn in den Rest des Codes integrie-
ren können, ohne die Dinge zu verschlechtern.
5
Kapitel g
Wie füge ich eine Funktion hinzu?
Die Trennung zahlt sich besonders bei Legacy Code aus, weil wir neuen Code
unabhängig von altem Code schreiben können.
Nachdem wir neuen Code geschrieben haben, können wir ihn refaktorisieren,
um duplizierten Code im alten und neuen Code zu entfernen.
Bei Legacy Code können wir den TDD-Algorithmus wie folgt erweitern:
0. Schreiben Sie für die zu ändernde Klasse einen Test.
1. Schreiben Sie einen Failing Test Case.
2. Sorgen Sie dafür, dass er kompiliert wird.
3. Sorgen Sie dafür, dass er bestanden wird. (Versuchen Sie dabei nicht, vorhande-
nen Code zu ändern.)
4. Entfernen Sie duplizierten Code.
5. Fangen Sie von vorne an.
Das folgende Beispiel zeigt, wie es funktioniert. Wir verfügen über eine getestete
Java-Klasse namens Mai 1 Forwarder, die zu einem Java-Programm gehört, das
Mailing-Lists verwaltet. Sie enthält eine Methode namens getFromAddress, die
wie folgt aussieht:
116
8.2
Programming by Difference
Was müssen wir tun, wenn eine neue Anforderung gestellt wird? Was ist erforder-
lich, wenn wir anonyme Mailing-Lists unterstützen müssen? Mitglieder einer sol-
chen Liste können posten, aber die »von«-Adresse ihrer Messages sollte auf eine
besondere E-Mail-Adresse gesetzt werden, die vom Wert von domai n abgeleitet ist
(eine Instanzvariable der MessageForwarder-Klasse). Hier ist ein Failing Test
Case für diese Änderung (wenn der Test ausgeführt wird, wird die expectedMes-
sage-Variable auf die Message gesetzt, die der MessageForwarder weiterleitet):
Was erreichen wir damit? Nun, wir haben das Problem gelöst, aber dafür für ein
sehr einfaches Verhalten eine neue Klasse in unser System eingefügt. Ist es sinn-
voll, eine Unterklasse einer ganzen Klasse zur Message-Weiterleitung zu bilden,
nur um die »von«-Adresse zu ändern? Langfristig nicht, aber hier hilft es uns,
unseren Test schnell zu bestehen. Und wenn der Test bestanden wird, können wir
ihn benutzen, um dieses neue Verhalten zu bewahren, wenn wir das Design
ändern wollen.
Das schien fast zu leicht zu sein. Es gibt jedoch einen Haken: Wenn wir diese
Technik wiederholt anwenden und einige Schlüsselaspekte unseres Designs nicht
beachten, nimmt seine Qualität schnell ab. Um zu sehen, was passieren kann,
wollen wir eine andere Änderung untersuchen. Wir wollen Messages an die Emp-
fänger der Mailing-List weiterleiten, diese aber auch per bcc (blind carbon copy;
unsichtbare Kopie) an einige andere Empfänger senden, die nicht auf der offiziel-
len Mailing-List stehen dürfen. Wir wollen sie als Offlist-Empfängerbezeichnen.
Es sieht einfach genug aus; wir könnten eine weitere Unterklasse von Message-
Forwarder erstellen und ihre processMessage-Methode so überschreiben, dass
sie Messages an die neuen Empfänger sendet (siehe Abbildung 8.2).
118
8.2
Programming by Difference
Das könnte funktionieren, wäre da nicht eine Sache: Was passiert, wenn wir einen
MessageForwarder brauchen, der beides tut: alle Messages an Offlist-Empfänger
sendet und alle Messages anonym weiterleitet?
Dies illustriert eines der großen Probleme beim umfangreichen Einsatz der Verer-
bung: Wenn wir Funktionen in separate Unterklassen einfügen, können wir nur
eine dieser Funktionen gleichzeitig nutzen.
Eine Möglichkeit, dieses Problem zu lösen, besteht darin, den Code zu refaktori-
sieren, bevor wir die Offlist-Empfänger-Funktion hinzufügen, damit wir diese sau-
ber einfügen können. Zu diesem Zweck können wir auf die Tests zurückgreifen,
die wir früher geschrieben haben, und mit ihnen verifizieren, ob wir das Verhalten
bewahren, wenn wir ein anderes Schema einführen.
Die Funktion zur anonymen Weiterleitung hätten wir auch ohne Unterklasse im-
plementieren können. Wir hätten die anonyme Weiterleitung zu einer Konfigura-
tionsoption machen können. Wir hätten etwa den Konstruktor der Klasse so
ändern können, dass er eine Collection von Properties akzeptiert:
Können wir in diesem Fall erreichen, dass unser Test bestanden wird? Betrachten
wir noch einmal den Test:
119
Kapitel g
Wie füge ich eine Funktion hinzu?
Mit der Properties Collection können wir auch die neue Funktion hinzufügen,
indem wir eine Property definieren, die die Offlist-Empfänger-Funktion aktiviert.
Sind wir fertig? Noch nicht. Die getFrom-Methode von MessageForwarder ist
jetzt recht chaotisch; doch mit unseren Tests können wir sehr schnell eine
Methode extrahieren, um den Code aufzuräumen. Im Moment sieht er wie folgt
aus:
120
8.2
Programming by Difference
Das ist etwas sauberer, aber die Funktionen für die anonyme Weiterleitung und
die Offlist-Empfänger sind jetzt in MessageForwarder eingebettet. Verstößt dies
gegen das Single Responsibility Principle (erster Einschub in Kapitel 20J? Vielleicht. Es
hängt davon ab, wie umfangreich der Code im Verhältnis zu der Aufgabe ist und
wie stark er mit dem Rest des Codes verknüpft ist. In diesem Fall ist es kein Pro-
blem festzustellen, ob die Liste anonym ist. Mit dem Property-Ansatz machen wir
bequem Fortschritte. Was können wir machen, wenn es zahlreiche Properties gibt
und der Code von MessageForwarder mit Bedingungsanweisungen überladen
wird? Dann könnten wir etwa eine Klasse anstelle einer Properties Collection ver-
wenden. Was passiert, wenn wir eine Klasse namens Mai 1 i ngConfi guration für
die Properties Collection erstellen würden (siehe Abbildung 8.3)?
MessageForwarder
+ MessageForwarder()
MailingConfiguration
+ processMessage(Message)
- forwardMessage(Message) + getProperty(String) : String
# getFromAddress(Message) + addProperty(String name, String value)
Sieht gut aus, aber ist dies nicht zu viel des Guten? Macht MailingConfigura-
tion nicht dasselbe wie eine Properties Collection?
MessageForwarder
+ MessageForwarderQ
MailingConfiguration
+ processMessage(Message)
- forwardMessage( Message) + getFromAddress(Message)
MessageForwarder
+ MessageForwarderf)
MailingConfiguration
+ processMessage(Message)
- forwardMessage(Message) + getFromAddress(Message)
+ buildRecipientList(List recipients) : List
Mit diesen Änderungen passt der Name der Klasse nicht mehr genau. Eine Konfi-
guration ist normalerweise etwas Passives. Diese Klasse erstellt und modifiziert
auf Anforderung aktiv Daten für MessageForwarders. Falls es nicht bereits eine
andere gleichnamige Klasse in dem System gibt, könnte der Name Mai 1 i ngLi st
gut passen. MessageForwarder-Objekte fordern Mai 1 i ngLi st-Objekte auf, »von«-
Adressen zu berechnen und Empfängerlisten zusammenzustellen. Wir können
sagen, dass Mailing-Lists die Aufgabe haben festzulegen, wie Messages geändert
werden sollen. Abbildung 8.6 zeigt unser Design nach der Umbenennung.
122
8.2
Programming by Difference
MessageForwarder
+ MessageForwarder()
MailingList
+ processMessage(Message)
- forwardMessage(Message) + getFromAddress(Message)
+ buildRecipientList(List recipients) : List
Programming by Difference ist eine nützliche Technik. Wir können Code damit
schnell ändern und Tests verwenden, um ein saubereres Design zu erstellen. Doch
wollen wir diese Methode korrekt anwenden, müssen wir einige Stolpersteine
beachten, etwa Verstöße gegen das Liskov Substitution Principle (LSP).
}
Square erbt die Methoden setWi dth und setHei ght von Rectangl e. Wie wird
die Fläche mit dem folgenden Code berechnet?
Rectangle r = new S q u a r e O ;
r.setWidth(3);
r.setHeight(4);
Wenn die Fläche 12 beträgt, ist Square eigentlich kein richtiges Quadrat, oder?
Wir könnten setWi dth und setHei ght so überschreiben, dass Square automa-
tisch »quadratisch« wird. Wir könnten sowohl mit setWi dth als auch mit
setHei ght die wi dth-Variable von Quadraten setzen, aber das könnte zu kon-
traintuitiven Ergebnissen führen.
123
Kapitel g
Wie füge ich eine Funktion hinzu?
Wer erwartet, dass alle Rechtecke eine Fläche von 12 haben werden, wenn ihre
Breite auf 3 und ihre Höhe auf 4 gesetzt wird, muss sich auf eine Überraschung
gefasst machen. Er würde 16 erhalten.
Dies ist ein klassisches Beispiel für einen Verstoß gegen das Liskov Substitution
Principle (LSP). Objekte von Unterklassen sollten in unserem gesamten Code
durch Objekte ihrer Oberklassen ausgetauscht werden können. Ist dies nicht
möglich, könnte in unserem Code ein Fehler schlummern.
Das LSP impliziert, dass Anwender einer Klasse Objekte einer Unterklasse nutzen
sollen können, ohne zu wissen, dass es sich um Objekte einer Unterklasse han-
delt. Es gibt keine mechanischen Verfahren, um LSP-Verstöße vollständig zu ver-
meiden. Ob eine Klasse LSP-konform ist, hängt von ihren Clients ab und was
diese erwarten. Doch einige Faustregeln können hilfreich sein:
Hier ist das Problem: Wenn wir konkrete Methoden überschreiben (etwa wie get-
FromAddress von MessageForwarder in AnonymousMessageForwarder), könn-
ten wir das Verhalten eines Teils des Codes ändern, der MessageForwarder-
Objekte verwendet. Wenn über unsere Anwendung Referenzen von MessageFor-
warder verstreut sind und wir eine davon auf einen AnonymousMessageForwar-
der setzen, könnte ihr Nutzer sie für einen einfachen MessageForwarder halten,
der die »von«-Adresse von der gerade verarbeiteten Message erhält und sie bei der
Verarbeitung von Messages benutzt. Würde es für Nutzer dieser Klasse eine Rolle
spielen, ob AnonymousMessageForwarder dies tut oder ob er eine andere beson-
dere Adresse als »von«-Adresse benutzt? Das hängt von der Anwendung ab. Im
Allgemeinen kommt Code durcheinander, wenn wir konkrete Methoden zu oft
überschreiben. Jemand könnte eine MessageForwarder-Referenz im Code
bemerken, sich die MessageForwarder-Klasse anschauen und meinen, der Code
für getFromAddress werde ausgeführt. Vielleicht hat er kein Indiz dafür, dass die
Referenz auf ein AnonymousMessageForwarder-Objekt verweist und dass dessen
getFromAddress-Methode verwendet wird. Wollten wir wirklich mit Vererbung
arbeiten, hätten wir die Klasse MessageForwarder und darin die Methode get-
FromAddress als abstract deklarieren und die konkreten Bodies der Methode in
den Unterklassen zur Verfügung stellen können. Abbildung 8.7 zeigt das zugehö-
rige Design nach diesen Änderungen.
124
8.3
Zusammenfassung
Ich bezeichne eine solche Hierarchie als normalisierte Hierarchie. In einer normali-
sierten Hierarchie enthält keine Klasse mehr als eine Implementierung einer
Methode. Anders ausgedrückt: Keine Klasse enthält eine Methode, die eine kon-
krete Methode überschreibt, die sie von einer Oberklasse geerbt hat. Wollen Sie
wissen, wie eine Klasse eine bestimmte Methode ausführt, können Sie in der
Klasse nachschauen. Entweder ist die Methode vorhanden oder sie ist abstrakt und
in einer der Unterklassen implementiert. In einer normalisierten Hierarchie müs-
sen Sie sich keine Sorgen machen, dass Unterklassen Verhalten überschreiben,
das sie von ihren Oberklassen geerbt haben.
Lohnt es sich, dies immer zu tun? Gelegentlich eine konkrete Methode zu über-
schreiben, hat keine Nachteile, solange Sie damit nicht gegen das Liskov Substitu-
tion Principle verstoßen. Doch Sie sollten gelegentlich prüfen, wie weit Klassen von
der normalisierten Form entfernt sind, und die Struktur gegebenenfalls verbes-
sern, wenn Sie das Herauslösen von Aufgaben vorbereiten.
Mit Programming by Difference können wir Systeme schnell ändern. Wenn wir dies
tun, können wir uns mit unseren Tests auf das neue Verhalten konzentrieren und
bei Bedarf bessere Strukturen implementieren. Mit Tests können solche Änderun-
gen sehr schnell ausgeführt werden.
8.3 Zusammenfassung
Mit den Techniken aus diesem Kapitel können Sie Funktionen zu jedem Code hin-
zufügen, den Sie einer Testkontrolle unterwerfen können. Die Literatur über Test-
Driven Development ist in den letzten Jahren gewachsen. Insbesondere empfehle
ich Ihnen die Bücher Test-Driven Development by Example von Kent Beck (Addison-
Wesley, 2002) und Test-Driven Development: A Practical Guide von Dave Astel
(Prentice Hall Professional Technical Reference, 2003).
125
Kapitel 9
Dies ist das schwierige Problem. Könnten wir eine Klasse immer leicht in einem
Test-Harnisch instanziieren, wäre dieses Buch erheblich kürzer. Leider ist dies
aber oft schwer.
Hier sind unsere vier häufigsten Probleme:
1. Es ist schwierig, Objekte der Klasse zu erstellen.
2. Es ist schwierig, die Klasse in dem Test-Harnisch zu kompilieren.
3. Der Konstruktor, den wir benutzen müssen, hat unerwünschte Nebeneffekte.
4. In dem Konstruktor werden wichtige Aufgaben gelöst, die wir überwachen müs-
sen.
In diesem Kapitel behandle ich mehrere Beispiele für diese Probleme in verschie-
denen Sprachen. Man kann diese Probleme auf mehreren Wegen angehen. Doch
diese Beispiele durchzulesen, ist eine hervorragende Methode, sich mit dem Arse-
nal der Techniken zur Aufhebung von Dependencies vertraut zu machen, ihre
Vor- und Nachteile kennen zu lernen und ihre Eignung in bestimmten Situatio-
nen abzuschätzen.
127
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Unter anderem soll uns diese Klasse mitteilen, ob Kunden kreditwürdig sind. Ist
dies der Fall, gibt sie ein Zertifikat mit der Kreditlinie des Kunden zurück; sonst
löst sie eine Ausnahme aus.
Wenn wir ein Objekt in einem Test-Harnisch erstellen müssen, ist es oft am bes-
ten, es einfach zu tun. Anstatt aufwendig zu analysieren, wie leicht oder schwierig
diese Aufgabe sein könnte, können wir genauso leicht eine JUnit-Testklasse erstel-
len:
128
9-i
Der Fall des irritierenden Parameters
d i t V a l i d a t o r gibt. Ein Blick auf den Code zeigt uns, dass wir eine RGHConnec-
tion, einen CreditMaster und ein Passwort brauchen. Jede dieser Klassen hat
nur einen Konstruktor. So sehen sie aus:
}
}
Wenn eine RGHConnecti on konstruiert wird, stellt sie eine Verbindung mit einem
Server her und ruft die Daten ab, die zur Bewertung der Kreditwürdigkeit eines
Kunden benötigt werden.
Die Klasse CreditMaster liefert uns Policy-Informationen für unsere Kreditent-
scheidungen. Bei der Konstruktion lädt ein CreditMaster die Informationen aus
einer Datei in den Arbeitsspeicher.
Es scheint also ziemlich leicht zu sein, diese Klasse in einen Test-Harnisch einzu-
fügen, oder? Nicht so schnell. Wir können den Test schreiben, aber können wir
mit ihm leben?
Es zeigt sich, dass es in einem Test nicht sehr sinnvoll ist, mit einem RGHConnec-
t i on-Objekt eine Verbindung zu einem Server herzustellen. Es dauert länger, und
der Server ist nicht immer zugänglich. Dagegen ist der CreditMaster unproble-
matisch. Wenn wir ein Credi tMaster-Objekt erstellen, wird seine Datei schnell
geladen. Da die Datei zudem read-only ist, kann sie durch unsere Tests auch nicht
beschädigt werden.
Was uns wirklich behindert, wenn wir den Validator erstellen wollen, ist die RGH-
Connecti on. Sie ist ein so genannter irritierender Parameter. Könnten wir eine Art
Fake-RGHConnection-Objekt erstellen und den C r e d i t V a l i d a t o r von seiner
Echtheit überzeugen, könnten wir alle möglichen Verbindungsprobleme vermei-
den. Betrachten wir einige Methoden von RGHConnecti on (siehe Abbildung 9.1).
129
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
RGHConnection
A b b . 9.1: RCHConnection
Anscheinend verfügt RGHConnecti on über Methoden für die Verwaltung der Ver-
bindung, connect, disconnect und retry sowie mehrere Geschäftsmethoden
wie etwa RFDIReportFor und ACTIOReportFor. Wenn wir eine neue Methode für
CreditValidator schreiben, müssen wir RFDIReportFor aufrufen, um alle
benötigten Daten abzurufen. Normalerweise werden diese Daten von dem Server
geliefert, aber weil wir keine echte Verwendung wollen, müssen wir ein Fake-
Objekt zur Verfügung stellen.
In diesem Fall können wir ein Fake-Objekt am besten erstellen, indem wir Extract
Interface (25.10) auf die RGHConnecti on-Klasse anwenden. Wenn Sie mit einem
Refactoring-Tool arbeiten, beherrscht es wahrscheinlich diese Technik. Falls nicht,
können Sie sie auch manuell recht einfach anwenden.
Nach Anwendung von Extract Interface (25.10) erhalten wir die Struktur, die Sie in
Abbildung 9.2 sehen.
«interface»
IRGHConnection
y- co/7/7ecf/y
¥• cZ/scomecffJ
l
RGHConnection
130
9-i
Der Fall des irritierenden Parameters
Jetzt können wir mit einer kleinen Fake-Klasse unsere ersten Tests schreiben, die
die benötigten Daten liefert:
assertEqualsCCertificate.VALID, result.getStatusO);
}
Da wir jetzt einen Validator erstellen können, können wir auch unsere getVal i -
dationPercent-Methode schreiben. Hier ist ein Test für diese Methode:
Der Test prüft, ob der Validierungsprozentsatz für ein einzelnes gültiges Kredit-
zertifikat etwa ioo Prozent beträgt.
131
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Die Test funktioniert; aber wenn wir Code für getVal i dati onPercent schreiben,
stellen wir etwas Interessantes fest, nämlich dass getVal i dati onPercent den
CreditMaster überhaupt nicht nutzt. Weshalb erstellen wir dann ein C r e d i t -
Master-Objekt und übergeben es an den Credi tVal i dator? Vielleicht ist dies gar
nicht erforderlich. Wir könnten den Credi tVal i dator in unserem Test wie folgt
erstellen:
Pass Null
(Null übergeben) Wenn Sie Tests schreiben und ein Objekt einen Parameter
erfordert, dessen Konstruktion schwierig ist, sollten Sie erwägen, stattdessen
null zu übergeben. Wird der Parameter bei der Ausführung Ihres Tests ver-
wendet, wird eine Ausnahme ausgelöst, die von dem Test-Harnisch abgefangen
132
9-i
Der Fall des irritierenden Parameters
wird. Wenn Sie Verhalten benötigen, das wirklich ein Objekt erfordert, können
Sie es konstruieren und an der jeweiligen Stelle als Parameter übergeben.
Pass Null ist in einigen Sprachen eine sehr brauchbare Technik. Sie funktioniert
in Java, C# und anderen Sprachen, die eine Ausnahme auslösen, wenn zur Lauf-
zeit null-Referenzen verwendet werden. Dies impliziert, dass die Anwendung
dieser Technik in C und C++ nur dann sinnvoll ist, wenn Sie sicher sein kön-
nen, dass das Runtime Null-Pointer-Fehler entdeckt. Falls nicht, werden Ihre
Tests aus unerfindlichen Gründen abstürzen - wenn Sie Glück haben! Wenn Sie
Pech haben, werden Ihre Tests einfach schweigen und hoffnungslos falsch sein.
Sie werden bei der Ausführung den Speicher korrumpieren, ohne dass Sie dies
bemerken.
In Java beginne ich oft mit folgendem Test und ergänze die Parameter nach
Bedarf:
Wichtig ist: Übergeben Sie null in Produktions-Code nur, wenn Sie keine andere
Wahl haben. Ich weiß, dass einige Bibliotheken dies erwarten; aber wenn Sie fri-
schen Code schreiben, gibt es bessere Alternativen. Sind Sie versucht, nul 1 in Pro-
duktions-Code zu verwenden, sollten Sie an den Stellen, an denen Sie nul 1 -Werte
übergeben und zurückgeben, den Einsatz eines anderen Protokolls erwägen.
Alternativ könnten Sie etwa das Null Object Pattern verwenden.
133
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Wir könnten auch null zurückgeben, aber dann müssten Clients den Rückga-
bewert ausdrücklich auf null prüfen.
Es gibt eine dritte Alternative. Ist es für den vorhergehenden Code wirklich
wichtig, ob es einen zu bezahlenden Mitarbeiter gibt? Muss sich der Code
darum kümmern? Was passiert, wenn wir eine Klasse namens Nul 1 Empl oyee
verwenden? Eine Nul 1 Employee-Instanz hat keinen Namen und keine Adresse,
und wenn Sie ihr die Anweisung geben, zu bezahlen, tut sie einfach nichts.
Pass Null und Extract Interface (25.10) sind zwei Möglichkeiten, um mit irritieren-
den Parametern umzugehen. Doch manchmal können Sie auch eine andere Alter-
native nutzen. Falls die problematische Dependency in einem Parameter nicht fest
in den Konstruktor eincodiert ist, können Sie die Dependency auch mit Subclass
and Override Method (25.21) aufheben. Dies könnte in diesem Fall möglich sein.
Wenn der Konstruktor von RGHConnecti on eine Verbindung mit seiner connect-
Methode herstellt, könnten wir die Dependency aufheben, indem wir connectO
in einer Test-Unterklasse überschreiben. Subclass and Override Method (25.21) kann
sehr nützlich sein, um Dependencies aufzuheben, aber wir müssen sicher sein,
dass wir bei ihrer Anwendung kein Verhalten ändern, das wir testen wollen.
134
9.2
Der Fall der verborgenen Dependency
zugänglich ist. Das folgende Beispiel zeigt dies anhand einer schlecht konzipier-
ten C++-Klasse, die eine Mailing-List verwaltet:
class mailing_list_dispatcher
{
public:
mailing_list_dispatcher ();
vi rtual ~mai1i ng_li st_di spatcher;
private:
mail_service *service;
int status;
};
Hier ist ein Teil des Konstruktors der Klasse. Er alloziert mit new ein m a i l _ s e r -
vi ce-Objekt in der Initialisierungsliste des Konstruktors. Dies ist schlechter Stil,
und er wird noch schlechter. Der Konstruktor leistet mit dem mai l _ s e r v i ce-
Objekt viel Detailarbeit. Außerdem verwendet er eine »magische Zahl«, 1 2 - was
bedeutet 12?
In einem Test können wir eine Instanz dieser Klasse erstellen, aber das hilft uns
wahrscheinlich nicht viel weiter. Zuerst müssen wir die Mail-Bibliotheken einbin-
den und das Mail-System so konfigurieren, dass es Registrierungen handhabt.
Und wenn wir die send_message-Funktion in unseren Tests verwenden, versen-
den wir echte Mails. Es wird schwierig sein, diese Funktionalität automatisch zu
testen, wenn wir nicht eine besondere Mailbox einrichten, wiederholt mit ihr Ver-
bindung aufnehmen und warten, dass Mail-Messages eintreffen. Dies könnte sich
vielleicht für einen umfassenden Systemtest eignen; doch da wir im Moment nur
neue getestete Funktionen in die Klasse einfügen wollen, wäre dies vielleicht zu
viel des Guten. Wie können wir einen Test eines einfachen Objekts so erstellen,
dass wir es um zusätzliche Funktionen erweitern können?
135
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Das grundlegende Problem liegt hier darin, dass die Dependency von mai 1 _ s e r -
vi ce in dem mai 1 i ng_l i st_di spatcher-Konstruktor verborgen ist. Könnten wir
das mai l _ s e r v i ce-Objekt durch ein Fake-Objekt ersetzen, könnten wir bei Ände-
rungen über das Fake-Objekt Feedback über die Klasse bekommen.
Unter anderem können wir mit der Technik Parameterize Constructor (25.14) eine
Dependency in einem Konstruktor externalisieren, indem wir sie an den Kon-
struktor übergeben.
Nach Anwendung von Parameterize Constructor (25.14) sieht der Konstruktor wie
folgt aus:
Der einzige Unterschied liegt darin, dass das mai l _ s e r v i ce-Objekt außerhalb der
Klasse erstellt und an sie übergeben wird. Dies scheint keine große Verbesserung
zu sein, gibt uns aber einen unglaublichen Vorteil. Mit Extract Interface (25.10)
können wir jetzt eine Schnittstelle für m a i l _ s e r v i c e erstellen. Ein Implementer
der Schnittstelle kann die Produktionsklasse sein, die echte Mails versendet. Ein
weiterer kann eine Fake-Klasse sein, die die Dinge überwacht, die wir unter Test-
kontrolle bringen wollen, und uns sicher sein lässt, dass sie tatsächlich passieren.
136
9.2
Der Fall der verborgenen Dependency
if (service->get_status() == MS_AVAILABLE) {
service->register(this, client_type, MARK_MESSAGES_OFF);
service->set_param(client_type, ML_NOBOUNCE | MI REPEATOFF);
}
el se
status = MAILJDFFLINE;
mai1ing_list_dispatcher::mai1ing_list_dispatcher(mail_service *service)
{
initialize(service);
}
Jetzt können wir einen Konstruktor mit der ursprünglichen Signatur erstellen.
Tests können den Konstruktor mit dem Parameter m a i l _ s e r v i c e aufrufen. Cli-
ents können dagegen den folgenden Konstruktor aufrufen; sie müssen nicht wis-
sen, dass sich etwas geändert hat.
Dieses Refactoring ist in Sprachen wie etwa C# und Java noch einfacher, weil
wir Konstruktoren in diesen Sprachen von anderen Konstruktoren aus aufrufen
können.
In C# würde der Code analog wie folgt aussehen:
137
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
class WatercolorPane
{
public:
WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop)
{
Mit einem Refactoring-Tool, das Methoden sicher extrahieren kann, könnten wir
Extract and Override Factory Method (25.7) auf den Code in einem Konstruktor
anwenden; aber das funktioniert nicht in allen Sprachen. In Java und C# wäre es
möglich, aber C++ erlaubt in Konstruktoren keine Aufrufe von virtuellen Funktio-
nen, die möglicherweise in abgeleiteten Klassen definiert sind. Außerdem wäre
dies im Allgemeinen keine gute Idee. Funktionen in abgeleiteten Klassen nehmen
oft an, sie könnten Variablen ihrer Basisklasse verwenden. Bevor der Konstruktor
der Basisklasse nicht vollkommen fertig ist, besteht die Gefahr, dass eine über-
schriebene Funktion, die er aufruft, auf eine Variable zugreifen will, die noch nicht
initialisiert wurde.
Eine weitere Option ist Supersede Instance Variable (25.22). Wir schreiben einen Set-
ter in der Klasse, mit dem wir eine andere Instanz setzen können, nachdem wir
das Objekt konstruiert haben.
138
9-3
Der Fall der verketteten Konstruktionen
class WatercolorPane
{
publi c:
WatercolorPaneCForm *border, WashBrush *brush, Pattern *backdrop)
{
In den meisten anderen Sprachen ist Supersede Instance Variable (25.22) ziemlich
unkompliziert. Der folgende Code zeigt dieselbe Aktion in Java. Wir müssen
nichts Besonderes tun, um das Objekt zu beseitigen, auf das sich cursor bezieht;
der Garbage Collector beseitigt es irgendwann. Aber wir dürfen die supersede-
Cursor-Methode keinesfalls in Produktions-Code verwenden. Wenn die Objekte,
die wir ersetzen, andere Ressourcen verwalten, können sie ernste Ressourcenpro-
bleme verursachen.
Mit dieser Methode können wir jetzt versuchen, ein FocusWidget außerhalb der
Klasse zu erstellen und danach das Objekt zu übergeben. Weil wir überwachen
wollen, können wir Extract Interface (25.10) oder Extract Implementer (25.9) auf die
FocusWi dget-Klasse anwenden und für die Übergabe ein Fake-Objekt erstellen.
Es lässt sich bestimmt einfacher konstruieren als das FocusWidget, das in dem
Konstruktor erstellt wird.
TEST(renderBorder, WatercolorPane)
{
13S
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Ich verwende Supersede Instance Variable (25.22) nur, wenn es sich nicht vermeiden
lässt. Die Gefahr von Problemen mit dem Ressourcen-Management ist zu groß.
Doch manchmal wende ich diese Technik in C++ an. Oft möchte ich Extract and
Override Factory Method (25.7) anwenden, was aber in C++-Konstruktoren nicht
möglich ist. Deshalb greife ich gelegentlich auf Supersede Instance Variable (25.22)
zurück.
Frameworks lösen viele Probleme und erleichtern uns den Einstieg in neue Pro-
jekte; aber sie bieten nicht die Art von Wiederverwendung, die sich Entwickler
früher bei der Software-Entwicklung versprochen haben. Bei einer Wiederverwen-
dung alten Stils können wir eine fremde Klasse einfach zu unserem Projekt hinzu-
fügen und nutzen. Es wäre schön, könnten wir dies routinemäßig tun; doch offen
gestanden machen wir uns meiner Meinung nach etwas vor, auch nur an eine sol-
che Art von Wiederverwendung zu denken, wenn wir nicht eine beliebige Klasse
aus einer durchschnittlichen Anwendung herausnehmen und ohne aufwendige
Arbeiten unabhängig in einem Test-Harnisch kompilieren können (traurig, trau-
rig)-
140
9-4
Der Fall der irritiererden globalen Dependency
}
Wir wollen eine F a c i l i t y in einem Test-Harnisch erstellen. Deshalb beginnen
wir mit einem entsprechenden Versuch:
Der Test wird ohne Fehler kompiliert, aber bei den ersten zusätzlichen Tests gibt
es ein Problem. Der Konstruktor verwendet eine Klasse namens PermitReposi-
tory, die mit einem bestimmten Satz von Genehmigungen initialisiert werden
muss, um die Tests korrekt einzurichten. Vertrackte Situation. Hier ist die anstö-
ßige Anweisung in dem Konstruktor:
Permit associatedPermit =
Permi tReposi tory.getlnstance().fi ndAssoci atedPermi t(noti ce);
141
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Instanz von Permi tReposi tory zurückgeben soll, die es in unserer Anwendung
geben kann. Das Feld, in dem diese Instanz gespeichert wird, ist ebenfalls statisch;
es befindet sich in der Permi tReposi tory-Klasse.
In Java zählt das Singleton Pattern zu den Mechanismen, mit denen Entwickler
globale Variablen definieren. Im Allgemeinen gibt es mehrere Gründe, die gegen
globale Variablen sprechen, darunter auch die Undurchsichtigkeit (Opazität). Mit
einem Blick auf ein Code-Fragment sollten wir seine Auswirkungen beurteilen
können. Wollen wir etwa verstehen, was das folgende Java-Code-Fragment
bewirkt, gibt es nur wenige Stellen, die wir prüfen müssen.
Wir wissen, dass ein Account-Objekt Dinge beeinflussen kann, die wir an den
Account-Konstruktor übergeben; doch wir übergeben dem Konstruktor nichts.
Account-Objekte können auch Objekte beeinflussen, die wir als Parameter an
Methoden übergeben; doch in diesem Fall übergeben wir nichts, was geändert
werden kann, sondern eine einfache Ganzzahl. Hier weisen wir den Rückgabe-
wert von getBal ance einer Variablen zu, und dies ist wirklich der einzige Wert,
der von diesen Anweisungen beeinflusst werden sollte.
Bei globalen Variablen wird diese Situation auf den Kopf gestellt. Ein Blick auf
eine Belasse wie etwa Account sagt uns nicht, ob sie auf Variablen zugreift oder
welche ändert, die an anderer Stelle des Programms deklariert sind. Ich brauche
nicht zu erwähnen, dass dadurch Programme schwieriger zu verstehen sind.
Permi tReposi tory ist ein Singleton. Deswegen ist es besonders schwer zu simu-
lieren; denn dieses Patterns basiert auf der Idee, es unmöglich zu machen, mehr
als eine Instanz eines Singletons in einer Anwendung zu erstellen. In Produk-
tions-Code mag das akzeptabel sein, aber beim Testen sollte eine Suite von Tests
eine Art von Mini-Anwendung sein: Sie sollte von den anderen Tests vollkommen
isoliert sein. Um Code mit Singletons in einem Test-Harnisch auszuführen, müs-
sen wir deshalb die Singleton-Property weniger streng handhaben. Wir gehen fol-
gendermaßen vor:
142
9-4
Der Fall der irritierenden globalen Dependency
Introduce Static Setter (25.12) ist nicht die einzige Methode, u m diese Situation z u
h a n d h a b e n . H i e r ist ein anderer Ansatz. Wir k ö n n e n f o l g e n d e resetForTes-
t i n g ( ) - M e t h o d e in das Singleton e i n f ü g e n :
143
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Funktioniert das? Noch nicht. Entwickler, die das Singleton Design Pattern (25.12)
verwenden, deklarieren oft den Konstruktor der Singleton-Klasse aus gutem
Grund als p r i v a t e . So können wir ausdrücklich dafür sorgen, dass niemand
außerhalb der Klasse eine weitere Instanz des Singletons erstellen kann.
An diesem Punkt gibt es einen Konflikt zwischen zwei Design-Zielen. Wir wollen
sicherstellen, dass ein System nur eine Instanz einer PermitRepository enthält,
und wir brauchen ein System, in dem wir die Klassen unabhängig testen können.
Können wir beide Ziele erfüllen?
Denken wir noch einmal nach. Warum soll es nur eine Instanz einer Klasse in
einem System geben? Das hängt vom System ab. Einige der häufigsten Antworten
lauten:
1. Wir modellieren die Wirklichkeit, und dort gibt es nur ein solches Ding. Einige
Hardware-Steuersysteme entsprechen dieser Auffassung. Entwickler erstellen
etwa für jede Platine, die sie steuern wollen, eine separate Klasse; da es nur
jeweils eine Platine gibt, sollte die Klasse ein Singleton sein. Dasselbe gilt für
Datenbanken. Es gibt nur eine Collection von Zugriffsrechten in unserem
Unternehmen; deshalb sollte der Zugriff über ein Singleton gesteuert werden.
2. Wenn wir zwei dieser Dinge erstellten, könnten wir ein ernstes Problem
bekommen. Auch hier ist die Hardware-Steuerung ein geeignetes Arbeitsfeld.
Angenommen, Sie programmierten aus Versehen zwei Steuerungen für Kern-
brennstäbe und zwei verschiedene Teile eines Programms griffen auf dieselben
Brennstäbe zu, ohne voneinander zu wissen.
3. Wenn man zwei solche Dinge erstellt, werden zu viele Ressourcen verbraucht.
Dies passiert oft. Bei den Ressourcen kann es sich um physische Dinge wie etwa
Speicherplatz auf der Festplatte handeln oder um abstrakte Dinge wie etwa die
Anzahl der Software-Lizenzen.
Dies sind die Hauptgründe, warum Entwickler eine einzige Instanz erzwingen
wollen, aber es sind nicht die Hauptgründe, warum Entwickler Singletons verwen-
den. Sie erstellen Singletons oft, weil sie eine globale Variable verwenden wollen,
144
9-4
Der Fall der irritierenden globalen Dependency
um sich den Aufwand zu ersparen, die Variable an die benötigten Stellen zu über-
geben.
Letzteres ist wirklich kein Grund, auf die Singleton-Eigenschaft Rücksicht zu neh-
men. Wir können den Konstruktor als protected oder public deklarieren oder
seinen Geltungsbereich auf das Package ausweiten und dennoch ein vernünftiges,
testbares System haben. Doch auch in den anderen Fällen lohnt es sich, diese
Alternative zu untersuchen. Wir können bei Bedarf auch andere Schutz-
mechanismen einführen. Wir könnten etwa eine Prüfung in unser Build-System
einbauen, um alle Quelldateien zu durchsuchen, ob s e t T e s t i nglnstance auch
ja nicht von testfremdem Code aufgerufen wird. Wir können dasselbe mit Lauf-
zeitprüfungen erreichen. Wird s e t T e s t i nglnstance zur Laufzeit aufgerufen,
können wir eine Warnung ausgeben oder das System anhalten und auf einen Ein-
griff des Operators warten. Tatsache ist, dass die Singleton-Eigenschaft in vielen
Prä-OO-Sprachen nicht erzwungen werden konnte; dennoch wurden viele sichere
Systeme entwickelt. Letztlich ist alles eine Frage eines verantwortungsvollen
Designs und einer entsprechenden Codierung.
Ist die Aufhebung der Singleton-Eigenschaft unproblematisch, können wir auf das
Team setzen. So sollte etwa jedes Team-Mitglied verstehen, dass es in der Anwen-
dung nur eine Instanz der Datenbank gibt und keine andere erstellt werden sollte.
Um die Singleton-Property von Permi tReposi t o r y abzuschwächen, können wir
den Konstruktor als p u b l i c deklarieren. Dies funktioniert so lange, wie wir mit
den öffentlichen Methoden von Permi tReposi t o r y alles Erforderliche tun kön-
nen, um ein Repository für unsere Tests einzurichten. Verfügt etwa Permi tRepo-
si t o r y über eine Methode namens addPermit, mit der wir die erforderlichen
Daten für unsere Tests einrichten können, könnte die Maßnahme ausreichen, für
unsere Tests Repositories einzurichten und zu nutzen. Manchmal bekommen wir
möglicherweise nicht den erforderlichen Zugriff, oder, schlimmer, macht das Sin-
gleton Dinge, die in einem Test-Harnisch nicht passieren sollten, wie etwa im Hin-
tergrund mit einer Datenbank zu kommunizieren. In diesen Fällen können wir
Subclass and Override Method (25.21) verwenden und mit abgeleiteten Klassen das
Testen vereinfachen.
Hier ist ein Beispiel für unser Permit-System. Zusätzlich zu der Methode und den
Variablen, die aus Permi tReposi t o r y ein Singleton machen, benutzen wir fol-
gende Methode:
// Werte auswählen
145
// Gültigkeit prüfen; falls ungültig, Fehler ausgeben
}
}
Wenn wir nicht mit der Datenbank kommunizieren können, können wir von Per-
mi tRepository eine Unterklasse ableiten:
Damit können wir die Singleton-Eigenschaft teilweise erhalten. Weil wir eine
Unterklasse von Permi tReposi tory verwenden, können wir den Konstruktor von
Permi tRepository als protected anstatt als publ i c deklarieren. Damit wird die
Erstellung von mehr als einem Permi tReposi tory verhindert, obwohl wir Unter-
klassen erstellen können.
146
9-4
Der Fall der irritierenden globalen Dependency
In vielen Fällen können wir mit Subclass and Override Method (25.21) ein solches
Fake-Singleton einrichten. Manchmal sind die Dependencies so umfangreich,
dass es einfacher ist, Extract Interface (25.10) auf das Singleton anzuwenden und
alle Referenzen in der Anwendung so zu ändern, dass sie den Namen der Schnitt-
stelle verwenden. Dies kann viel Arbeit sein, aber mit Lean on the Compiler (23.4)
kann die Änderung einfacher werden. Nach der Extraktion sieht die Permi tRepo-
si tory-Klasse wie folgt aus:
protected PermitRepositoryC) {}
Die IPermi tReposi tory-Schnittstelle wird Signaturen für alle öffentlichen nicht-
statischen Methoden von Permi tReposi tory haben.
Wenn Sie eine Sprache verwenden, die über ein Refactoring-Tool verfügt, könnten
Sie die Extraktion dieser Schnittstelle automatisch ausführen; andernfalls ist es
vielleicht einfacher, mit Extract Implementer (25.9) zu arbeiten.
Diese Technik wird als Introduce Static Setter (25.12) bezeichnet. Sie können damit
Tests trotz umfangreicher globaler Dependencies einrichten. Leider trägt sie nicht
viel dazu bei, diese globalen Dependencies aufzuheben. Sie können dieses Pro-
147
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
blem mit Parameterize Method (25.15) und Parameterize Constructor (25.14) ange-
hen. Bei diesen Refactorings tauschen Sie eine globale Referenz entweder gegen
eine temporäre Variable in einer Methode oder ein Feld in einem Objekt ein. Para-
meterize Method (25.15) hat den Nachteil, dass Sie viele zusätzliche Methoden
bekommen können, die das Verstehen der Klassen erschweren. Parameterize
Constructor (25.14) hat den Nachteil, dass jedes Objekt, das gegenwärtig die Globals
verwendet, ein zusätzliches Feld erhält. Das Feld muss an seinen Konstruktor
übergeben werden, damit die Klasse, die das Objekt erstellt, auch auf die Instanz
zugreifen kann. Wenn zu viele Objekte dieses zusätzliche Feld benötigen, kann
der Speicherbedarf der Anwendung erheblich zunehmen, aber oft ist dies ein
Indiz für andere Design-Probleme.
Betrachten wir den schlimmsten Fall: Angenommen, eine Anwendung habe meh-
rere Hundert Klassen, die zur Laufzeit Tausende von Objekten erstellen, und jedes
davon brauche Zugriff auf die Datenbank. Ohne auch nur die Anwendung anzu-
schauen, denke ich zuerst an die Frage, warum? Wenn das System mehr tut, als
auf eine Datenbank zuzugreifen, kann es so strukturiert werden, dass einige Klas-
sen diese anderen Dinge erledigen und andere Daten abrufen und speichern.
Wenn wir uns die Mühe machen, die Aufgaben in einer Anwendung zu trennen,
können Dependencies lokal eingegrenzt werden; vielleicht müssen wir nicht in
jedem Objekt eine Datenbank referenzieren. Einige Objekte werden mit Daten ini-
tialisiert, die von der Datenbank abgerufen wurden. Andere berechnen Daten, die
von ihren Konstruktoren zur Verfügung gestellt werden.
Wenn Sie eine globale Variable finden, die wirklich überall verwendet wird, enthält
Ihr Code nicht genügend Ebenen (siehe Kapitel 15, My Application Is All API Calls,
und Kapitel 17, My Application Has No Structure).
148
9-5
Der Fall der schrecklichen Include-Dependencies
sich auf die C-Funktionen beschränken. Können Sie Ihr Team nicht überreden,
eine OO-Sprache zu verwenden? Kein Problem; hier ist ein C++-Compiler; schrei-
ben Sie das Programm mit der C-Teilmenge von C++ und nehmen Sie OO-Funk-
tionen nach Bedarf hinzu.
Obwohl C++ eine Zeit lang sehr beliebt war, fiel seine Beliebtheit letztlich hinter
Java und einigen neueren Sprachen zurück. Es gab gute Argumente dafür, die
Rückwärtskompatibilität zu C anzubieten, aber viel mehr sprach dafür, Sprachen
zu entwickeln, die das Programmieren vereinfachen. Wiederholt haben C++-
Teams die Erfahrung gemacht, dass die Standardkonfiguration der Sprache für die
Wartung von Software nicht ideal ist und dass sie darüber hinausgehen mussten,
um ihre Systeme schlank und wartungsfreundlich zu halten.
Dies soll keine Kritik an C++ sein. C++ ist eine wichtige Sprache und es gibt
unglaublich viel C++-Code - aber mit diesem Code zu arbeiten, erfordert beson-
dere Vorsicht.
149
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen
Der folgende Code zeigt einen Teil der Deklaration einer riesigen C++-Klasse
namens Scheduler. Er enthält mehr als 200 Methoden, aber ich habe nur fünf
ausgewählt. Neben ihrem Umfang leidet die Klasse unter gewichtigen und ver-
schlungenen Dependencies von vielen anderen Klassen. Wie können wir in einem
Test einen Scheduler erstellen?
#i fndef SCHEDULER_H
#defi ne SCHEDULERJH
#include "Meeting.h"
#include "MaiIDaemon.h"
#include "SchedulerDisplay.h"
#include "DayTime.h"
class Scheduler
{
public:
Scheduler(const string& owner);
~Scheduler();
};
#endif
#include "TestHarness.h"
#include "Scheduler.h"
TEST(create,Scheduler)
{
Scheduler scheduler("fred");
}
Wenn wir eine Datei anlegen und versuchen, einen Schedul er in einem Test zu
erstellen, stehen wir vor dem i ncl ude-Problem. Um einen Schedul er zu kompi-
lieren, müssen wir dafür sorgen, dass Compiler und Linker alle Dinge kennen, die
der Scheduler braucht, und alle Dinge, die diese Dinge brauchen, usw. Glückli-
cherweise liefert das Build-System zahlreiche Fehlermeldungen und informiert
uns über diese Dinge in erschöpfendem Detail.
150
9-5
Der Fall der schrecklichen Include-Dependencies
Wir könnten einfach alle #i ncl ude-Direktiven aus der Quelldatei der Schedul er-
Klasse kopieren, doch wahrscheinlich brauchen wir nicht alle. Am besten ist es,
sie einzeln nacheinander hinzuzufügen und zu entscheiden, ob wir die jeweiligen
Dependencies wirklich brauchen. Oft können wir sie einfach mit zusätzlichen For-
ward-Deklarationen vermeiden.
#include "TestHarness.h"
#include "Scheduler.h"
TEST(create,Schedul er)
{
Scheduler scheduler("fred");
}
#include "TestHarness.h"
#include "Scheduler.h"
#include "Fakes.h"
151
Kapitel io
Ich kann dieseMethodenicht in einem Test-Harnisch ausführen
TEST(create,Scheduler)
{
Scheduler scheduler("fred");
}
Nachdem man dies einige Male getan hat, ist es ziemlich einfach und mechanisch,
eine C++-Klasse in einem Harnisch instanziierbar zu machen, aber es gibt einige
gewichtige Nachteile. Wir müssen dieses separate Programm erstellen, und auf
Sprachebene heben wir eigentlich keine Dependencies auf; deshalb wird der Code
durch diese Maßnahme nicht übersichtlicher. Schlimmer noch: Duplizierte Defi-
nitionen (in diesem Beispiel SchedulerDi splay: :di spl ayEntry), die wir in die
Testdatei einfügen, müssen so lange gewartet werden, wie wir mit diesen Tests
arbeiten.
Ich reserviere diese Technik für sehr große Klassen mit schwerwiegenden Depen-
dency-Problemen. Diese Technik sollte nur selten und nicht leichtfertig angewen-
det werden. Wenn diese Klasse im Laufe der Zeit in zahlreiche kleinere Klassen
zerlegt wird, kann es nützlich sein, ein separates Testprogramm für eine Klasse zu
erstellen. Es kann für viele Refactorings als Testpunkt dienen. Wenn Sie mehr
Klassen extrahieren und unter Testkontrolle bringen, wird dieses separate Testpro-
gramm im Laufe der Zeit überflüssig.
152
9.6
Der Fall der Zwiebel-Parameter
Um sie zu erstellen, müssen wir ihr ein Schedul i ngTask-Objekt übergeben; aber
um dieses Objekt zu erstellen, müssen wir den einzigen Konstruktor der Schedu-
1 i ngTask-Klasse aufrufen:
Wenn wir feststellen, dass wir weitere Objekte benötigen, um Scheduler und
MeetingResolver zu erstellen, könnten wir uns die Haare raufen. Das Einzige,
was uns vor totaler Verzweiflung bewahrt, ist die Tatsache, dass es wenigstens eine
Klasse geben muss, die keine Objekte einer anderen Klasse als Argumente erfor-
dert. Gibt es eine solche Klasse nicht, gibt es keine Methode, das System jemals zu
kompilieren.
In einer solchen Situation müssen wir genau untersuchen, was wir tun wollen.
Wir müssen Tests schreiben, aber welche Daten sollen uns die Parameter liefern,
die an den Konstruktor übergeben werden? Wenn wir in den Tests keine Daten
dieser Parameter brauchen, können wir Pass Null (Einschub in g.i) anwenden.
Wenn wir nur ein rudimentäres Verhalten brauchen, können wir Extract Interface
(25.10) oder Extract Implementer (25.9) auf die dringlichste Dependency anwenden
und mit der Schnittstelle ein Fake-Objekt erstellen. In diesem Fall ist Schedu-
1 i ngTask die dringlichste Dependency von Schedul i ngTaskPane. Wenn wir ein
Fake-Schedul ingTask-Objekt erstellen können, können wir auch ein Schedu-
1 i ngTaskPane-Objekt erstellen.
Leider ist SchedulingTask von einer Klasse namens SerialTask abgeleitet; sie
überschreibt nur einige protected Methoden ihrer Basisklasse. Alle öffentlichen
Methoden befinden sich in SerialTask. Können wir Extract Interface (25.10) auf
SchedulingTask anwenden, oder müssen wir sie auch für SerialTask benut-
zen? In Java ist dies nicht erforderlich. Wir können eine Schnittstelle für Schedu-
1 i ngTask erstellen, die auch Methoden von SerialTask einbindet.
In diesem Fall haben wir das Glück, Java zu verwenden. In C++ könnten wir die-
sen Fall nicht wie folgt handhaben. Es gibt keine separaten Schnittstellen. Schnitt-
stellen werden normalerweise als Klassen implementiert, die nur rein virtuelle
Funktionen enthalten. Würde dieses Beispiel nach C++ portiert, würde Schedu-
1 i ngTask eine abstrakte Klasse werden, weil sie eine reine virtuelle Funktion von
Schedul i ngTask erbt. Um eine Scheduli ngTask zu instanziieren, müssten wir
in Schedul ingTask einen Body für die runQ-Methode definieren, die an die
153
Kapitel io
Ich kann dieseMethodenicht in einem Test-Harnisch ausführen
run()-Methode von Seri alTask delegiert. Glücklicherweise wäre dies nicht allzu
schwierig. Hier ist der entsprechende Code:
class SerialTask
{
public:
Virtual v o i d run();
};
class ISchedulingTask
{
public:
V i r t u a l v o i d run() = 0;
};
class SchedulingTask : public SerialTask, public ISchedulingTask
{
public:
Virtual void run() {
SerialTask:: run() ;
}
};
In jeder Sprache, in der wir Schnittstellen oder Klassen erstellen können, die
sich wie Schnittstellen verhalten, können wir damit Dependencies systematisch
aufheben.
154
9-7
Der Fall des Alias-Parameters
manchmal ist dies praktisch nicht machbar. Betrachten wir eine andere Klasse aus
dem Baugenehmigungssystem aus einem vorhergehenden Abschnitt:
Permit associatedPermit =
Permi tReposi tory. GetlnstanceO .
f •i ndAssoci atedFromOri gi nati on(permi t);
if (associatedPermit.isValidü && !permit.isValid()) {
basePermit = associatedPermit;
}
eise if (!permit.isValidO) {
permit. v a l i d a t e O ;
basePermit = permit;
}
el se
throw new PermitViolation(permit);
}
Wir wollen diese Klasse in einem Harnisch instanziieren, aber es gibt einige Pro-
bleme. Unter anderem greifen wir wieder auf ein Singleton zu: PermitReposi-
tory. Wir können dieses Problem mit Techniken lösen, die Sie aus dem früheren
Abschnitt Der Fall der irritierenden globalen Dependency kennen. Doch bevor wir uns
überhaupt diesem Problem zuwenden können, müssen wir ein anderes lösen. Es
ist schwer, das Ori gi nati onPermi t-Objekt zu erstellen, das wir an den Konstruk-
tor übergeben müssen. Ori gi nati onPermi t-Objekte haben schreckliche Depen-
dencies. Meistens denke ich dann sofort daran, Extract Interface auf die
Ori gi nati onPermi t-Klasse anzuwenden, um diese Dependency aufzuheben,
aber es ist nicht einfach. Abbildung 9.4 zeigt die Struktur der Permi t-Hierarchie.
155
Kapitel io
Ich kann dieseMethodenicht in einem Test-Harnisch ausführen
I
FacilityPermit IFacility Permit
I
OriginationPermit [>
----- - — (OriginationPermit
Oh Schreck! Dies ist lächerlich viel Arbeit, und das Ergebnis gefällt mir nicht
besonders. Schnittstellen eignen sich, Dependencies aufzuheben, aber wenn wir
fast eine Eins-zu-eins-Beziehung zwischen Klassen und Schnittstellen erstellen
müssen, wird das Design unübersichtlich. Verstehen Sie mich nicht falsch: Wenn
wir mit dem Rücken zur Wand stehen, wäre es in Ordnung, dieses Design zu im-
plementieren; aber wenn es andere Möglichkeiten gibt, sollten wir sie untersu-
chen. Glücklicherweise ist dies der Fall.
Extract Interface (25.10) ist nur eine Methode, um die Dependency von einem Para-
meter aufzuheben. Manchmal lohnt es sich, zu fragen, warum die Dependency
nachteilig ist. Manchmal ist die Erstellung schwierig. Manchmal haben die Para-
meter einen unerwünschten Nebeneffekt. Vielleicht kommuniziert er mit dem
Dateisystem oder einer Datenbank. Vielleicht dauert die Ausführung seines Codes
einfach zu lange. Mit Extract Interface (25.10) können wir alle diese Probleme
umgehen; aber dafür kappen wir brutal die Verbindung zu einer Klasse. Wenn nur
Teile einer Klasse problematisch sind, können wir einen anderen Ansatz verfolgen
und nur die Verbindung zu ihnen kappen.
156
9-7
Der Fall des Alias-Parameters
Betrachten wir die Ori gi nati onPermi t-Klasse genauer. Wir wollen sie nicht in
einem Test verwenden, weil sie stillschweigend auf eine Datenbank zugreift, wenn
wir sie anweisen, sich selbst zu validieren:
// Validierungsdaten abfragen
// Validierungs-Flag setzen
// Datenbank schließen
}
}
In einem Test können wir dies nicht gebrauchen: Wir müssten einige Fake-Ein-
träge in die Datenbank vornehmen, was von dem Datenbankadministrator nicht
gern gesehen wird. Wir müssten ihn zum Essen einladen, wenn er dies heraus-
fände, und selbst dann wäre er immer noch empört. Sein Job ist auch so schwer
genug.
Alternativ können wir Subclass and Override Method (25.21) anwenden und eine
Klasse namens FakeOrigi nati onPermi t erstellen, die Methoden zur Verfügung
stellt, mit denen das Validierungs-Flag leicht geändert werden kann. Dann könn-
ten wir in Unterklassen die val i date-Methode überschreiben, um das Validie-
rungs-Flag beim Testen der Industri al Faci 1 i ty-Klasse nach Bedarf zu setzen.
Hier ist ein brauchbarer erster Test:
In vielen Sprachen kann man solche Klassen zur Laufzeit erstellen. Obwohl ich es
in Produktions-Code nur ungern tue, ist dies beim Testen sehr praktisch. Sonder-
falle können sehr leicht abgehandelt werden.
157
Kapitel io
Ich kann diese Methode nicht in einem Test-Harnisch ausführen
Subclass and Override Method (25.21) hilft uns, Dependencies von Parametern auf-
zuheben, aber manchmal ist das Herauslösen von Methoden in eine Klasse nicht
das Ideale. Wir hatten Glück, dass die störenden Dependencies in dieser val i -
date-Methode isoliert waren. In ungünstigeren Fällen sind sie mit Logik ver-
mischt, die wir benötigen, und wir müssen zuerst Methoden extrahieren. Wenn
wir über ein Refactoring-Tool verfügen, kann dies leicht sein; andernfalls könnten
einige Techniken aus Kapitel 22, Ich muss eine Monster-Methode ändern und kann
keine Tests dafür schreiben, helfen.
158
Kapitel i o
159
Kapitel io
Ich kann diese Methode nicht in einem Test-Harnisch ausführen
erspart uns die Mühe, einen Zugang zu der privaten Methode zu suchen, und hat
noch einen anderen Vorteil. Wenn wir mittels öffentlicher Methoden testen, testen
wir sie so, wie sie in dem Code verwendet werden. Dies kann dazu beitragen,
unsere Arbeit ein wenig einzugrenzen. In Legacy Code enthalten Klassen oft
Methoden von sehr zweifelhafter Qualität. Der Refactoring-Aufwand, um eine pri-
vate Methode für alle Aufrufer zugänglich zu machen, kann ziemlich groß sein.
Obwohl sehr allgemeine Methoden, die für viele Aufrufer zugänglich sind, nütz-
lich sein können, gilt doch: Jede Methode muss gerade so funktional sein, dass sie
die Anforderung des jeweiligen Aufrufers erfüllt, dass sie verständlich ist und dass
sie leicht änderbar ist. Wenn wir eine private Methode mittels öffentlicher Metho-
den testen, die auf sie zugreifen, ist die Gefahr gering, sie zu allgemein zu
machen. Wenn die Methode eines Tages veröffentlicht werden muss, sollte der
erste Nutzer außerhalb der Klasse Testfälle schreiben, die genau erklären, was die
Methode tut und wie sie korrekt aufgerufen wird.
Schön und gut; aber in einigen Fällen wollen wir nur einen Testfall für eine private
Methode schreiben, deren Aufruf tief in einer Klasse vergraben ist. Wir wollen
konkretes Feedback und Tests, die erklären, wie sie verwendet wird - oder, wer
weiß, vielleicht ist es nur zu mühsam, sie über den Umweg einer öffentliche
Methode der Klasse zu testen.
Wie also schreiben wir einen Test für eine private Methode? Dies muss eine der
häufigsten Fragen beim Testen sein. Zum Glück können wir diese Frage sehr
direkt beantworten: Müssen wir eine private Methode testen, sollten wir sie öffent-
lich machen. Wenn wir uns deswegen Sorgen machen, ist dies meistens ein Indiz
dafür, dass unsere Klasse zu viel leistet und wir dies korrigieren sollten. Betrach-
ten wir die Fälle. Warum sollten wir uns Sorgen machen, eine private Methode zu
veröffentlichen? Hier sind einige Gründe:
1. Die Methode ist nur eine Dienstfunktion, für die sich kein Client interessiert.
2. Wenn Clients die Methode verwenden, könnten sie unabsichtlich Ergebnisse
von anderen Methoden der Klasse beeinflussen.
Der erste Grund wiegt nicht schwer. Eine zusätzliche öffentliche Methode in der
Schnittstelle einer Klasse ist verzeihlich, obwohl wir herausfinden sollten, ob die
Methode nicht in einer anderen Klasse besser aufgehoben wäre. Der zweite Grund
wiegt schwerer; doch glücklicherweise gibt es eine Abhilfe: Die privaten Methoden
können in eine neue Klasse verschoben werden. Sie können in dieser Klasse veröf-
fentlicht werden; und unsere Klasse kann intern eine Instanz dieser Klasse erstel-
len. Dadurch werden die Methoden testbar; und das Design wird besser.
Auch wenn diese Ratschläge recht tiefgreifend zu sein scheinen, haben sie einige
sehr positive Effekte. Tatsache bleibt: Gutes Design ist testbar, und Design, das
nicht testbar ist, ist schlecht. Die erste Antwort ist in solchen Fällen die Anwen-
dung von Techniken aus Kapitel 20, Diese Klasse ist zu groß und soll nicht noch grö-
160
lO.l
Der Fall der verborgenen Methode
ßer werden. Doch wenn es noch nicht viele Tests gibt, müssen wir vorsichtig
vorgehen und einige andere Dinge erledigen, bevor wir das Vorhandene zerlegen
können.
Wie könnten wir dieses Problem in einem realistischen Fall lösen? Hier ist ein Teil
einer Klassendeklaration in C++:
class CCAImage
{
private:
void setSnapRegion(int x, int y, int dx, int dy);
public:
void s n a p O ;
};
Wir könnten die Methode einfach veröffentlichen. Leider könnte dies einige sehr
negative Konsequenzen haben. Die CCAImage-Klasse enthält einige Variablen, die
den gegenwärtigen Speicherort der Snap-Region bestimmen. Wenn jemand in
Produktions-Code die setSnapRegion-Methode außerhalb der snapO-Methode
aufruft, könnte dies zu ernsten Problemen mit dem Tracking-System der Kamera
führen.
Nun ist dies das Problem. Bevor ich einige Lösungen beschreibe, möchte ich fra-
gen, wie es zu diesem Chaos gekommen ist. Der eigentliche Grund, warum wir
die Image-Klasse nicht unter Testkontrolle bringen können, liegt darin, dass sie zu
viele Aufgaben hat. Idealerweise wäre es großartig, sie mit den Techniken aus
Kapitel 20 in kleinere Klassen zu zerlegen, aber wir müssen gründlich überlegen,
ob wir im Moment einen so hohen Refactoring-Aufwand treiben wollen. Dies zu
tun, wäre hervorragend, aber ob es möglich ist, hängt davon ab, wann das nächste
Release erfolgen soll, wie viel Zeit wir haben und welche Risiken damit verbunden
sind.
161
Kapitel IO
Ich kann diese M e t h o d e nicht in e i n e m Test-Harnisch ausführen
Wenn wir es uns nicht leisten können, die Aufgaben im Moment zu trennen, kön-
nen wir trotzdem Tests für die zu ändernde Methode schreiben? Glücklicherweise
ja, und so geht's:
class CCAImage
{
protected:
void setSnapRegion(int x, int y, int dx, int dy);
public:
void snap();
};
Als Nächstes bilden wir eine Unterklasse von CCAImage, um auf diese Methode
zuzugreifen:
In den meisten modernen C++-Compilern können wir auch eine usi ng-Dekla-
ration in der Test-Unterklasse verwenden, um die Delegation automatisch aus-
zuführen:
class TestingCCAImage : public CCAImage
{
public:
// Alle CCAImage-Implementierungen von setSnapRegion in der
// öffentlichen Schnittstelle veröffentlichen. Alle Aufrufe
// an CCAImage delegieren,
using CCAImage::setSnapRegion;
}
Danach können wir setSnapRegion von CCAImage in einem Test, wenn auch nur
indirekt, aufrufen. Aber ist dies eine gute Idee? Weiter vorne wollten wir die
Methode nicht veröffentlichen, aber hier machen wir etwas Ähnliches. Wir dekla-
rieren sie als protected und machen sie leichter zugänglich.
Offen gestanden stört mich dies nicht. Die Tests einrichten zu können, ist eine
faire Gegenleistung. Ja, mit dieser Änderung können wir gegen die Einkapselung
verstoßen. Wenn wir darüber nachdenken, wie Code funktioniert, müssen wir
162
10.2
Der Fall der »hilfreichen« Sprachfunktion
Zugriffsschutz unterlaufen
In vielen OO-Sprachen, die jünger als C++ sind, können wir per Reflection und
besonderen Rechten zur Laufzeit auf private Variablen zugreifen. Dies kann
zwar praktisch sein, ist aber eigentlich ein Betrug. Es ist sehr hilfreich, wenn wir
Dependencies aufheben, aber ich sehe es nicht gerne, dass Tests, die auf private
Variablen zugreifen, in Projekten bleiben. Dies ist eine Art Ausrede, die ein
Team wirklich davon abhält, zu erkennen, wie schlecht der Code wird. Es mag
sich leicht masochistisch anhören, aber der Schmerz, den wir beim Arbeiten
mit Legacy Code empfinden, kann einen unglaublichen Anreiz für Änderungen
geben. Wir können eine undurchsichtige Methode beseitigen, aber solange wir
uns nicht mit den eigentlichen Gründen, das heißt mit Klassen mit zu vielen
Aufgaben und verschlungenen Dependencies, auseinandersetzen, kommen wir
nicht wirklich weiter. Wenn schließlich jeder mitbekommt, wie schlecht der
Code geworden ist, sind die Kosten für eine Verbesserung ins Lächerliche
gestiegen.
list.Add(file.InputStream);
}
}
return list;
}
163
Kapitel IO
Ich kann diese M e t h o d e nicht in e i n e m Test-Harnisch ausführen
Wir würden dieses Code-Fragment gerne ändern und vielleicht ein wenig refakto-
risieren, aber Tests zu schreiben, wird schwierig sein. Wir würden gerne ein Http-
Fi leCollection-Objekt erstellen und mit HttpPostedFi 1 e-Objekten füllen,
aber dies ist unmöglich. Erstens hat die HttpPostedFi le-Klasse keinen öffentli-
chen Konstruktor. Zweitens ist die Belasse versiegelt. In C# bedeutet dies, dass wir
keine Instanz von HttpPostedFi le erstellen und keine Unterklasse von dieser
Klasse ableiten können. HttpPostedFi le gehört zur .NET-Bibliothek. Zur Lauf-
zeit erstellt eine andere Klasse Instanzen dieser Klasse, auf die wir aber nicht
zugreifen können. Die HttpFileCollection-Klasse hat dieselben Probleme:
keine öffentlichen Konstruktoren und keine Methode, um abgeleitete Klassen zu
erstellen.
Warum hat Bill Gates uns dies angetan? Schließlich haben wir immer brav unsere
Software gekauft und unsere Lizenzen aktualisiert. Ich glaube nicht, dass er uns
hasst. Aber wenn, vielleicht hasst uns dann auch Larry Ellison, weil dieses Pro-
blem nicht nur in den Sprachen von Microsoft existiert. Java von Oracle/Sun ver-
fügt über eine entsprechende Syntax, um die Bildung von Unterklassen zu
verhindern. Java kennzeichnet besonders sicherheitsrelevante Klassen mit dem
Schlüsselwort final. Wenn man eine Unterklasse von HttpPostedFi le oder
selbst einer Klasse wie etwa St ring erstellen könnte, könnte man Schad-Code
schreiben und an Code weitergeben, der diese Klassen verwendet. Die Gefahr ist
sehr real, aber seal ed und f i nal sind ziemlich durchgreifende Tools; sie binden
uns die Hände.
In diesem Fall haben wir Glück, weil wir nur die Collection durchlaufen. Glückli-
cherweise verfügt die versiegelte HttpFi leCollection-Klasse, die unser Code
verwendet, über eine nicht versiegelte Oberklasse namens NameObjectCollec-
ti onBase. Wir können von ihr eine Unterklasse ableiten und ein Objekt an diese
Unterklasse übergeben, um auf die getKSRStreams-Methode zuzugreifen. Die
Änderung ist sicher und leicht, wenn wir Lean on the Compiler (23.4) anwenden.
list.Add(file.InputStream);
10.2
Der Fall der »hilfreichen« Sprachfunktion
}
}
return list;
}
Damit lösen wir ein Problem. Das nächste Problem ist schwieriger. Wir brauchen
HttpPostedFi 1 es, um getKSRStreams in einem Test auszuführen, aber wir kön-
nen sie nicht erstellen. Was brauchen wir von ihnen? Anscheinend brauchen wir
eine Klasse, die einige Properties zur Verfügung stellt: FileName und Content-
Length. Mit Skin and Wrap the API (35) können wir eine gewisse Distanz zwi-
schen uns und der HttpPostedFi le-Klasse schaffen. Zu diesem Zweck
extrahieren wir eine Schnittstelle (IHttpPostedFi 1 e) und schreiben einen Wrap-
per (HttpPostedFi leWrapper):
Weil wir eine Schnittstelle haben, können wir auch eine Klasse für die Tests erstel-
len:
Mit Lean on the Compiler (25.4) ändern wir unseren Produktions-Code, damit wir
HttpPostedFi 1 eWrapper- oder FakeHttpPostedFi 1 e-Objekte über die Ihttp-
PostedFi 1 e-Schnittstelle verwenden können, ohne zu wissen, welcher Typ über-
geben wurde.
165
Kapitel io
Ich kann diese M e t h o d e nicht in e i n e m Test-Harnisch ausführen
Ii st.Add(file.InputStream) ;
}
}
return list;
}
Störend ist nur, dass wir die ursprüngliche HttpFi 1 eCol 1 e c t i on in dem Produk-
tions-Code durchlaufen, jedes enthaltene HttpPostedFi 1 e einhüllen und es dann
in eine neue Collection einfügen müssen, die wir an die getKSRStreams-Methode
übergeben. Das ist der Preis der Sicherheit.
Im Ernst: Man könnte sealed und f i n a l leicht für Fehlkonstrukte halten, die
nicht zu den Programmiersprachen hätten hinzugefügt werden sollen. Aber der
eigentliche Fehler liegt bei uns. Wenn wir uns direkt von Bibliotheken abhängig
machen, die sich unserer Kontrolle entziehen, betteln wir geradezu um Ärger.
Möglicherweise stellen die Mainstream-Programmiersprachen eines Tages beson-
dere Zugriffsmechanismen für Tests zur Verfügung, aber bis es so weit ist, ist es
gut, gelegentlich Mechanismen wie etwa s e a l e d und f i n a l anzuwenden. Und
wenn wir Bibliotheksklassen verwenden müssen, die sie anwenden, ist es sinnvoll,
sie hinter einem Wrapper zu verbergen, damit wir etwas Spielraum für unsere
Änderungen haben. In den Kapiteln 14, Dependencies von Bibliotheken bringen mich
um, und Kapitel 15, Meine Anwendung besteht nur aus API-Aufrufen, werden Techni-
ken zur Lösung dieses Problems näher beschrieben.
Programmkomponenten greifen aufeinander zu. Oft stoßen wir auf Objekte mit
Methoden ohne Rückgabewerte. Wir rufen ihre Methoden auf, und sie erledigen
eine Aufgabe, aber wir (der aufrufende Code) erfährt davon nichts. Das Objekt ruft
Methoden von anderen Objekten auf, und wir bekommen keinen Hinweis auf
etwaige Ergebnisse.
166
10.3
Der Fall des nicht erkennbaren Nebeneffekts
}
}
Diese alte Java-Klasse macht alles. Sie erstellt GUI-Komponenten, empfängt Mel-
dungen von ihnen und verwendet diese in ihrem actionPerformed-Handler, sie
berechnet, was angezeigt werden soll, und zeigt es dann an. Das Ganze läuft
zudem besonders ungewöhnlich ab: Sie baut zunächst einen detaillierten Text auf,
erstellt dann ein weiteres Fenster und zeigt den Text darin an. Wenn das Fenster
seine Funktion erfüllt hat, greift sie Daten direkt daraus ab, verarbeitet sie ein
wenig und weist sie dann einem ihrer eigenen Textfelder zu.
Was können wir tun? Zuerst können wir die GUI-spezifischen von den GUI-unab-
hängigen Aufgaben trennen. Da wir in Java arbeiten, können wir eines der verfüg-
baren Refactoring-Tools nutzen. Zunächst extrahieren wir mit mehreren Extract
Method (A.iJ-Refactorings diverse Aufgaben aus dieser Methode.
Die Methode selbst ist hauptsächlich ein Hook für Meldungen des Windowing-
Frameworks. Zuerst ruft sie den Namen einer Anweisung aus dem an sie über-
gebenen ActionEvent ab. Wenn wir den gesamten Body der Methode extrahie-
167
Kapitel io
Ich kann diese Methode nicht in einem Test-Harnisch ausführen
ren, können wir uns von möglichen Dependencies von der ActionEvent-Klasse
isolieren.
di splay.setText(accountDescri ption);
}
}
Aber dies reicht nicht aus, um den Code testbar zu machen. Im nächsten Schritt
müssen wir Methoden für den Code extrahieren, der auf das andere Frame
zugreift, damit wir detail Display-Frame zu einer Instanzvariablen der Klasse
machen können.
public AccountDetailFrame(...) { .. }
168
io-3
Der Fall des nicht erkennbaren Nebeneffekts
di splay.setText(accountDescri ption);
}
}
}
Jetzt können wir den Code, der dieses Frame verwendet, in einen Satz von Metho-
den extrahieren. Wie sollten wir die Methoden benennen? Anregungen für Namen
liefern uns die Funktionen, die diese Code-Fragmente aus der Perspektive der
Klasse erfüllen, oder was sie für diese Klasse berechnen. Außerdem sollten die
Namen nicht die Display-Komponenten erwähnen. Wir können in dem extrahier-
ten Code Display-Komponenten verwenden, aber die Namen sollten diese Tatsa-
che verbergen. So können wir für jedes Code-Fragment entweder eine
Anweisungs-Methode oder eine Abfrage-Methode erstellen.
Comrnand/Query Separation
Command/Query Separation (Anweisungs-Abfrage-Trennung) ist ein Design-
Prinzip, das zuerst von Bertrand Meyer beschrieben wurde. Einfach ausgedrückt
besagt es: Eine Methode sollte eine Anweisung oder eine Abfrage sein, aber
nicht beides. Ein Command (Anweisung) ist eine Methode, die den Zustand des
Objekts ändern kann, aber die keinen Wert zurückgibt. Eine Query (Abfrage) ist
eine Methode, die einen Wert zurückgibt, aber das Objekt nicht modifiziert.
Warum ist dieses Prinzip wichtig? Es gibt mehrere Gründe; doch das Wichtigste
ist die Kommunikation. Ist eine Methode eine Query, sollten wir nicht ihren
Body studieren müssen, um herauszufinden, ob wir sie mehrfach hintereinan-
der aufrufen können, ohne Nebenwirkungen zu verursachen.
di splay.setText(accountDescripti on);
}
}
}
}
void setDescription(String description) {
detailDisplay = new Detai 1 F r a m e O ;
detaiIDi splay.setDescription(descri ption);
detailDisplay.show();
}
String getAccountSymbol() {
return detaiIDisplay.getAccountSymbol();
}
void setDisplayText(String description) {
display.setText(description);
}
Danach können wir Suhclass and Override Method (25.21) anwenden und den Code
testen, der in der performCommand-Methode übrig geblieben ist. Mit der folgen-
den Unterklasse von AccountDetai 1 Frame können wir etwa verifizieren, ob das
170
io.3
Der Fall des nicht erkennbaren Nebeneffekts
String getAccountSymbol() {
return accountSymbol;
}
Wenn wir Dependencies auf diese Weise sehr konservativ isolieren, dass wir
Methoden mit automatischen Refactorings extrahieren, könnten wir vor dem
Ergebnis zurückzucken. So ist etwa eine setDescription-Methode, die ein
Frame erstellt und anzeigt, richtig hässlich. Was passiert, wenn wir sie zweimal
aufrufen? Wir müssen dieses Problem lösen, aber solch grobe Extraktionen sind
ein vernünftiger erster Schritt. Danach können wir prüfen, ob wir das Frame nicht
an einer besseren Stelle erstellen können.
Wo stehen wir jetzt? Am Anfang stand eine Klasse mit einer wichtigen Methode:
performAction. Abbildung io.i zeigt unser Ergebnis.
AccountDetailFrame
- display : TextField
- detailDisplay : DetailFrame
+ performAction(ActionEvent)
+ performCommand(String)
+ getAccountSymbol : String
+ setDisplayText(String)
+ setDescription(String)
171
Kapitel i o
Ich kann diese Methode nicht in einem Test-Harnisch ausführen
Auch wenn das aus dem UML-Diagramm nicht hervorgeht, verwenden get-
AccountSymbol und setDescri pti on nur das detail Display-Feld. Die set-
Di spl ayText-Methode verwendet nur das Textfeld di spl ay. Wir könnten dies als
separate Aufgaben betrachten und den Code weiter refaktorisieren (siehe Abbil-
dung 10.2).
11
AccountDetailDisplay
- display : TextField
+ setDisplayText(String)
Dies ist ein außerordentlich grobes Refactoring des ursprünglichen Codes, aber
wenigstens trennt es die Aufgaben einigermaßen. Das AccountDetai 1 Frame ist
an das GUI gebunden (es ist eine Unterklasse von Frame), und es enthält immer
noch Geschäftslogik. Mit weiterem Refactoring können wir darüber hinausgehen,
aber wenigstens können wir jetzt die Methode, die Geschäftslogik enthielt, in
einem Testfall ausführen. Es ist ein positiver Schritt vorwärts.
Die Symbol Source-Klasse ist eine konkrete Klasse, die die Entscheidung repräsen-
tiert, ein anderes Frame zu erstellen und Daten daraus abzurufen. Doch wir haben
sie hier Symbol Source genannt, weil ihre Aufgabe aus der Sicht des AccountDe-
tai 1 Frame darin besteht, Symboldaten auf jede erforderliche Weise abzurufen.
Ich wäre nicht überrascht, wenn Symbol Source zu einer Schnittstelle würde,
sollte diese Entscheidung je geändert werden.
Die in diesem Beispiel ausgeführten Schritte sind sehr häufig. Mit einem Refacto-
ring-Tool können wir leicht Methoden einer Klasse extrahieren und dann anfan-
gen, Gruppen von Methoden zu identifizieren, die in neue Klassen verschoben
werden können. Mit einem guten Refactoring-Tool können Sie einen Teil dieses
Prozesses automatisieren, wenn es sicher ist. Doch dadurch wird nur die manu-
elle Bearbeitung zwischen den Anwendungen des Tools zum kritischsten Teil der
Arbeit. Vergessen Sie nicht: Es ist in Ordnung, Methoden mit schlechten Namen
oder einer schlechten Struktur zu extrahieren, um Tests einzurichten. Sicherheit
hat Vorrang. Nachdem die Tests eingerichtet sind, können Sie den Code säubern.
172
Kapitel n
Bei besonders verschlungenem Legacy Code muss ich oft lange überlegen, wo ich
meine Tests schreiben sollte. Dies hängt von den Änderungen selbst und ihren
Auswirkungen über ganze Ketten von Objekten hinweg ab. Doch das gehört schon
seit Beginn des Computer-Zeitalters zum Alltag eines Entwicklers.
Es gibt viele Gründe, Programme zu analysieren. Bloß reden Programmierer nicht
viel darüber. Es gehört einfach zum Job; und wir nehmen einfach an, jeder Kollege
wüsste, wie es geht. Leider hilft uns das nicht viel, wenn wir mit grässlich ver-
schlungenem Code konfrontiert werden, bei dem unsere Denkfähigkeiten an
Grenzen stoßen. Wir wissen, dass wir ihn refaktorisieren sollten. Aber wie können
wir ohne Tests feststellen, ob unsers Refactorings korrekt sind?
Die Techniken in diesem Kapitel sollten dieses Problem lösen helfen. Oft brau-
chen wir fortgeschrittene Analysemethoden, um in Programmen die besten Stel-
len für Tests zu finden.
173
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
Teile des Codes werden sich nicht anders verhalten. Sie erzeugen keine anderen
Ergebnisse, weil sie getBal ancePoi nt() weder direkt noch indirekt aufrufen.
int getBalancePoint() {
const int SCALE_FACTOR = 3;
int result = startingLoad + (LOAD_FACTOR * residual * SCALE_FACTOR);
foreach(Load load in loads) {
result += 1 oad.getPointWeight() * SCALE_FACTOR;
}
return result;
174
11.1
Effekte analysieren
return ((Declaration)declarations.get(index));
}
Wir können eine Skizze erstellen, die die Auswirkungen von Änderungen in
declarations auf getDeclarationCountQ hat (siehe Abbildung ii.i).
175
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
getDeclaration
C any d e c l a r a t i o n s
in d e c l a r a t i o n s ^ '
Abb. 11.2: Einfluss von declarations und der in ihr enthaltenen Objekte auf
getDeclarati onCount
Die Rückgabewerte von Aufrufen von getDecl arati on (i nt i ndex) können sich
ändern, wenn declarations oder darin enthaltene Deklarationen geändert wer-
den.
^^^eclaration^^^-
T C getinterface
3
Wir können diese Skizzen zu einer größeren Skizze kombinieren (siehe Abbil-
dung 11.4).
Ich bezeichne diese Diagramme einfach als Effektskizzen (effect sketches). Sie haben
eine simple Syntax: eine separate Blase für jede Variable, die beeinflusst werden
kann, und für jede Methode, deren Rückgabewert sich ändern kann. Manchmal
stammen die Variablen aus demselben Objekt, manchmal aus einem anderen. Es
spielt keine Rolle: Wir zeichnen einfach eine Blase für Dinge, die sich ändern, und
einen Pfeil zu allen Dingen, deren Wert sich aufgrund dieser Änderungen zur
Laufzeit ebenfalls ändern kann.
176
11.1
Effekte analysieren
Die meisten Methoden von gut strukturiertem Code haben einfache Effektstruk-
turen. Tatsächlich ist es ein Maß für die Qualität von Software, dass recht kom-
plizierte Effekte auf die Außenwelt das Ergebnis der Kombination einer Reihe
viel einfacherer Effekte in dem Code sind. Fast alle Vereinfachungen der Effekt-
skizze eines Code-Fragments machen dieses verständlicher und wartungs-
freundlicher.
Vergrößern wir das Bild des Systems, aus dem die vorhergehende Klasse stammt,
um weitere Effekte zu studieren. CppCl ass-Objekte werden in einer Klasse
namens C1 assReader erstellt. Tatsächlich konnten wir feststellen, dass sie nur in
dieser Klasse erstellt werden.
177
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
if (classToken.getTypeO == Token.CLASS
&& className. g e t T y p e O == Token.IDENT
&& 1 brace.getTypeO == Token.LBRACE
&& rbrace.getTypeO == Token.RBRACE
&& semicolon.getTypeO == Token.SEMIC) {
parsedClass = new CppCIass(className.getText() , declarations);
}
}
Erinnern Sie sich daran, was Sie über CppCI ass erfahren haben? Wissen wir, dass
sich die Liste der Deklarationen nicht ändern wird, nachdem ein CppCI ass-Objekt
erstellt worden ist? Unsere Sicht auf CppCI ass sagt uns dies nicht. Wir müssen
herausfinden, wie die Deklarationsliste mit Werten gefüllt wird. Ein Blick auf
einen größeren Teil der Klasse zeigt uns, dass Deklarationen nur an einer Stelle in
CppCI ass in einer Methode namens matchVi rtual Declaration hinzugefügt
werden, die von matchBody in parse aufgerufen wird.
Es hat den Anschein, dass alle Änderungen dieser Liste vor der Erstellung des
CppCIass-Objekts erfolgen. Weil wir neue Deklarationen in die Liste einfügen
und sie nirgends referenzieren, werden sich auch die Deklarationen nicht ändern.
Wie hilft uns dieses Wissen? Würde uns CppCI ass unerwartete Werte liefern,
würden wir wissen, dass wir nur Dinge an bestimmten Stellen prüfen müssten.
178
11.2
Vorwärtsanalyse (Reasoning Forward)
Anders ausgedrückt: Sie müssen vom Punkt der Änderung ausgehend eine Vor-
wärtsanalyse ausführen. Diese Denkmethode liefert Ihnen einen ersten Zugang zu
einer Technik, geeignete Stellen für Tests zu finden.
79
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
if (current.getNameO.equals(name)) {
return current;
}
}
return null;
}
}
InMemoryDi rectory ist eine kleine Java-Klasse. Wir können ein InMemoryDi rec-
tory-Objekt erstellen, Elemente zu ihm hinzufügen, einen Index erzeugen und
dann auf die Elemente zugreifen. Elements sind Objekte, die, genau wie Dateien,
Text enthalten. Wenn wir einen Index erzeugen, erstellen wir ein Element namens
i ndex und hängen die Namen aller anderen Elemente an seinen Text an.
Idealerweise sollten Erstellung und Pflege des Indexes ein Nebeneffekt der Aktion
sein, Elemente hinzuzufügen. Wird das erste Element hinzugefügt, sollte das
i ndex-Element erstellt werden; und es sollte den Namen des hinzugefügten Ele-
ments enthalten. Beim zweiten Mal sollte dasselbe i ndex-Element mit dem
Namen des neu hinzugefügten Elements aktualisiert werden. Es wäre einfach
genug, Tests für das neue Verhalten zu schreiben; aber wir haben keine Tests für
das gegenwärtige Verhalten. Wie stellen wir fest, wo wir sie einfügen sollten?
In diesem Beispiel ist die Antwort klar genug: Wir brauchen Tests, die addEle-
ment auf verschiedene Weisen aufrufen, einen Index erzeugen und dann diverse
Elemente abrufen, um zu prüfen, ob sie korrekt sind. Woher wissen wir, dass wir
die richtigen Methoden verwenden? In diesem Fall ist das Problem einfach. Die
Tests beschreiben einfach, wie wir das Verzeichnis verwenden wollen. Wir könn-
ten sie wahrscheinlich schreiben, ohne auch nur einen Blick auf den Verzeichnis-
Code zu werfen, weil wir recht genau wissen, wie sich das Verzeichnis verhalten
soll. Leider können Tests nicht immer so leicht konzipiert werden. Ich hätte in die-
sem Beispiel eine große komplizierte Klasse der Art verwenden können, wie sie
oft in Legacy-Systemen lauern, aber das hätte Sie wahrscheinlich gelangweilt, und
Sie hätten das Buch geschlossen. Nehmen wir also an, dies sei eine schwierige
Klasse. Wie können wir an dem Code ablesen, was wir testen sollten? Dieselbe
Denkmethode gilt auch für sperrigere Probleme.
180
11.2
Vorwärtsanalyse (Reasoning Forward)
Beginnen wir mit generatelndex. Von wem wird die Methode aufgerufen? Nicht
von anderen Methoden in der Klasse, sondern nur von Clients. Müssen wir in
generatelndex etwas ändern? Wir erstellen ein neues Element und fügen es zu
dem Verzeichnis hinzu; deshalb kann generatelndex eine Auswirkung auf die
elements-Collection der Klasse haben (siehe Abbildung 11.5).
Worauf wirkt die el ements-Collection selbst ein? Wo wird sie sonst noch verwen-
det? Anscheinend wird sie in getElementCount und getElement und auch in
addEl ement verwendet. Doch Letzteres müssen wir nicht berücksichtigen, weil
sich addEl ement immer auf die gleiche Weise verhält, unabhängig davon, wie wir
die elements-Collection manipulieren: Kein Nutzer von addEl ements kann
durch irgendetwas beeinflusst werden, was wir mit der elements-Collection
anstellen (siehe Abbildung 11.6).
Sind wir fertig? Nein - unsere Änderungspunkte befanden sich in der generate-
lndex- und der addEl ement-Methode; deshalb müssen wir auch untersuchen, wie
addEl ement die umgebende Software beeinflusst. Anscheinend ist dies die ele-
ments-Collection (siehe Abbildung 11.7).
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
^^^dd Element^^
^^^elements^^^
Wir könnten untersuchen, worauf ei ements einwirkt; doch das haben wir bereits
getan, weil generatelndex ebenfalls elements beeinflusst.
^^^ddElemenT^^
^^eneratelnde^^
^^etElementCourv^
Der einzige Weg, wie Nutzer der InMemoryDi rectory-Klasse die Effekte überwa-
chen können, führt über die Methoden getElementCount und getElement.
Könnten wir Tests für diese Methoden schreiben, könnten wir vielleicht auch alle
Effekte unserer Änderung überwachen.
Doch haben wir möglicherweise etwas übersehen? Was ist mit Ober- und Unter-
klassen? Wenn Daten von InMemoryDirectory public, protected oder
Package-weit gültig sind, könnte eine Methode in einer Unterklasse sie auf eine
Weise verwenden, von der wir nichts wissen. In diesem Beispiel sind die Instanz-
variablen von InMemoryDi rectory privat, deshalb müssen wir uns darum nicht
kümmern.
182
11.2
Vorwärtsanalyse (Reasoning Forward)
Wenn Sie Effektskizzen erstellen, sollten Sie prüfen, ob Sie alle Clients der
untersuchten Klasse gefunden haben. Wenn Ihre Klasse eine Oberklasse oder
Unterklassen hat, gibt es vielleicht Clients, die Sie nicht bedacht haben.
Sind wir fertig? Nun, eine Sache haben wir vollkommen ignoriert. Wir verwenden
in dem Verzeichnis die El ement-Klasse, aber sie erscheint nicht in unserer Effekt-
skizze. Was macht die Klasse?
Wenn wir generatelndex aufrufen, erstellen wir ein Element und rufen wieder-
holt ihre addText-Methode auf. Hier ist der Code von El ement:
public String g e t T e x t O {
return text;
}
}
Glücklicherweise ist er sehr einfach. Wir zeichnen eine Blase für das neue Ele-
ment, das von generatelndex erstellt wird (siehe Abbildung 11.9).
183
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
Das neue Element wird mit Text gefüllt; dann fügt generatelndex es zu der Col-
lection hinzu; das heißt, das neue Element beeinflusst die Collection (siehe Abbil-
dung ii.io).
Aus unserer vorhergehenden Analyse wissen wir, dass die addText-Methode die
elements-Collection beeinflusst, die ihrerseits Auswirkungen auf die Rückgabe-
werte von getElement und getElementCount hat. Wenn wir prüfen wollen, ob
der Text korrekt generiert wird, können wir getText eines Elements aufrufen, das
von getElement zurückgegeben wird. Dies sind die einzigen Stellen, an denen
wir Tests schreiben müssen, um die Auswirkungen unserer Änderungen zu kon-
trollieren.
Wie schon erwähnt, ist dieses Beispiel ziemlich klein, aber es zeigt repräsentativ
den Ablauf der erforderlichen Analyse, um die Auswirkungen von Änderungen in
Legacy Code abzuschätzen. Wir suchen geeignete Stellen für unsere Tests, und
zuerst müssen wir herausfinden, an welchen Stellen wir Änderungen beobachten
können und welche Auswirkungen sie haben. Kennen wir diese Stellen, können
wir für unsere Tests geeignete auswählen.
184
11.3
Effektfortpflanzung (Effect Propagation)
Jede Sprache hat Regeln, wie Parameter von Methoden gehandhabt werden.
Standardmäßig werden in vielen Fällen Referenzen auf Objekte übergeben. So
werden etwa in Java und C# standardmäßig nicht komplette Objekte, sondern
nur »Handies (Referenzen)« der Objekte an Methoden übergeben. Folglich
kann jede Methode den Zustand von Objekten über die Referenz modifizieren.
Einige Sprachen verfügen über Schlüsselwörter, mit denen Sie eine Änderung
eines übergebenen Objekts verhindern können, etwa das Schlüsselwort const
in C++, das in der Deklaration eines Methodenparameters verwendet wird.
public String g e t T e x t O {
return text;
}
}
Diese Klasse ist fast dieselbe wie die Element-Klasse aus dem InMemoryDi r e c -
tory-Beispiel. Tatsächlich wurde nur eine Code-Zeile geändert: die zweite Zeile in
addText. Die Signaturen der Methoden von Element helfen uns auch nicht wei-
ter, die Auswirkungen dieser Elemente auf Views festzustellen. Informationen zu
verbergen, ist großartig, aber nur, solange wir diese Informationen nicht brau-
chen.
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
public Coordinate() {}
186
11.4
Tools für Effektanalysen
public CoordinateO {}
public Coordinate(double x, double y) {
this.x = y;
this.y = y;
}
Der Unterschied zwischen diesen beiden Klassen ist recht subtil. In der ersten
Version der Klasse sind die Variablen x und y als p r i v a t e deklariert, in der zwei-
ten sind sie Package-weit gültig. Wenn wir sie in der ersten Version etwas ändern,
werden Clients nur über die di stance-Funktion beeinflusst, unabhängig davon,
ob Clients Coordi nate oder eine Unterklasse von Coordi nate verwenden. In der
zweiten Version könnten Clients in dem Package direkt auf die Variablen zugrei-
fen. Wir sollten auf solche Probleme achten oder die Variablen versuchsweise als
p r i v a t e deklarieren und problematische Stellen mit dem Compiler suchen.
Unterklassen von Coordinate können die Instanzvariablen ebenfalls benutzen;
deshalb müssen wir auch prüfen, ob die Variablen in Methoden von Unterklassen
verwendet werden.
Die Sprache genau zu kennen, ist wichtig, weil subtile Regeln oft Stolpersteine
sein können. Betrachten wir ein C++-Beispiel:
187
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
double g e t R h o O const;
double getThetaO const;
};
Steht in C++ das Schlüsselwort const hinter einer Methodendeklaration, kann die
Methode die Instanzvariablen des Objekts nicht modifizieren. Oder doch? Ange-
nommen, die Oberklasse von Pol arCoordi nate sähe wie folgt aus:
class Coordinate {
protected:
mutable double first, second;
};
Die »Regeln« für eine Code-Basis sind nicht unbedingt großartige Anweisungen
für den Programmierstil, wie etwa »Verwende niemals protected Variablen«,
sondern kontextabhängig. In dem CppCl ass-Beispiel vom Anfang des Kapitels
wollten wir der Übung halber herausfinden, wodurch ein CppCl ass-Objekt nach
seiner Erstellung beeinflusst werden könnte. Hier ist ein Ausschnitt dieses Codes:
188
11.6
Effektskizzen vereinfachen
}
Wir merkten an, jemand könnte die Deklarationsliste nach der Übergabe an den
Konstruktor ändern. Dies wäre ein ideales Beispiel für eine »Aber das wäre
dumm«-Regel. Wüssten wir bei der Analyse von CppClass von Anfang an, dass
sich die Liste nicht ändern wird, wäre unsere Untersuchung viel einfacher.
189
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
Der Unterschied ist subtil; der Code verwendet jetzt getDecl arati on intern. Die
beiden folgenden Abbildungen zeigen zum Vergleich die entsprechenden Skiz-
zen.
Die Änderung ist nur klein, aber einschneidend. Die getlnterface-Methode ver-
wendet jetzt getDecl arati on intern. Wenn wir getinterface testen, testen wir
immer auch getDecl arati on.
n.6
Effektskizzen vereinfachen
Wenn wir winzige Teile duplizierten Codes entfernen, erhalten wir oft Effektskiz-
zen mit einem kleineren Satz von Endpunkten. Dadurch werden Testentscheidun-
gen oft vereinfacht.
191
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?
Müssen wir feststellen, wo wir unsere Tests schreiben sollen, müssen wir
zunächst wissen, was von unseren Änderungen beeinflusst wird. Wir müssen ihre
Effekte analysieren. Wir können dabei informell oder strenger mit kleinen Skiz-
zen vorgehen; aber es lohnt sich, das Verfahren zu üben. In besonders verschlun-
genem Code ist es eine der wenigen Fähigkeiten, auf die wir uns stützen können,
um Tests einzurichten.
Kapitel 12
In einigen Fällen ist es leicht, Tests für eine Klasse zu schreiben, aber bei Legacy
Code ist es oft schwierig. Es kann schwierig sein, Dependencies aufzuheben.
Wenn Sie Klassen in einen Test-Harnisch einfügen wollen, um die Arbeit zu ver-
einfachen, gehören eng benachbarte Änderungen zu den ärgerlichsten Dingen,
auf die Sie stoßen können. Sie müssen eine neue Funktion zu einem System hin-
zufügen und stellen fest, dass Sie drei oder vier eng verwandte Klassen ändern
müssen. Für jede einzelne würden Sie Stunden brauchen, um sie unter Testkon-
trolle zu bringen. Sicher, Sie wissen, dass der Code danach besser sein wird, aber
müssen Sie wirklich alle diese Dependencies einzeln aufheben? Vielleicht nicht.
Oft zahlt es sich aus, »eine Ebene zurückzugehen«, um eine Stelle zu finden, an
der wir Tests für mehrere Änderungen auf einmal schreiben können. Wir können
Tests für eine einzige öffentliche Methode schreiben, um Änderungen in mehre-
ren privaten Methoden zu prüfen; oder wir können Tests an der Schnittstelle eines
Objekts schreiben, um die Zusammenarbeit mehrerer Objekte zu prüfen, die
darin enthalten sind. So können wir unsere Änderungen testen, aber auch etwas
mehr Freiraum für weitere Refactorings im jeweiligen Bereich schaffen. Die
Struktur des von den Tests abgedeckten Codes kann sich radikal ändern, solange
die Tests sein Verhalten fixieren.
Tests auf höherer Ebene können beim Refactoring nützlich sein. Oft ziehen Ent-
wickler sie feinkörnigen Tests jeder Klasse vor, weil sie glauben, Änderungen
seien schwieriger, wenn zahlreiche kleine Tests für eine Schnittstelle geschrie-
ben werden, die sich wandeln muss. Tatsächlich sind Änderungen oft einfacher,
als Sie vielleicht erwarten, weil Sie die Tests und dann den Code ändern und so
die Struktur in kleinen sicheren Schritten modifizieren können.
Auch wenn Tests auf höherer Ebene ein wichtiges Tool sind, sollten sie kein
Ersatz für Unit-Tests sein, sondern nur ein erster Schritt für ihre Einrichtung.
193
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?
12.1 Abfangpunkte
Ein Abfangpunkt ist einfach ein Punkt in einem Programm, an dem Sie die Aus-
wirkungen einer bestimmten Änderung beobachten können. In einigen Anwen-
dungen sind solche Punkte schwieriger zu finden als in anderen. Wenn die Teile
einer Anwendung ohne nennenswerte natürliche Seams zusammengefügt sind,
kann es sehr problematisch sein, einen vernünftigen Abfangpunkt zu finden. Oft
müssen Sie einige Wirkungsanalysen durchführen und viele Dependencies aufhe-
ben. Wie fangen wir an?
Am besten identifizieren Sie zuerst die Stellen, an denen Sie Änderungen machen
müssen, und studieren, von diesen Punkten ausgehend, die Wirkungen dieser
Änderungen. Jede Stelle, an der Sie Wirkungen beobachten können, ist ein
Abfangpunkt, aber vielleicht nicht der beste. Ihr Urteilsvermögen ist gefragt.
194
Abfangpun
total.add(getTaxO);
return total;
}
Wir müssen die Berechnung der Frachtkosten für New York ändern. Der Gesetz-
geber hat vor Kurzem eine Steuer eingeführt, die unser ortsansässiges Frachtzen-
trum betrifft. Leider müssen wir die Kosten an den Kunden weitergeben. Wir
wollen die Logik für die Versandkostenberechnung in eine neue Klasse namens
Shi ppi ngPri cer extrahieren. Danach sollte der Code wie folgt aussehen:
Die gesamte vorher in getVal ue geleistete Arbeit wird von einem Shi ppi ngPri -
cer-Objekt erledigt. Wir müssen auch den Konstruktor von Invoi ce ändern, um
ein Shi ppi ngPri cer-Objekt zu erstellen, das die Rechnungsdaten kennt.
(^^^etValue^^
Da wir auch den Konstruktor ändern wollen, müssen wir den Code untersuchen,
der davon abhängt. In diesem Fall erstellen wir in dem Konstruktor ein neues
Objekt: einen Shi ppi ngPri cer. Dieses Objekt beeinflusst nur die Methoden, die
es benutzen; und dies ist nur die getVal ue-Methode (siehe Abbildung 12.2).
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?
Wo sind unsere Abfangpunkte? Praktisch können wir hier jede Blase des Dia-
gramms als Abfangpunkt verwenden, vorausgesetzt, wir können auf das von ihnen
Repräsentierte zugreifen. Wir könnten versuchen, den Test mit der shi ppi ngPri-
cer-Variablen durchzuführen, aber sie ist eine private Variable der Invoi ce-
Klasse; deshalb können wir nicht auf sie zugreifen. Selbst wenn die Variable für
Tests zur Verfügung stünde, wäre shi ppi ngPri cer ein ziemlich schmaler Abfang-
punkt. Wir könnten beobachten, was wir mit dem Konstruktor gemacht haben (das
shi ppi ngPri cer-Objekt erzeugen), und prüfen, ob das shi ppi ngPri cer-Objekt
die erwartete Aufgabe erfüllt, aber wir können damit nicht prüfen, ob getVal ue
die Änderung korrekt durchführt.
Wir können Tests schreiben, die die makeStatement-Methode von Bi Iii ng-
Statement ausführen, und anhand ihres Rückgabewertes prüfen, ob wir den Code
korrekt geändert haben. Besser noch: Wir können Tests schreiben, die getVal ue
von Invoi ce aufrufen, und dort prüfen. Es könnte sogar weniger Arbeit sein.
Sicher wäre es schön, Bi 11 i ngStatement unter Testkontrolle zu bringen, aber es
ist im Moment einfach nicht erforderlich. Wenn wir Bi 11 i ngStatement später
ändern müssen, können wir die Klasse dann unter Testkontrolle bringen.
196
12.1
Abfangpunkte
197
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?
Mit Tests, die diese Gruppe von Klassen charakterisieren, erzielen wir eine grö-
ßere Abdeckung für das Refactoring. Wir können die Struktur von Invoi ce und
Item ändern und dabei die Tests für Bi 1 1 i ngStatement als Invariante verwen-
den. Hier ist ein guter erster Test, um Bi 1 1 i ngStatement, Invoi ce und Item
zusammen zu charakterisieren:
void testSimpleStatementO {
Invoi ce invoi ce = new Invoice();
invoice.addltem(new ltem(0,new Money(lO));
BillingStatement Statement = new Bi11ingStatementO;
Statement.addlnvoice(invoice);
assertEquals("", Statement.makeStatementO);
}
Wir können feststellen, was Bi 1 1 i ngStatement als Rechnung über einen Artikel
erstellt, und den Test ändern, damit er diesen Wert verwendet. Danach können wir
weitere Tests hinzufügen, um festzustellen, wie statement-Objekte für verschie-
dene Kombinationen von Rechnungen und Artikeln formatiert werden. Wir soll-
ten besonders sorgfältig Testfälle schreiben, die Bereiche des Codes prüfen, an
denen wir Seams einführen.
Beachten Sie, dass alle Wirkungen über makeStatement beobachtet werden kön-
nen. Vielleicht sind sie so nicht leicht zu beobachten, aber wenigstens ist dies eine
Stelle, an der es möglich ist, alle zu beobachten. Ich bezeichne eine solche Stelle in
einem Design mit dem Terminus Einschnürpunkt (engl, pinch point). Ein Ein-
schnürpunkt ist ein Engpass in einer Wirkungsskizze (11.1), eine Stelle, an der man
Tests schreiben kann, die einen weiten Satz von Änderungen abdecken. Wenn Sie
in Ihrem Design einen Einschnürpunkt finden können, kann er Ihre Arbeit erheb-
lich vereinfachen.
198
12.1
Abfangpunkte
*
BillingStatement Invoice
\! *
InventoryControl Item
Auch wenn wir es früher nicht bemerkt haben: Auch Item verfügt über eine
Methode namens needsReorder. Die InventoryControl-Klasse ruft sie auf,
wenn sie herausfinden muss, ob sie eine Bestellung aufgeben muss. Ändert sich
dadurch unsere Wirkungsskizze der Änderungen, die wir vornehmen müssen?
Keinen Deut. Wenn wir ein shippi ngCarrier-Feld zu Item hinzufügen, wird
die needsReorder-Methode überhaupt nicht beeinflusst; deshalb ist Billing-
Statement immer noch unser Einschnürpunkt, unsere enge Stelle, an der wir tes-
ten können.
Ändern wir das Szenarium etwas stärker. Angenommen, wir müssten Methoden
zu Item hinzufügen, mit denen wir den Lieferanten eines Artikels (Item) setzen
199
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?
und abrufen können. Der Name des Lieferanten wird von der InventoryControl-
Klasse und dem Bi Iii ngStatement verwendet. Abbildung 12.7 zeigt die Auswir-
kungen auf unsere Wirkungsskizze.
Jetzt sieht es nicht mehr so gut aus. Die Wirkungen unserer Änderungen können
mit der makeStatement-Methode von BillingStatement und durch Variablen
beobachtet werden, die von der run-Methode von InventoryControl beeinflusst
werden; aber es gibt keinen einzigen Abfangpunkt mehr. Doch zusammen können
die Methode run und makeStatement als Einschnürpunkt betrachtet werden;
zusammen sind sie nur zwei Methoden, und um Probleme zu entdecken, ist die-
ser Platz schmaler als die acht Methoden und Variablen, die für die Änderungen
angefasst werden müssten. Wenn wir dort Tests einrichten können, werden zahl-
reiche Änderungen abgedeckt.
Einschnürpunkt
Ein Einschnürpunkt ist eine Verengung in einer Wirkungsskizze, an der wir mit
Tests einiger weniger Methoden Änderungen in vielen Methoden beobachten
können.
200
12.2
Ein Design mit Einschnürpunkten beurteilen
Manchmal ist es ziemlich leicht, Einschnürpunkte für einen Satz von Änderungen
zu finden, doch in vielen Fällen ist es fast unmöglich. Eine einzige Klasse oder
Methode könnte Dutzende von Dingen beeinflussen, und eine entsprechende
Wirkungsskizze könnte wie ein großer verzweigter Baum aussehen. Was können
wir dann machen? Wir können etwa unsere Änderungspunkte überprüfen. Viel-
leicht wollen wir zu viel auf einmal tun. Dann könnten Sie versuchen, Einschnür-
punkte für nur eine oder zwei Änderungen gleichzeitig zu finden. Wenn Sie gar
keinen Einschnürpunkt finden, versuchen Sie einfach, Tests für einzelne Ände-
rungen so nah wie möglich zu schreiben.
Sie können Einschnürpunkte auch finden, indem Sie in einer Wirkungsskizze (u.i)
nach gemeinsamen Verwendungen suchen. Eine Methode oder Variable könnte
drei Nutzer haben, aber das bedeutet nicht, dass sie auf drei verschiedene Weisen
verwendet werden. Angenommen, wir müssten die needsReorder-Methode der
Item-Klasse aus dem vorhergehenden Beispiel refaktorisieren. Ich habe den Code
nicht gezeigt; doch würden wir die Wirkungen skizzieren, würden wir einen Ein-
schnürpunkt finden, der die run-Methode von InventoryControl und die make-
Statement-Methode von Bi 11 i ngStatement umfasste; aber noch schmaler ginge
es beim besten Willen nicht. Wäre es in Ordnung, Tests nur für eine dieser Klas-
sen und nicht die anderen zu schreiben? Die wesentliche Frage lautet: »Kann ich,
falls ich diese Methode beschädige, den Fehler an dieser Stelle entdecken?« Die
Antwort hängt davon ab, wie die Methode verwendet wird. Wenn sie auf dieselbe
Weise auf Objekte angewendet wird, die vergleichbare Werte haben, könnte es in
Ordnung sein, an einer Stelle zu testen, aber nicht an den anderen. Arbeiten Sie
sich mit einem Kollegen durch die Analyse.
Was genau ist ein Einschnürpunkt? Eine natürliche Grenze für Einkapselungen.
Ein Einschnürpunkt ist ein schmaler Trichter für alle Wirkungen in einem umfang-
reichen Code-Fragment. Wenn die Methode Bi 11 i ngStatement. makeStatement
ein Einschnürpunkt für eine Reihe von Rechnungen und Artikeln ist, wissen wir,
wo wir nachschauen müssen, wenn sich der Code nicht den Erwartungen entspre-
chend verhält. Dann muss das Problem in der BillingStatement-Klasse oder
den Rechnungen und Artikeln liegen. Ähnlich müssen wir nichts über Rechnun-
gen und Artikel wissen, um makeStatement aufzurufen. Dies deckt sich weitge-
hend mit der Definition der Einkapselung: Wir müssen uns nicht um die Interna
kümmern; doch wenn wir dies tun, müssen wir die Externa nicht beachten, um
die Interna zu verstehen. Wenn ich nach Einschnürpunkten suche, erkenne ich oft,
201
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?
wie Aufgaben anders auf Klassen verteilt werden können, um die Einkapselung zu
verbessern.
Dies ist nicht die einzige Methode, um herauszufinden, wie Aufgaben in einer
Klasse getrennt werden können; manchmal liefert der Name Hinweise, wie etwa
in diesem Fall (es gibt zwei Methoden mit dem Wort Token im Namen). Dies
kann Ihnen helfen, eine umfangreiche Klasse aus einer anderen Sicht zu
betrachten, und dies könnte zu einigen guten Klassenextraktionen führen.
Erstellen Sie der Übung halber eine Wirkungs skizze für Änderungen in einer
umfangreich Klasse und achten Sie nicht auf die Namen der Blasen. Betrachten
Sie nur ihre Gruppierung. Gibt es natürliche Grenze der Einkapselung? Ist dies
der Fall, betrachten Sie die Blasen innerhalb einer Grenze. Welchen Namen
würden Sie für diese Gruppe von Methoden und Variablen verwenden: So könn-
ten Sie den Namen einer neuen Klasse finden. Könnte es helfen, bestimmte
Namen zu ändern?
Tun Sie dies möglichst zusammen mit Ihren Team-Kollegen. Diskussionen über
geeignete Namen wirken vorteilhaft weit über Ihre gegenwärtige Arbeit hinaus.
Sie helfen Ihnen und Ihrem Team, eine gemeinsame Sicht des Systems und sei-
ner künftigen Entwicklungsrichtung zu gewinnen.
202
12.3
Fallen bei Einschnürpunkten
Tests an Einschnürpunkten zu schreiben, ist ein ideales Vorgehen, um mit der inva-
siven Arbeit an einem Teil eines Programms zu beginnen. Sie machen eine Inves-
tition, indem Sie einen Satz von Klassen herauspräparieren und sie an einen
Punkt bringen, an dem Sie sie zusammen in einem Test-Harnisch instanziieren
können. Nachdem Sie Ihre Charakterisierungs-Tests (IJ.I) geschrieben haben, kön-
nen Sie den Code straflos ändern. Sie haben eine kleine Oase in Ihrer Anwendung
geschaffen, in der die Arbeit einfacher geworden ist. Doch Vorsicht - es könnte
eine Falle sein.
Der Trick beim Schreiben von Unit-Tests für neuen Code liegt darin, Klassen so
unabhängig voneinander wie möglich zu testen. Wenn Sie bemerken, dass Ihre
Tests zu umfangreich werden, sollten Sie die getestete Klasse in kleinere unabhän-
gige Teile zerlegen, die leichter getestet werden können. Gelegentlich müssen Sie
Kollaborateure simulieren, weil Unit-Tests nicht erkennen sollen, wie sich eine
Gruppe von Objekten zusammen verhält, sondern nur, wie sich ein einziges
Objekt verhält. Dies können wir leichter mit einem Fake-Objekt testen.
Wenn wir Tests für vorhandenen Code schreiben, ist es umgekehrt. Manchmal
lohnt es sich, einen Teil einer Anwendung herauszulösen und mit Tests auszurüs-
ten. Nachdem wir diese Tests eingerichtet haben, können wir leichter engere Unit-
Tests für alle Klassen schreiben, die wir bei unserer Arbeit benutzen. Schließlich
werden die Tests am Einschnürpunkt überflüssig.
Mit Tests an Einschnürpunkten ziehen Sie eine Art von Grenzlinie, die einen
bestimmten Bereich Ihres Codes einschließt, den Sie gezielt durch Refactoring
und zusätzliche Tests entwickeln und verbessern. Im Laufe der Zeit können Sie
auf die Tests an dem Einschnürpunkt verzichten und sich bei Ihrer Entwicklungsar-
beit von den Tests für die einzelnen Klassen leiten lassen.
203
Kapitel 13
Bugs in Legacy Code zu finden, ist normalerweise kein Problem. Tatsächlich kann
die Suche nach Bugs, strategisch gesehen, ein falscher Einsatz der Kräfte sein. Es
ist normalerweise besser, sich von Anfang an darauf zu konzentrieren, dem Team
zu helfen, und konsistent korrekten Code zu schreiben. Gewinnen wird der, der
sich bemüht, Bugs im Code von vornherein zu vermeiden.
Automatisierte Tests sind ein sehr wichtiges Tool, aber nicht um Fehler zu suchen
- jedenfalls nicht direkt. Im Allgemeinen sollten automatisierte Tests ein neues
Ziel beschreiben oder ein bereits vorhandenes Verhalten bewahren. Im natürli-
chen Ablauf der Entwicklung werden aus Tests, die neue Ziele spezifizieren, Tests,
die vorhandenes Verhalten bewahren. Sie werden Fehler finden, aber normaler-
weise nicht bei der ersten Ausführung eines Tests. Sie zeigen sich bei späteren
Ausführungen, wenn Sie Verhalten an Stellen ändern, mit denen Sie nicht gerech-
net haben.
Was hat dies mit Legacy Code zu tun? In Legacy Code können wir oft überhaupt
nicht auf Tests von erforderlichen Änderungen zurückgreifen; deshalb gibt es
keine Methode, um wirklich zu verifizieren, ob wir bei Änderungen das Verhalten
bewahren. Deshalb können wir bei Änderungen den betroffenen Bereich besten-
falls mit Tests »abpolstern«, die uns eine Art Sicherheitsnetz bieten. Wir werden
bei diesem Verfahren Fehler finden und beseitigen müssen; doch wenn wir uns
bei Legacy Code das Finden und Beseitigen aller Bugs zum Ziel setzen, werden
wir fast immer niemals fertig werden.
205
Kapitel 13
Ich muss etwas ändern, weiß aber nicht, welche Tests ich schreiben soll
13.1 Charakterisierungs-Tests
Okay, wir brauchen also Tests - wie schreiben wir sie? Wir könnten etwa versu-
chen herauszufinden, was die Software tun soll, und dann entsprechende Tests
schreiben. Wir könnten zu diesem Zweck alte Anforderungsdokumente und Pro-
jektmemos ausgraben und einfach anfangen, Tests zu schreiben. Allerdings wäre
dies kein intelligenter Ansatz. In fast jedem Legacy-System ist das, was das System
tut, wichtiger als das, was es tun soll. Wenn wir Tests aufgrund von Annahmen
schreiben, was das System tun soll, sind wir wieder bei der Fehlersuche. Bugs zu
finden, ist wichtig; aber unser jetziges Ziel besteht darin, Tests einzurichten, die
uns helfen, den Code deterministischer zu ändern.
Die Tests, die wir brauchen, wenn wir Verhalten bewahren wollen, bezeichne ich
als Charakterisierungs-Tests. Solche Tests charakterisieren (beschreiben, definieren)
das jetzige Verhalten eines Code-Fragments. Annahmen über und Wünsche an
sein Verhalten sind tabu. Die Tests dokumentieren das tatsächliche gegenwärtige
Verhalten des Systems.
Im folgenden Beispiel bin ich hinreichend sicher, dass ein PageCenerator nicht
den String "fred" generieren wird:
void testCenerator() {
PageGenerator generator = new PageGeneratorO;
assertEqua1s("fred", generator.generateO) ;
}
Führen Sie Ihren Test durch und lassen Sie ihn scheitern. Damit stellen Sie fest,
was der Code unter dieser Bedingung tatsächlich tut. So generiert etwa ein frisch
erstellter PageGenerator in dem voranstehenden Code einen leeren String, wenn
seine generate-Methode aufgerufen wird:
.F
Time: 0.01
There was 1 failure:
1) testCenerator(PageGeneratorTest)
junit.framework.ComparisonFailure: expected:<fred> but was:<>
at PageGeneratorTest.testGenerator (PageGeneratorTest.java:9)
206
i3.i
Charakterisierungs-Tests
FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0
void testGeneratorO {
PageGenerator generator = new PageGeneratorO;
assertEquals("", generator.generate());
}
Jetzt wird der Test bestanden. Mehr als das: Er dokumentiert eine grundlegende
Tatsache über PageGenerator: Wenn wir ein solches Objekt erstellen und sofort
seine generate-Methode aufrufen, generiert es einen leeren String.
Mit demselben Trick können wir auch sein Verhalten bei der Übergabe anderer
Daten feststellen:
void testGeneratorO {
PageGenerator generator = new PageGeneratorO;
generator.assoc(RowMappi ngs.getRow(Page.BASE_R0W));
assertEquals("fred", generator.generate());
}
In diesem Fall teilt uns die Fehlermeldung des Test-Harnisches mit, dass der
String <nodexcarry>l. 1 vectrai</carryx/node> generiert wird. Deshalb
können wir diesen String zum erwarteten Wert des Tests machen:
void testGeneratorO {
PageGenerator generator = new PageGeneratorO;
generator.assoc(RowMappi ngs.getRow(Page.BASE_ROW));
assertEquals("<nodexcarry>l. 1 vectrai</carryx/node>", genera-
tor. generateO);
}
So vorzugehen, wirkt irgendwie verdreht, wenn man von der herkömmlichen Test-
auffassung ausgeht. Wir fügen Werte, die in Tests generiert werden, in die Tests
ein, um deren Verhalten zu ändern. Testen wir denn danach überhaupt noch
etwas?
Was passiert, wenn die Software einen Fehler enthält? Die erwarteten Werte, die
wir in unsere Tests einfügen, könnten einfach falsch sein.
Dieses Problem verschwindet, wenn wir unsere Tests aus einer anderen Perspek-
tive betrachten. Es sind keine Tests, die quasi einen Goldstandard definieren, dem
207
Kapitel 13
Ich muss etwas ändern, weiß aber nicht, welche Tests ich schreiben soll
die Software gerecht werden muss. Im Moment wollen wir keine Bugs finden. Ein
Verfahren zur Fehlersuche wollen wir später hinzufügen, wenn wir eine
bestimmte Art von Fehlern suchen, nämlich Abweichungen vom gegenwärtigen
Verhalten des Systems. Aus dieser Perspektive sehen unsere Tests anders aus: Sie
sind keine »moralische« Forderung, sondern dokumentieren nur, was Teile des
Systems wirklich tun. Wenn wir das Verhalten der Teile beobachten können, kön-
nen wir dieses Wissen mit unseren Kenntnissen über das erwartete Verhalten des
Systems kombinieren, um Änderungen vorzunehmen. Offen gestanden: Es ist
sehr wichtig zu wissen, was das System an einer Stelle tatsächlich tut. Normaler-
weise können wir feststellen, welches Verhalten wir hinzufügen müssen, indem
wir mit anderen Entwicklern sprechen oder einige Berechnungen durchführen;
aber abgesehen von Tests können wir nur herausfinden, was ein System tatsäch-
lich tut, wenn wir die Abläufe des Computers im Kopf simulieren, Code lesen und
überlegen, welche Werte an bestimmten Stellen des Programms vorliegen. Einige
Entwickler tun dies schneller als andere; aber unabhängig davon, wie schnell wir
eine solche Analyse durchführen können, ist sie ziemlich mühsam und ver-
schwenderisch, wenn sie laufend wiederholt werden muss.
Wichtig ist: Hier schreiben wir keine Blackbox-Tests. Wir können den Code studie-
ren, den wir charakterisieren. Der Code selbst kann uns Hinweise auf seine Funk-
tion geben; und Fragen lassen sich idealerweise mit Tests beantworten. Doch ohne
Neugier geht es nicht. Sie müssen wissen wollen, was der Code tut. Schreiben Sie
Tests, bis Sie sein Verhalten zu Ihrer Zufriedenheit verstehen. Ist damit der ganze
Code abgedeckt? Vielleicht nicht. Machen Sie trotzdem den nächsten Schritt. Wie
208
13.2
Klassen charakterisieren
wollen Sie Code ändern? Können Sie mit den vorhandenen Tests etwaige Pro-
bleme erkennen? Falls nicht, fügen Sie weitere Tests hinzu, bis Sie mit der Abde-
ckung zufrieden sind. Können Sie dieses Zufriedenheit nicht erreichen, sollten Sie
die Software mit einem anderen Verfahren ändern, etwa indem Sie zunächst einen
Teil der Komponente in Angriff nehmen.
Die Tests, mit denen wir Code charakterisieren, sind sehr wichtig. Sie dokumen-
tieren das tatsächliche Verhalten des Systems. Wie bei jeder Dokumention müs-
sen Sie darüber nachdenken, was für den Leser wichtig ist. Was sollte er über die
209
Kapitel 13
Ich muss etwas ändern, weiß aber nicht, welche Tests ich schreiben soll
Klasse wissen? Was muss er über sie wissen? In welcher Reihenfolge sollten die
Informationen präsentiert werden? Wenn Sie ein xUnit-Framework verwenden,
sind Tests einfach Methoden in einer Datei.
Sie können sie in eine Reihenfolge bringen, die es den Entwicklern erleichtert, den
Code zu verstehen. Beginnen Sie mit einigen leichten Fällen, die die Hauptauf-
gabe der Klasse verdeutlichen, und gehen Sie dann zu Fällen, die ihre Eigenheiten
betonen. Sorgen Sie dafür, dass die wichtigen Aspekte der Klasse durch Tests abge-
deckt werden. Wenn Sie Ihre Änderungen in Angriff nehmen, werden Sie oft fest-
stellen, dass die Tests, die Sie geschrieben haben, der Arbeit recht angemessen
sind, die Sie sich vorgenommen haben. Ob wir bewusst darüber nachdenken oder
nicht, sind die Änderungen, die zu machen wir uns vorgenommen haben, oft
durch unsere Neugier geleitet.
Die Antwort hängt von der Situation ab. Wenn das System niemals produktiv
eingesetzt worden ist, ist die Antwort einfach: Sie sollten den Fehler beheben.
Sonst müssen Sie die Möglichkeit ausloten, dass jemand von diesem Verhalten
abhängt, auch wenn Sie es als Bug einschätzen. Oft müssen Sie den Code näher
analysieren, um herauszufinden, wie Sie einen Bug ohne unerwünschte Neben-
wirkungen beseitigen können.
Ich neige dazu, Bugs zu beseitigen, sobald sie gefunden werden. Wenn Verhal-
ten ganz klar falsch ist, sollte es korrigiert werden. Wenn Sie den Verdacht
haben, ein Verhalten sei falsch, sollten Sie es im Test-Code als verdächtig mar-
kieren und dann seine Priorität erhöhen. Stellen Sie so schnell wie möglich fest,
ob es tatsächlich ein Fehler ist und wie Sie am besten damit umgehen.
210
13-3
Gezielt testen
}
Wir wollen die Fuel Share-Klasse sehr direkt ändern. Wir haben bereits einige
Tests für sie geschrieben; deshalb sind wir bereit. Hier ist die Änderung: Wir wol-
len die oberste if-Anweisung in eine neue Methode extrahieren und diese
Methode dann in die ZonedHawthorneLease-Klasse verschieben. Die lease-Vari-
able in dem Code ist eine Instanz dieser Klasse.
lease.postReading(readingDate, gallons);
}
Welche Art von Test brauchen wir, um zu prüfen, ob wir hier korrekt refaktorisie-
ren? Eine Sache ist sicher: Wir wissen, dass wir folgende Anweisungen überhaupt
nicht modifizieren:
211
Kapitel 13
Ich muss etwas ändern, weiß aber nicht, welche Tests ich schreiben soll
Es wäre schön, wenn wir testen könnten, wie der Wert unter dem
Lease. CORP_MIN-Limit berechnet wird, aber dies ist nicht unbedingt erforderlich.
Andererseits wird die folgende el se-Anweisung aus dem ursprünglichen Code
geändert:
el se
el se
cost += 1.2 * totalPrice;
Diese Änderung ist zwar klein, aber vorhanden. Es wäre besser, wenn wir sicher-
stellen können, dass die el se-Anweisung in einem unserer Tests ausgeführt wird.
Betrachten wir noch einmal die ursprüngliche Methode:
lease.postReading(readingDate, gallons);
}
Wenn wir ein Fuel Share-Objekt mit einem Monatsvertrag erstellen können und
addReadi ng für eine Menge von Gallonen größer als Lease. C0RP_MIN ausführen
wollen, durchlaufen wir den folgenden Zweig der el se-Anweisung:
212
13-3
Gezielt testen
Wenn Sie einen Test für einen Zweig schreiben, sollten Sie sich fragen, ob es,
abgesehen von der Ausführung dieses Zweiges, andere Wege gibt, wie der Test
bestanden werden könnte. Sind Sie nicht sicher, sollten Sie mit Sensing Variable
(22.3) oder dem Debugger herauszufinden versuchen, ob der Test dies leistet.
Wenn Sie solche Zweige charakterisieren, müssen Sie unbedingt auch herausfin-
den, ob Ihre Inputs mit einem besonderen Verhalten dazu führen könnten, dass
ein Test bestanden wird, wenn er scheitern sollte. Hier ist ein Beispiel. Angenom-
men, der Code repräsentiere Geldbeträge nicht mit int-, sondern mit double-
Werten:
lease.postReading(readingDate, gallons);
}
Wir könnten ernste Probleme bekommen. Und ich meine nicht die möglichen
Rundungsfehler, durch die wahrscheinlich überall Bruchteile von Cents verloren
gehen. Wenn wir unsere Inputs nicht sorgfältig auswählen, könnten wir beim
Extrahieren von Methoden Fehler machen, ohne dies je zu bemerken. So könnten
wir etwa eine Methode extrahieren und eins ihrer Argumente als i nt und nicht als
double deklarieren. In Java und vielen anderen Sprachen werden double-Werte
automatisch in i nt-Werte umgewandelt; das Runtime trunkiert einfach den Wert.
Wenn wir nicht für Inputs sorgen, die uns ausdrücklich auf diesen Fehler hinwei-
sen, übersehen wir ihn.
Betrachten wir ein Beispiel. Was wäre die Auswirkung auf den vorhergehenden
Code, wenn Lease.C0RP_MIN den Wert 10 und corpBase den Wert 12.0 hat,
wenn wir folgenden Test ausführen?
213
Kapitel 13
Ich muss etwas ändern, weiß aber nicht, welche Tests ich schreiben soll
share.addReading(l, n e w D a t e O ) ;
assertEquals(12, share.getCostO) ;
}
Weil 1 kleiner als 10 ist, addieren wir einfach 12 .0 zu dem Anfangswert von cost,
der 0 beträgt. Am Ende der Berechnung hat cost den Wert 12 .0. Dies ist in Ord-
nung; doch was passiert, wenn wir die Methode wie folgt extrahieren und den
Wert von cost nicht als doubl e, sondern als 1 ong deklarieren?
Unser Test wird immer noch bestanden, auch wenn wir den Wert von cost bei der
Rückgabe stillschweigend trunkieren. Ein double-Wert wird in einen int-Wert
umgewandelt, aber er wird nicht wirklich voll getestet. Er macht dasselbe, was er
auch ohne Umwandlung täte, wenn wir einfach einen int-Wert einem int-Wert
zuwiesen.
Beim Refactoring müssen wir im Allgemeinen zwei Dinge prüfen: Existiert das
Verhalten nach dem Refactoring noch, und ist es korrekt verknüpft?
Wie können wir diese Situation handhaben? Es gibt einige allgemeine Strategien.
Erstens könnten wir die erwarteten Werte für ein Code-Fragment manuell berech-
nen. Bei jeder Umwandlung sehen wir, ob es ein Trunkierungsproblem gibt. Zwei-
tens könnten wir Zuweisungen mit einem Debugger schrittweise analysieren, um
festzustellen, welche Umwandlungen von einem bestimmten Satz von Inputs aus-
gelöst werden. Drittens könnten wir mit Sensing Variables (22.3) verifizieren, ob ein
bestimmter Pfad abgedeckt ist und ob die Umwandlungen geprüft werden.
214
13-4
Eine Heuristik für das Schreiben von Charakterisierungs-Tests
215
Kapitel 14
Vermeiden Sie es, direkte Aufrufe von Bibliotheksklassen über Ihren ganzen
Code zu verstreuen. Vielleicht denken Sie, Sie müssten die Aufrufe niemals
ändern; aber oft genug ist dies reines Wunschdenken.
Als ich dies schrieb, war die Welt der Software-Entwicklung im Wesentlichen in
zwei große Lager gespalten: Java und .NET. Sowohl Microsoft als auch Sun (heute
Teil von Oracle) haben versucht, ihre Plattformen so breit wie möglich anzulegen,
und zahlreiche Bibliotheken geschaffen, damit die Anwender bei der jeweiligen
Plattform bleiben. In gewisser Weise profitieren viele Projekte von dieser Situa-
tion; aber Sie können sich trotzdem zu sehr auf bestimmte Bibliotheken verlassen.
Jeder nachhaltige Einsatz einer Bibliotheksklasse ist eine Stelle, die Sie explizit als
Naht betrachten könnten. Einige Bibliotheken definieren für alle ihre konkreten
Klassen hervorragende Schnittstellen. In anderen Fällen sind Klassen konkret und
als final (endgültig) oder seahd (abgeschlossen, versiegelt) deklariert oder sie verfü-
gen über nicht-virtuelle Schlüsselfunktionen, die in Tests nicht durch simulierte
Aufrufe ersetzt werden können. In diesen Fällen ist es für Sie manchmal am bes-
ten, auszugrenzende Klassen unter einem dünnen Wrapper zu verbergen. Verges-
217
Kapitel 14
Dependencies von Bibliotheken bringen mich um
sen Sie nicht, Ihrem Anbieter mitzuteilen, dass er Ihre Arbeit mit seiner
Bibliothek unnötig erschwert.
218
Kapitel 15
Bauen, kaufen oder borgen. Diese Entscheidung müssen wir bei der Entwicklung
von Software immer wieder treffen. Oft meinen wir, wir könnten bei der Entwick-
lung von Anwendungen Zeit und Mühe sparen, wenn wir die Bibliothek eines
Anbieters kaufen, ein Open-Source-Produkt einsetzen oder einfach größere Teile
von Code aus Bibliotheken wiederverwenden würden, die wir mit unserer Platt-
form (J2EE, .NET usw.) bekommen haben. Wir müssen vieles beachten, wenn wir
Code integrieren wollen, den wir nicht leicht ändern können. Wir müssen wissen,
wie stabil er ist, ob er ausreicht und ob er leicht anwendbar ist. Wenn wir uns
schließlich für die Nutzung fremden Codes entscheiden, stehen wir oft vor einem
anderen Problem: Unsere Anwendungen sehen letztlich aus, als bestünden sie
nur aus wiederholten Aufrufen einer fremden Bibliothek. Wie können wir solchen
Code ändern?
Spontan sind wir versucht zu sagen, der Code müsse nicht wirklich getestet wer-
den. Schließlich machen wir keine wirklich signifikanten Dinge; wir rufen nur
hier und dort eine Methode auf, und unser Code ist einfach. Er ist wirklich ein-
fach. Was kann schiefgehen?
Viele Legacy-Projekte haben derart bescheiden angefangen. Der Code wächst und
wächst, und die Dinge sind nicht mehr ganz so einfach. Im Laufe der Zeit entde-
cken wir vielleicht immer noch Bereiche von Code ohne API-Aufrufe; aber sie sind
in ein Flickwerk aus nicht testbarem Code eingebettet. Nach jeder Änderung müs-
sen wir die Anwendung ausführen, um zu prüfen, ob sie noch funktioniert; und
damit sind wir wieder beim zentralen Dilemma von Programmierern eines
Legacy-Systems. Änderungen haben einen ungewissen Ausgang; wir haben zwar
nicht den ganzen Code geschrieben, aber wir müssen ihn warten.
Die Handhabung von Systemen, die von Bibliotheksaufrufen durchsetzt sind, ist
in vielerlei Hinsicht schwieriger als die selbst geschriebener Systeme. Erstens kön-
nen wir oft kaum erkennen, wie sich die Struktur verbessern ließe, weil wir nur
die API-Aufrufe sehen. Der Code enthält einfach keine Hinweise auf das Design.
Zweitens gehört das API nicht uns. Andernfalls könnten wir Schnittstellen, Klas-
sen und Methoden umbenennen, um die Dinge für uns klarer zu machen, oder
wir könnten Methoden zu Klassen hinzufügen, um sie für verschiedene Teile des
Codes zugänglich zu machen.
219
Kapitel 15
Meine Anwendung besteht nur aus API-Aufrufen
Hier ist ein Beispiel. Dieses Listing enthält sehr schlechten Code für einen Mai-
ling-List-Server. Wir sind nicht einmal sicher, ob er funktioniert.
import java.io.IOException;
import java.util.Properties;
import javax.mail.*;
import javax.mail.internet.*;
220
Meine Anwendung besteht nur aus API-Aufrufen
221
Kapitel 15
Meine A n w e n d u n g besteht nur aus API-Aufrufen
222
Meine Anwendung besteht nur aus API-Aufrufen
die Betreffzeile prüfen und ändern, um anzuzeigen, dass die Message von der
Mailing-List kommt. Dafür müssen wir echte Arbeit leisten.
Wenn wir versuchen, die Aufgaben (Verantwortlichkeiten, engl, resposibilities) des
Codes zu trennen, könnte unsere Liste etwa wie folgt aussehen:
1. Wir brauchen etwas, das alle eingehenden Messages entgegennehmen und in
unser System einspeisen kann.
2. Wir brauchen etwas, das einfach eine Mail-Message versenden kann.
3. Wir brauchen etwas, das aus jeder eingehenden Message mit einer Empfänger-
liste neue Messages ableiten kann.
4. Wir brauchen etwas, das die meiste Zeit schläft, aber periodisch aufwacht, um
zu prüfen, ob weitere Mail eingegangen ist.
Wie haben diese Aufgaben mit dem Java-Mail-API zu tun? Die Aufgaben 1 und 2
sind definitiv an das Mail-API gebunden. Bei Aufgabe 3 ist es etwas komplizierter.
Die benötigten Message-Klassen gehören zum Mail-API, aber wir können die Auf-
gabe wahrscheinlich unabhängig testen, indem wir eingehende Messages simulie-
ren. Aufgabe 4 hat mit Mail eigentlich nichts zu tun; sie muss einfach einen
Thread so einrichten, dass er in bestimmten Zeitabständen aufwacht.
Abbildung 15.1 zeigt ein kleines Design, das diese Aufgaben trennt.
223
Kapitel 15
Meine A n w e n d u n g besteht nur aus API-Aufrufen
ListDriver steuert das System. Er enthält einen Thread, der die meiste Zeit
schläft und periodisch aufwacht, um zu prüfen, ob neue Mail vorhanden ist. Zu
diesem Zweck weist er den Mai 1 Recei v e r an, zu prüfen, ob neue Mail vorhanden
ist. Der Mail Recei ver liest die Mail und sendet die Messages nacheinander an
einen MessageForwarder. Der MessageForwarder erstellt für jeden Empfänger
auf der Liste Messages und versendet sie mit dem Mai 1 Sender.
Dieses Design ist recht sauber. Die MessageProcessor- und Mail Service-
Schnittstellen sind praktisch, weil wir damit die Klassen unabhängig testen kön-
nen. Insbesondere ist es großartig, dass wir mit der MessageForwarder-Klasse in
einem Test-Harnisch arbeiten können, ohne tatsächlich Mail zu versenden. Dies
ist leicht machbar, indem wir eine FakeMai 1 Sender-Klasse definieren, die die
Mai 1 Servi ce-Schnittstelle implementiert.
Fast jedes System enthält eine Kernlogik, die aus den API-Aufrufen herausge-
schält werden kann. Obwohl dieser Fall klein ist, ist er tatsächlich schlechter als
die meisten. MessageForwarder ist die Systemkomponente, deren Aufgabe vom
Senden und Empfangen von Mail am unabhängigsten ist; aber sie verwendet
immer noch Message-Klassen des Java-Mail-APIs. Es scheint nicht viele Stellen für
einfache alte Java-Klassen zu geben. Dennoch gibt uns die Zerlegung des Systems
in vier Klassen und zwei Schnittstellen in dem Diagramm eine gewisse Schich-
tung. Die Hauptlogik der Mailing-List befindet sich in der MessageForwarder-
Klasse; und diese können wir testen. In dem ursprünglichen Code war sie vergra-
ben und unzugänglich. Er ist fast unmöglich, ein System in kleinere Teile zu zer-
legen, ohne Teile zu erzeugen, die auf einer »höheren Ebene« angesiedelt sind als
andere.
Bei einem System, das anscheinend nur aus API-Aufrufen besteht, hilft es, sich
vorzustellen, dass es nur ein großes Objekt sei, und dann die Heuristiken zur Auf-
gabentrennung aus Kapitel 20, Diese Klasse ist zu groß, und ich möchte sie nicht wei-
ter vergrößern, anzuwenden. Vielleicht erreichen wir nicht sofort ein besseres
Design, aber allein die Aufgaben zu identifizieren, kann unsere Entscheidungen
bei der weiteren Arbeit verbessern.
Okay, so also sähe ein besseres Design aus. Schön zu wissen, dass es möglich ist,
aber zurück in die Wirklichkeit: Wie sollen wir vorgehen? Es gibt im Wesentlichen
zwei Ansätze:
1. Skin and Wrap the API (das API häuten und einhüllen)
2. Responsibility-Based Extraction (Aufgabenbasierte Extraktion)
Bei dem Ansatz Skin and Wrap the API definieren wir Schnittstellen, die das API
so eng wie möglich nachbilden, und erstellen dann Wrapper, die das API einhül-
len. Um das Risiko von Fehlern zu minimieren, können wir dabei die Methode
Preserve Signatures (23.3) anwenden. Die Technik Skin and Wrap the API hat unter
anderem den Vorteil, dass wir alle Dependencies von dem zugrunde liegenden
224
Meine Anwendung besteht nur aus API-Aufrufen
Dies ist der Code in dem Mailing-List-Server, der die Mail-Messages tatsächlich
versendet:
Wollten wir die Dependency von der Transport-Klasse auflösen, könnten wir für
sie einen Wrapper erstellen; aber in diesem Code erstellen wir das Transport-
Objekt nicht, sondern erhalten es von der Session-Klasse. Können wir für Ses-
sion einen Wrapper erstellen? Eigentlich nicht - Session ist eine final Klasse.
In Java können f i nal Klassen keine Unterklassen haben.
Was sind in dem voranstehenden Code-Fragment die Aufgaben? Nun, sein über-
greifendes Ziel ist das Versenden einer Message. Was ist dafür erforderlich? Es
braucht eine SMTP-Sitzung und eine Transportverbindung. Im folgenden Code
wurde die Aufgabe des Message-Versands in eine separate Methode, sendMes-
sage, extrahiert und diese in eine neue Klasse, Mai 1 Sender, eingefügt.
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import java.util.Properties;
225
Kapitel 15
Meine A n w e n d u n g besteht nur aus API-Aufrufen
Viele Teams verwenden beide Techniken: einen dünnen Wrapper zum Testen und
einen höher angesiedelten Wrapper, um der Anwendung eine bessere Schnitt-
stelle zu präsentieren.
226
Kapitel 16
Hier ist eine typische Situation: Sie entdecken eine Funktion, die Sie zu einem
System hinzufügen müssen. Sie beginnen mit dem Studium des Codes. Manch-
mal finden Sie schell alles, was Sie wissen müssen, aber bei Legacy Code kann es
auch länger dauern. Dabei erstellen Sie eine mentale Liste der Dinge, die Sie erle-
digen müssen, und wägen verschiedene Ansätze gegeneinander ab. Irgendwann
haben Sie den Eindruck, Fortschritte zu machen, und trauen sich zu anzufangen.
Manchmal machen Sie die vielen Dinge ganz konfus, die Sie zu assimilieren ver-
suchen. Den Code zu lesen, scheint nicht zu helfen; und Sie gehen einfach von
Ihrem jetzigen Kenntnisstand aus, beginnen zu arbeiten und hoffen das Beste.
Es gibt andere Methoden, ein System zu verstehen; doch viele Entwickler wenden
sie nicht an, weil sie zu sehr damit beschäftigt sind, den Code der unmittelbar
anstehenden Methode zu verstehen. Schließlich macht sich jemand, der etwas zu
verstehen versucht, verdächtig, sich vor der Arbeit zu drücken. Wenn wir diese
Lernphase etwas schneller hinter uns bringen könnten, können wir wirklich
anfangen, unser Geld zu verdienen. Hört sich das dumm an? Für mich schon.
Aber viele Menschen verhalten sich so - und das ist schade, denn wir können
unsere Arbeit mit einigen technisch unaufwendigen Maßnahmen auf ein solide-
res Fundament stellen.
227
Kapitel 16
Ich verstehe den Code nicht gut genug, u m ihn zu ändern
16.1 Notizen/Skizzen
Stellt sich beim Lesen von Code Verwirrung ein, lohnt es sich, Skizzen zu zeich-
nen und Notizen zu machen. Schreiben Sie den Namen der letzten wichtigen
Sache auf; schreiben Sie dann den Namen der vorletzten auf. Wenn Sie eine
Beziehung zwischen beiden sehen, ziehen Sie eine Linie. Diese Skizzen müssen
keine ausgewachsenen UML-Diagramme oder Funktionsaufruf-Graphen mit
einer besonderen Notation sein. Doch wenn die Verwirrung zunimmt, könnte es
sich lohnen, seine Gedanken mit formaleren Methoden oder saubereren Zeich-
nungen zu ordnen. Systeme helfen uns, ein System von einer anderen Warte aus
zu sehen und den Überblick zu bewahren, wenn wir etwas sehr Komplexes ver-
stehen wollen.
Abbildung 16.1 gibt eine Skizze wieder, die ich mit einem anderen Programmierer
anlegte, als wir zusammen Code studierten. Wir entwarfen sie auf der Rückseite
eines Memos. (Die Namen in der Skizze wurden geändert, um Unschuldige zu
schützen.)
Die Skizze ist jetzt nicht sehr verständlich, war aber unserem Gespräch zweck-
dienlich. Wir lernten ein wenig und fanden einen Ansatz für unsere Arbeit.
Macht das nicht jeder? Ja und nein. Wenige Entwickler machen es gewohnheits-
mäßig. Ich vermute, der Grund dafür liegt darin, dass es keine richtigen Anleitun-
gen für solche Dinge gibt; und wir lassen uns leicht zu dem Gedanken verleiten,
wir müssten gleich ein Code-Fragment schreiben oder UML-Syntax verwenden,
wenn wir zu Bleistift und Papier greifen. Nichts gegen UML, aber Ellipsen, Linien
und Figuren, die für Abwesende unverständlich wären, erfüllen denselben Zweck.
Die Präzision wird nicht auf dem Papier gefordert. Das Papier ist nur ein Hilfsmit-
tel, um das Gespräch zu vereinfachen und wichtige Konzepte besser zu behalten.
228
16.2
Listing Markup
Bei dem Versuch, ein System zu verstehen, hat das Skizzieren von Teilen seines
Designs einen riesigen Vorteil: Es ist formlos und ansteckend. Dabei müssen Sie
Ihr Team nicht motivieren, sich zu beteiligen. Sie müssen nur warten, bis Sie mit
jemandem arbeiten, der ein Code-Fragment verstehen möchte, und dann Ihre
Erklärungen mit einer kleinen Skizze erläutern. Wenn Ihr Partner diesen Teil des
Systems wirklich verstehen möchte, wird er auf Sie eingehen und den Code
anhand der Skizze mit Ihnen diskutieren.
Wenn Sie die ersten lokalen Skizzen eines Systems erstellen, sind Sie oft versucht,
Zeit zu investieren, um das Gesamtbild zu verstehen. In Kapitel 17, Meine Anwen-
dung hat keine Struktur, finden Sie Techniken, mit denen Sie eine umfangreiche
Code-Basis leichter verstehen und betreuen können.
16.2.1 Aufgabentrennen
Wenn Sie Aufgaben trennen wollen, markieren Sie zusammengehörige Code-
Fragmente durch ein gemeinsames Symbol. Benutzen Sie mehrere Farben, falls
möglich.
22g
Kapitel 16
Ich verstehe den Code nicht gut genug, um ihn zu ändern
Als ich diese Technik zum ersten Mal einem Mitarbeiter erklärte, hielt er sie für
Zeitvergeudung. Aber wir lernten den Code, den wir uns vorgenommen hatten, in
einer halben Stunde, in der wir Fragmente verschoben, gründlich kennen. Danach
war er von der Idee überzeugt.
Scratch Refactoring eignet sich hervorragend dazu, zum Kern der Dinge vorzusto-
ßen und die Arbeitsweise eines Code-Fragments wirklich kennen zu lernen; aber
es gibt einige Risiken. Erstens könnten wir beim Refactoring einen großen Fehler
machen, der uns zu falschen Annahmen über das Verhalten des Systems verleitet.
In einem solchen Fall machen wir uns ein falsches Bild von dem System, das uns
später Ärger bereiten kann, wenn wir den Code wirklich refaktorisieren. Das
230
i6-4
Ungenutzten Code löschen
zweite Risiko ist verwandt. Wir könnten uns so in eine bestimmte Lösung verren-
nen, dass wir keine Alternativen mehr sehen. Das klingt vielleicht gar nicht so
schlimm, kann aber gefährlich sein. Es gibt viele Gründe, warum wir beim wirkli-
chen Refactoring letztlich nicht dieselbe Struktur erzeugen wie bei unserer Explo-
ration. Wir könnten etwa später eine bessere Methode entdecken, den Code zu
strukturieren. Unser Code könnte sich inzwischen geändert haben; oder vielleicht
haben wir neue Erkenntnisse gewonnen. Wenn wir uns auf eine Lösung festlegen,
die wir bei einem Scratch Refactoring entwickelt haben, verpassen wir vielleicht
wichtige Einsichten.
Mit Scratch Refactoring können Sie sich Gewissheit verschaffen, dass Sie die
wesentlichen Aspekte des Codes verstehen; allein das kann die weitere Arbeit
erleichtern. Sie können einigermaßen sicher sein, dass nicht hinter jeder Ecke ein
Ungeheuer lauert - oder wenigstens können Sie sich entsprechend vorbereiten.
86
Kapitel 17
Dieses Problem lässt sich nicht leicht lösen; und die Dringlichkeit ist von System
zu System verschieden. In einigen Fällen laufen die Entwickler gegen eine Wand.
Sie können kaum noch neue Funktionen hinzufügen, weswegen das gesamte
Unternehmen in eine Krise geraten kann. Entwickler werden beauftragt, heraus-
zufinden, ob es besser wäre, das System umzubauen oder neu zu schreiben. In
anderen Unternehmen schleppt sich ein solches System jahrelang dahin. Man
akzeptiert, dass es länger als nötig dauert, um neue Funktionen hinzuzufügen,
betrachtet dies aber als einen Teil der Geschäftskosten. Niemand weiß, wie viel
besser es sein könnte oder wie viel Geld wegen der schlechten Struktur des Sys-
tems verloren geht.
Wenn Teams die Architektur ihrer Systeme nicht kennen, verschlechtert sich diese
im Laufe der Zeit. Was hindert Teams, ihre Systeme zu kennen?
• Das System kann so komplex sein, dass es lange dauert, sich einen Überblick
zu verschaffen.
• Das System kann so komplex sein, dass ein Überblick unmöglich ist.
• Das Team arbeitet fast nur noch reaktiv und löst so viele Notfälle nacheinander,
dass es den Überblick verliert.
Traditionell ist in vielen Unternehmen der Systemarchitekt für die Lösung dieses
Problems zuständig. Er hat normalerweise die Aufgabe, das große Ganze auszu-
arbeiten und im Blick zu behalten sowie die Entscheidungen zu treffen, die die
Arbeit des Teams immer wieder auf dieses große Bild ausrichten. Dies kann funk-
tionieren, hat aber einen großen Nachteil: Ein Architekt muss ständig mit dem
Team Kontakt halten, damit der Code zielführend bleibt. Abweichungen können
233
Kapitel 17
Meine Anwendung hat keine Struktur
auf zwei Wegen passieren: Jemand ändert den Code unsachgemäß; oder das große
Bild selbst ändert sich. In einigen der schlimmsten Situationen, die ich in Teams
kennen gelernt habe, hatte der Architekt einer Gruppe ein vollkommen anderes
Bild des Systems als die Programmierer. Dieser Fall tritt oft ein, wenn der Archi-
tekt andere Aufgaben hat, nicht auf den Code zugreifen kann oder nicht oft genug
und nicht gut genug mit dem Rest des Teams kommunizieren kann, um wirklich
zu wissen, was in dem Code passiert. Als Folge davon kann die Kommunikation
im Unternehmen zusammenbrechen.
Die brutale Wahrheit lautet: Die Architektur ist zu wichtig, um sie ausschließlich
einigen wenigen Entwicklern zu überlassen. Einen Architekten zu haben, ist in
Ordnung; aber der Schlüssel, die Integrität einer Architektur zu bewahren, liegt
darin, die Architektur und ihre Bedeutung jedem Team-Mitglied zu vermitteln.
Wer den Code anfasst, sollte die Architektur kennen; und jeder andere, der den
Code anfasst, sollte von dem profitieren, was diese Person gelernt hat. Wenn jedes
Team-Mitglied bei seiner Arbeit von denselben Konzepten ausgeht, wird die über-
greifende System-Intelligenz des Teams verstärkt. Besteht etwa ein Team aus 20
Mitarbeitern, von denen nur drei die Architektur im Detail kennen, müssen diese
drei sich sehr anstrengen, um die anderen 17 Entwickler in der Spur zu halten;
andernfalls machen diese einfach Fehler, weil sie das Gesamtbild nicht kennen.
234
17-1
Die Geschichte des Systems erzählen
Am Anfang einer solchen Darlegung wird sich ein seltsames Gefühl einstellen.
Um die System-Architektur wirklich so kurz darzustellen, müssen Sie vereinfa-
chen. Vielleicht sagen Sie: »Das Gateway erhält Rule Sets von der aktiven Daten-
bank«; doch während Sie dies sagen, protestiert ein anderer Teil von Ihnen: »Nein!
Das Gateway erhält Rule Sets von der aktiven Datenbank, aber es bekommt sie
auch von dem gegenwärtigen Working Set.« Das Einfachere zu sagen, gibt Ihnen
das Gefühl zu lügen; Sie erzählen einfach nicht die ganze Geschichte. Aber Sie
erzählen eine einfachere Geschichte, die eine einfacher zu verstehende Architek-
tur beschreibt. Warum etwa muss das Gateway Rule Sets von mehr als einer Stelle
bekommen? Wäre es nicht einfacher, dies zu vereinheitlichen?
Teams leisten nicht ihr Bestes, wenn sie an einem System arbeiten, das sie nicht
voll durchschauen. Eine einfache Geschichte, wie ein System funktioniert, kann
hier als Landkarte dienen, die Ihnen den Weg zu den richtigen Stellen weist, an
denen Sie Funktionen einfügen können. Sie kann auch die Angst vor einem Sys-
tem verringern.
In einem Team sollten Sie die Geschichte des Systems oft erzählen, um die
gemeinsame Sicht zu verankern. Erzählen Sie sie auf verschiedene Weisen.
Wägen Sie die Bedeutungen der Konzepte gegeneinander ab. Wenn Sie überlegen,
wie Sie das System ändern können, werden Sie feststellen, dass einige Änderun-
gen besser zu der Geschichte passen. Das heißt: Die kürzere Geschichte wirkt
dann etwas weniger wie eine Lüge. Müssen Sie zwischen zwei Methoden zur Erle-
digung einer Arbeit wählen, kann Ihnen die Geschichte Hinweise geben, welche
zu einem einfacher zu verstehenden System führen wird.
Hier ist ein Beispiel für eine derartige Geschichte. Es geht um JUnit. Die
Geschichte setzt voraus, dass Sie etwas über die Architektur von JUnit wissen. Ist
dies nicht der Fall, sollten Sie sich vorher den Quellcode von JUnit anschauen, den
Sie von folgender Adresse herunterladen können: www .junit.org.
235
Kapitel 17
Meine Anwendung hat keine Struktur
1. JUnit enthält viele weitere Klassen. Ich erwähne Test und TestResult nur,
weil ich sie für die Hauptklassen halte. Für mich ist ihre Interaktion die Kern-
Interaktion im System. Andere können eine andere, gleichermaßen gültige
Sicht der Architektur haben.
2. Anwender erstellen keine Test-Objekte. Test-Objekte werden per Reflection von
Testfall-Klassen erstellt.
3. Test ist keine Klasse, sondern eine Schnittstelle. Die Tests, die in JUnit ausge-
führt werden, werden normalerweise in Unterklassen einer Klasse namens
TestCase geschrieben, die Test implementiert.
4. Anwender fragen TestResults im Allgemeinen nicht nach Fehlern ab.
TestResults registrieren Listener, die benachrichtigt werden, wenn ein
TestResul t von einem Test Informationen erhält.
5. Tests berichten nicht nur Fehler, sondern mehr: Sie melden die Anzahl der aus-
geführten Tests und die Anzahl der Fehler. (Fehler sind Probleme, die in dem
Test auftreten und nicht explizit geprüft werden. Ein Scheitern bedeutet, dass
eine definierte Prüfung nicht bestanden wird.)
Können wir aus diesen Vereinfachungen lernen, wie man JUnit vereinfachen
könnte? Ein wenig. Einige einfache xUnit-Test-Frameworks definieren Test als
Klasse und verzichten ganz auf TestCase. Andere Frameworks fassen Fehler und
Scheitern zusammen und berichten sie auf gleiche Weise.
1. TestSui tes enthalten mehr als einen Satz von Tests, die sie gemeinsam aus-
führen. Sie erstellen per Reflection auch Instanzen von Klassen, die von Test-
Case abgeleitet sind.
2. Diese Vereinfachung ist eine Art Überbleibsel der ersten. Tatsächlich führen
Tests sich selbst nicht aus. Sie übergeben sich selbst an die TestResul t-Klasse,
die ihrerseits per Callback die Test-Ausführungsmethode in dem Test selbst auf-
ruft. Dieses Hin und Her erfolgt auf einer recht niedrigen Ebene. Doch die Ver-
einfachung ist praktischer, auch wenn sie nicht alles sagt. Tatsächlich wurde
JUnit so angewendet, als es noch etwas einfacher war.
236
17-1
Die Geschichte des Systems erzählen
Nein. Tatsächlich ist Test eine Schnittstelle. Es gibt eine Klasse namens Test-
Case, die Test implementiert. Anwender leiten eine Unterklasse von TestCase
ab und fügen dann ihre Tests als publ i c voi d Methoden in diese Unterklasse ein,
deren Namen mit dem Wort Test beginnen. Die TestSui te-Klasse stellt per
Reflection eine Gruppe von Tests zusammen, die mit einem einzigen Aufruf der
run-Methode der TestSuite ausgeführt werden können.
Wir können weitermachen, aber was ich bis jetzt dargelegt habe, vermittelt Ihnen
ein Gefühl für die Technik. Wir beginnen mit einer kurzen Beschreibung. Wenn
wir vereinfachen und Details weglassen, um ein System zu beschreiben, abstra-
hieren wir. Wenn wir uns zwingen, ein System sehr einfach zu beschreiben, fin-
den wir oft neue Abstraktionen.
Ist ein System schlecht, das nicht so einfach ist wie die einfachste Geschichte, die
wir darüber erzählen können? Nicht unbedingt. Wenn Systeme wachsen, werden
sie zwangsläufig komplizierter. Die Geschichte gibt uns eine Richtung vor.
Angenommen, wir wollten eine neue Funktion zu JUnit hinzufügen. Sie soll
einen Bericht aller Tests generieren, die keine Assertions aufrufen, wenn wir die
Tests ausführen. Welche Hinweise liefert uns die Beschreibung von JUnit?
JUnit enthält zwei Hauptklassen. Die erste heißt Test, die andere TestResult.
Anwender erstellen Tests und führen sie aus, indem sie ein TestResult an sie
übergeben. Wenn ein Test scheitert, teilt er dies TestResul t mit. Die Anwender
können dann alle aufgetretenen Fehler aus TestResul t abrufen.
Es scheint, als hätten Tests jetzt eine ganz andere Aufgabe, nämlich Berichte zu
generieren, die wir nirgends erwähnen.
Was wäre, wenn wir die Funktion auf andere Weise hinzufügten? Wir könnten die
Interaktion zwischen TestCase und TestResult so ändern, dass TestResult
eine Anzahl der ausgeführten Assertions empfängt, wenn ein Test ausgeführt
wird. Dann können wir eine Klasse erstellen, die den Bericht generiert, und sie bei
TestResult als Listener registrieren. Welchen Einfluss hätte dies auf die
Geschichte des Systems? Dies könnte ein guter Grund sein, es ein wenig mehr zu
verallgemeinern. Tests melden TestResul ts nicht nur die Anzahl gescheiterter
Prüfungen, sondern auch die Anzahl der Fehler, die Anzahl der ausgeführten
237
Kapitel 17
Meine Anwendung hat keine Struktur
Tests und die Anzahl der ausgeführten Assertions. Wir könnten unsere kurze
Geschichte wie folgt ändern:
JUnit enthält zwei Hauptklassen. Die erste heißt Test, die andere TestResul t.
Anwender erstellen Tests und führen sie aus, indem Sie ein TestResul t an sie
übergeben. Wenn ein Test ausgeführt wird, übergibt er Information über das
Ergebnis an TestResul t. Die Anwender können dann Informationen über alle
ausgeführten Tests aus TestResul t abrufen.
Ist das besser? Offen gesagt, gefiel mir die ursprüngliche Version mit den Berich-
ten über die scheiternden Fälle. Für mich gehört dies zum Kernverhalten von
JUnit. Wenn wir den Code so ändern, dass TestResults die Anzahl der ausge-
führten Assertions protokolliert, lügen wir immer noch ein wenig; aber wir über-
gehen bereits andere Informationen, die wir von den Tests an die Test-Ergebnisse
übergeben. Die Alternative, die Aufgabe, einige Testfällen auszuführen und darü-
ber zu berichten, an TestCase zu übertragen, wäre eine größere Lüge: Wir sagen
überhaupt nichts über diese zusätzliche Aufgabe von TestCase. Wir sind in einer
besseren Position, wenn Tests bei ihrer Ausführung die Anzahl der ausgeführten
Assertions berichten. Unsere erste Geschichte wird ein wenig verallgemeinert,
aber wenigstens ist sie im Grunde noch wahr. Dies bedeutet, dass unsere Ände-
rungen besser zur Architektur des Systems passen.
In den 1980er Jahren befassten sich Ward Cunningham und Kent Beck mit die-
sem Problem. Sie versuchten, Entwicklern zu einem Einstieg in objektorientiertes
Denken zu verhelfen. Damals verwendet Cunningham ein Tool namens Hyper-
Card, mit dem man Karteikarten auf einem Computer-Bildschirm darstellen und
238
17.2
Naked CRC
sie miteinander verknüpfen konnte. Plötzlich war die Einsicht da. Warum sollte
man Klassen nicht mit richtigen Karteikarten repräsentieren? Sie wären greifba-
rer; und man könnte leichter über sie reden. Sollen wir über die Transaction-
Klasse reden? Warum nicht? Hier ist ihre Karte - sie enthält ihre Aufgaben und
Kollaborateure.
CRC ist die Abkürzung für Class, Responsibility and Collaborations (Klasse, Aufgabe
und Kollaborationen). Man schreibt einen Klassennamen in die Kopfzeile einer
Karte und listet dann darunter ihre Aufgaben und Kollaborateure auf (andere Klas-
sen, mit denen diese Klasse kommuniziert). Meint man, eine Aufgabe gehöre
nicht in eine bestimmte Klasse, wird die Aufgabe durchgestrichen und in eine
andere Klassenkarte eingetragen; oder es wird eine ganz neue Klassenkarte ange-
legt.
Obwohl CRC eine Zeit lang recht populär war, wurde die Technik schließlich
durch Diagramme verdrängt. Fast jeder OO-Lehrer des Planeten hatte eine eigene
Notation für Klassen und Beziehungen entwickelt. Schließlich gab es eine aufwen-
dige, mehrere Jahre dauernde Anstrengung, die Notationen zu konsolidieren. Das
Ergebnis war UML; und viele Entwickler dachten, damit wären die Diskussionen
über das Design von Systemen beendet. Sie sahen in der Notation eine Methode
und betrachteten folglich UML als Methode, um Systeme zu entwickeln: Zeichne
viele Diagramme und schreibe danach den Code. Es dauerte eine Weile, bis Ent-
wickler erkannten, dass UML wohl eine gute Notation zur Dokumentation von
Systemen war, dass es aber nicht die einzige Methode war, mit den Konzepten zu
arbeiten, aus denen wir Systeme entwickeln. An diesem Punkt erinnerte ich mich
an eine viel bessere Methode, in einem Team über Design zu reden. Einer meiner
Test-Freunde hatte sie Naked CRC getauft, weil sie wie CRC funktioniert, außer
dass man nichts auf die Karten schreibt. Leider ist es nicht ganz leicht, sie in
einem Buch zu beschreiben. Hier ist mein bester Versuch.
Vor mehreren Jahren traf ich Ron Jeffries auf einer Konferenz. Er zeigte mir, wie
man eine Architektur mit Karteikarten so erklären konnte, dass die Interaktionen
greifbar und leicht merkbar waren. Sie funktioniert folgendermaßen: Die Person,
die das System beschreibt, benutzt einen Satz leerer Karteikarten und legt sie
nacheinander auf einen Tisch. Sie kann die Karten verschieben, darauf zeigen
oder sie nach Bedarf anderweitig manipulieren, um zu vermitteln, wie die typi-
schen Objekte in dem System beschaffen sind und wie sie interagieren.
239
Kapitel 17
Meine Anwendung hat keine Struktur
»Jede Session hat zwei Verbindungen, eine eingehende und eine ausgehende«
(legt die beiden Karten auf die ursprüngliche und zeigt nacheinander auf beide).
»Beim Start wird auf dem Server hier drüben eine Session erstellt« (legt eine Karte
auf die rechte Seite).
»Wir können serverseitig mehrere Sessions haben« (legt einen weiteren Satz von
Karten für eine neue Server-Session und ihre Verbindungen auf die rechte Seite).
240
17-3
Conversation Scrutiny
»Wenn ein Client abstimmt, wird seine Stimme an die serverseitige Session
gesendet« (zeigt mit der Hand von einer der Verbindungen der clientseitigen Ses-
sion zu einer Verbindung einer serverseitigen Session).
»Die Server-Session antwortet mit einer Bestätigung und speichert dann die
Stimme im Abstimmungsmanager« (zeigt von der Server-Session zurück auf die
Client-Session und zeigt dann von dieser Server-Session zu dem Abstimmungs-
manager).
»Danach weist der Abstimmungsmanager jede Server-Session an, seiner Client-
Session die neue Anzahl der Stimmen mitzuteilen« (zeigt von der Abstimmungs-
manager-Karte nacheinander auf jede Server-Session).
Sicher lässt diese Beschreibung zu wünschen übrig, weil ich die Karten nicht auf
einem Tisch auslegen oder so darauf zeigen kann, als säßen wir zusammen an
einem Tisch. Dennoch ist diese Technik ziemlich leistungsstark. Sie macht Teile
eines Systems begreifbar. Man muss keine Karteikarten verwenden; auch andere
Gegenstände sind geeignet. Wesentlich ist, dass Sie durch Bewegung und Position
zeigen können, wie Teile des Systems interagieren. Oft kann man so komplexe
Szenarien leichter verstehen. Außerdem prägen sich Designs durch solche Kartei-
karten-Sitzungen besser ein.
Ich arbeitete einmal mit mehreren Mitgliedern eines Teams. Sie wollten ein
umfangreiches Code-Fragment von mehreren Threads aus ausführbar machen.
Der Code war recht kompliziert, und es gab mehrere Gelegenheiten für einen
Deadlock. Wir erkannten, dass wir einen Deadlock in dem Code vermeiden könn-
ten, wenn wir garantieren könnten, dass Ressourcen in einer bestimmten Reihen-
folge gesperrt und freigegeben würden. Wir begannen nach Wegen zu suchen, wie
wir den Code ändern könnten, um dies zu ermöglichen. Die ganze Zeit sprachen
wir über diese neue Locking Policy und Möglichkeiten, Counts in Arrays zu ver-
241
Kapitel 17
Meine Anwendung hat keine Struktur
Die Schreckliche dabei war: Das Team war nicht unerfahren! Es gab einige
andere sehr gut aussehende Bereiche der Code-Basis; aber umfangreiche Frag-
mente prozeduralen Codes haben eine hypnotische Wirkung: Sie scheinen um
mehr zu betteln.
Achten Sie darauf, wie Sie über Ihr Design reden. Verwenden Sie im Gespräch die-
selben Konzepte wie im Code? Es würde mich überraschen, wäre dies der Fall.
Zwar muss Software strengere Constraints erfüllen, als ein einfaches Gesprächs-
objekt abzugeben; aber wenn Gespräch und Code zu weit auseinanderdriften, ist
es wichtig, nach dem Grund zu fragen. Die Antwort besteht normalerweise aus
einer Kombination zweier Dinge: Der Code wurde nicht an den Verständnisstand
des Teams angepasst, oder das Team muss den Code anders auffassen. Auf jeden
Fall sollten Sie auf die natürlichen Begriffe achten, mit denen die Entwickler das
Design beschreiben. Wenn sie sich über Design unterhalten, wollen sie von ihren
Kollegen verstanden werden. Dieses Verständnis gehört in den Code.
242
Kapitel 18
Programmierer haben bei ihren ersten Unit-Tests oft ein ungutes Gefühl und
empfinden sie als Arbeitshindernisse. Wenn sie ihr Projekt studieren, vergessen
sie manchmal, ob sie gerade Test- oder Produktions-Code lesen. Zu wissen, dass
Test-Code umfangreich sein kann, hilft auch nicht weiter. Solange Sie nicht konse-
quent einige Konventionen befolgen, kann die Menge des Codes überwältigend
sein.
Welche anderen Klassen spielen beim Testen eine Rolle? Oft ist es nützlich, für
einige Kollaborateure der Klassen in einem Package oder Verzeichnis Klassen zu
simulieren. Dafür verwende ich per Konvention das Präfix Fake. Dadurch werden
alle diese Klassen alphabetisch in einem Browser in einer Gruppe zusammenge-
fasst, aber etwas getrennt von den Hauptklassen in dem Package. Dies ist hilf-
reich, weil die Fake-Klassen oft Unterklassen von Klassen in anderen
Verzeichnissen sind.
Beim Test wird oft eine andere Art von Belasse, die Testing Subclass (Test-Unter-
klasse), verwendet. Eine Testing Subclass ist eine Klasse, die Sie schreiben, wenn
Sie eine Klasse testen wollen, die über Dependencies verfügt, die Sie isolieren oder
unwirksam machen wollen. Es ist die Unterklasse, die Sie schreiben, wenn Sie die
Subclass and Override Method (25.21) anwenden. Eine Testing Subclass erhält bei mir
per Konvention das Präfix Testi ng. So werden alle Test-Unterklassen in einem
Package oder Verzeichnis alphabetisch zusammen dargestellt.
243
Kapitel i g
Der Test-Code ist im Weg
• CheckingAccount
• CheckingAccountTest
• FakeAccountOwner
• FakeTransaction
• SavingsAccount
• SavingsAccountTest
• TestingCheckingAccount
• TestingSavingsAccount
Produktionsklassen stehen direkt vor ihren Testklassen. Die Fake-Klassen und die
Testi ng-Unterklassen bilden jeweils Gruppen.
Diese Anordnung ist für mich kein Dogma. Sie hat sich oft bewährt, aber es gibt
viele Varianten und Gründe, ein anderes Schema zu verwenden. Wichtig ist der
ergonomische Aspekt: Wie leicht können Sie zwischen Ihren Klassen und Tests
navigieren?
Manchmal spielt der Unterschied keine Rolle, wie folgendes Beispiel zeigt. In Java
kann ein Package zwei verschiedene Verzeichnisse umfassen:
244
18.2
Der Speicherort für Tests
source
com
orderprocessi ng
dailyorders
test
com
orderprocessi ng
dailyorders
In Java werden die Produktionsklassen von dailyorders unter source und die
zugehörigen Testklassen unter test automatisch demselben Package zugeordnet.
Einige IDEs zeigen Ihnen die Klassen in diesen beiden Verzeichnissen sogar in
derselben View, weshalb Sie sich nicht um ihren physischen Speicherort küm-
mern müssen.
In vielen anderen Sprachen und Umgebungen spielt der Speicherort eine Rolle.
Wenn man aufwendig in einer Verzeichnisstruktur navigieren muss, um zwi-
schen dem Code und seinen Tests zu wechseln, wirkt dies wie ein lästiger
Zusatzaufwand. Man hört einfach auf, Tests zu schreiben; und die Arbeit geht
langsamer voran.
Vor allem aber sollten Sie triftige Gründe dafür haben, Test- und Produktions-
Code zu trennen. Teams trennen oft den Code aus ästhetischen Gründen. Doch
später wird die Navigation in dem Projekt schmerzhaft. Man kann sich daran
gewöhnen, Tests direkt hinter den Produktions-Sources einzufügen. Haben Sie
erst eine Zeit lang mit dieser Methode gearbeitet, wirkt sie einfach normal.
245
Kapitel i g
Der Titel dieses Kapitels ist etwas provozierend. Sichere Änderungen sind in jeder
Sprache möglich, aber einige Sprachen machen Änderungen einfacher als andere.
Obwohl die Objektorientierung einen großen Teil der Branche erobert hat, gibt es
viele andere Sprachen und Programmiermethoden. Es gibt Regel-basierte Spra-
chen, funktionale Programmiersprachen, Constraint-basierte Programmierspra-
chen usw. Doch sie sind alle nicht so weit verbreitet wie die einfachen alten
prozeduralen Sprachen wie etwa C, COBOL, FORTRAN, Pascal oder BASIC.
Dies ist die standardmäßige, aber nicht die einzige Vorgehensweise. In diesem
Kapitel beschreibe ich Techniken, um Dependencies in prozeduralen Program-
247
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
men lokal aufzuheben, um den Code leichter verifizierbar zu ändern und die
Entwicklung voranzutreiben, wenn wir eine Sprache verwenden, die über Migra-
tionspfade zur 0 0 verfügt.
if (buffer_di rty(buf)) {
/* Move buffer to dirty list if jiffies is clear */
newtime = jiffies + (flag ? bdf_prm.b_un.age_super :
bdf_prm.b_un.age_buffer);
if(!buf->b_flushtime || buf->b_flushtime > newtime)
buf->b_flushtime = newtime;
} eise {
buf->b_flushtime = 0;
}
}
Um diese Funktion zu testen, können wir den Wert der j i ffi es-Variablen setzen,
ein buffer_head erstellen, ihn an die Funktion übergeben und dann nach dem
Aufruf ihre Werte prüfen. Bei vielen Funktionen haben wir nicht so viel Glück.
Manchmal ruft eine Funktion eine weitere Funktion auf, die wiederum eine
andere Funktion aufruft. Dann ruft sie etwas wirklich Schwieriges auf: eine Funk-
tion, die eine I/O-Operation ausführt oder in einer Bibliothek eines anderen
Anbieters steht. Wir wollen testen, was der Code tut, aber zu oft lautet die Antwort:
»Etwas Cooles, aber was genau, erfahren Sie nicht.«
#include "ksrlib.h"
248
19-2
Ein schwieriger Fall
current = current->next;
}
return err;
}
Dieser Code ruft eine Funktion namens ksr_notify auf, die eine unerwünschte
Nebenwirkung hat. Sie sendet eine Benachrichtigung an ein Third-Party-System;
beim Testen wollen wir dies nicht.
Wir könnten dieses Problem etwa mit einem Link Seam (4.3) handhaben. Wenn
wir den Code testen wollen, ohne die Funktionen in dieser Bibliothek aufzurufen,
können wir eine Bibliothek erstellen, die Fakes enthält: Funktionen, die denselben
Namen wie die ursprünglichen Funktionen haben, aber nicht deren Arbeit leisten,
sondern meist nur einen leeren Body enthalten. In diesem Fall können wir folgen-
den Body für ksr_noti fy schreiben:
Wir können die Funktion in eine Bibliothek einfügen und den Code damit verlin-
ken. Die scan_packets-Funktion wird sich genau wie vorher verhalten, bis auf
eine Ausnahme: Sie sendet keine Benachrichtigung. Aber das ist in Ordnung,
wenn wir anderes Verhalten in der Funktion unterdrücken wollen, bevor wir sie
ändern.
Sollten wir diese Strategie anwenden? Das hängt davon ab. Enthält die ksr-Biblio-
thek zahlreiche Funktionen, deren Aufruf mit der Hauptlogik des Systems wenig
zu tun hat, würde es sinnvoll sein, eine Bibliothek mit Fakes zu erstellen und beim
Testen einzubinden. Wollen wir dagegen diese Funktionen näher analysieren oder
ihre Rückgabewerte variieren, ist Link Seams (4.3) weniger geeignet; tatsächlich ist
die Technik ziemlich mühsam. Da die Funktionen beim Linken ersetzt werden,
können wir für jedes Executable nur eine Funktionsdefmition zur Verfügung stel-
len. Soll sich eine ksr_noti fy-Fake-Funktion in verschiedenen Tests unterschied-
lich verhalten, müssen wir Code in den Body einfügen und Bedingungen in dem
Test einrichten, die die jeweils gewünschte Methode einbinden - insgesamt ein
etwas chaotisches Verfahren. Leider haben wir bei vielen prozeduralen Sprachen
keine anderen Möglichkeiten.
249
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
#include "ksrlib.h"
#ifdef TESTING
#define ksr_notify(code,packet)
#endif
while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_P0RT) {
ksr_notify(scan_result, current);
}
current = current->next;
}
return err;
}
#ifdef TESTING
#include <assert.h>
int main () {
struct rnode_packet packet;
packet.body = ...
return 0;
}
#endif
Die Präprozessor-Direktive TESTING entfernt hier beim Testen den Aufruf von
ks r_noti fy und stellt einen kleinen Stub mit Tests zur Verfügung.
Tests und Quellcode in einer Datei zu kombinieren, ist nicht die übersichtlichste
Vorgehensweise. Oft wird die Navigation im Code erschwert. Eine Alternative
besteht darin, Dateien so einzubinden, dass die Tests und der Produktions-Code in
verschiedene Dateien stehen:
#include "ksrlib.h"
#include "scannertestdefs.h"
while(current) {
250
19-2
Ein schwieriger Fall
current = current->next;
}
return err;
}
#include "testscanner.tst"
Mit dieser Änderung sieht dieser Code dem Code ohne Test-Infrastruktur akzepta-
bel ähnlich. Der einzige Unterschied ist die #i ncl ude-Anweisung am Ende der
Datei. Mit einer Forward-Deklaration der zu testenden Funktionen können wir
den gesamten Inhalt der i ncl ude-Datei in die obere einfügen.
Um die Tests auszuführen, müssen wir nur TESTING definieren (setzen) und diese
Datei mit einem separaten Build erstellen. Ist TESTING definiert, wird die mai n ()-
Funktion in testscanner. tst kompiliert und in ein Executable gelinkt, das die
Tests ausführt. Die mai n ( )-Funktion in dieser Datei testet nur die Scanning-Rou-
tinen. Wir können den Code auch so einrichten, dass Gruppen von Tests gleichzei-
tig ausgeführt werden, indem wir für alle unsere Tests separate Testfunktionen
definieren.
#ifdef TESTING
#include <assert.h>
void test_port_invalid() {
struct rnode_packet packet;
packet.body = .. .
void test_body_not_corrupt() {
}
void test_header() {
}
#endif
In einer anderen Datei können wir sie von mai n aus aufrufen:
int main() {
test_port_invalid();
test_body_not_corrupt();
251
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
test_header();
return 0;
}
Oft müssen wir uns dabei andere Lösungen überlegen. Hier ist ein Beispiel. Wir
wollen eine Funktion namens send_command schreiben, die mit einer Funktion
namens mart_key_send eine ID, einen Namen und einen Anweisungs-String an
ein anderes System sendet. Der Code der Funktion ist kein Problem. Sie könnte
etwa wie folgt aussehen:
} eise {
19-3
Neues Verhalten hinzufügen
}
sprintf(inessage, ' %s%s%s " , header, command_string, footer);
mart_key_send(message);
free(message);
}
Aber wie könnte ein Test für eine derartige Funktion aussehen? Insbesondere,
wenn wir nur an der Stelle herausfinden können, was passiert, an der
mart_key_send aufgerufen wird? Probieren wir einen etwas anderen Ansatz.
Wir können die gesamte Logik vor dem Aufruf von mart_key_send testen, wenn
sich dieser in einer anderen Funktion befände. Wir können unseren ersten Test
wie folgt schreiben:
} eise {
}
free(message);
}
In vielen Fällen bringt uns eine solche Umformulierung weiter. Wir fügen die
gesamte reine Logik in einen Satz von Funktionen ein, damit sie frei von proble-
matischen Dependencies bleiben. So erhalten wir kleine Wrapper-Funktionen wie
etwa send_command, die unsere Logik mit unseren Dependencies verknüpft. Die
Lösung ist nicht perfekt, funktioniert aber, wenn die Dependencies nicht den
gesamten Code durchdringen.
253
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
In anderen Fällen müssen wir Funktionen schreiben, die mit externen Aufrufen
übersät sind. In diesen Funktionen wird nicht viel berechnet, aber die Reihenfolge
ihrer Aufrufe ist sehr wichtig. So könnte etwa eine umkomplizierte Funktion zur
Berechnung von Hypothekenzinsen wie folgt aussehen:
db_retrieve(1oan->id);
db_retrieve(loan->1ender_id);
db_update(1oan->id, 1oan->record);
1oan->interest = ...
}
Was tun wir in einem solchen Fall? In vielen prozeduralen Sprachen ist es am bes-
ten, zunächst auf die Tests zu verzichten und die Funktion so gut wie möglich zu
schreiben. Vielleicht können wir auf einer höheren Ebene testen, ob sie funktio-
niert. Aber in C gibt es noch eine Möglichkeit. C unterstützt Funktionszeiger, mit
denen wir einen weiteren Seam einrichten können:
Wir können eine struct erstellen, die Zeiger auf Funktionen enthält:
struct database
{
void (*retrieve) (struct record_id id);
void (*update)(struct record_id id, struct record_set *record);
};
Wir können diese Zeiger mit den Adressen der Datenbank-Zugriffsfunktionen ini-
tialisieren. Wir können die struct an neue Funktionen übergeben, die wir schrei-
ben und die auf die Datenbank zugreifen müssen. Im Produktions-Code können
die Funktionen auf die echten Datenbank-Zugriffsfunktionen verweisen; beim
Testen können sie auf Fakes zeigen.
Aber bei anderen können wir diese Funktionen sehr natürlich in einem objektori-
entierten Stil aufrufen:
254
ig-4
Die Objektorientierung nutzen
Diese Technik ist nicht auf C beschränkt. Sie kann in den meisten Sprachen ver-
wendet werden, die Funktionszeiger, Delegates oder ähnliche Mechanismen ver-
wenden.
while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_P0RT) {
ksr_notify(scan_result, current);
}
current = current->next;
}
return err;
}
255
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
Zunächst kompilieren wir den Code nicht unter C, sondern unter C++. Die Ände-
rung kann mehr oder weniger umfangreich sein. Wir können versuchen, das
gesamte Projekt in C++ zu kompilieren, oder schrittweise vorgehen.
Wenn der Code unter C++ kompiliert wird, können wir die Deklaration der
ksr_noti fy-Funktion in eine Klasse einhüllen:
class ResultNotifier
{
public:
Virtual void ksr_notify(int scan_result,
struct rnode_packet *packet);
};
Wir können auch eine neue Quelldatei für die Klasse mit einer Standard-Imple-
mentierung erstellen:
Dabei haben wir weder den Namen der Funktion noch ihre Signatur geändert. Mit
Preserve Signatures (23.1) minimieren wir das Fehlerrisiko.
Als Nächstes deklarieren wir in einer Quelldatei eine globale Instanz von Resul t-
Noti fi er:
Jetzt können wir neu kompilieren und uns von den Fehlern an die Stellen leiten
lassen, an denen wir den Code ändern müssen. Weil wir die Deklaration von
ksr_notify in eine Klasse eingefügt haben, sieht der Compiler auf globaler
Ebene die Deklaration nicht mehr.
#include "ksrlib.h"
while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_P0RT) {
256
19-4
Die Objektorientierung nutzen
ksr_notify(scan_result, current);
}
current = current->next;
}
return err;
}
Um den Code zu kompilieren, können wir jetzt das global Resul tNotifier-
Objekt mit einer externen Deklaration sichtbar machen und den Namen des
Objekts vor ksr_notify setzen:
#include "ksrlib.h"
while(current) {
scan_result = loc_scan(current->body, flag);
if(scan_result & INVALID_PORT) {
gl obal ResultNoti fier.ksr_noti fy(scan_result, cu rrent);
}
current = current->next;
}
return err;
}
An diesem Punkt funktioniert der Code wie in der ursprünglichen Methode. Die
ksr_notify-Methode von ResultNotifier delegiert an die ksr_notify-Funk-
tion. Was bringt uns das? Im Moment noch gar nichts. Als Nächstes müssen wir
herausfinden, wie wir dieses Resul tNoti fi er-Objekt bei der Produktion und ein
anderes beim Testen nutzen können. Es gibt mehrere Möglichkeiten. Weiter in die
eingeschlagene OO-Richtung führt uns wieder einmal Encapsulate Global Refe-
rences (25.4). Wir fügen scan_packets in eine andere Klasse ein, die wir Scanner
nennen wollen.
class Scanner
{
public:
int scan_packets(struct rnode_packet "-packet, int flag);
};
Jetzt können wir die Klasse mit Parameterize Constructor (25.14) so ändern, dass sie
einen übergebenen Resul tNoti fi er verwendet:
257
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
class Scanner
{
private:
ResultNotifier& notifier;
public:
ScannerO;
Scanner(ResultNotifier& notifier);
Scanner::Scanner(ResultNotifier& notifier)
: notifier(notifier)
{}
Danach suchen wir die Stellen, an denen scan_packets verwendet wird, erstellen
eine Instanz von Scanner und rufen sie von der Instanz aus auf.
Diese Änderungen sind ziemlich sicher und mechanisch. Sie sind kein Beispiel
für ein großartiges objektorientiertes Design, aber als Keil gut genug, um Depen-
dencies auszuhebein und Tests einzuführen, damit wir vorankommen.
Stellen Sie sich jetzt vor, wir können diese Deklarationen alle in eine Datei einfü-
gen und in eine Klassendeklaration einschließen:
class program
{
public:
258
19-5
Es ist alles objektorientiert
};
Jetzt können wir zu jeder Funktionsdefinition gehen (hier ist eine):
und den Namen der Klasse vor den der Funktion setzen:
Jetzt müssen wir eine neue mai n ( )-Funktion des Programms schreiben:
Ändern wir damit das Verhalten des Systems? Im Grunde nicht. Diese rein
mechanische Änderung berührte die Bedeutung und das Verhalten des Pro-
gramms in keinster Weise. Tatsächlich war das alte C-System nur ein großes
Objekt. Mit Encapsulate Global References (25.4) können wir neue Objekte erstellen
und das System in Komponenten zerlegen, damit wir leichter damit arbeiten kön-
nen.
Was können wir sonst noch machen, außer Dependencies zu extrahieren, wenn
unsere Sprache 0 0 unterstützt? Zum einen können wir uns inkrementell einem
besseren Objekt-Design annähern. Im Allgemeinen bedeutet dies, dass Sie ver-
wandte Funktionen in Klassen zusammenfassen und zahlreiche Methoden extra-
hieren müssen, um verfilzte Aufgaben zu entflechten (siehe Kapitel 20).
Prozeduraler Code bietet uns nicht so viele Optionen wie objektorientierter Code;
aber wir können in prozeduralem Legacy Code Fortschritte machen. Die speziel-
259
Kapitel 19
Mein Projekt ist nicht objektorientiert. Wie kann ich es sicher ändern?
len Seams einer prozeduralen Sprache haben einen wesentlichen Einfluss auf die
Schwierigkeit der Arbeit. Wenn Ihre prozedurale Sprache objektorientiert weiter-
entwickelt wurde, sollten Sie dieser Richtung folgen. Object seams (4.3) leisten weit
mehr, als nur Tests einzurichten. Link- und Präprozessor-Seams eignen sich her-
vorragend dazu, Code unter Testkontrolle zu bringen, aber darüber hinaus tragen
sie nicht viel zur Verbesserung des Designs bei.
Kapitel 20
Einmal arbeitete ich mit einem Team, dessen System auf dem Papier eine gut aus-
sehende Architektur hatte. Ich erfuhr, welches die Hauptklassen waren und wie
sie normalerweise miteinander kommunizierten. Dann zeigte man mir einige
hübsche UML-Diagramme dieser Struktur. Doch als ich den Code zum ersten Mal
sah, war ich überrascht. Jede Klasse konnte in etwa zehn andere Klassen zerlegt
werden; dies würde helfen, die dringendsten Probleme zu beseitigen.
Was sind die Probleme großer Klassen? Zunächst einmal Verwirrung. Bei einer
Klasse mit 50 oder 60 Methoden ist es oft schwer herauszufinden, was Sie ändern
müssen und was davon beeinflusst wird. In den schlimmsten Fällen enthalten
große Klassen eine unglaubliche Menge von Instanzvariablen; und es ist kaum
abzuschätzen, was Sie mit der Änderung einer Variablen bewirken. Ein weiteres
Problem ist die Aufgabenverteilung. Erfüllt eine Klasse zwanzig oder mehr Aufga-
ben, gibt es wahrscheinlich sehr viele Gründe, sie zu ändern. Vielleicht arbeiten in
derselben Iteration mehrere Programmierer an verschiedenen Aspekten der
Klasse. Wenn sie parallel arbeiten, kann dies besonders wegen des dritten Pro-
blems zu ernsten Konflikten führen: Große Klassen sind unglaublich schwer zu
testen. Einkapselung ist gut, nicht wahr? Nun, sagen Sie das bloß keinem Tester!
Er könnte Ihnen den Kopf abbeißen. Zu große Klassen verbergen zu viel. Einkap-
selung ist gut, wenn Sie uns hilft, über Code nachzudenken. Sie hilft uns zu erken-
nen, dass bestimmte Dinge nur unter bestimmten Umständen geändert werden
können. Doch wenn wir zu viel einkapseln, verrottet der Code einfach vor sich hin.
Da die Auswirkungen von Änderungen schwer abzuschätzen sind, bleibt oft nur
261
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
die Programmiermethode Edit and Pray (9). An diesem Punkt dauern Änderun-
gen entweder viel zu lange; oder die Anzahl der Fehler wächst. Mangel an Klarheit
muss immer bezahlt werden.
Das erste Problem bei großen Klassen ist die Frage, wie wir arbeiten können, ohne
die Situation zu verschlimmern. Die Schlüsseltechniken sind hier Sprout Class
(6.2) und Sprout Method (6.1). Der Code von Änderungen sollte möglichst in eine
neue Klasse oder eine neue Methode eingefügt werden. Mit Sprout Class (6.2) kön-
nen wir wirksam verhindern, dass es viel schlimmer wird. Sicher müssen Sie,
wenn Sie neuen Code in eine neue Klasse einfügen, von der ursprünglichen
Klasse delegieren, aber wenigstens machen Sie sie nicht viel größer. Sprout Method
(6.1) hilft auch, aber etwas subtiler. Zwar erstellen Sie eine zusätzliche Methode,
wenn Sie Code in eine neue Methode einfügen, aber wenigstens identifizieren
und benennen Sie eine Aufgabe der Klasse. Oft geben Ihnen die Namen von
Methoden Hinweise, wie Sie eine Klasse zerlegen können.
Die Schlüsseltherapie für große Klassen ist das Refactoring, mit dem Sie sie in
Sätze kleinerer Klassen zerlegen können. Das Hauptproblem dabei ist herauszu-
finden, wie die kleineren Klassen aussehen sollten. Glücklicherweise gibt es eine
Richtschnur.
Single-Responsibility-Prinzip (SRP)
Jede Klasse sollte eine einzige Verantwortlichkeit, Zuständigkeit oder Aufgabe
(engl, responsibility; dt. wörtl. Verantwortlichkeit) haben: Sie sollte in dem System
einen einzigen Zweck erfüllen, und es sollte nur einen Grund geben, sie zu
ändern.
Diese kleine Klasse wertet Strings aus, die Rule-Ausdrücke (Regeln) in einer obs-
kuren Sprache enthalten. Welche Aufgaben hat sie? Der Name der Klasse teilt uns
eine Aufgabe mit: Sie parst. Aber ist dies ihr Hauptzweck? Das Parsen scheint es
nicht zu sein. Sie scheint auch auszuwerten.
Was macht sie sonst noch? Sie enthält einen String namens current, den sie
parst. Sie enthält auch ein Feld, das die gegenwärtige Position beim Parsen
anzeigt. Beides scheint zur Kategorie des Parsens zu gehören.
262
Diese Klasse ist zu groß und soll nicht noch größer werden
RuleParser
- current: string
- variables : HashMap
- currentPosition : int
+ evaluate(string) : int
- branchingExpression(Node left, Node right) : int
- causalExpression(Node left, Node right) int
- variableExpression(Node node) : int
- valueExpression(Node node) : int
- nextTermQ : string
- hasMoreTerms() : boolean
+ addVariable(string name, int value)
Ein weiteres Feld, vari abl es, enthält einen Satz von Variablen, die der Parser ver-
wendet, damit er arithmetische Ausdrücke wie etwa a + 3 auswerten kann. Wird
die Methode addVariable mit den Argumenten a und 1 aufgerufen, wird der
Ausdruck a + 3 zu 4 ausgewertet. Deshalb scheint diese Klasse noch eine weitere
Aufgabe zu erfüllen: Variablen-Management.
Gibt es noch mehr Aufgaben? Sagen uns die Methodennamen mehr? Gibt es eine
natürliche Gruppierung der Namen der Methoden? Es scheint, dass die Methoden
sich zu folgenden Gruppen zusammenfassen lassen:
• evaluate
• branchingExpression, causa!Expression, variableExpression, value-
Expression
• nextTerm, hasMoreTerms
• addVariable
Die eval uate-Methode ist ein Einstiegspunkt der Klasse. Sie ist eine von nur zwei
öffentlichen Methoden. Der Methodenname bezeichnet eine Hauptaufgabe der
Klasse: Auswertung (engl, evaluation). Alle Methoden, die mit dem Suffix Expres-
sion enden, ähneln sich. Sie haben nicht nur ähnliche Namen, sondern akzeptie-
ren alle Nodes als Argumente und geben einen int-Wert zurück, der den Wert
eines Teilausdrucks repräsentiert. Auch die Methoden nextTerm und hasMore-
Terms ähneln sich. Sie scheinen für eine Art der Tokenbildung für Terme zustän-
dig zu sein. Wie bereits erwähnt wurde: Die addVariable-Methode hat mit
Variablen-Management zu tun.
• Parsen
• Auswertung von Ausdrücken
• Zerlegung von Termen in Tokens
• Variablen-Management
263
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
Müssten wir von Grund auf ein Design entwerfen, in dem alle diese Aufgaben
getrennt sind, könnte das Ergebnis etwa wie Abbildung 20.2 aussehen.
Treiben wir dies zu weit? Vielleicht. Oft fassen Entwickler von Interpretern kleine-
rer Sprachen das Parsen und die Auswertung von Ausdrücken zusammen und
werten die Ausdrücke einfach beim Parsen aus. Aber obwohl dies bequem sein
kann, skaliert dieses Verfahren oft nicht gut, wenn die Sprache wächst. Eine wei-
tere, etwas magere Aufgabe wird von der Symbol Tabl e erfüllt. Wenn diese Variab-
lennamen Ganzzahlen zuordnen soll, gewinnen wir mit dieser Klasse nicht viel;
eine Hash-Tabelle oder Liste würde diesen Zweck auch erfüllen. Hübsches
Design, aber raten Sie mal? Es ist recht hypothetisch. Falls wir diesen Teil des Sys-
tems nicht umschreiben, ist unser kleines Mehrklassen-Design nur ein Luft-
schloss.
Bei großen Klassen aus der Praxis liegt der Schlüssel zum Erfolg darin, die ver-
schiedenen Aufgaben zu identifizieren und dann einen Weg zu finden, schritt-
weise fokussierte Aufgaben einzuführen.
264
20.1
Aufgaben erkennen
In diesem Abschnitt beschreibe ich einen Satz von Heuristiken, mit denen wir
Aufgaben in vorhandenem Code erkennen können. Beachten Sie, dass wir keine
Aufgaben erfinden, sondern nur entdecken, was vorhanden ist. Unabhängig von
der Struktur des Legacy Codes erfüllen seine Komponenten identifizierbare Funk-
tionen. Manchmal sind diese nur schwer zu erkennen, aber diese Techniken kön-
nen helfen. Sie sollten auch dann versuchen, sie auf Code anzuwenden, wenn Sie
ihn nicht sofort ändern müssen. Je besser Sie die Aufgaben in dem Code erken-
nen, desto mehr lernen Sie über ihn.
Diese Technik der Gruppierung von Methoden ist ein ziemlich guter Anfang, ins-
besondere bei sehr umfangreichen Klassen. Wichtig dabei ist: Sie müssen nicht
alle Namen in neuen Klassen kategorisieren! Suchen Sie nach Methoden, die
anscheinend alle an einer Aufgabe beteiligt sind. Wenn Sie einige der Aufgaben
identifizieren können, die etwas weniger mit der Hauptaufgabe der Klasse zu tun
haben, haben Sie eine Richtung entdeckt, in die Sie den Code im Laufe der Zeit
weiterentwickeln können. Warten Sie, bis Sie eine der kategorisierten Methoden
ändern müssen; entscheiden Sie dann, ob Sie den Code an dieser Stelle in eine
Klasse extrahieren wollen.
Die Gruppierung von Methoden ist auch eine geeignete Übung für Teams. Hän-
gen Sie in Ihrem Team-Raum Poster für alle Hauptklassen auf; schreiben Sie dar-
auf die Namen ihrer Methoden. Versuchen Sie mit dem Team, die Methoden zu
gruppieren, und halten Sie die Gruppen separat fest. Entscheiden Sie dann mit
265
Kapitel 20
Diese Klasse ist zu groß und soll nicht noch größer werden
dem Team, welche Gruppen und Methoden Sie als nächste in Angriff nehmen
wollen.
Die weiter vorne behandelte RuleParser-Klasse ist ein klassisches Beispiel für
dieses Problem. Sie verfügt über zwei öffentliche Methoden: evaluate und add-
Variable. Alle anderen Methoden sind privat. Die RuleParser-Klasse würde
ziemlich seltsam aussehen, machten wir nextTerm und hasMoreTerms öffentlich.
Die Anwender des Parsers könnten auf die Idee kommen, diese beiden Methoden
zusammen mit eval uate zu benutzen, um Ausdrücke zu parsen und auszuwer-
ten. Doch in einer TermTokeni zer-Klasse wäre viel weniger seltsam und tatsäch-
lich geboten, diese Methoden öffentlich zu machen. Dadurch wird die
Einkapselung von RuleParser nicht verringert. Denn obwohl nextTerm und
hasMoreTerms in TermTokenizer öffentlich sind, greift ein RuleParser-Objekt
privat auf sie zu (siehe Abbildung 20.3).
TermTokenizer
RuleParser
+ Tokenizer(String)
+ evaluate(String) : int + nextTerm () : String
+ hasMoreTermsQ : boolean
Heuristik #3: Suchen Sie nach Entscheidungen, die sich ändern können.
Suchen Sie nach Entscheidungen - nicht Entscheidungen, die Sie im Code tref-
fen, sondern nach Entscheidungen, die Sie bereits getroffen haben. Sind
irgendwo Aktionen (etwa eine Kommunikation mit einer Datenbank oder
einem anderen Satz von Objekten usw.) fest eincodiert? Können Sie sich vorstel-
len, dass sich das ändert?
266
20.1
Aufgaben erkennen
Wenn Sie eine große Klasse zerlegen wollen, ist es verlockend, sich auf die Namen
der Methoden zu konzentrieren. Schließlich sind sie die sichtbarsten Komponen-
ten einer Klasse. Aber diese Namen erzählen nicht die ganze Geschichte. Oft lösen
die Methoden großer Klassen viele Aufgaben auf verschiedenen Abstraktionsebe-
nen. So könnte etwa eine Methode namens updateScreen () Anzeigetext generie-
ren, formatieren und an mehrere andere GUI-Objekte senden. Der
Methodenname allein sagt Ihnen nicht, was alles in der Methode passiert und wie
viele Aufgaben in diesem Code erledigt werden.
Deshalb lohnt es sich, per Refactoring kleinere Methoden zu extrahieren, bevor Sie
wirklich ganze Klassen in Angriff nehmen. Welche Methoden sollten Sie extrahie-
ren? Ich beantworte diese Frage, indem ich nach Entscheidungen suche. Wie viele
Dinge werden in dem Code vorausgesetzt? Ruft der Code Methoden eines
bestimmten API auf? Nimmt er an, dass er immer auf dieselbe Datenbank
zugreift? Tut der Code diese Dinge, ist es sinnvoll, die Methoden zu extrahieren,
die das Beabsichtigte auf einer höheren Ebene repräsentieren. Wenn Sie etwa
bestimmte Daten aus einer Datenbank abrufen, extrahieren Sie eine Methode, die
Sie nach den abgerufenen Daten benennen. Nach diesen Extraktionen ist die
Anzahl der Methoden viel größer, aber wahrscheinlich ist das Gruppieren der
Methoden einfacher. Noch besser: Vielleicht haben Sie so einige Ressourcen hinter
einem Satz von Methoden vollständig eingekapselt. Wenn Sie für sie eine Klasse
extrahieren, haben Sie einige Dependencies von systemnahen Details aufgehoben.
Sie können solche »Häufungen« auch finden, wenn Sie eine kleine Skizze der
Beziehungen in einer Klasse anfertigen. Solche Skizzen werden als Funktionsskiz-
zen (engl, feature sketches) bezeichnet. Sie zeigen, welche Methoden und Instanzva-
riablen jeder Methode in einer Klasse verwendet werden, und sind ziemlich leicht
zu erstellen. Hier ist ein Beispiel:
267
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
class Reservation
{
private int duration;
private int dailyRate;
private Date date;
private Customer customer;
private List fees = new ArrayListO;
int getAdditionalFees() {
int total = 0;
for(Iterator it = fees.iteratorO ; it.hasNextO; ) {
total += ((FeeRider) (i t.nextO)) .getAmount() ;
}
return total;
}
268
20.1
Aufgaben erkennen
^uratio^
^ustome^ ^customer^
Wenn Sie bereits das Kapitel über Effektskizzen gelesen haben, haben Sie viel-
leicht bemerkt, dass diese Funktionsskizzen ähnlich wie Effektskizzen (11.1) ausse-
hen. Der Hauptunterschied liegt darin, dass die Pfeile in die umgekehrte
Richtung weisen. Bei Funktionsskizzen zeigen Pfeile in die Richtung einer
Methode oder Variablen, die von einer anderen Methode oder Variablen verwen-
det werden. Bei Effektskizzen weisen Pfeile auf Methoden oder Variablen, die von
anderen Methoden und Variablen beeinflusst werden.
269
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
Ist es verwirrend, dass sie ähnlich aussehen? Eigentlich nicht. Diese Skizzen
sind Einmalwerkzeuge. Sie können damit einem Partner vor einer Änderung
kurz darlegen, was Sie tun wollen. Danach werfen Sie sie einfach weg. Es lohnt
sich nicht, sie aufzuheben; deshalb besteht keine Gefahr, sie miteinander zu ver-
wechseln.
Abbildung 20.6 zeigt die Skizze, nachdem wir Kreise für jede Funktion und
Linien für alle Funktionen hinzugefügt haben, die sie verwenden:
Was können wir aus dieser Skizze lernen? Offensichtlich gibt es eine gewisse
Gruppenbildung in dieser Klasse. Die Variablen duration, dai lyRate, date und
customer werden hauptsächlich von getPri nci pal Fee, extend und extend-
ForWeek verwendet. Ist eine dieser Methoden öffentlich? Ja, extend und extend-
ForWeek sind öffentlich, getPri nci pal Fee dagegen nicht. Wie würde unser
System aussehen, wenn wir diese Gruppe in eine separate Klasse einfügten (siehe
Abbildung 20.7)?
270
20.1
Aufgaben erkennen
Die große Blase in dem Diagramm könnte eine neue Klasse sein. Sie müsste
extend, extendForWeek und getPri nci pal Fee als öffentliche Methoden enthal-
ten, aber alle anderen Methoden könnten privat sein. Wir könnten fees, addFee,
getAdditionalFees und getTotalFee in der Reservation-Klasse lassen und
an die neue Klasse delegieren (siehe Abbildung 20.8).
?
Reservation
+ extend(days)
+ extend(days)«delegates»
+ extendForWeek()
+ extend ForWeekQ « d e l e g a t e s »
+ getPrincipalFeeQ
+ addFee(FeeRider)
+ getTotalFee()
- getAdditionalFeeQ
271
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
Bevor wir die Klassen so trennen, müssen wir herausfinden, ob diese neue Klasse
eine wohl definierte Aufgabe erfüllt. Können wir einen aussagekräftigen Namen
für sie finden? Sie scheint zwei Dinge zu tun: eine Reservierung zu verlängern
und die Gebühr dafür zu berechnen. Es scheint, Reservation eigne sich als
Name, aber wir verwenden ihn bereits für die ursprüngliche Klasse.
Allerdings könnten wir auch umgekehrt vorgehen. Anstatt den gesamten Code in
der großen Blase zu extrahieren, könnten wir den anderen Code extrahieren (siehe
Abbildung 20.9).
Wir könnten die extrahierte Klasse FeeCalculator nennen. Dies könnte funktio-
nieren, aber die getTotal Fee-Methode muss getPri nci pal Fee bei der Reservie-
rung aufrufen - oder etwa nicht?
Wie wäre es, getPri nci pal Fee in Reservation aufzurufen und dann diesen
Wert an den FeeCal cul ator zu übergeben? Hier ist eine Skizze des Codes:
272
Aufgaben erken
public getTotalFee() {
int baseFee = getPrincipalFeeO;
return calculator.getTotalFee(baseFee);
}
}
FeeCalculator
Reservation
+ addFee(FeeRider)
+ extend (days)
+ getTotalFeeQ
+ extendForWeekQ
- getAdditionalFeeQ
+ addFee(FeeRider) «delegates»
+ getTotalFee()
- getPrincipalFeeO
Wir könnten sogar überlegen, getPri nci pal Fee in FeeCal cul ator zu verschie-
ben, damit die Aufgaben besser zu dem Klassennamen passen; aber wenn wir
bedenken, dass getPrincipal Fee von mehreren Variablen in Reservation
abhängt, könnte es besser sein, sie an Ort und Stelle zu lassen.
Manche Skizzen enthalten keine Pinch Points. Doch wenigstens helfen Ihnen die
Namen und die Dependencies zwischen den Funktionen weiter.
Anhand der Skizze können Sie verschiedene Methoden ausprobieren, die Klasse
zu zerlegen, indem Sie Gruppen von Funktionen mit einem Kreis umschließen.
Die Linien, die vom Umfang der Kreise durchschnitten werden, können die
Schnittstelle einer neuen Klasse definieren. Wenn Sie die Gruppen einkreisen,
sollten Sie sich auch einen Klassennamen für die jeweilige Gruppe überlegen.
Nebenbei bemerkt: Unabhängig davon, ob Sie letztlich Klassen extrahieren, kön-
nen Sie so hervorragend Ihre Namensgebungsfähigkeiten trainieren. Es ist auch
eine gute Methode, Design-Alternativen zu untersuchen.
Laut Single-Responsibility-Prinzip (SRP) sollen Klassen eine und nur eine Aufgabe
erfüllen. Ist dies der Fall, sollte es nicht schwer sein, sie in einem einzigen Satz
auszudrücken. Versuchen Sie dies mit einer der großen Klassen Ihres Systems.
Wenn Sie daran denken, was der Client braucht und von der Klasse erwartet,
fügen Sie Klauseln zu dem Satz hinzu. Die Klasse tut dieses und dieses und dieses
und jenes. Ist eine dieser Aufgaben wichtiger als alle anderen? Ist dies der Fall,
haben Sie möglicherweise die Hauptaufgabe der Klasse gefunden. Dann sollten
die anderen Aufgaben wahrscheinlich per Refactoring in andere Klassen ausgela-
gert werden.
274
20.1
Aufgaben erkennen
ScheduledJob
+ addPredecessor(ScheduledJob)
+ addSuccessor(ScheduledJob)
+ getDuration(): int
+ show():
+ refeshQ
+ run()
+ postMessagef): void
+ isVisible() : boolean
+ isModifiedQ : boolean
+ persist()
+ acquireResources()
+ releaseResources()
+ isRunning()
+ getElapsedTime()
+ pause()
+ resume()
+ getActivitiesQ
JobPool
+ addJob(Job)
ScheduledJob
+ addPredecessor(ScheduledJob)
+ addSuccessor(ScheduledJob)
+ getDuration() : int ScheduledJobView
+ show(); - showQ
+ refeshQ - isVisiblef)
+ run() - refreshQ
+ postMessage() : void
+ isVisible(): boolean
+ isModifiedQ: boolean
+ persist() ResourceAcquistion
+ acquireResources() - ResourceAcquistion(Job, Resource)
+ releaseResourcesQ - acquire()
+ isRunning() • releaseQ
+ getElapsedTime()
+ pause()
+ resume()
+ getActivitiesQ
JobController
+ pausef)
+ resumeQ
+ isRunningQ
275
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
Jetzt können Clients, die sich nur für die Steuerung von Jobs interessieren, Sche-
dul edDob-Objekte als JobController-Objekte behandeln. Diese Technik, für
bestimmte Kunden eine separate Schnittstelle zu erstellen, entspricht dem Inter-
face-Segregation-Prinzip.
276
20.1
Aufgaben erkennen
Wenn wir Schnittstellen für bestimmte Clients definiert haben, können wir anfan-
gen, Code aus der großen Klasse in eine neue Klasse zu verschieben, die die
ursprüngliche Klasse verwendet (siehe Abbildung 20.14).
Diese Art zu refaktorisieren, ist fast immer schwieriger, als es sich anhört. Oft
müssen Sie zu diesem Zweck mehrere Methoden in der öffentlichen Schnittstelle
der ursprünglichen Klasse (Schedul ed Job) veröffentlichen, damit das neue Front-
end (StandardJobController) für seine Arbeit auf alle benötigten Daten und
Methoden zugreifen kann. Oft ist eine solche Änderung recht aufwendig. Jetzt
muss der Client-Code geändert werden, um die neue Klasse anstelle der alten zu
benutzen. Damit Sie dies sicher durchführen können, müssen Sie Tests für diese
Clients haben. Doch dieses Refactoring hat den Vorteil, dass Sie damit die Schnitt-
stelle einer großen Klasse nach und nach abbauen können. Beachten Sie, dass
ScheduledJob nicht mehr die Methoden enthält, die sich in JobController
befinden.
Heuristik #6: Wenn alles andere scheitert, setzen Sie Scratch Refactoring ein.
Wenn Sie Schwierigkeiten haben, Aufgaben in einer Klasse zu kennen, wenden
Sie Scratch Refactoring an.
277
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
Scratch Refactoring (16.1) ist ein leistungsstarkes Tool. Denken Sie nur daran, dass
es ein künstliches Hilfsmittel ist. Die Dinge, die Sie sehen, wenn Sie »kratzen«,
sind nicht unbedingt die Dinge, die Sie später beim Refactoring verwenden.
Es ist leicht, sich von der schieren Menge der verschiedenen Aufgaben überwälti-
gen zu lassen, die Sie in einer Klasse identifizieren können. Denken Sie daran,
dass Ihre gegenwärtigen Änderungen Ihnen auch etwas über die Möglichkeiten
sagen können, weitere Änderungen der Software vorzunehmen. Oft reicht es aus,
bestimmte Änderungsmöglichkeiten zu erkennen, um neuen Code für eine her-
ausgelöste Aufgabe zu schreiben.
20.3.1 Strategie
Was sollten wir tun, wenn wir alle diese separaten Aufgaben identifiziert haben?
Sollten wir uns eine Woche lang mit den großen Klassen des Systems befassen?
Sollten wir sie alle in kleine Teile zerlegen? Wenn Sie Zeit dafür haben, wäre dies
großartig; doch das kommt selten vor. Es kann auch riskant sein. In solchen Fällen
habe ich fast immer erlebt, dass Systeme durch umfangreiche Refactoring-
Anstrengungen für eine Weile instabil werden, selbst wenn die Teams sorgfältig
278
20-3
Die nächsten Schritte
vorgehen und ihre Arbeit mit Tests absichern. Wenn Sie sich in einer frühen
Phase Ihres Release-Zyklus befinden, risikobereit sind und Zeit haben, kann ein
Refactoring vorteilhaft sein. Lassen Sie sich von den Bugs nur nicht von anderen
Refactoring-Aufgaben abhalten.
Große Klassen werden am besten wie folgt zerlegt: Identifizieren Sie die Aufga-
ben; sorgen Sie dafür, dass jedes Team-Mitglied diese Klassen versteht; zerlegen
Sie dann die Klassen nach Bedarf. Bei diesem Vorgehen verteilen Sie das Ände-
rungsrisiko und können nebenher andere Arbeiten erledigen.
2 0 . 3 . 2 Taktik
Bei den meisten Legacy-Systemen können Sie anfangs allerhöchstem darauf hof-
fen, das SRP auf der Implementierungsebene anzuwenden: Im Wesentlichen
extrahieren Sie Klassen aus Ihrer großen Klasse und delegieren an sie. Das SRP
auf Schnittstellenebene einzuführen, erfordert mehr Arbeit. Die Clients Ihrer
Klasse müssen geändert werden, und Sie brauchen Tests dafür. Ein Vorteil: Das
SRP auf Implementierungsebene einzuführen, erleichtert seine spätere Anwen-
dung auf Schnittstellenebene. Betrachten wir zunächst den Implementierungsfall.
Die Techniken, mit denen Sie Klassen extrahieren, hängen von mehreren Fakto-
ren ab. Wie leicht können Sie die betroffenen Methoden durch Tests absichern?
Schreiben Sie alle Instanzvariablen und Methoden auf, die Sie verschieben müs-
sen. Dies sollte Ihnen zeigen, für welche Methoden Sie Tests schreiben sollten. Bei
der RuleParser-Klasse (siehe weiter vorne) zogen wir das Herauslösen einer
TermTokenizer-Klasse in Betracht. Dafür hätten wir die Felder current und
currentPosition sowie die Methoden hasMoreTerms und nextTerm verschie-
ben müssen. Da diese Methoden privat sind, hätten wir nicht direkt Tests dafür
schreiben können. Wir hätten sie öffentlich machen können (denn schließlich
wollten wir sie ja sowieso verschieben), aber genauso leicht hätten wir einen
Rul eParser in einem Test-Harnisch erstellen und ihm einen Satz von Strings zur
Auswertung übergeben können. Unsere Tests würden dann hasMoreTerms und
nextTerm indirekt abdecken und könnten sie sicher in eine neue Klasse verschie-
ben.
Leider lassen sich viele große Klassen nur schwer in einem Test-Harnisch instan-
ziieren. In Kapitel 9, Ich kann diese Klasse nicht in einen Test-Harnisch einfügen, fin-
den Sie Tipps, mit denen Sie in diesem Fall weiterkommen. Wenn Sie die Klasse
instanziieren können, können Sie vielleicht mit den Tipps aus Kapitel 10, Ich kann
diese Methode nicht in einem Test-Harnisch ausführen, auch Tests einrichten.
Wenn Sie Tests einrichten können, können Sie eine Klasse mit dem Extract Class-
Refactoring sehr unkompliziert extrahieren. Es stammt aus dem Buch Refactoring:
Improvingthe Design ofExisting Code (Addison-Wesley, 1999; dt. Refactoring - Oder
wie Sie das Design vorhandener Software verbessern) von Martin Fowler. Doch auch
279
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden
ohne Tests können Sie, allerdings etwas riskanter, weiterarbeiten. Der folgende
Ansatz ist sehr konservativ. Er funktioniert mit oder ohne Refactoring-Tool:
1. Identifizieren Sie eine Aufgabe, die Sie in eine andere Klasse auslagern wollen.
2. Prüfen Sie, ob Sie Instanzvariablen in die neue Klasse verschieben müssen. Ist
dies der Fall, verschieben Sie sie in einen separaten Teil der Klassendeklaration,
um sie von den anderen Instanzvariablen räumlich zu trennen.
3. Wollen Sie ganze Methoden in die neue Klasse verschieben, extrahieren Sie ihre
Bodies in neue Methoden. Geben Sie jeder neuen Methode den Namen der alten
Methode, setzen Sie aber ein eindeutiges gemeinsames Präfix vor den Namen,
etwa MOVINC, ganz in Großbuchstaben. Wenn Sie kein Refactoring-Tool verwen-
den, denken Sie beim Extrahieren der Methoden an Preserve Signatures (23.1).
Wenn Sie eine Methode extrahieren, fügen Sie sie in den separaten Teil der Klas-
sendeklaration hinter den Variablen ein, die Sie verschieben.
4. Wollen Sie Teile von Methoden in die andere Klasse verschieben, extrahieren Sie
die Teile aus den ursprünglichen Methoden. Verwenden Sie auch hier das Präfix
MOVING für ihre Namen und fügen Sie sie in den separaten Abschnitt ein.
5. An diesem Punkt sollte Ihre Klasse einen Abschnitt enthalten, in dem sich die
Instanzvariablen befinden, die Sie verschieben müssen, sowie eine Reihe von
Methoden, die Sie ebenfalls verschieben wollen. Durchsuchen Sie den Text der
gegenwärtigen Klasse und aller ihrer Unterklassen, um zu prüfen, dass keine
der Variablen, die Sie verschieben wollen, außerhalb der Methoden verwendet
werden, die Sie verschieben werden. Es ist wichtig, bei diesem Schritt nicht Lean
on the Compiler (23.4) anzuwenden. In vielen OO-Sprachen können abgeleitete
Klassen Variablen mit demselben Namen wie Variablen in einer Basisklasse
deklarieren. Dies wird oft als Shadowing (dt. Schattierung) bezeichnet. Wenn
Ihre Klasse Variablen schattiert und die Variablen an anderen Stellen verwendet
werden, könnten Sie das Verhalten Ihrer Codes ändern, wenn Sie die Variablen
verschieben. Ähnlich werden Sie, wenn Sie mit Lean on the Compiler (23.4) Ver-
wendungen einer Variablen suchen, die von einer anderen schattiert wird, nicht
alle Stellen finden, an der sie benutzt wird. Wenn Sie die Deklaration einer
schattierenden Variablen auskommentieren, wird nur die schattierte Variable
sichtbar.
6. An diesem Punkt können Sie alle separierten Instanzvariablen und Methoden
direkt in die neue Klasse verschieben. Erstellen Sie eine Instanz der neuen
Klasse in der alten Klasse, und suchen Sie mit Lean on the Compiler (23.4) Stel-
len, an denen die verschobenen Methoden nicht mehr von der alten Klasse, son-
dern von der Instanz aufgerufen werden müssen.
7. Wenn der Code nach dem Verschieben kompiliert wird, können Sie das Präfix
MOVING der verschobenen Methoden entfernen. Navigieren Sie mit Lean on the
Compiler (23.4) an die Stellen, an denen Sie die Namen ändern müssen.
280
20.4
Nach dem Extrahieren von Klassen
Die Schritte für dieses Refactoring sind recht aufwendig; doch wenn Ihr Code-
Fragment sehr komplex ist, sind sie erforderlich, wenn Sie Klassen sicher ohne
Tests extrahieren wollen.
Wenn Sie Klassen ohne Tests extrahieren, kann einiges schiefgehen. Die subtil-
sten Bugs, die wir einführen können, haben mit der Vererbung zu tun. Eine
Methode aus einer Klasse in eine andere zu verschieben, ist ziemlich sicher. Sie
können Ihre Arbeit mit Lean on the Compiler (23.4) unterstützen, aber in den meis-
ten Sprachen gibt es Probleme, wenn Sie eine Methode verschieben wollen, die
eine andere Methode überschreibt. In diesem Fall wird jetzt bei einem Aufruf der
Methode in der ursprünglichen Klasse eine gleichnamige Methode der Basisklasse
aufgerufen. Eine ähnliche Situation kann bei Variablen eintreten. Eine Variable in
einer Unterklasse kann eine gleichnamige Variable in einer Oberklasse verbergen.
Eine Verschiebung macht nur die vorher verborgene Variable sichtbar.
281
Kapitel 21
Dies kann eines der frustrierendsten Probleme in Legacy-Systemen sein. Sie müs-
sen eine Änderung vornehmen und denken: »Ach, das ist alles?« Dann entdecken
Sie, dass sich an Dutzenden Stellen in Ihrem System ähnlicher Code befindet und
Sie dieselbe Änderung immer wieder vornehmen müssen. Sie bekommen das
Gefühl, mit einem umstrukturierten System hätten Sie dieses Problem vielleicht
nicht; aber woher sollen Sie die Zeit dafür nehmen? Deshalb müssen Sie mit
einem weiteren wunden Punkt in dem System leben, der die Arbeit mit ihm noch
unerfreulicher macht.
Wenn Sie Refactoring beherrschen, sind Sie in einer besseren Position. Sie wissen,
dass duplizierten Code zu entfernen, keine so große Sache sein muss wie etwa ein
Umbau der Architektur des Systems. Sie können den Code in kleinen Schritten im
Zuge Ihrer Arbeit ändern. Ihr System wird nach und nach immer besser, solange
nicht andere hinter Ihrem Rücken neuen duplizierten Code in das System einfü-
gen. In einem solchen Fall müssten Sie mit den Betroffenen einige klare Worte
reden; aber das ist ein anderes Thema. Hier sollten Sie sich fragen, ob sich der
Aufwand lohnt. Was bekommen Sie, wenn Sie duplizierten Code eifrig ausmisten?
Die Ergebnisse sind überraschend. Betrachten wir ein Beispiel.
Hier sind die Listings der beiden Anweisungsklassen. Können Sie hier duplizier-
ten Code entdecken?
import java.io.OutputStream;
283
Kapitel 21
Ich ändere im ganzen System denselben Code
import java.io.OutputStream;
284
Ich ändere im ganzen System denselben Code
AddEmployeeCmd LoginCommand
285
Kapitel 21
Ich ändere im ganzen System denselben Code
Anscheinend enthalten die Klassen viel duplizierten Code, aber was soll's? Der
Code ist nicht sehr umfangreich. Wir könnten ihn refaktorisieren, duplizierten
Code ausschneiden und die Klassen verkleinern; doch wird dadurch unser Leben
einfacher? Vielleicht ja, vielleicht nein; das ist durch einen Blick auf den Code
schwer zu sagen.
outputStream.write(userName.getBytesO);
outputStream.write(OxOO);
outputStream.write(passwd.getBytesO);
outputStream.write(OxOO);
In C erhalten Strings bei der Ausgabe ein terminierendes Null-Zeichen (0x00). Sie
können den duplizierten Code wie folgt extrahieren: Erstellen Sie eine Methode
namens wri teFi el d, die einen String und einen Output-Stream übernimmt, den
String in den Stream ausgibt und mit einem Null-Zeichen abschließt.
286
21.1
Erste Schritte
so
void c() { a a O ; b(); a O ; bb(); }
oder so
void c() { a O ; ab(); ab(); b O ; }
zerlegen. Welche Variante sollten wir wählen? In Wahrheit spielt dies strukturell
kaum eine Rolle. Beide Formen sind besser als die Ausgangsform; und bei
Bedarf können wir die Gruppierung durch Refactoring schnell ändern. Diese
Entscheidungen legen uns nicht endgültig fest. Ich konzentriere mich lieber auf
die Namen, die ich verwenden würde. Wenn mir ein Name für zwei wiederholte
Aufrufe von a() in einem Kontext sinnvoller erscheint als ein Name für einen
Aufruf von a(), gefolgt von einem Aufruf von b(), verwende ich ihn.
Außerdem fange ich möglichst klein an. Wenn ich winzige Teile von duplizier-
tem Code entfernen kann, tue ich dies zuerst, weil dadurch das große Bild klarer
wird.
Wir können diese Methode jetzt an jeder Stelle einsetzen, an der ein String/Null-
Paar ausgegeben wird. Wir führen periodisch unsere Tests aus, um zu prüfen, ob
wir nichts beschädigt haben. Hier ist die wri te-Methode von Logi nCommand nach
der Änderung:
Damit ist dieses Problem für die Logi nCommand-Klasse gelöst, aber in der AddEm-
ployeeCmd-Klasse hilft uns die Methode nicht weiter, obwohl in dieser ähnliche
wiederholte String/Null-Sequenzen ausgeben werden. Weil beide Klassen Anwei-
sungsklassen sind, können wir für sie eine gemeinsame Oberklasse namens Com-
mand einführen und die writeField-Methode in die Oberklasse verschieben,
damit sie in beiden Anweisungsklassen verwendet werden kann (siehe Abbildung
21.2).
287
Kapitel 21
Ich ändere im ganzen System denselben Code
Die wri te-Methode für Logi nCommand sieht wie folgt aus:
Der Code ist etwas sauberer, aber wir sind noch nicht fertig. Die wri te-Methoden
von AddEmployeeCmd und Logi nCommand haben dieselbe Form: Sie geben den
header, getSi ze() und commandChar aus; dann schreiben sie eine Reihe von Fel-
dern und schließlich den footer. Wenn wir den Unterschied, das Schreiben der
Felder extrahieren, erhalten wir folgende wri te-Methode von Logi nCommand:
288
21.1
Erste Schritte
writeBody(outputStream);
outputStream.write(footer);
}
AddEmpl oyeeCmd hat dieselbe wri te-, aber eine andere wri teBody-Methode:
Wenn zwei Methoden in etwa gleich aussehen, extrahieren Sie die Unterschiede
in andere Methoden. Dann erhalten Sie oft zwei identische Fragmente und kön-
nen eins verwerfen.
Die wri te-Methoden beider Klassen sehen genau gleich aus. Können wir die
wri te-Methode nach oben in die Command-Klasse verschieben? Noch nicht. Auch
wenn beide gleich aussehen, verwenden sie Daten ihrer Klassen: header, footer
und commandChar. Wenn wir nur eine einzige wri te-Methode verwenden wollen,
müssten wir Methoden der Unterklassen aufrufen, um diese Daten abzurufen.
Betrachten wir die Variablen in AddEmpl oyeeCmd und Logi nCommand:
289
Kapitel 21
Ich ändere im ganzen System denselben Code
Jetzt ist es sicher, die wri te-Methode in die Oberklasse zu verschieben. Danach
sieht die Command-Klasse wie folgt aus:
Beachten Sie, dass wir eine abstrakte Methode für wri teBody in Command einfüh-
ren müssten (siehe Abbildung 21.3).
Abb. 21.3: wri teFi el d, wri teBody und wri te in der Oberklasse
291
Kapitel 21
Ich ändere im ganzen System denselben Code
Diese Klasse ist ziemlich schlank. A d d E m p l o y e e C m d sieht sehr ähnlich aus. Sie
enthält die Methoden getSi ze u n d g e t C o m m a n d C h a r u n d sonst k a u m etwas.
Betrachten w i r die getSi ze-Methoden etwas genauer:
292
21.1
Erste Schritte
ren sie die Größen ihrer jeweiligen Felder. Wir können die Werte, die
unterschiedlich berechnet werden, also die Größen der Felder, in eine separate
Methode, getBodySi ze(), extrahieren:
Dann erhalten wir in beiden Methoden denselben Code. Wir addieren die Größe
aller Buchhaltungsdaten und fügen dann die Größe des Bodys hinzu, die der
Summe der Größen aller Felder entspricht. Danach können wir getSize in die
Command-Klasse verschieben und getBodySi ze in den Unterklassen implementie-
ren (siehe Abbildung 21.4).
Wo stehen wir jetzt? Die Methode getBodySi ze ist in AddEmployeeCmd wie folgt
implementiert:
Wir haben hier recht offensichtlich duplizierten Code ignoriert. Er ist zwar kürzer,
aber wir wollen es genau nehmen und ihn vollständig entfernen:
293
Kapitel 21
Ich ändere im ganzen System denselben Code
Wenn wir die getFi el dSi ze-Methode in die Command-Klasse verschieben, können
wir sie auch in der getBodySi ze-Methode von LoginCommand verwenden:
Gibt es jetzt überhaupt noch duplizierten Code? Ja, aber nicht sehr viel. Sowohl
LoginCommand als auch AddEmployeeCmd übernehmen eine Liste von Parame-
tern, rufen deren Größe ab und geben sie aus. Was ist abgesehen von der com-
mandChar-Variablen für die verbleibenden Unterschiede zwischen den zwei
Klassen verantwortlich? Können wir den duplizierten Code entfernen, indem wir
ihn ein wenig verallgemeinern? Wenn wir eine Liste in der Basisklasse deklarie-
ren, können wir ihn wie folgt in jeden Unterklassenkonstruktor einfügen:
Wenn wir die Felder in den Unterklassen in die Liste einfügen, können wir die
Body-Größe mit demselben Code berechnen:
int getBodySizeO {
int result = 0;
for(Iterator it = fields.iteratorO; it.hasNextO; ) {
String field = (String)it.nextO ;
result += getFieldSize(field);
}
return result;
}
294
21.1
Erste Schritte
p r i v a t e v o i d writeBodyCOutputstream outputStream) {
for(Iterator it = fields. iteratorO; it.hasNextO; ) {
String field = (String)it.next() ;
writeField(outputStream, field);
}
}
private int getFieldSize(String field) {
return field.getBytesO .length + 1;
}
295
Kapitel 21
Ich ändere im ganzen System denselben Code
throws Exception {
outputStream.write(header);
outputStream.wri te (getSizeO);
outputStream.write(commandChar);
writeBody(outputStream);
outputStream.write(footer);
}
}
Die Klassen Logi nCommand und AddEmpl oyeeCmd sind jetzt unglaublich schlank:
Okay, wo stehen wir jetzt? Wir haben so viel duplizierten Code entfernt, dass die
ursprünglichen Klassen nur noch ganz schlank sind. Die gesamte Funktionalität
befindet sich in der Command-Klasse. Doch brauchen wir überhaupt separate Klas-
sen für die beiden Anweisungen? Was wären die Alternativen?
Wir könnten die Unterklassen verwerfen und eine statische Methode in die Com-
mand-Klasse einfügen, mit der wir eine Anweisung senden können:
296
21.1
Erste Schritte
Aber dies wäre für Clients viel Arbeit. Eins ist sicher: Wir müssen zwei verschie-
dene commandChar-Werte senden, und der Nutzer soll sich nicht darum kümmern
müssen.
Stattdessen könnten wir für die Anweisung zwei separate statische Methoden ver-
wenden:
Aber dann müsste unser gesamter Client-Code geändert werden. Im Moment ent-
hält unser Code zahlreiche Stellen, an denen wir AddEmployeeCmd- und Login-
Command-Objekte konstruieren.
Vielleicht ändern wir die Klassen im Moment gar nicht. Denn eigentlich schaden
die schlanken Unterklassen nicht. Sind wir fertig? Nein; eine Kleinigkeit müssen
wir noch erledigen; wir hätten dies eigentlich schon früher machen sollen. Wir
sollten AddEmployeeCmd in AddEmployeeCommand umbenennen, damit die
Namen beider Unterklassen eine konsistente Form haben. Konsistente Namen
verringern das Fehlerrisiko.
Abkürzungen
Abkürzungen in Klassen- und Methodennamen sind problematisch. Sie zu ver-
wenden, kann in Ordnung sein, wenn dies konsistent erfolgt, aber im Allgemei-
nen benutze ich sie ungern.
Kapitel 21
Ich ändere im ganzen System denselben Code
Ein Team, mit dem ich arbeitete, versuchte, die Wörter manager und manage-
ment in fast jedem Klassennamen im System zu verwenden. Diese Namenskon-
vention war nicht sehr hilfreich. Doch was sie noch schlimmer machte: Die
Wörter manager und management wurden auf unglaublich viele verschiedene
Arten abgekürzt. So hießen einige Klassen etwa XXXXMgr und andere XXXXMngr.
Wollte man eine Klasse benutzen, musste man meistens tatsächlich nachschla-
gen, wie ihr Name korrekt lautete. Mehr als 50 Prozent der Zeit lag ich falsch,
wenn ich versuchte, das richtige Suffix einer bestimmten Klasse zu erraten.
Jetzt haben wir also allen duplizierten Code entfernt. Haben wir die Situation
damit verbessert oder verschlechtert? Betrachten wir einige Szenarien. Was pas-
siert, wenn wir eine neue Anweisung hinzufügen müssen? Wir könnten einfach
eine neue Unterklasse von Command ableiten. Im ursprünglichen Design hätten
wir eine neue Anweisung erstellen und dann per Cut/Copy/Paste Code aus einer
anderen Anweisung übernehmen und alle Variablen ändern können. Aber
dadurch hätten wir noch mehr duplizierten Code produziert und die Situation ver-
schlechtert. Außerdem wäre diese Methode fehleranfällig. So könnten wir etwa die
Variablen falsch verwenden. Mit dem ursprünglichen Code hätte es definitiv etwas
länger gedauert.
Ist der neue Code weniger flexibel als der alte? Was passiert, wenn wir Anweisun-
gen senden müssen, die nicht aus Strings bestehen? Eigentlich haben wir dieses
Problem bereits gelöst. Die AddEmployeeCommand-Klasse nimmt bereits eine
Ganzzahl entgegen, die wir in einen String umwandeln, um sie dann als Anwei-
sung zu senden. Wir können dasselbe auch mit beliebigen anderen Typen tun. Wir
müssen sie irgendwie in einen String umwandeln, um sie zu senden. Dies könn-
ten wir im Konstruktor der jeweiligen neuen Unterklasse tun.
Was machen wir, wenn eine Anweisung ein anderes Format hat? Angenommen,
wir bräuchten einen neuen Anweisungstyp, der andere Anweisungen in seinen
Body einbetten kann. Wir könnten dies leicht mit einer neuen Unterklasse der
Command-Klasse lösen, in der wir deren wri teBody-Methode überschreiben:
298
21.1
Erste Schritte
Stellen Sie sich den Aufwand vor, hätten wir den duplizierten Code nicht entfernt.
Dieses letzte Beispiel hebt etwas sehr Wichtiges hervor: Wenn Sie duplizierten
Code über Belassen hinweg entfernen, erhalten Sie sehr kleine fokussierte Metho-
den. Jede leistet etwas, was keine andere Methode tut; und dadurch erhalten wir
einen unglaublichen Vorteil: Orthogonalität.
In diesem Beispiel haben wir viele Dinge gemacht: Methoden und Variablen von
Klasse zu Klasse verschoben oder Methoden zerlegt; aber meistens war die Arbeit
mechanisch. Wir haben einfach duplizierten Code gesucht und entfernt. Das ein-
zig wirklich Kreative sind die Namen für die neuen Methoden. Im ursprünglichen
Code wurden keine Felder oder Anweisungs-Bodies erwähnt, obwohl die Konzepte
darin realisiert waren. So wurden etwa einige Variablen unterschiedlich behandelt;
deshalb bezeichneten wir sie als Felder. Am Ende war unser Design sauber und
orthogonal, obwohl wir nicht das Gefühl hatten, Design-Arbeit zu leisten. Es war
eher so, dass wir die »Essenz« des Codes aus dem Vorhandenen herausarbeiteten.
299
Kapitel 21
Ich ändere im ganzen System denselben Code
hervor. Die Methode ist nicht perfekt. So wäre es wünschenswert, wenn die Com-
mand-Methode
folgendermaßen aussähe:
Jetzt haben wir einen Knopf für Header und einen anderen für Footer. Wir können
bei Bedarf Knöpfe hinzufügen, aber es ist schön, wenn dies auf natürliche Weise
passiert.
Duplizierten Code zu entfernen, ist eine leistungsstarke Methode, um ein Design
herauszuarbeiten. Sie macht nicht nur das Design flexibler, sondern auch Ände-
rungen schneller und einfacher.
Open/Closed-Prinzip
Das Open/Closed-Prinzip wurde zuerst von Bertrand Meyer formuliert. Danach
soll Code offen für Erweiterungen, aber geschlossen für Änderungen sein. Was
bedeutet das? Es bedeutet, dass wir bei einem guten Design den Code kaum
ändern müssen, um neue Funktionen hinzuzufügen.
Hat der Code, den wir in diesem Kapitel entwickelt haben, im Ergebnis diese
Eigenschaften? Ja. Ich habe gerade einige Änderungsszenarien beschrieben; bei
vielen müssen nur sehr wenige Methoden geändert werden; bei anderen kön-
nen wir die Funktion einfach mit einer neuen Unterklasse hinzufügen. Natür-
lich ist es dann wichtig, duplizierten Code zu entfernen. In Programming hy
Difference (8.2) finden Sie Näheres zu diesem Thema und die Integration durch
Refactoring.
Wenn wir duplizierten Code entfernen, erfüllt unser Code oft natürlich die For-
derungen des Open/Closed-Prinzips.
300
Kapitel 22
Eines der schwierigsten Dinge beim Arbeiten mit Legacy Code ist der Umgang
mit umfangreichen Methoden. In vielen Fällen können Sie ein Refactoring langer
Methoden mit den Techniken Sprout Method (6.1) und Sprout Class (6.2) vermei-
den. Doch selbst wenn Sie es vermeiden können, ist es eine Schande, dies zu tun.
Lange Methoden sind in einer Code-Basis wie Sümpfe. Jedes Mal, wenn Sie sie
ändern müssen, müssen Sie erneut versuchen, sie zu verstehen. Oft brauchen Sie
mehr Zeit, den Code zu ändern, als erforderlich wäre, wenn er sauberer wäre.
Lange Methoden sind eine Qual, aber Monster-Methoden sind noch schlimmer.
Eine Monster-Methode ist eine Methode, die so lang und so komplex ist, dass Sie
am liebsten einen großen Bogen darum machen würden. Monster-Methoden kön-
nen Hunderte oder Tausende von Zeilen lang sein und so viele Einrückungen ent-
halten, dass eine vernünftige Navigation fast unmöglich ist. Wenn Sie Monster-
Methoden begegnen, sind Sie versucht, sie auf einigen Metern Endlospapier aus-
zudrucken und in einem Gang auszulegen, um sie mit Ihren Kollegen zu untersu-
chen.
Auf einer Geschäftsreise sprach mich einmal ein Freund an: »Heh, das musst du
dir unbedingt anschauen.« Er holte seinen Laptop aus seinem Hotelzimmer und
zeigte mir eine Methode, die mehr als tausend Zeilen lang war. Mein Freund
wusste, dass ich mich näher mit Refactoring befasst hatte und sagte: »Wie um
alles in der Welt kann man so was refaktorisieren?« Wir fingen an zu überlegen.
Wir wussten, dass Tests der Schlüssel waren, aber wo fängt man bei einer solch
riesigen Methode an zu testen?
In diesem Kapitel umreiße ich, was ich seit damals gelernt habe.
301
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
22.1.1 Aufzählungsmethoden
Eine Aufzählungsmethode (engl, bulleted method) ist eine Methode, die fast keine
Einrückungen enthält. Sie besteht einfach aus einer Abfolge von Code-Fragmen-
ten, die an eine listenartige Aufzählung erinnern. Ein Teil des Codes in den Frag-
menten mag eingerückt sein, aber die Methode selbst wird nicht von
Einrückungen beherrscht. Wenn Sie eine Aufzählungsmethode mit halb geschlos-
senen Augen anschauen, sehen Sie etwas Ähnliches wie in Listing 22.1.
if (c.vipProgramStatus == VIP_DIAM0ND) {
upgradeQuery = true;
}
if (!upgradeQuery)
RIXInterface::extend(lastCookie, days + additionalDays);
eise {
302
22.1
Spielarten von Monstern
}
Listing 22.i: Aufzählungsmethode
Dies ist die allgemeine Form einer Aufzählungsmethode. Wenn Sie Glück haben,
hat jemand Leerzeilen zwischen die Abschnitte oder Kommentare eingefügt, um
zu zeigen, dass die betreffenden Fragmente etwas Unterschiedliches tun. Idealer-
weise könnten Sie einfach jedes dieser Fragmente in eine separate Methode extra-
hieren, aber oft lassen sich Monster-Methoden nicht leicht auf diese Weise
refaktorisieren. Der Leerraum zwischen den Abschnitten ist ein wenig irrefüh-
rend, weil oft temporäre Variablen in einem Abschnitt deklariert und im nächsten
verwendet werden. Die Methode zu zerlegen, ist oft nicht so einfach wie das Kopie-
ren und Einfügen von Code. Dennoch sind Aufzählungsmethoden etwas weniger
furchterregend als die anderen Spielarten, hauptsächlich werden wir wegen des
Fehlens wilder Einrückungen einigermaßen den Überblick behalten können.
if (!RIXInterface::available()) {
RIXInterface::doEvents(100);
PostLogMessage(0, 0, "delay on reservation creation");
int holdCookie = -1;
switch(status) {
case N0T_AVAILABLE_UPGRADE_LUXURV:
holdCookie = RIXInterface::holdReservation(Luxury,l.startingDate);
if (holdCookie != -1) {
holdCookie |= 9;
}
break;
case NOT_AVAILABLE_UPGRADE_SUV:
holdCookie = RIXInterface::holdReservation(SUV,l.startingDate);
break;
303
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
case NOT_AVAILABLE_UPGRADE_VAN:
holdCookie = RIXInterface::holdReservation(Van,l.startingDate);
break;
case AVAILABLE:
default:
RIXInterface::holdReservation;
State = Held;
break;
}
}
Aber eine solche tief verschachtelte Methode hat fast dieselben Eigenschaften wie
eine Aufzählungsmethode. Dagegen zeigt Listing 22.3 die Art tief verschachtelter
Methode, die Ihre ganze Wertschätzung verdient.
while (!RIXInterface::available()) {
RIXInterface::doEvents(lOO);
PostLogMessage(0, 0, "delay on reservation creation");
int holdCookie = -1;
switch (status) {
case NOT_AVAILABLE_UPGRADE_LUXURY:
holdCookie = RIXInterface::holdReservation(Luxury,l.startingDate);
if (holdCookie != -1) {
if (1 == CIC && customerlD == 45) {
// Special #1222
while (RIXInterface::notBusy()) {
int code = RIXInterface::getOpCode(customerlD) ;
if (code == 1 I I customerlD > 0)) {
PostLogMessage(l, 0, "QEX PID");
for (int n = 0; n < 12; n++) {
int total = 2000;
if (State == Initial | | State == Held) {
total += getTotalByLocation(location);
tempTotal = total;
if (location == GIG && days > 2) {
if (State == Held)
total += 30;
}
}
RIXInterface::serveIDCode(n, total);
}
304
22.1
Spielarten von Monstern
} eise {
RIXInterface::serveCode(customerlD) ;
}
}
}
}
break;
case NOT_AVAILABLE_UPGRADE_SUV:
holdCookie = RIXInterface::ho1dReservation(SUV,1,startingDate);
break;
case NOT_AVAILABLE_UPGRADE_VAN:
holdCookie = RIXInterface::ho1dReservation(Van,l.startingDate);
break;
case AVAILABLE:
default:
RIXInterface::ho1dReservation(type,l.startingDate);
State = Held;
break;
}
}
}
Listing 22.3: Sehr tief verschachtelte Methode
Wenn Sie lange Methoden refaktorisieren, spielt es eine große Rolle, ob Sie über
ein Refactoring-Tool verfügen oder nicht. Fast jedes Refactoring-Tool unterstützt
das Extract-Method-Refactoring (Methode extrahieren), weil diese Methode
unglaublich wirksam ist. Wenn ein Tool Methoden für Sie sicher extrahieren kann,
müssen Sie den extrahierten Code nicht mit Tests verifizieren. Das Tool leistet
diese Analyse für Sie. Sie müssen nur lernen, wie Sie Code mit dem Tool extrahie-
ren können, um Methoden zu verbessern.
Steht Ihnen kein einschlägiges Tool zum Extrahieren von Methoden zur Verfü-
gung, ist das Säubern von Monster-Methoden schwieriger. Oft müssen Sie konser-
vativer vorgehen, weil Sie Ihre Arbeit mit den Tests steuern, die Sie einrichten
können.
305
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
Wenn Sie automatische Refactorings ohne Tests durchführen, sollten Sie nur
das Tool benutzen. Nach einer Reihe von automatischen Refactorings können
Sie oft Tests einrichten, mit denen Sie dann Ihre späteren manuellen Änderun-
gen prüfen können.
class CommoditySelectionPanel
{
306
22.2
Monster mit automatischer Refactoring-Unterstützung zähmen
}
In dieser Methode kann vieles bereinigt werden. Ungewöhnlich ist etwa, dass
diese Panel-Klasse, die idealerweise nur zur Anzeige verwendet wird, einen Filter
enthält. Diesen Code zu entwirren, ist jedoch schwierig. Wenn wir Tests für die
Methode in der jetzigen Form schreiben wollen, könnten wir den listbox-
Zustand prüfen; aber damit kämen wir nicht weit, um das Design zu verbessern.
class CommoditySelectionPanel
{
public void u p d a t e O {
if (commoditiesAreReadyForUpdateO) {
clearDisplayO;
updateCommoditiesO: ;
}
}
private boolean commoditiesAreReadyForUpdateO {
return commodities.sizeO > 0 && commodities.GetSource() .equals("local") ;
}
3°7
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
Offen gestanden, sieht die Struktur in dem Code von update nicht grundsätzlich
anders aus; es handelt sich immer noch nur um eine i f-Anweisung, die eine
Anweisung einschließt. Aber die Arbeit wird jetzt an die Methoden delegiert. Die
update-Methode sieht nur noch wie ein Skelett des ursprünglichen Codes aus.
Und was ist mit diesen Namen? Sie wirken etwas künstlich, oder? Aber sie eignen
sich als Ausgangspunkt. Wenigstens kann der Code mit ihnen auf höherer Ebene
kommunizieren; und sie führen Seams ein, mit denen wir Dependencies aufhe-
ben können. Mit Subclass and Override Method (25.21) bekommen wir display-
Commodity und clearDisplay in den Griff. Danach können wir überlegen, ob
wir eine Display-Klasse anlegen und diese Methoden in sie verschieben sollten,
wobei wir den Prozess mit unseren Tests überwachen. Doch hier wäre es passen-
der zu prüfen, ob wir update und updateCommodi ti es in eine andere Klasse ver-
schieben und clearDisplay und displayCommodity in dieser Klasse lassen
können, um die Tatsache zu nutzen, dass diese Klasse ein Panel, ein Display, ist.
Wir können die Methoden später umbenennen, wenn wir die passende Struktur
gefunden haben. Nach einem zusätzlichen Refactoring entspricht unser Design
etwa Abbildung 22.1.
CommoditySelectionPanel CommodityFilter
< + updatef)
+ clearDisplay()
+ displayCommodity(CommodityView) - commoditiesAreReadyforllpdateQ
- updateCommoditiesQ
- isSingleBrokerCommodity(Commodity)
Wenn Sie Methoden mit einem Tool automatisch extrahieren, sollten Sie sich vor
allem merken, dass Sie einen großen Teil der groben Arbeit sicher mit dem Tool
erledigen können. Um die Details können Sie sich dann später kümmern, nach-
dem Sie weitere Tests eingerichtet haben. Machen Sie sich um Methoden, die
nicht in die Klasse zu passen scheinen, keine Sorgen. Oft sind sie ein Indiz für
Funktionalität, die Sie später in eine neue Klasse extrahieren müssen. In Kapitel
20, Diese Klasse ist zu groß und soll nicht noch größer werden, wird näher auf dieses
Thema eingegangen.
308
22.3
Die Herausforderung des manuellen Refactorings
Die folgende Liste enthält nicht alle, sondern nur die häufigsten Fehler:
1. Wir können vergessen, eine Variable an die extrahierte Methode zu übergeben.
Oft meldet uns der Compiler die fehlende Variable (es sei denn, sie hat densel-
ben Namen wie eine Instanzvariable); aber wir könnten einfach annehmen, es
handele sich um eine lokale Variable, und sie in der neuen Methode deklarieren.
2. Wir könnten der extrahierten Methode einen Namen geben, der eine gleichna-
mige Methode in einer Basisklasse verbirgt oder überschreibt.
3. Wir könnten bei der Übergabe von Parametern oder der Zuweisung von Rück-
gabewerten einen Fehler machen. Dies kann etwas wirklich Dummes sein, wie
etwa die Rückgabe des falschen Wertes. Ein subtilerer Fehler wäre etwa die
Rückgabe oder Übernahme falscher Typen in der neuen Methode.
Es kann einiges schiefgehen. Die Techniken in diesem Abschnitt können das
Risiko beim Extrahieren verringern, wenn wir ohne Tests arbeiten.
309
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
310
22.3
Die Herausforderung des manuellen Refactorings
Nachdem wir diese Variable eingefügt haben, müssen wir noch für den Input sor-
gen, um einen Fall zu produzieren, der diese Bedingung erfüllt. Wenn wir dies
tun, können wir die entsprechende Logik extrahieren; und unsere Tests sollten
immer noch bestanden werden.
Der folgende Test zeigt uns, dass ein node hinzugefügt wird, wenn der Node den
Typ TF_G hat:
void testAddNodeOnBasicChildO
{
DOMBuilder builder = new DomBuilderO;
List children = new A r r a y L i s t O ;
children.addCnew XDOMNNode(XDOMNNode,TF_G));
builder.processNode(new XDOMNSnippetO , children);
assertTrue(builder.nodeAdded);
}
Der folgende Test zeigt, dass ein node nicht hinzugefügt wird, wenn er den fal-
schen Typ hat:
Nachdem wir diese Tests eingerichtet haben, sollten wir zuversichtlich den Body
der Bedingung extrahieren können, die bestimmt, ob node-Objekte hinzugefügt
werden. Wir können die gesamte Bedingung kopieren. Unser neuer Test zeigt,
dass der node hinzugefügt wird, wenn die Bedingung durchlaufen wird.
3ii
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
}
}
}
private boolean isBasicChi1d(XDOMNNode node) {
return node.typeO == TF_G
|| node.typeO == TF_H
| | node.typeO == TF_GLOT && node.isChildO);
}
}
Später können wir das Flag und den Test entfernen.
In diesem Fall habe ich eine boolesche Variable verwendet. Ich wollte nur prüfen,
ob der node nach dem Extrahieren der Bedingung immer noch hinzugefügt
wurde. Ich war ziemlich zuversichtlich, den gesamten Body der Bedingung extra-
hieren zu können, ohne Fehler einzufügen; deshalb habe ich nicht die gesamte
Logik der Bedingung getestet. Diese Tests stellten nur eine schnelle Prüfung zur
Verfügung, die Bedingung nach dem Extrahieren auch wirklich immer noch auf
dem Code-Pfad lag. Weitere Anleitungen zum Umfang der Tests beim Extrahieren
von Methoden finden Sie in Targeted Testing (13.3) in Kapitel 13, Ich muss etwas
ändern, weiß aber nicht, welche Tests ich schreiben soll.
Wenn Sie mit Introduce Sensing Variable arbeiten, können Sie die Überwachungs-
variablen über eine Reihe von Refactorings hinweg in der Klasse lassen und sie
erst nach Ihrer Refactoring-Sitzung löschen. Ich tue dies oft, damit ich alle Tests
sehen kann, die ich beim Extrahieren schreibe, und sie leicht rückgängig machen
kann, wenn ich Code auf andere Weise extrahieren möchte. Am Ende lösche ich
diese Tests oder refaktorisiere sie so, dass sie die extrahierten Methoden anstelle
der ursprünglichen testen.
312
22.3
Die Herausforderung des manuellen Refactorings
decken können. Da jeder eine andere Auffassung von »klein« hat, will ich es noch
anders ausdrücken: Mit »klein« meine ich hier Code-Fragmente mit zwei oder
drei, allerhöchstem fünf Zeilen, die Sie leicht benennen können. Wesentlich ist,
dass Sie sich bei diesen kleinen Extraktionen auf den Coupling Count (die Kopp-
lungszahl) der Extraktion konzentrieren. Der Coupling Count ist die Anzahl der
Werte, die an die zu extrahierende Methode übergeben oder von ihr zurückgege-
ben werden. Wenn wir etwa eine max-Methode aus der folgenden Methode extra-
hieren, hat ihr Coupling Count den Wert 3:
}
Der Coupling Count der Methode ist 3: zwei Variablen hinein und eine Variable
heraus. Sie sollten beim Extrahieren Methoden mit kleinem Coupling Count bevor-
zugen, weil dann das Fehlerrisiko geringer ist. Wählen Sie Code-Fragmente mit
einer geringen Anzahl von Zeilen zum Extrahieren aus und zählen Sie die Variab-
len, die hineingehen und herauskommen. Zugriffe auf Instanzvariablen zählen
nicht; sie passieren die Schnittstelle der Methode nicht, die wir extrahieren.
Das Hauptrisiko beim Extrahieren einer Methode ist ein Fehler bei der Typum-
wandlung, etwa eine i nt als doubl e zu übergeben. Wir können dieses Risiko eher
vermeiden, wenn wir nur Methoden mit einem niedrigen Coupling Count extrahie-
ren. Haben wir eine mögliche Extraktion entdeckt, sollten wir herausfinden, wo
jede der übergebenen Variablen deklariert ist, um zu prüfen, ob wir die richtige
Methodensignatur verwenden.
Wenn Extraktionen mit einem niedrigen Coupling Count sicherer sind, sollten
Extraktionen mit einem Coupling Count von 0 am sichersten sein - und das
stimmt. Wir können große Fortschritte mit einer Monster-Methode machen,
indem wir nur Methoden extrahieren, die keine Parameter übernehmen und
keine Werte zurückgeben. Eigentlich sind diese Methoden nur Anweisungen, eine
Funktion auszuführen. Sie weisen das Objekt an, eine seiner Variablen zu verän-
dern oder, was wir nicht hoffen wollen, einen globalen Zustand zu modifizieren.
Doch wie auch immer: Wenn Sie versuchen, ein solches Code-Fragment zu benen-
313
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
nen, gewinnen Sie oft weitere Einsichten in seine Funktion und seinen Einfluss
auf das Objekt. Eine solche Einsicht kann weitere Einsichten auslösen und dazu
führen, dass Sie Ihr Design aus anderen, produktiveren Perspektiven betrachten.
Wenn Sie Extract What You Know anwenden, sollten Sie nur Fragmente auswäh-
len, die nicht zu umfangreich sind. Und wenn ihr Coupling Count größer als 0 ist,
lohnt sich oft der Einsatz einer Überwachungsvariablen. Schreiben Sie nach dem
Extrahieren einige Tests für die extrahierte Methode.
Wenn Sie diese Technik auf kleine Fragmente einer Monster-Methode anwenden,
werden Sie längere Zeit kaum Fortschritte bemerken, aber auch kleine Fortschritte
stärken Ihr Selbstvertrauen. Jedes Mal, wenn Sie ein weiteres kleines Fragment
extrahieren, wissen Sie, dass die Methode wieder ein wenig sauberer geworden ist.
Im Laufe der Zeit verstehen Sie die Reichweite der Methode und was Sie mir ihr
machen wollen, vielleicht besser.
Wenn ich kein Refactoring-Tool nutzen kann, extrahiere ich oft zuerst O-Count-
Methoden, nur um ein Gefühl für die übergreifende Struktur zu bekommen. Oft
ist dies eine gute Vorarbeit für die Tests und die weitere Arbeit.
Bei einer Aufzählungsmethode denken Sie vielleicht, dass Sie viele O-Count-
Methoden extrahieren könnten und dass jedes Code-Fragement in der Aufzäh-
lungsmethode geeignet sei. Manchmal finden Sie derartige Fragmente, aber oft
verwenden die Fragmente temporäre Variablen, die vor ihnen deklariert worden
sind. Manchmal müssen Sie die »Fragment-Struktur« einer Aufzählungsmethode
ignorieren und Methoden mit einem niedrigen Coupling Count in einzelnen Frag-
menten und Fragment-übergreifend suchen.
22.3.3 » G l e a n i n g D e p e n d e n c i e s «
Manchmal enthält eine Monster-Methode Code, der für den Hauptzweck der
Methode zweitrangig ist. Er ist erforderlich, aber nicht sehr komplex; und wenn
Sie ihn aus Versehen beschädigen, fällt dies sofort auf. Doch auch wenn dies alles
wahr ist, können Sie es einfach nicht riskieren, die Hauptlogik der Methode zu
beschädigen. In solchen Fällen können Sie eine Technik namens Gleaning Depen-
dencies (Dependencies zusammentragen) anwenden. Sie schreiben Tests für die
Logik, die Sie erhalten müssen. Danach extrahieren Sie Dinge, die von den Tests
nicht abgedeckt werden. Wenn Sie dies tun, können Sie wenigstens sicher sein,
das wichtige Verhalten zu bewahren. Hier ist ein einfaches Beispiel:
3H
22.3
Die Herausforderung des manuellen Refactorings
view.showUpdate(entry, view.GREEN);
}
ei se {
}
}
Wenn wir einen Fehler im Display-Code machen, sehen wir ihn ziemlich schnell.
Doch einen Fehler in der add-Logik zu finden, kann recht lange dauern. In einem
solchen Fall können wir Tests für die Methode schreiben und verifizieren, dass die
Addition unter den richtigen Bedingungen erfolgt. Wenn wir dann sicher sind,
dass das gesamte Verhalten abgedeckt ist, können wir den Display-Code mit der
Gewissheit extrahieren, dass unsere Extraktion die Addition von Eingaben nicht
beeinflusst.
Eine Alternative ist Break Out Method Object (25.2) (Methodenobjekt herauslösen).
Diese Technik wurde zum ersten Mal von Ward Cunningham beschrieben; sie ver-
körpert die Idee einer erfundenen Abstraktion. Wenn Sie ein Methodenobjekt her-
auslösen, erstellen Sie eine Klasse, deren einzige Aufgabe darin besteht, die Arbeit
Ihrer Monster-Methode zu erledigen. Die Parameter der Methode werden zu Para-
metern eines Konstruktors der neuen Klasse; und der Code der Monster-Methode
kann in eine Methode namens run oder execute der neuen Klasse verschoben
315
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
werden. Danach können wir refaktorisieren. Wir können die temporären Variab-
len in der Methode in Instanzvariablen umwandeln und zur Überwachung ver-
wenden, wenn wir die Methode zerlegen.
Ein Methodenobjekt herauszulösen, ist eine ziemlich drastische Maßnahme; aber
im Gegensatz zur Einführung einer Überwachungsvariablen gehören die Variab-
len, die Sie verwenden, zum Produktions-Code. Deshalb können Sie Tests entwi-
ckeln, die Sie behalten können. Ein ausführliches Beispiel finden Sie in Break Out
Method Object (25.2).
22.4 Strategie
Die Techniken, die ich in diesem Kapitel beschrieben habe, können Ihnen helfen,
Monster-Methoden für zusätzliches Refactoring oder das Hinzufügen von Funk-
tionen zu zerlegen. Dieser Abschnitt enthält einige Tipps, welche Faktoren Sie bei
dieser Arbeit gegeneinander abwägen müssen.
Wenn Sie die Bedingung und den Body in zwei verschiedene Methoden extrahie-
ren, können Sie die Logik der Methode später besser umbauen:
if (orderNeedsRecalculation(order)) {
recalculateOrder(order, rateCalculator);
}
Ich bezeichne dies als Skelettierung, weil die Methode danach nur ein Skelett ent-
hält: die Kontrollstruktur und Delegationen an andere Methoden.
316
22.4
Strategie
order. recalculateO;
}
Wenn Sie die Bedingung und den Body in dieselbe Methode extrahieren, können
Sie eine gemeinsame Sequenz von Operationen besser identifizieren:
recalculateOrder(order, rateCalculator);
Vielleicht ist der Rest der Methode nur eine Sequenz von Operationen, die nachei-
nander ausgeführt werden, was klarer wird, wenn wir diese Sequenz sehen kön-
nen.
Halt! Habe ich Ihnen gerade widersprüchliche Ratschläge gegeben? In der Tat. Tat-
sächlich wechsle ich oft zwischen Methoden skelettieren und Sequenzen suchen, je
nachdem welche Technik mir beim Refactoring helfen und/oder den Code klarer
machen könnte.
In Aufzählungsmethoden suche ich eher nach Sequenzen; bei tief verschachtelten
Methoden bemühe ich mich eher um eine Skelettierung. Doch letztlich hängt die
Strategie von Ihren Design-Einsichten beim Extrahieren ab.
317
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben
namens recal cul ate; deshalb sollten wir überlegen, was diese Neuberechnung
von der ursprünglichen unterscheidet, und diese Information in den Namen ein-
fügen oder wir sollten die vorhandene recal cul ate-Methode umbenennen. Doch
unabhängig davon sieht es so aus, als gehöre dieses Code-Fragment in diese
Klasse.
Obwohl die Versuchung groß ist, Code direkt in eine andere Klasse zu extrahieren,
sollten Sie es nicht tun. Verwenden Sie zunächst den umständlichen Namen. Der
Name recal cul ateOrder ist umständlich; aber so können wir Extraktionen leicht
rückgängig machen und untersuchen, ob wir das richtige Code-Fragment für
unsere weitere Arbeit extrahiert haben. Wir können die Methode später immer
noch in eine andere Klasse verschieben, wenn sich die beste Richtung für unsere
Änderungen selbst offenbart. Bis dahin machen wir durch das Extrahieren in die
gegenwärtige Klasse Fortschritte und sind weniger fehleranfällig.
318
Kapitel 23
Code ist ein seltsames Baumaterial. Die meisten Materialien, aus denen Dinge
hergestellt werden, wie etwa Metall, Holz, Stein oder Kunststoff, ermüden. Sie bre-
chen, wenn sie über längere Zeit verwendet werden. Code ist anders. Wenn Sie
ihn in Ruhe lassen, bricht er nie. Abgesehen von einer verirrten kosmischen Strah-
lung, die ein Bit auf Ihrem Speichermedium ändert, schleichen sich Fehler nur
ein, wenn jemand den Code bearbeitet. Wenn Sie eine Maschine aus Metall immer
wieder laufen lassen, geht sie irgendwann kaputt. Führen Sie denselben Code
immer wieder aus, läuft er einfach immer wieder.
Dies bürdet uns als Entwickler eine erhebliche Last auf. Wir dürfen erstens keine
Fehler verursachen und müssen zweitens besonders aufpassen, da dies ziemlich
leicht ist. Wie leicht kann man Code ändern? Mechanisch ist es unglaublich leicht.
Man braucht eine Quelldatei nur in einem Texteditor zu öffnen und kann den
schönsten Unsinn produzieren. Geben Sie ein Gedicht ein. Einige lassen sich
kompilieren. (Sie glauben das nicht? Besuchen Sie www.ioccc.org und schauen
Sie sich Details im The Obfiiscated C Code Contest an.) Doch Spaß beiseite; es ist
wirklich erstaunlich, wie leicht Software beschädigt werden kann. Jeder Program-
mierer hat schon einem mysteriösen Bug nachgespürt, nur um zu entdecken, dass
er irgendwo aus Versehen ein einzelnes Zeichen eingetippt hat. Vielleicht ist ein
Buchdeckel beim Aufklappen auf der Tastatur gelandet; vielleicht ist auch seine
Katze über die Tastatur gelaufen; seltsame Dinge passieren. Und Code ist ziemlich
fragiles Material.
In diesem Kapitel beschreibe ich mehrere Verfahren, mit denen Sie das Fehlerri-
siko beim Editieren verringern können. Einige sind mechanisch, andere psycholo-
gisch (autsch!); doch es ist wichtig, sie zu befolgen, insbesondere wenn Sie
Dependencies in Legacy Code aufheben wollen, um Tests einzurichten.
319
Kapitel 23
Wie erkenne ich, dass ich nichts kaputtmache?
An der Tastatur können wir jeden Tastenanschlag einer von zwei Kategorien
zuordnen: Entweder ändert er das Verhalten der Software oder nicht. Geben Sie
etwa Text in einen Kommentar ein, ändern Sie kein Verhalten. Geben Sie Text in
ein Stringliteral ein, hat das meistens Auswirkungen auf das Verhalten. Befindet
sich das Stringliteral in Code, der niemals aufgerufen wird, ändert sich kein Ver-
halten. Der Tastenanschlag, mit dem Sie später einen Aufruf einer Methode been-
den, die das Stringliteral verwendet, ändert Verhalten. Deshalb ist, technisch
gesehen, das Niederdrücken der Leertaste zwecks Formatierung Ihres Codes ein
Refactoring in einem sehr eingeschränkten Sinn. Manchmal ist auch das Eintip-
pen von Code Refactoring. Doch ein numerisches Literal in einem Ausdruck zu
ändern, der in Ihrem Code verwendet wird, ist kein Refactoring; es ist eine funktio-
nale Änderung; und es ist wichtig, dass Sie das beim Eintippen wissen.
Es ist ein zentraler Aspekt des Programmierens, genau zu wissen, was jeder Tas-
tenanschlag bewirkt. Dies bedeutet nicht, wir müssten allwissend sein, aber alles,
was uns hilft, zu wissen - wirklich zu wissen - , wie wir beim Tippen unsere Soft-
ware beeinflussen, kann uns helfen, die Anzahl unserer Fehler zu verringern. Test-
driven development (8.1) ist in dieser Hinsicht eine sehr leistungsstarke Methode.
Wenn Sie Ihren Code in einen Test-Harnisch einfügen und seine Tests in weniger
als einer Sekunde ausführen können, können Sie die Tests jederzeit bei Bedarf
ausführen, um unglaublich schnell und wirklich sicher zu erfahren, welche Aus-
wirkungen eine Änderung hat.
Falls es beim Erscheinen dieses Buches nicht bereits so weit ist, vermute ich,
dass demnächst jemand eine IDE entwickeln wird, mit der man einen Satz von
Tests spezifizieren kann, die bei jedem Tastenanschlag ausgeführt werden. Dies
wäre eine unglaubliche Methode, die Feedback-Schleife zu schließen.
Tests fördern das Hyperaware Editing. Dasselbe gilt für Pair Programming (siehe
später in diesem Kapitel). Wird man durch Hyperaware Editing erschöpft? Nun,
man wird von allem, was zu viel ist, erschöpft. Der Schlüssel liegt darin, dass die
Tätigkeit nicht frustriert. Hyperaware Editing ist ein Flow-Zustand, in dem man
einfach die Welt aussperren und mit erhöhter Aufmerksamkeit mit seinem Code
arbeiten kann. Der Zustand kann tatsächlich sehr erfrischend sein. Meine
Erschöpfung ist viel stärker, wenn ich kein Feedback erhalte. Dann bekomme ich
Angst, dass ich den Code unwissentlich beschädige. Ich bemühe mich, mir alle
Stellen zu merken, an denen ich Code geändert habe, um den Überblick zu behal-
320
23-2
»Single-Goal Editing«
ten, und denke darüber nach, wie ich mich später selbst davon überzeugen kann,
dass ich wirklich das Beabsichtigte getan habe.
Kennen Sie das: Sie beginnen, an einer Sache zu arbeiten, und denken dann:
»Hmm, vielleicht sollte ich das hier bereinigen«? Deshalb hören Sie auf, ein wenig
Code zu refaktorisieren, und fangen an, darüber nachzudenken, wie der Code
wirklich aussehen sollte; und dann halten Sie inne. Die Funktion, an der Sie gear-
beitet haben, muss immer noch erledigt werden; deshalb gehen Sie zurück zu der
ursprünglichen Stelle, an der Sie den Code bearbeiteten. Sie entscheiden, dass Sie
eine Methode aufrufen müssen, und springen dann zu dieser Methode. Dort ent-
decken Sie aber, dass die Methode noch etwas anderes tun sollte; deshalb beginnen
Sie, sie zu ändern, während die ursprüngliche Änderung auf ihre Fertigstellung
wartet und Ihr Team-Partner neben Ihnen tief durchatmet und schreit: »Ja, ja, ja!
Korrigiere das, und dann machen wir das andere.« Sie fühlen sich wie ein Renn-
pferd auf der Rennbahn, und Ihr Partner ist nicht wirklich eine Hilfe. Er treibt Sie
wie ein Jockey oder, schlechter, wie ein Spieler auf der Tribüne an.
Nun ja, so läuft's in einigen Teams. Ein Paar hat eine aufregende Programmiersit-
zung, aber in den letzten drei Vierteln korrigiert es den Code wieder, den es im ers-
321
Kapitel 23
Wie erkenne ich, dass ich nichts kaputtmache?
ten Viertel der Sitzung kaputtgemacht hat. Hört sich schrecklich an, nicht wahr?
Doch andererseits macht es manchmal richtig Spaß. Hinterher verlassen Sie die
Maschine mit Ihrem Partner wie Helden. Sie haben den Stier bei den Hörnern
gepackt und das Biest bezwungen. Sie sind der Größte.
Ich habe ein kleines Mantra, das ich mir beim Arbeiten vorsage: »Programmieren
ist die Kunst, nur eine Sache auf einmal zu tun.« Wenn ich mit einem Partner pro-
grammiere, bitte ich ihn immer, mich beim Wort zu nehmen und zu fragen: »Was
tust du«? Wenn ich in meiner Antwort mehr als eine Sache nenne, wählen wir
eine Sache aus. Umgekehrt stelle ich meinem Partner dieselbe Frage. Wir kom-
men einfach schneller voran. Beim Programmieren können Sie sich ziemlich
leicht zu viel auf einmal vornehmen. Dann probieren Sie letztlich nur dieses und
jenes aus, um den Code zum Laufen zu bringen, ohne ihn gezielt zu verändern
und wirklich zu wissen, was er tut.
322
23-3
»Preserve Signatures«
Im Allgemeinen versuchen wir, diese Arbeit mit Tests unter Kontrolle zu halten.
Wenn wir Tests eingerichtet haben, können wir viele Fehler entdecken, die wir bei
der Änderung von Code machen. Leider müssen wir viele Systeme zunächst ein
wenig ohne Tests refaktorisieren, um sie überhaupt so testfähig zu machen, dass
wir den Code weiter refaktorisieren können. Diese ersten Refactorings (die Techni-
ken zur Aufhebung von Dependencies in dem Katalog in Kapitel 25) sollen ohne
Tests durchgeführt werden und müssen deshalb besonders konservativ erfolgen.
Als ich anfing, diese Techniken einzusetzen, war die Versuchung groß, zu viel zu
tun. Wenn ich den gesamten Body einer Methode extrahieren musste, anstatt nur
die Argumente zu kopieren und einzufügen, wenn ich eine Methode deklarierte,
führte ich gleich auch andere Säuberungsarbeiten aus. Angenommen, ich musste
den Body der folgenden Methode extrahieren und sie statisch machen (Expose
Static Method (25.5J):
Dann extrahierte ich sie wie folgt und erstellte neben der Methode zusätzlich eine
Reihe von Hilfsklassen:
323
Kapitel 23
Wie erkenne ich, dass ich nichts kaputtmache?
In dem vorhergehenden Beispiel würde mein Code zuletzt wie folgt aussehen:
Die dazu erforderliche Bearbeitung der Argumente war sehr leicht und bestand
im Wesentlichen aus wenigen Schritten:
1. Ich kopierte die gesamte Argumente-Liste in meine Zwischenablage:
List orders,
int dailyTarget,
double interestRate,
int compensationPercent
3. Dann fügte ich den Inhalt der Zwischenablage in die neue Methodendeklaration
ein:
processOrdersO;
processOrders(List orders,
int dailyTarget,
double interestRate,
int compensationPercent);
324
2
3-4
»Lean on the Compiler«
6. Schließlich löschte ich die Typen und behielt nur die Namen der Argumente:
processOrders(orders,
dailyTarget,
interestRate,
compensationPercent);
Wenn Sie diese Schritte immer wieder ausführen, laufen sie schließlich automa-
tisch ab, und Sie können Ihren Änderungen mehr trauen. Sie können sich dann
auf andere Probleme konzentrieren, die Fehler verursachen können, wenn Sie
Dependencies aufheben, etwa ob Ihre neue Methode eine gleichnamige Methode
mit derselben Signatur in einer Basisklasse verbirgt.
double domestic_exchange_rate;
double foreign_exchange_rate;
Mehrere Methoden in derselben Datei greifen auf diese Variablen zu. Nun suche
ich eine Möglichkeit, die Variablen beim Testen zu ändern, und setze deshalb die
Technik Encapsulate Global References (25.4) ein.
Zu diesem Zweck schreibe ich eine Klasse, die die Deklarationen einhüllt, und
deklariere eine Variable dieser Klasse.
class Exchange
{
public:
double domestic_exchange_rate;
double foreign_exchange_rate;
325
Kapitel 23
Wie erkenne ich, dass ich nichts kaputtmache?
};
Exchange exchange;
Jetzt kompiliere ich die Klasse, um alle Stellen zu finden, an denen der Compiler
domesti c_exchange_rate und foreign_exchange_rate nicht findet, und
ändere den Code so, dass der Zugriff über das exchange-Objekt erfolgt. Hier ist
ein Beispiel, wie der Code vor und nach dieser Änderung aussieht:
Daraus wird:
Die zentrale Idee dieser Technik liegt darin, sich vom Compiler zu den Stellen füh-
ren zu lassen, an denen Sie den Code ändern müssen. Dies bedeutet nicht, dass
Sie nicht mehr darüber nachdenken sollten, was Sie ändern müssen, sondern nur,
dass Sie in einigen Fällen die Laufarbeit (das Suchen) dem Compiler überlassen.
Sie müssen sehr genau wissen, was der Compiler finden soll und was nicht, damit
Sie sich nicht in falscher Sicherheit wiegen.
Sie können mit dieser Technik auch die Struktur Ihres Programms ändern, wie
das Beispiel in Encapsulate Global References (25.4) zeigt. Sie können damit auch
Typ-Änderungen durchführen. Ein häufiger Anwendungsfall ist die Änderung des
Typs einer Variablendeklaration von einer Klasse in eine Schnittstelle und die Ver-
wendung der Fehler, um die Methoden zu ermitteln, die in der Schnittstelle ent-
halten sein müssen.
Lean on the Compiler ist nicht immer praktikabel. Wenn Ihre Builds lange dauern,
könnte es sinnvoller sein, die Stellen zu suchen, an denen Sie Änderungen vor-
nehmen müssen. In Kapitel 7, Änderungen brauchen eine Ewigkeit, werden Maß-
nahmen beschrieben, wie Sie dieses Problem lösen können. Aber wenn Sie Lean
on the Compiler einsetzen, ist es eine nützliche Technik. Doch Vorsicht: Sie können
subtile Bugs einführen, wenn Sie die Technik blind einsetzen.
Die Vererbung ist das Sprachkonstrukt mit den meisten Fehlerrisiken. Hier ist ein
Beispiel:
326
23-4
»Lean on the Compiler«
public int g e t X O {
return x;
}
Wir suchen alle Stellen, an denen diese Methode verwendet wird, damit wir sie
auskommentieren können:
/*
public int g e t X O {
return x;
} */
327
Kapitel 24
Mit Legacy Code zu arbeiten, ist zweifellos schwierig. Obwohl jede Situation
anders ist, gibt es eines, was die Aufgabe für Sie als Programmierer lohnenswert
macht oder nicht, nämlich was Sie davon haben. Für einige Menschen ist es die
finanzielle Gegenleistung, und das ist in Ordnung - wir müssen alle unseren
Lebensunterhalt verdienen. Aber es sollte wirklich noch andere Gründe geben,
warum Sie programmieren.
Wenn Sie Glück hatten, haben Sie diesen Beruf aus Spaß am Programmieren
gewählt. Sie hatten Ihren ersten Computer bekommen und waren von den vielfäl-
tigen Möglichkeiten begeistert: all die coolen Dinge, die Sie tun konnten, indem
Sie einen Computer programmierten. Es gab etwas zu lernen und zu meistern,
und Sie dachten: »Toll, das macht Spaß. Ich kann Karriere machen, wenn ich
darin sehr gut werde.«
Nicht jeder wird so Programmierer; doch auch Menschen, die aus anderen Moti-
ven diesen Beruf gewählt haben, können herausfinden, was beim Programmieren
Spaß macht. Wenn Sie dies können - und wenn es auch einige Ihrer Kollegen
können - , spielt es nicht wirklich eine Rolle, an welcher Art von System Sie arbei-
ten. Sie können hübsche Dinge damit tun. Die Alternative ist einfach nur Trüb-
sinn. Die Arbeit macht keinen Spaß; und offen gestanden, verdienen wir alle
etwas Besseres.
Oft wünschen sich Menschen, die ihre Zeit mit der Arbeit an Legacy-Systemen
verbringen, sie könnten an Green-Field-Systemen (von Grund auf neu erstellte
Systeme) arbeiten. Es macht Spaß, Systeme von Grund auf zu erstellen. Doch
offen gesagt: Green-Field-Systeme haben eigene Probleme. Immer wieder habe
ich das folgende Szenarium erlebt: Ein vorhandenes System wird im Laufe der
Zeit immer unübersichtlicher und sperriger. Mitarbeiter des Unternehmens
sind frustriert, dass Änderungen des Systems immer länger dauern. Die besten
Mitarbeiter (und manchmal die Störenfriede!) werden einem neuen Team zuge-
wiesen, das die Aufgabe bekommt, »ein Ersatzsystem mit einer besseren Archi-
tektur« zu entwickeln. Am Anfang läuft alles prima. Sie kennen die Probleme
mit der alten Architektur und verbringen einige Zeit mit der Entwicklung eines
neuen Designs. Inzwischen arbeiten die anderen Entwickler weiter an dem alten
System. Da dieses System produktiv eingesetzt wird, werden alle Anforderungen
329
Kapitel 24
Wir fühlen uns überwältigt. Es wird nicht besser.
Tatsächlich ist das Gras bei der Green-Field-Entwicklung nicht viel grüner.
Der Schlüssel zum Erfolg bei der Arbeit mit Legacy Code liegt darin, die eigenen
Motive zu entdecken. Obwohl viele Programmierer Einzelgänger sind, kann kaum
etwas in einer guten Umgebung die Zusammenarbeit mit Menschen ersetzen, die
man respektiert und die Spaß an ihrer Arbeit haben. Ich habe einige meiner bes-
ten Freunde bei der Arbeit kennen gelernt; sie sind bis heute die Menschen, mit
denen ich rede, wenn ich etwas Neues gelernt habe oder Spaß beim Programmie-
ren hatte.
Auch der Kontakt mit der größeren Gemeinschaft ist hilfreich. Heute können Sie
so leicht wie nie zuvor Kontakt mit anderen Programmierern aufnehmen, um
Erfahrungen und Erkenntnisse auszutauschen. Über Mailing-Lists, Konferenzbe-
suche und andere Ressourcen im Internet können Sie Netze knüpfen, Strategien
und Techniken austauschen und sich ganz allgemein über die Software-Entwick-
lung auf dem Laufenden halten.
Auch wenn die Mitarbeiter an einem Projekt ihre Arbeit lieben und ein System
wirklich verbessern wollen, kann eine andere Form des Trübsinns aufkommen.
Manchmal werden Mitarbeiter durch die schiere Größe der Code-Basis entmutigt.
Sie könnten mit Ihrem Team zehn Jahre daran arbeiten und dennoch nicht mehr
als zehn Prozent verbessern. Ist das kein Grund, deprimiert zu sein? Nun, ich
habe Teams kennen gelernt, die Legacy Code im Umfange von mehreren Millio-
nen Zeilen betreuten und die jeden Tag als Herausforderung und als Chance
betrachteten, die Dinge zu verbessern und Spaß zu haben. Ich habe auch Teams
mit einer weit besseren Code-Basis erlebt, die trübsinnig waren. Die Einstellung,
mit der wir unsere Arbeit angehen, ist wichtig.
Entwickeln Sie einige kleinere Programme mit TDD außerhalb der Arbeit. Pro-
grammieren Sie ein wenig zum Spaß. Spüren Sie dem Unterschied zwischen
ihren kleinen Projekten und dem großen Projekt bei der Arbeit nach. Es ist gut
möglich, dass Ihr Projekt bei der Arbeit Ihnen dasselbe Gefühl vermitteln kann,
33°
Wir fühlen uns überwältigt. Es wird nicht besser.
wenn Sie die Teile, mit denen Sie arbeiten, in einem schnellen Testharnisch aus-
führen können.
Wenn die Arbeitsmoral in Ihrem Team wegen der schlechten Qualität des Codes
niedrig ist, können Sie Folgendes ausprobieren: Greifen Sie die hässlichsten und
widerspenstigsten Klassen des Projekts heraus und bringen Sie sie unter Testkon-
trolle. Wenn Sie das schlimmste Problem als Team bewältigt haben, bekommen
Sie das Gefühl, Ihre Situation im Griff zu haben. Ich habe dies immer wieder
erlebt.
Wenn Sie anfangen, die Kontrolle über Ihre Code-Basis zu übernehmen, fangen
Sie an, Oasen mit gutem Code zu entwickeln. In ihnen zu arbeiten, kann wirklich
ein Vergnügen sein.
Teil III
Techniken zur
Aufhebung von
Dependencies
In diesem Teil:
Kapitel 25
Techniken zur Aufhebung von Dependencies 335
333
Kapitel 25
In diesem Kapitel habe ich einen Satz Techniken zur Aufhebung von Dependen-
cies zusammengestellt. Diese Liste ist nicht erschöpfend, sondern enthält nur
einige Techniken, die ich mit Teams verwendet habe, um Klassen so weit zu ent-
koppeln, dass sie unter Testkontrolle gestellt werden konnten. Technisch handelt
es sich bei diesen Techniken um Refactorings; denn jede Technik bewahrt Verhal-
ten. Aber im Gegensatz zu den meisten bis jetzt in der Branche beschriebenen
Refactorings sollen diese Refactorings ohne Tests ausgeführt werden, um Tests
einzuführen. Wenn Sie die Schritte sorgfältig ausführen, ist die Gefahr von Feh-
lern in den meisten Fällen gering. Bevor Sie diese Refactorings benutzen, sollten
Sie Kapitel 23, Wie erkenne ich, dass ich nichts kaputtmache?, lesen.
Die Tipps in diesem Kapitel können Ihnen helfen, Tests mit diesen Techniken
sicher einzurichten. Wenn Sie sie beachten, sollten Sie tiefgreifendere Änderun-
gen mit einer größeren Gewissheit vornehmen können, dass Sie nichts kaputtma-
chen.
Durch diese Techniken wird Ihr Design nicht sofort besser. Tatsächlich werden
Sie, wenn Sie ein Gefühl für gutes Design entwickelt haben, bei einigen dieser
Techniken zurückzucken. Diese Techniken können Ihnen helfen, Methoden, Klas-
sen und Gruppen von Klassen unter Testkontrolle zu bringen, und so Ihr System
wartbarer machen. Dann können Sie das Design mit Test-unterstützten Refacto-
rings verbessern.
Einige der Refactorings in diesem Kapitel wurden von Martin Fowler in seinem
Buch Refactoring: Improving the Design of Existing Code (Addison-Wesley, 1999)
beschrieben. Ich habe die Schritte umgestellt und sie so zugeschnitten, dass sie
sicher ohne Tests eingesetzt werden können.
335
Kapitel 25
Techniken zur Aufhebung von Dependencies
muss ich die Auswirkung der Methode auf den Parameter testen. In vielen Fällen
werden Probleme durch die Klasse des Parameters erschwert. Wenn ich die Klasse
ändern kann, kann ich die Dependency mit Extract Interface (25.10) aufheben.
Extract Interface ist oft die beste Wahl, um Parameter-Dependencies aufzuheben.
Im Allgemeinen wollen wir Dependencies, die das Testen verhindern, mit einer
einfachen Maßnahme aufheben, die keine Fehlermöglichkeiten einführt. Doch in
einigen Fällen funktioniert Extract Interface (25.10) nicht sehr gut. Wenn der Typ
des Parameters sehr systemnah ist oder von einer bestimmten Implementierung
abhängt, kann das Extrahieren einer Schnittstelle die Aufgabe erschweren oder
unmöglich sein.
Verwenden Sie Adapt Parameter, wenn Sie Extract Interface (25.10) nicht auf die
Klasse eines Parameters anwenden können oder wenn es schwierig ist, einen
Parameter zu simulieren.
In dieser Klasse akzeptiert die popul ate-Methode eine HttpServl etRequest als
Parameter. HttpServl etRequest ist eine Schnittstelle aus dem J2EE-Standard für
Java von Sun. Wollten wir popul ate in der jetzigen Form testen, müssten wir eine
Klasse erstellen, die HttpServl etRequest implementiert und eine Möglichkeit
zur Verfügung stellt, sie mit den Parameterwerten zu füllen, die sie beim Testen
zurückgeben muss. Laut Java-SDK-Dokumentation enthält HttpServl etRequest
über zwanzig Methodendeklarationen; dabei sind die Deklarationen ihrer überge-
ordneten Schnittstelle nicht mitgezählt. Diese müssten wir alle implementieren.
Es wäre großartig, mit Extract Interface (25.10) eine weniger umfangreiche Schnitt-
stelle zu erstellen, die nur die benötigten Methoden enthielte; aber wir können
keine Schnittstelle aus einer anderen Schnittstelle extrahieren. In Java müsste
HttpServl etRequest von der Klasse abgeleitet werden, die wir extrahieren, und
wir dürfen eine Standard-Schnittstelle nicht auf diese Weise modifizieren. Glück-
licherweise stehen uns andere Optionen zur Verfügung.
336
25.1
»Adapt Parameter«
Wenn ich Dependencies aufhebe, versuche ich immer, mir das wahrscheinliche
Ergebnis bereits vorher vorzustellen. Dann kann ich entscheiden, ob ich mit den
Konsequenzen leben kann. In diesem Fall wird unser Produktions-Code ziemlich
ähnlich aussehen, und wir werden erheblichen Aufwand treiben müssen, um
HttpServl etRequest, eine API-Schnittstelle, zu verwalten. Gibt es eine Methode,
mit der wir das Aussehen des Codes verbessern und die Aufhebung der Depen-
dency vereinfachen können? Tatsächlich gibt es eine solche Methode. Wir können
die eingehenden Parameter einhüllen und unsere Dependency von der API-
Schnittstelle vollkommen aufheben. Danach wird der Code wie folgt aussehen:
Was haben wir hier gemacht? Wir haben eine neue Schnittstelle namens Parame-
terSource eingeführt. Sie enthält an diesem Punkt nur eine einzige Methode
namens getParameterForName. Im Gegensatz zu der getParmeterValue-
Methode von HttpServl etRequest gibt getParameterForName nur den ersten
Parameter zurück, weil wir uns in diesem Kontext nur für den ersten Parameter
interessieren.
Hier ist eine Fake-Klasse, die Parameter Source implementiert. Wir können sie in
unserem Test verwenden:
337
Kapitel 25
Techniken zur Aufhebung von Dependencies
Oberflächlich sieht dies vielleicht so aus, als machten wir Dinge der Schönheit hal-
ber schöner; aber Legacy-Code-Basen haben generell das Problem, dass es oft
keine Abstraktionsschichten gibt. Der wichtigste Code in einem System ist oft mit
systemnahen API-Aufrufen vermischt. Wir haben bereits gesehen, wie dadurch
das Testen schwierig werden kann, aber die Probleme gehen über das Testen hin-
aus. Es ist schwieriger, Code zu verstehen, der mit breiten Schnittstellen mit Dut-
zenden ungenutzter Methoden übersät ist. Enge Abstraktionen, die genau auf Ihre
Anforderungen zugeschnitten sind, kommunizieren klarer und bieten Ihnen eine
bessere Naht.
Adapt Parameter ist ein Fall, in dem wir Preserve Signatures (23.1) nicht anwen-
den. Gehen Sie besonders vorsichtig vor.
Adapt Parameter kann riskant sein, wenn die vereinfachte Schnittstelle, die Sie für
die Parameter-Klasse erstellen, zu stark von der gegenwärtigen Schnittstelle des
Parameters abweicht. Wenn wir bei diesen Änderungen nicht aufpassen, könnten
wir subtile Bugs einführen. Wie immer wollen wir Dependencies so weit aufhe-
ben, dass wir Tests einrichten können. Sie sollten Änderungen bevorzugen, bei
denen Sie sich sicher fühlen, und weniger Änderungen anstreben, die die beste
Struktur ergeben. Dies hat Zeit, bis die Tests vorbei sind. So sollten wir etwa in die-
sem Fall ParameterSource so ändern, dass Clients nicht auf null prüfen müssen,
wenn sie Methoden der Klasse aufrufen (Details finden Sie in Null Object Pattern
(9-1))-
338
25-2
»Break Out Method Object«
Sicherheit hat Vorrang. Nachdem Sie Tests eingerichtet haben, können Sie tief-
greifende Änderungen viel sicherer ausführen.
25.1.1 Schritte
Adapt Parameter funktioniert wie folgt:
1. Erstellen Sie die neue Schnittstelle, die Sie in der Methode benutzen werden.
Machen Sie sie so einfach und ausdrucksstark wie möglich, versuchen Sie aber
nicht, eine Schnittstelle zu erstellen, die mehr als triviale Änderungen in der
Methode erfordert.
2. Erstellen Sie einen Produktions-Implementer für die neue Schnittstelle.
3. Erstellen Sie einen Fake-Implementer für die Schnittstelle.
4. Schreiben Sie einen einfachen Testfall, in dem Sie den Fake an die Methode
übergeben.
5. Machen Sie die erforderlichen Änderungen in der Methode, um den neuen
Parameter zu verwenden.
6. Führen Sie Ihren Test aus, um zu verifizieren, dass Sie die Methode mit dem
Fake testen können.
Hier ist ein Beispiel in C++ (umfangreiche Abschnitte der Klasse und der Methode
wurden entfernt, um Bäume zu retten):
339
Kapitel 25
Techniken zur Aufhebung von Dependencies
class GDIBrush
{
public:
void draw(vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection);
private:
void drawPoint(int x, int y, COLOR color);
};
void GDIBrush::draw(vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
{
for(vector<points>::iterator it = renderingRoots.begin() ;
it != renderingRoots.endC);
++it) {
point p = *it;
Zuerst erstellen wir eine neue Klasse für die Zeichenarbeit. Wir können sie Ren-
derer nennen. Dann fügen wir einen publ i c Konstruktor in die Klasse ein. Die
Argumente des Konstruktors sollten eine Referenz der ursprünglichen Klasse
sowie die Argumente der ursprünglichen Methode umfassen. Für Letzteres brau-
chen wir Preserve Signatures (23.1).
class Renderer
{
public:
Renderer(GBIBrush -brush,
vector<point>& renderingRoots,
ColorMatrix Scolors,
vector<point>& selection);
};
Nachdem wir den Konstruktor erstellt haben, fügen wir für die Konstruktorargu-
mente Instanzvariablen hinzu und initialisieren sie. Auch hier wenden wir Preserve
Signatures (23.1) an, indem wir mehrere Cut/Copy/Paste-Operationen ausführen.
340
25-2
»Break Out Method Object«
class Renderer
{
private:
GDIBrush -brush;
vector<point>& renderingRoots;
ColorMatrixS colors;
vector<point>& selection;
publi c:
Renderer(CDIBrush *brush,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: brush(brush), renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}
};
Was hat sich damit an unserer Position grundlegend geändert? Wir nehmen eine
Referenz eines GDIBrush entgegen, und wir können solche Objekte nicht in unse-
rem Test-Harnisch instanziieren. Was also haben wir gewonnen? Doch wir sind
noch nicht fertig.
Nachdem wir den Konstruktor erstellt haben, können wir in die Klasse eine wei-
tere Methode einfügen, die die Arbeit der ursprünglichen draw()-Methode leistet.
Nennen wir sie ebenfalls draw().
class Renderer
{
private:
GDIBrush *brush;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;
public:
Renderer(GDIBrush *brush,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: brush(brush), renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}
void draw();
};
Jetzt kopieren wir den Body der ursprünglichen draw()-Methode in die draw()-
Methode von Renderer und schauen per Lean on the Compiler (23.4) das Ergebnis
an.
341
Kapitel 25
Techniken zur Aufhebung von Dependencies
void Renderer::draw()
{
for(vector<points>::iterator it = renderingRoots.begin() ;
it != renderingRoots.end();
++it) {
point p = *i t;
Falls draw() von Renderer Referenzen von Instanzvariablen oder Methoden von
GDIBrush enthält, wird der Code nicht kompiliert. Um die Fehler zu beseitigen,
können wir Getter für die Variablen erstellen und die Methoden, von denen die
Klasse abhängt, als public deklarieren. In diesem Fall gibt es nur eine Depen-
dency, eine private Methode namens drawPoint. Nachdem wir sie in GDIBrush
als publ i c deklariert haben, können wir von einer Referenz der Renderer-Klasse
auf sie zugreifen. Der Code wird kompiliert.
Jetzt können wir von dem neuen Renderer aus die draw-Methode von GDIBrush
delegieren.
class PointRenderer
{
public:
Virtual void drawPoi nt(i nt x, int y, COLOR color) = 0;
};
class GDIBrush : public PointRenderer
342
25-2
»Break Out Method Object«
{
public:
void drawPoint(int x, int y, COLOR color);
};
class Renderer
{
private:
PointRender -'pointRenderer;
vector<point>& renderingRoots;
ColorMatrix& colors;
vector<point>& selection;
public:
RendererC PointRenderer -renderer,
vector<point>& renderingRoots,
ColorMatrix& colors,
vector<point>& selection)
: pointRenderer(pointRenderer),
renderingRoots(renderingRoots),
colors(colors), selection(selection)
{}
void draw();
};
void Renderer::draw()
{
for(vector<points>::iterator it = renderingRoots.beginO;
it != renderingRoots.end();
++it) {
point p = ''it;
pointRenderer->drawPoi nt(p.x,p.y,colors[n]) ;
}
}
Abbildung 25.1 zeigt das Design in UML.
Unser Ergebnis ist ein wenig seltsam. Wir haben eine Klasse (GDIBrush), die eine
neue Schnittstelle (PointRenderer) implementiert; und diese Schnittstelle wird
nur von einem Objekt (einem Renderer) benutzt, der von der Klasse erstellt wird.
Vielleicht haben Sie ein ungutes Gefühl, weil wir Details, die vorher in der
ursprünglichen Klasse privat waren, öffentlich gemacht haben, damit wir diese
Technik verwenden können. Jetzt ist die drawPoi nt-Methode, die in GDIBrush
privat war, für jeden zugänglich. Doch Sie sollen bedenken, dass dies nicht wirk-
lich das Ende ist.
Im Laufe der Zeit wird es lästig, die ursprüngliche Klasse nicht in einem Test-Har-
nisch instanziieren zu können, und Sie werden Dependencies so aufheben, dass
343
Kapitel 25
Techniken zur Aufhebung von Dependencies
dies möglich sein wird. Dann werden Sie sich andere Optionen anschauen. Muss
etwa Poi ntRenderer eine Schnittstelle sein? Kann es eine Klasse sein, die ein
GDIBrush enthält? Wenn ja, können Sie das System vielleicht allmählich in ein
Design überführen, das auf diesem neuen Renderer-Konzept basiert.
«Interface»
PointRenderer
+ drawPoint(x : int, y : int, color :COLOR)
1
1
1
1
1
1
1
1
GDIBrush Renderer
+ draw(renderingRoots : vector<point>&, + Renderer(PointRenderer 'renderer,
colors : ColorMatrix&, renderingRoots : vector<point>&,
selection : vector<point>&) colors : ColorMatrix&,
+ drawPointfx : int. Y : int, color: COLOR) selection : vector<point>&)
+ drawQ
Dies ist eins der einfachen Refactorings, mit denen wir die Klasse möglicherweise
unter Testkontrolle bringen können; die resultierende Struktur könnte viele wei-
tere einladen.
Break Out Method Object hat mehrere Varianten. Im einfachsten Fall verwendet
die ursprüngliche Methode keine Instanzvariablen oder Methoden der
ursprünglichen Klasse. Wir müssen ihre keine Referenz der ursprünglichen
Klasse übergeben. In anderen Fällen verwendet die Methode nur Daten der
ursprünglichen Klasse. Gelegentlich ist es sinnvoll, diese Daten in eine neue
Klasse zur Datenspeicherung zu überführen und dies als Argument an das
Methodenobjekt zu übergeben.
Der in diesem Abschnitt gezeigte Fall ist der schlimmste; wir müssen Methoden
der ursprünglichen Klasse verwenden; deshalb erstellen wir mit Extract Interface
(25.10) eine Abstraktion zwischen dem Methodenobjekt und der ursprünglichen
Klasse.
25.2.1 Schritte
Break out Method Object funktioniert sicher ohne Tests wie folgt:
344
25-3
»Definition Completion«
345
Kapitel 25
Techniken zur Aufhebung von Dependencies
private:
CArray<ROOTID, ROOTID& > rootids;
};
Dies ist die Deklaration einer kleinen Klasse einer C++-Anwendung. Nutzer erstel-
len CLateBindingDispatchDrivers und verknüpfen dann mit der BindName-
Methode Namen mit IDs. Wenn wir diese Klasse in einem Test verwenden, wollen
wir eine andere Methode für die Bindung von Namen zur Verfügung stellen. In
C++ können wir zu diesem Zweck Definition Completion (Definitionen vervollstän-
digen) verwenden. Die Bi ndName-Methode wird in der Header-Datei der Klasse
deklariert. Wie können wir in einem Test eine andere Definition verwenden? Wir
können die Header-Datei mit der Deklaration dieser Klasse in die Testdatei einbin-
den und vor unseren Tests alternative Definitionen der Methoden zur Verfügung
stellen.
CLateBindingDispatchDriver::CLateBindingDispatchDriver() {}
CLateBindingDispatchDriver::~CLateBindingDispatchDriver() {}
TEST(AddOrder,BOMTreeCt rl)
{
CLateBindingDispatchDriver driver; CBOMTreeCtrl ctrl (&dri ver);
ctrl.AddOrder(COrderFactory::makeDefault()); L0NGS_EQUAL(1,
ctrl .OrderCountQ);
}
Methoden, die direkt in der Testdatei definiert sind, werden beim Testen verwen-
det. Methoden, die für uns keine Rolle spielen, erhalten einen leeren Body. Wir
können auch Überwachungsmethoden einfügen, die wir in allen unseren Tests
verwenden können.
Wenden wir Definition Completion in C oder C++ an, müssen wir ein separates Exe-
cutable (ausführbare Datei) der Tests erstellen, die unsere alternativen Definitio-
nen verwenden. Andernfalls gibt es beim Linken Konflikte mit den echten
Definitionen. Ein weiterer Nachteil liegt darin, dass wir jetzt über zwei verschie-
dene Sätze von Definitionen der Methoden einer Klasse verfügen, einen in einer
Quelldatei für die Tests und einen anderen in einer Quelldatei mit dem Produk-
tionscode. Dies kann die Wartung erschweren. Es kann auch Debugger verwirren,
wenn wir die Umgebung nicht korrekt einrichten. Im Allgemeinen empfehle ich
346
2
5-4
»Encapsulate Global References«
25.3.1 Schritte
Definition Completion funktioniert in C++ wie folgt:
1. Identifizieren Sie eine Klasse mit Definitionen, die Sie ersetzen möchten.
2. Verifizieren Sie, dass sich die Methodendefinitionen in einer Quelldatei und
nicht in einer Header-Datei befinden. Falls sie in einer Header-Datei stehen, ver-
schieben Sie sie in die Quelldatei.
3. Binden Sie die Header-Datei in die Test-Quelldatei der zu testenden Klasse ein.
4. Verifizieren Sie, dass die Quelldateien der Klasse nicht Teil des Builds sind.
5. Kompilieren Sie den Code, fehlende Methoden zu finden.
6. Fügen Sie Methodendefinitionen in die Test-Quelldatei ein, bis der Code voll-
ständig kompiliert wird.
bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG230_SIZE];
void AGGController::suspend_frame()
{
frame_copy(AGG230_suspendedframe, AGG230_activeframe);
clear(AGG230_activeframe);
flush_frame_buffers();
}
void AGGController::flush_frame_buffers()
{
for (int n = 0; n < AGG230_SIZE; ++n) {
AGG230_activeframe[n] = false;
AGG230_suspendedframe[n] = false;
}
}
347
Kapitel 25
Techniken zur Aufhebung von Dependencies
Der Code im folgenden Beispiel arbeitet mit einigen globalen Arrays. Die
suspend_f rame-Methode muss auf ein aktives und ein gesperrtes Frame zugrei-
fen. Auf den ersten Blick sieht es so aus, als könnten Sie die Frames zu Mitglie-
dern der AGGControll er-Klasse machen, aber einige andere (nicht gezeigte)
Belassen verwenden die Frames. Was können wir tun?
Wir könnten die Frames mit Parameterize Method (25.15) etwa als Parameter an die
suspend_f rame-Methode übergeben; aber dann müssten wir sie an alle Methoden
als Parameter übergeben, die von suspend_frame aufgerufen werden und sie
benötigen, wie etwa f 1 ush_f rame_buffer in diesem Beispiel.
Werden mehrere Globals immer an den gleichen Stellen verwendet oder modifi-
ziert, gehören sie in dieselbe Klasse.
Am besten untersuchen wir die Daten und das aktive und gesperrte Frame und
überlegen, ob wir einen guten Namen für eine neue Klasse für beide Frames fin-
den. Dies ist nicht immer leicht. Wir müssen überlegen, wozu die Daten in dem
Design verwendet werden. Wir werden sicher auch Methoden in die neue Klasse
einfügen; und es ist gut möglich, dass der Code für diese Methoden bereits an
anderen Stellen existiert, an denen die Daten verwendet werden.
Wenn Sie eine Klasse benennen, denken Sie an die Methoden, die sie enthalten
wird. Der Name sollte gut sein, muss aber nicht perfekt sein. Sie können die
Klasse später immer noch umbenennen.
348
25-4
»Encapsulate Global References«
Vielleicht wird der von Ihnen gewählte Klassenname bereits verwendet. Dann
sollten Sie prüfen, ob Sie die andere, gleichnamige Komponente umbenennen
können.
class Frame
{
public:
// AGG230_SIZE als Konstante deklarieren
enum { AGG230_SIZE = 256 };
bool AGG230_activeframe[AGG230_SIZE];
bool AGG230_suspendedframe[AGG23CLSIZE];
};
Wir haben die Namen der Daten absichtlich nicht geändert, um den nächsten
Schritt zu vereinfachen. Als Nächstes deklarieren wir eine globale Instanz der
Frame-Klasse:
Frame frameForAGG230;
Als Nächstes kommentieren wir die ursprünglichen Deklarationen der Daten aus
und versuchen einen Build (kompilieren den Code):
// bool AGG230_activeframe[AGG230_SIZE];
// bool AGG230_suspendedframe[AGG230_SIZE];
Um diese Fehler zu beseitigen, gehen wir alle nacheinander durch und fügen
f rameForAGG230. vor jeder Referenz ein, die Probleme macht.
void AGGController::suspend_frameO
{
frame_copy(frameForAGG230.AGG230_suspendedframe,
frameForAGG2 30.AGG2 30_acti veframe);
clear( frameForAGG20.AGG230_activeframe);
flush_frame_buffer();
}
Danach ist unser Code hässlicher, wird aber kompiliert und funktioniert korrekt.
Diese Transformation hat also das Verhalten bewahrt. Jetzt können wir ein Frame-
349
Kapitel 25
Techniken zur Aufhebung von Dependencies
Objekt an den Konstruktor der AGGControl 1 er-Klasse übergeben und die Tren-
nung realisieren, die wir für unsere weitere Arbeit brauchen.
Ein Member (Mitglied) einer Klasse anstelle einer einfachen globalen Variablen
zu referenzieren, ist nur der erste Schritt. Danach könnten Sie Introduce Static
Setter (25.12) einsetzen oder den Code mit Parameterize Constructor (25.14) oder
Parameterize Method (25.15) parametrisieren.
Wir haben also eine neue Klasse eingeführt, indem wir globale Variablen in eine
neue Klasse eingefügt und sie publ i c gemacht haben. Warum sind wir so vorge-
gangen? Schließlich haben wir doch einige Zeit über den Namen der neuen Klasse
und ihre Methoden nachgedacht. Wir hätten auch mit der Erstellung eines Fake-
Frame-Objekts beginnen können, an das wir in AGG_Controll er hätten delegie-
ren können; und wir hätten die gesamte Logik, die diese Variablen verwendet, in
eine echte Frame-Klasse verschieben können. Wir hätten dies tun können, aber
damit hätten wir sehr viel auf einmal versucht. Noch schlimmer: Wenn wir keine
Tests haben und sie mit einem Minimum an Arbeit einrichten wollen, ist es am
besten, die Logik so weit wie möglich in Ruhe zu lassen. Wir sollten vermeiden, sie
zu verschieben, und uns bemühen, die Trennung durch Einfügen von Nahtstellen
(Seams) herbeizuführen, die es uns ermöglichen, eine Methode anstelle einer
anderen aufzurufen oder auf bestimmte Daten anstelle von anderen zuzugreifen.
Wenn wir mehr Tests eingerichtet haben, können wir später ungestraft das Verhal-
ten aus einer Klasse in eine andere verschieben.
Wenn wir das Frame an AGGControl 1 er übergeben haben, können wir durch
Umbenennung einiger Komponenten mehr Klarheit schaffen. Nach diesem Re-
factoring sieht unser Code wie folgt aus:
class Frame
{
publi c:
enum { BUFFER_SIZE = 256 };
bool activebuffer[BUFFER_SIZE];
bool suspendedbuffer[BUFFER_SIZE];
};
Frame frameForACC230;
void AGGControl!er::suspend_frame()
{
frame_copy(frame.suspendedbuffer, frame.activebuffer);
clear(frame.activeframe);
flush_frame_buffer();
}
42
25-4
»Encapsulate Global References«
Die Verbesserung scheint nicht groß zu sein, aber sie ist ein außerordentlich wert-
voller erster Schritt. Nachdem wir die Daten in eine Klasse verschoben haben,
haben wir eine Trennung herbeigeführt und können den Code im Laufe der Zeit
erheblich verbessern. Vielleicht führen wir irgendwann sogar eine FrameBuffer-
Klasse ein.
Bei Encapsulate Global References sollten Sie mit Daten oder kleinen Methoden
anfangen. Umfangreichere Methoden können in die neue Belasse verschoben
werden, wenn mehr Tests eingerichtet sind.
Ein Code-Fragment, das wir unter Testkontrolle bringen wollen, enthält Aufrufe
von zwei Funktionen: GetOption(const string optionName) und SetOp-
tion(string name, Option option). Die Funktionen sind ungebunden, das
heißt, sie gehören keiner Klasse an, werden aber an vielen Stellen im Code ver-
wendet. Ein Beispiel:
void ColumnModel::update()
{
alignRowsO;
Option resizeWidth = : :GetOption("ResizeWidth") ;
if (resizeWidth.isTrueO) {
resizeO;
} eise {
resizeToDefaultO;
}
}
In solchen Fällen könnten wir auf bewährte Techniken wie etwa Parameterize
Method (23.13) und Extract and Override Getter (23.8) zurückgreifen; aber wenn die
Aufrufe über mehrere Methoden und mehrere Klassen verteilt sind, wäre es sau-
berer, Encapsulate Global References zu verwenden. Zu diesem Zweck erstellen wir
wie folgt eine neue Klasse:
class OptionSource
{
public:
Virtual -OptionSourceO = 0 ;
351
Kapitel 25
Techniken zur Aufhebung von Dependencies
Dieses Refactoring war vorteilhaft. Wir haben eine Nahtstelle eingeführt und dele-
gieren letztlich einfach nur an die API-Funktion. Jetzt können wir die Klasse para-
metrisieren, damit sie ein Opti onSource-Objekt übernimmt und wir beim Testen
ein Fake-Objekt und in der Produktion das echte Objekt verwenden können.
In dem vorhergehenden Beispiel haben wir die Funktionen in eine Klasse einge-
fügt und virtuell gemacht. Hätten wir auch anders vorgehen können? Ja. Wir hät-
ten ungebundene (freie) Funktionen definieren können, die an andere
ungebundene Funktionen delegieren. Wir hätten sie auch als statische Funktionen
in eine neue Klasse einfügen können. Doch mit keinem dieser Ansätze hätten wir
saubere Nahtstellen bekommen. Wir hätten den Link Searn (4.3) oder den Prepro-
352
25-5
»Expose Static Method«
25.4.1 Schritte
Encapsulate Global References funktioniert wie folgt:
1. Identifizieren Sie die Globals, die Sie einkapseln wollen.
2. Erstellen Sie eine Klasse, von der Sie sie referenzieren wollen.
3. Kopieren Sie die Globals in die Klasse. Sind einige davon Variablen, handhaben
Sie ihre Initialisierung in der Klasse.
4. Kommentieren Sie die ursprünglichen Deklarationen der Globals aus.
5. Deklarieren Sie eine globale Instanz der neuen Klasse.
6. Suchen Sie mit Lean on the Compiler (23.4) alle unaufgelösten Referenzen der
alten Globals.
7. Fügen Sie den Namen der globalen Instanz der neuen Klasse vor jeder unaufge-
lösten Referenz ein.
8. Verwenden Sie an Stellen, an denen Sie Fakes verwenden möchten, Introduce
Static Setter (23.12), Parameterize Constructor (23.14), Parameterize Method (23.13)
oder Replace Global Reference with Getter (23.20).
Die folgende Klasse enthält eine val i date-Methode. Wir müssen eine neue Vali-
dierungsbedingung hinzufügen. Leider lässt sich die zugehörige Klasse nur
schwer instanziieren. Das folgende Code-Fragement zeigt nur die zu ändernde
Methode:
class RSCWorkflow
{
353
Kapitel 25
Techniken zur Aufhebung von Dependencies
Wenn Sie Dependencies ohne Tests aufheben, sollten Sie, wann immer dies mög-
lich ist, bei Methoden Preserve Signatures (23.1) anwenden. Wenn Sie komplette
Methodensignaturen per Cut/Copy/Paste manipulieren können, ist das Risiko
geringer, Fehler einzuführen.
Der hier gezeigte Code hängt nicht von Instanzvariablen oder -methoden ab. Was
würde passieren, wäre die validate-Methode public s t a t i c ? Jeder könnte an
jeder Stelle des Codes die folgende Anweisung einfügen und ein Paket validieren:
RSCWorkflow.validate(packet);
Möglicherweise hat sich der Autor der Klasse niemals vorgestellt, jemand könne
diese Methode eines Tages als s t a t i c , geschweige denn als public deklarieren.
Ist dies deshalb schlecht? Nicht unbedingt. Einkapselung ist für Klassen eine her-
vorragende Technik, aber der statische Bereich einer Klasse gehört eigentlich nicht
dazu. In einigen Sprachen befindet er sich sogar in einer anderen Klasse, die
manchmal als die Metaklasse der Klasse bezeichnet wird.
Eine s t a t i c Methode greift nicht auf private Daten der Klasse zu, sondern ist nur
eine Utility-Methode (Hilfsmethode). Wenn Sie die Methode als public deklarie-
ren, können Sie Tests dafür schreiben, die Ihnen später helfen, die Methode in
eine andere Klasse zu verschieben.
Statische Methoden und Daten verhalten sich wirklich so, als gehörten sie zu
einer anderen Klasse. Statische Daten existieren, solange ein Programm läuft,
nicht solange eine Instanz existiert. Sie können deshalb ohne eine Instanz abge-
rufen werden.
354
25-5
»Expose Static Method«
Der statische Bereich einer Klasse kann als »Bereitstellungsraum« für Dinge
aufgefasst werden, die nicht vollständig zu der Klasse gehören. Eine Methode,
die keine Instanzdaten verwendet, sollte als s t a t i c deklariert werden, damit sie
auffällig bleibt, bis Sie herausgefunden haben, zu welcher Klasse sie wirklich
gehört.
Hier ist die RSCWorkflow-Klasse, nachdem wir eine statische Methode für val i -
date extrahiert haben.
In einigen Sprachen können Sie Expose Static Method leichter ausführen. Anstatt
eine statische Methode aus Ihrer ursprünglichen Methode zu extrahieren, können
Sie einfach die ursprüngliche Methode als statisch deklarieren. Wenn die Methode
von anderen Klassen verwendet wird, kann sie immer noch von einer Instanz der
eigenen Klasse aufgerufen werden. Hier ist ein Beispiel:
Doch in einigen Sprachen gibt der Compiler bei diesem Vorgehen eine Warnung
aus. Am besten versuchen Sie, den Code in einen Zustand zu bringen, in dem er
ohne Warnungen kompiliert wird.
Wenn Sie befürchten, jemand könne die statisch gemachten Methoden so verwen-
den, dass später Dependency-Probleme verursacht werden, können Sie die stati-
sche Methode mit einer nicht-öffentlichen Zugriffsmethode schützen. In
Sprachen wie etwa Java und C#, die die Sichtbarkeit einer Methode auf ein
Package oder die jeweilige Klasse beschränken können, können Sie den Zugriff
auf die statische Methode durch protected einschränken und mit einer Testing
355
Kapitel 25
Techniken zur Aufhebung von Dependencies
Subclass darauf zugreifen. In C++ haben Sie dieselben Optionen: Sie können die
statische Methode als protected deklarieren oder einen Namespace verwenden.
25.5.1 Schritte
Expose Static Method funktioniert wie folgt:
1. Schreiben Sie einen Test, der auf die Methode zugreift, die Sie als public
s t a t i c Methode der Klasse veröffentlichen wollen.
2. Extrahieren Sie den Body der Methode in eine statische Methode. Denken Sie an
Preserve Signatures (23.1). Sie werden einen anderen Namen für die Methode ver-
wenden müssen. Oft können Sie die Namen der Parameter für den Namen der
neuen Methode verwenden. Wenn etwa eine Methode namens val i date ein
Packet akzeptiert, können Sie ihren Body in eine statische Methode namens
val i datePacket extrahieren.
3. Kompilieren Sie.
4. Gibt es Fehler, die mit dem Zugriff auf Instanzdaten oder -methoden zu tun
haben, prüfen Sie, ob Sie diese ebenfalls statisch machen können. Ist dies mög-
lich, tun Sie dies, damit das System kompiliert wird.
}
}
PageLayout ruft eine statische Funktion namens formStyl es einer Klasse
namens StyleMaster auf und weist den Rückgabewert der Instanzvariablen
styl es zu. Wie können wir unsere Dependency von formStyl es oder Styl eMaster
356
2 5 .6
»Extract and Override Call«
aufheben? Eine Möglichkeit besteht darin, den Aufruf in eine neue Methode zu
extrahieren und in einer Testing Subclass zu überschreiben. Dieses Verfahren wird
als Extract and Override Call (Aufruf extrahieren und überschreiben) bezeichnet.
}
protected List formStyl es (StyleTemplate template, int id) {
return StyleMaster.formStyles(template, id);
}
}
Nachdem wir unsere eigene lokale formStyl es-Methode definiert haben, können
wir sie überschreiben, um die Dependency aufzuheben. Da wir im folgenden Bei-
spiel keine Stile für die Dinge benötigen, die wir testen wollen, können wir einfach
eine leere Liste zurückgeben:
Extract and Override Call ist ein sehr nützliches Refactoring; ich verwende es sehr
oft. Es ist eine ideale Methode, um Dependencies von globalen Variablen und sta-
tischen Methoden aufzuheben. Im Allgemeinen neige ich zu dieser Methode, es
sei denn, es gibt viele verschiedene Aufrufe desselben Globals. In einem solchen
Fall verwende ich stattdessen oft Replace Global Reference with Getter (23.20) oder
Parameterize Constructor (23.14).
Mit einem automatischen Refactoring-Tool ist Extract and Override Call trivial.
Dann verwenden Sie Extract Method (Anhang 1). Haben Sie ein solches Tool nicht,
verwenden Sie die folgenden Schritte. Sie können damit jeden Aufruf sicher extra-
hieren, selbst wenn Sie keine Tests eingerichtet haben.
357
Kapitel 25
Techniken zur Aufhebung von Dependencies
25.6.1 Schritte
Extract and Override Call funktioniert wie folgt:
1. Identifizieren Sie den Aufruf, den Sie extrahieren wollen. Suchen Sie die Dekla-
ration seiner Methode. Kopieren Sie seine Methodensignatur unter Beachtung
von Preserve Signatures (23.1).
2. Erstellen Sie in der gegenwärtigen Klasse eine neue Methode mit der Signatur,
die Sie kopiert haben.
3. Kopieren Sie den Aufruf in die neue Methode und ersetzen Sie den alten Aufruf
durch einen Aufruf der neuen Methode.
4. Erstellen Sie eine Test-Unterklasse und überschreiben Sie die neue Methode.
358
25-7
»Extract and Override Factory Method«
Extract and Override Factory Method ist ziemlich leistungsstark, hat aber einige
sprachspezifische Probleme. So können Sie die Methode etwa nicht in C++ ver-
wenden. C++ erlaubt keine Aufrufe von virtuellen Funktionen in abgeleiteten
Klassen. In Java und in vielen anderen Sprachen ist dies erlaubt. In C++ sind
Supersede Instance Variable (23.22) und Extract and Override Getter (23.8) brauch-
bare Alternativen. Dieses Problem wird in dem Beispiel in Supersede Instance
Variable (23.22) ausführlich behandelt.
}
protected TransactionManager makeTransactionManagerO {
Reader reader = new ModelReader( AppConfiguration.getDry-
ConfigurationO);
Persister persister = new XMLStore(AppConfiguration.getDry-
ConfigurationO);
return new TransactionManager(reader, persister);
}
}
Wenn diese Factory-Methode definiert ist, können wir eine Unterklasse bilden und
die Methode darin so überschreiben, dass sie bei Bedarf einen neuen Transak-
tionsmanager zurückgibt:
25.7.1 Schritte
Extract and Override Factory Method funktioniert wie folgt:
1. Identifizieren Sie die Erstellung eines Objekts in einem Konstruktor.
2. Extrahieren Sie die gesamte Logik für die Erstellung in eine Factory-Methode.
3. Erstellen Sie eine Testing Subclass und überschreiben Sie die darin enthaltene
Factory-Methode, um Dependencies von problematischen Typen im Test zu ver-
meiden.
359
Kapitel 25
Techniken zur Aufhebung von Dependencies
Im Wesentlichen erstellen Sie bei diesem Refactoring einen Getter für die Instanz-
variable, die Sie durch ein Fake-Objekt ersetzen wollen, und refaktorisieren den
Code so, dass der Getter anstelle der Klasse verwendet wird. Dann können Sie
Unterklassen von dem Getter ableiten und ihn überschreiben, um andere Objekte
unter Testkontrolle zu bringen.
// WorkflowEngine.cpp
Wo rkf1owEngi ne::Workf1owEngi ne()
{
Reader *reader = new ModelReader(AppConfig.getDryConfigurationO) ;
Persister -persister = new XMLStore(AppConfiguration.getDryConfigurationO)I
tm = new TransactionManager(reader, persister);
// WorkflowEngine.h
class WorkflowEngine
{
private:
TransactionManager *tm;
protected:
TransactionManager *getTransactionManager() const;
360
2 5 .8
»Extract and Override Getter«
public:
WorkflowEngine ();
// WorkflowEngine.cpp
WorkflowEngine: :WorkflowEngine()
:tm (0)
if (tm == 0) {
Reader *reader = new ModelReader( AppConfig.getDryConfigurationO);
Persister *persister = new XMLStore(AppConfiguration.getDry-
ConfigurationO);
tm = new TransactionManager(reader,persister);
}
return tm;
}
Als Erstes führen wir einen Lazy Getter ein, eine Funktion, die den Transaktions-
manager beim ersten Aufruf erstellt. Dann ersetzen wir alle Verwendungen der
Variablen durch Aufrufe des Getters.
Ein Lazy Getter ist eine Methode, die für alle Aufrufer wie ein normaler Getter
aussieht. Der wesentliche Unterschied liegt darin, das Lazy Getter das Objekt,
das sie zurückgeben sollen, bei ihrem ersten Aufruf erstellen. Zu diesem Zweck
enthalten sie normalerweise Logik, die wie folgt aussieht. Beachten Sie, wie die
Instanzvariable thi ng initialisiert wird:
Thing getThingO {
if (thing == null) {
thing = new T h i n g O ;
}
return thing;
}
Lazy Getter werden auch in dem Singleton Design Pattern (23.12) verwendet.
Diesen Getter können wir dann in einer Unterklasse überschreiben und so ein
anderes Objekt zurückgeben:
361
Kapitel 25
Techniken zur Aufhebung von Dependencies
return StransactionManager;
}
FakeTransactionManager transactionManager;
};
Wenn Sie Extract and Override Getter verwenden, müssen Sie die Lebensdauer
von Objekten sehr genau bedenken, insbesondere wenn Sie mit einer Sprache
ohne Garbage Collector (wie etwa C++) arbeiten. Die Test-Instanz muss in einer
Methode genauso gelöscht werden wie in dem Code, der die Produktions-
Instanz löscht.
In einem Test können wir bei Bedarf leicht auf den Fake-Transaktionsmanager
zugreifen:
TEST(transactionCount, WorkflowEngine)
{
auto_ptr<TestWorkflowEngine> engine(new TestWorkflowEngine);
engine.>run();
L0NGS_EQUAL(0, engi ne.>transactionManager.getTransacti o n C o u n t O ) ;
}
Ein Nachteil von Extract and Override Getter liegt darin, dass die Variable verwendet
werden kann, bevor sie initialisiert wurde. Deshalb sollten Sie dafür sorgen, dass
der ganze Code in der Klasse den Getter benutzt.
Ich verwende Extract and Override Getter eher selten. Enthält ein problematisches
Objekt nur eine einzige Methode, ist es viel einfacher, Extract and Override Call
(25.6) zu verwenden. Doch Extract and Override Getter ist die bessere Wahl, wenn
ein Objekt zahlreiche problematische Methoden enthält. Wenn Sie alle diese Pro-
bleme durch Extrahieren und Überschreiben eines Getters lösen können, sind Sie
damit klar im Vorteil.
25.8.1 Schritte
Extract and Override Getter funktioniert wie folgt:
1. Identifizieren Sie das Objekt, für das Sie einen Getter brauchen.
2. Extrahieren Sie die gesamte Logik, die erforderlich ist, um das Objekt in einem
Getter zu erstellen.
3. Ersetzen Sie alle Verwendungen des Objekts durch Aufrufe des Getters und ini-
tialisieren Sie in allen Konstruktoren die Referenz auf das Objekt mit null.
4. Fügen Sie die Logik für den ersten Aufruf des Getters ein. Das Objekt soll kon-
struiert und der Referenz zugewiesen werden, wenn die Referenz null ist.
5. Bilden Sie eine Unterklasse der Klasse und überschreiben Sie den Getter, um
zum Testen ein Alternativ-Objekt zur Verfügung zu stellen.
362
25.9
»Extract Implementer«
Es gibt jedoch eine Sache, auf die ich normalerweise verzichte: Ich setze ungern
ein »I«-Präfix (Großbuchstabe I) vor den Namen der Klasse, um den Namen für
die neue Schnittstelle zu bilden, wenn dies nicht bereits Konvention in der Code-
Basis ist. Es gibt kaum etwas Schlechteres, als mit einer unvertrauten Code-Basis
zu arbeiten, in der die Hälfte der Typnamen mit I anfangen und die andere Hälfte
nicht. Sie wissen nie, ob Sie den korrekten Typnamen eingeben. Entweder verges-
sen Sie das benötigte I oder nicht.
Die Vergabe von Namen ist ein Kernaspekt des Designs. Geeignete Namen ver-
stärken das Verständnis eines Systems und erleichtern die Arbeit damit. Unpas-
sende Namen untergraben das Verständnis und machen das Leben der
betroffenen Programmierer zur Hölle.
Wenn der Name einer Belasse als Name einer Schnittstelle weniger geeignet ist
und ich nicht auf automatische Refactoring-Tools zurückgreifen kann, kann ich
mit Extract Implementer die benötigte Trennung erzielen. Um einen Implementer
einer Klasse zu extrahieren, wandeln wir die Klasse in eine Schnittstelle um,
indem wir eine Unterklasse von ihr ableiten und alle ihre konkreten Methoden
nach unten in diese Unterklasse verschieben. Hier ist ein Beispiel in C++:
// ModelNode.h
class ModelNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double: m_weight;
void: createSpanningLinksO;
public:
void addExteriorNode(ModelNode *newNode);
void addlnternalNodeCModelNode *newNode);
363
Kapitel 25
Techniken zur Aufhebung von Dependencies
void c o l o r i z e O ;
};
Zunächst wird die Deklaration der Model Node-Klasse vollständig in eine andere
Header-Datei kopiert und der Name der Kopie in ProductionModelNode geän-
dert. Hier ist ein Teil der Deklaration der kopierten Klasse:
// ProductionModelNode.h
class ProductionModeNode
{
private:
list<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double: m_weight;
void: createSpanningLinksC);
public:
void addExteriorNode(ModelNode *newNode);
void addlnternalNodeCModelNode *newNode);
void c o l o r i z e O ;
};
Als Nächstes werden aus dem Model Node-Header alle Deklarationen nicht-öffent-
licher Variablen und Methoden entfernt. Dann deklarieren wir alle verbleibenden
publ i c Methoden als rein virtuell (abstrakt):
// ModelNode.h
class ModelNode
{
public:
V i r t u a l void addExteriorNode(ModelNode *newNode) = 0;
Virtual void addlnternalNodeCModelNode *newNode) = 0;
V i r t u a l void c o l o r i z e O = 0;
};
An diesem Punkt ist Model Node eine reine Schnittstelle. Sie enthält nur abstrakte
Methoden. Da wir in C++ arbeiten, sollten wir auch einen rein virtuellen Destruk-
tor deklarieren und ihn in einer Implementierungsdatei definieren:
// ModelNode.h
class ModelNode
{
public:
Virtual -ModelNode O = 0 ;
V i r t u a l void addExteriorNode(ModelNode *newNode) = 0;
Virtual void addlnternalNodeCModelNode *newNode) = 0;
Virtual void c o l o r i z e O = 0;
364
25.9
»Extract Implementer«
};
// ModelNode.cpp
ModelNode::-ModelNode()
{}
#include "ModelNode.h"
class ProductionModelNode : public ModelNode
{
private:
1ist<ModelNode *> m_interiorNodes;
list<ModelNode *> m_exteriorNodes;
double: m_weight;
void: createSpanningLinksC);
public:
void addExteriorNode(ModelNode *newNode);
void addInternalNode(ModelNode *newNode);
void colorizeO;
};
An diesem Punkt sollte ProductionModel Node sauber kompiliert werden. Wenn
Sie den Rest des Systems erstellen, entdecken Sie Stellen, an denen versucht wird,
Model Nodes zu instanziieren. Sie können sie so ändern, dass stattdessen Produc-
ti onModel Nodes erstellt werden. In diesem Refactoring ersetzen wir das Erstellen
von Objekten einer konkreten Klasse durch Objekte einer anderen; deshalb wird
unsere Dependency-Situation insgesamt nicht wirklich besser. Sie sollten jedoch
prüfen, ob Sie Dependencies mit einer Factory weiter reduzieren können.
25.9.1 Schritte
1. Kopieren Sie die Deklaration der Quellklasse. Weisen Sie ihr einen anderen
Namen zu. Es ist nützlich, für extrahierte Klassen eine Namenskonvention zu
verwenden. Ich verwende oft das Präfix Production, um anzuzeigen, dass die
neue Klasse im Produktions-Code die Schnittstelle implementiert.
2. Wandeln Sie die Quellklasse in eine Schnittstelle um, indem Sie alle nicht-
öffentlichen Methoden und alle Variablen entfernen.
3. Machen Sie alle verbleibenden öffentlichen Methoden zu abstrakten Methoden.
Wenn Sie in C++ arbeiten, sorgen Sie dafür, dass keine der von Ihnen abstrakt
gemachten Methoden von nicht-virtuellen Methoden überschrieben wird.
4. Untersuchen Sie, ob alle Dateien erforderlich sind, die in die Schnittstellendatei
importiert oder eingebunden werden. Oft können Sie viele entfernen. Mit Lean
365
Kapitel 25
Techniken zur Aufhebung von Dependencies
on the Compiler (23.4) können Sie diese Dateien entdecken. Löschen Sie einfach
nacheinander die Dateien und kompilieren Sie den Code. Fehlermeldungen des
Compilers zeigen an, ob die Datei benötigt wird.
5. Ändern Sie Ihre Produktionsklasse so, dass sie die neue Schnittstelle imple-
mentiert.
6. Kompilieren Sie die Produktionsklasse, um zu prüfen, ob alle Methodensigna-
turen in der Schnittstelle implementiert sind.
7. Kompilieren Sie den Rest des Systems, um alle Stellen zu lokalisieren, an denen
Instanzen der Quellklasse erstellt wurden. Ersetzen Sie diese durch Objekte der
neuen Produktionsklasse.
2 5 . 9 . 2 Ein k o m p l e x e r e s Beispiel
Extract Implementer ist relativ einfach, wenn die Vererbungshierarchie der Quell-
klasse keine Ober- oder Unterklassen enthält. Andernfalls müssen wir etwas
geschickter vorgehen. Abbildung 25.2 zeigt noch einmal Model Node, aber in Java
mit einer Oberklasse und einer Unterklasse:
Bei diesem Design sind Node, Model Node und LinkageNode konkrete Klassen.
Model Node verwenden protected Methoden von Node. Sie stellt auch Methoden
zur Verfügung, die von ihrer Unterklasse, LinkageNode, verwendet werden.
Extract Implementer erfordert eine konkrete Klasse, die in eine Schnittstelle umge-
wandelt werden kann. Danach haben Sie eine Schnittstelle und eine konkrete
Klasse.
366
25.9
»Extract Implementer«
In dieser Situation können wir Extract Implementer auf die Node-Klasse anwenden,
indem wir die ProductionNode-Klasse in der Vererbungshierarchie unter Node
einordnen. Wir ändern auch die Vererbungsbeziehung so, dass Model Node nicht
die Klasse Node, sondern die Klasse ProductionNode erbt (siehe Abbildung 25.3).
«Interface»
Node
Als Nächstes wenden wir Extract Implementer auf Model Node an. Weil Model Node
bereits über eine Unterklasse verfügt, fügen wir die Klasse ProductionModel-
Node in der Hierarchie zwischen Model Node und Li nkageNode ein. Danach kön-
nen wir die Model Node-Schnittstelle von Node ableiten (siehe Abbildung 25.4).
Wenn eine Klasse derartig in eine Hierarchie eingebettet ist, müssen Sie überle-
gen, ob Extract Interface (23.10) besser geeignet ist, und andere Namen für Ihre
Schnittstellen wählen. Dies ist ein viel direkteres Refactoring.
367
Kapitel 25
Techniken zur Aufhebung von Dependencies
Es gibt wenigstens drei Methoden, Extract Interface auszuführen, sowie einige klei-
nere Stolpersteine, auf die Sie achten müssen:
Zweitens: Sie können Methoden inkrementell mit den Schritten extrahieren, die
ich in diesem Abschnitt beschreibe.
Drittens: Sie können mehrere Methoden einer Klasse auf einmal ausschneiden
und ihre Deklarationen in eine Schnittstelle einfügen. Dieses Verfahren ist nicht
so sicher wie die ersten beiden Verfahren, aber immer noch ziemlich sicher. Oft ist
es das einzige praktikable Verfahren, eine Schnittstelle zu extrahieren, wenn Sie
keine automatisierte Unterstützung haben und Ihre Builds sehr lange dauern.
Im folgenden Beispiel wird eine Schnittstelle mit der zweiten Methode extrahiert.
Außerdem beschreibe ich einige Dinge, auf die Sie achten müssen.
void testPaydayO
{
Transaction t = new PaydayTransaction(getTestingDatabaseO);
t.run();
assertEquals(getSamp1eCheck(12), getTestingDatabase().findCheck(12));
}
368
25-10
»Extract Interface«
PaydayTransaction
+ PaydayTransaction(database : PayrollDatabase, log: TransactionLog)
+ run()
1
1!
TransactionLog
+ saveTransaction(transaction : Transaction)
+ recordError(code : int)
Doch damit der Code kompiliert wird, müssen wir ein TransactionLog überge-
ben. Zu diesem Zweck erstellen wir einen Aufruf einer noch nicht existierenden
Klasse, FakeTransactionLog:
void t e s t P a y d a y O
{
FakeTransactionLog aLog = new FakeTransactionLogO;
Transaction t = new PaydayTransaction(getTestingDatabaseC), aLog);
t.run();
Damit der Code kompiliert wird, müssen wir eine Schnittstelle für die T r a n s -
actionLog-Klasse extrahieren, eine Klasse namens FakeTransactionLog erstel-
len, um die Schnittstelle zu implementieren, und es dann PaydayTransaction
ermöglichen, ein FakeTransactionLog zu übernehmen.
Der Reihe nach: Wir extrahieren die Schnittstelle. Wir erstellen eine neue leere
Klasse namens TransactionRecorder. Wenn Sie sich fragen, woher dieser Name
stammt, lesen Sie den folgenden Einschub.
Interface-Benennung
Interfaces (Schnittstellen) sind als Programm-Konstrukte relativ neu. Java und
viele .NET-Sprachen verfügen über dieses Konstrukt. In C++ müssen Sie es
simulieren, indem Sie eine Klasse mit ausschließlich rein virtuellen Funktionen
erstellen.
369
Kapitel 25
Techniken zur Aufhebung von Dependencies
Als die ersten Interfaces in Sprachen eingeführt wurden, fügten einige Program-
mierer ein I (Großbuchstabe I) vor dem Namen der Belasse ein, von der das Inter-
face abgeleitet war. So erhielt etwa die Schnittstelle einer Klasse namens Account
den Namen IAccount. Diese Benennung hat den Vorteil, dass man nicht über den
Namen nachzudenken braucht, wenn man die Schnittstelle extrahiert. Der Klas-
senname erhält einfach ein Standardpräfix. Der Nachteil besteht darin, dass Sie viel
Code erzeugen, der wissen muss, ob er mit einer Schnittstelle zu tun hat. Idealer-
weise sollte dies für den Code keine Rolle spielen. Außerdem enthält Ihre Code-
Basis letztlich Komponenten, deren Name mal mit dem Präfix I und mal ohne
beginnen. Wollen Sie wieder auf eine normale Belasse zurückgehen, müssen Sie
das I aufwendig im gesamten Code ändern. Verzichten Sie auf diese Änderung,
bleibt der Name als subtile Lüge im Code.
Wenn Sie neue Klassen entwickeln, ist es am einfachsten, einfache Klassennamen
zu definieren, selbst für große Abstraktionen. Bei einer Buchhaltungs-Software
könnten wir etwa mit einer Klasse beginnen, die einfach als Account (engl. Konto)
benannt ist. Dann können wir die ersten Tests schreiben, um neue Funktionalität
hinzuzufügen. Wenn Sie irgendwann Account als Schnittstelle definieren möch-
ten, könnten Sie zunächst eine Unterklasse von Account ableiten, alle Daten und
Methoden in die Unterklasse verschieben und dann Account als Schnittstelle defi-
nieren. Wenn Sie so vorgehen, müssen Sie nicht Ihren Code durchsuchen und den
Typ jeder Referenz in Account ändern.
In Fällen wie etwa dem PaydayTransaction-Beispiel, in dem wir bereits über
einen passenden Schnittstellennamen (TransactionLog) verfügen, können wir
dasselbe tun. Der Nachteil ist, dass das Verschieben der Daten und Methoden in
eine neue Unterklasse aufwendig ist. Aber wenn das Risiko gering genug ist,
wende ich diese Technik manchmal an. Sie heißt Extract Implementer (25.9).
Wenn ich nur wenige Tests habe und eine Schnittstelle extrahieren will, um mehr
Tests einzurichten, versuche ich oft, einen neuen Namen für die Schnittstelle zu
finden. Manchmal dauert es eine Weile, einen passenden Namen zu finden. Wenn
Sie nicht über Tools verfügen, die die Klassen für Sie umbenennen, zahlt es sich
aus, zu versuchen, einen geeigneten Namen zu etablieren, bevor die Anzahl der
Stellen, an denen er benutzt wird, zu umfangreich wird.
interface TransactionRecorder
{
}
Jetzt gehen wir zurück und implementieren die neue Schnittstelle in Trans-
actionLog.
370
25-10
»Extract Interface«
Der Code sollte problemlos kompiliert werden, da wir nur einige neue Klassen ein-
geführt und eine Klasse so geändert haben, dass sie eine leere Schnittstelle imple-
mentiert.
An diesem Punkt gehen wir das Refactoring mit aller Kraft an. Wir ändern den Typ
jeder Referenz an den Stellen, an denen wir die Schnittstelle verwenden wollen.
PaydayTransacti on verwendet ein Transacti onLog, soll aber Transacti onRe-
corder verwenden. Wenn Sie den Code nach dieser Änderung kompilieren, wer-
den mehrere Fälle gemeldet, in denen Methoden von TransactionRecorder
aufgerufen werden. Wir können die Fehler nacheinander beseitigen, indem wir
die Methodendeklarationen zu der Transacti onRecorder-Schnittstelle und leere
Methodendefinitionen zu FakeTransactionLog hinzufügen.
In diesem Fall rufen wir nur die Methode saveTransaction von Trans-
acti onRecorder auf. Weil TransactionRecorder diese Methode noch nicht ent-
hält, meldet der Compiler einen Fehler. Damit der Test kompiliert wird, fügen wir
diese Methode in TransactionRecorder und FakeTransactionLog ein.
interface TransactionRecorder
{
void saveTransaction(Transaction transaction);
}
37i
Kapitel 25
Techniken zur Aufhebung von Dependencies
Wenn Sie eine Schnittstelle extrahieren, müssen Sie nicht alle p u b l i c Metho-
den der Klasse extrahieren. Mit Lean on the Compiler (23.4) finden Sie die Metho-
den, die verwendet werden.
372
25-10
»Extract Interface«
class BondRegistry
{
public:
Bond *newFixedYield(Client *client) { ... }
};
Außerdem haben wir eine Unterklasse, die eine Methode mit demselben
Namen und derselben Signatur enthält:
class PremiumRegistry : public BondRegistry
{
public:
Bond *newFixedYield(Client *client) { ... }
};
Wenn wir eine Schnittstelle von BondRegi stry extrahieren
class BondProvider
{
public:
Virtual Bond *newFixedYield(Client *client) = 0;
};
und sie in BondRegi stry implementieren:
class BondRegistry : public BondProvider { ... };
könnten wir in dem folgenden Code einen Fehler auslösen, indem wir ihm ein
Premi umRegi stry übergeben:
}
Bevor wir die Schnittstelle extrahierten, wurde die newFi xedYi el d-Methode der
Klasse BondRegistry aufgerufen, weil die Variable registry den Compile-
Time-Typ BondRegistry hat. Deklarieren wir jedoch beim Extrahieren der
Schnittstelle newFi xedYi eld als Virtual, ändern wir das Verhalten. Es wird
die Methode von Premi umBondRegi stry aufgerufen. Wenn wir in C++ eine
Methode in einer Basisklasse als vi rtual deklarieren, werden die Methoden,
die sie in den Unterklassen überschreiben, vi rtual. In Java oder C# existiert
dieses Problem nicht! In Java sind alle Instanzmethoden virtuell. In C# sind die
Dinge ein wenig sicherer, weil das Hinzufügen einer Schnittstelle vorhandene
Aufrufe von nicht-virtuellen Methoden nicht beeinflusst.
373
Kapitel 25
Techniken zur Aufhebung von Dependencies
Diese Methode kann an eine nicht-virtuelle oder sogar eine statische Methode
delegieren. Sie müssen nur dafür sorgen, dass die Methode für alle Unterklas-
sen unter der Klasse, aus der Sie die Schnittstelle extrahieren, das Richtige tut.
25.10.1 Schritte
Extract Interface funktioniert ohne automatische Unterstützung wie folgt:
1. Erstellen Sie eine neue Schnittstelle mit einem Namen Ihrer Wahl. Fügen Sie
noch keine Methoden hinzu.
2. Definieren Sie die Klasse, aus der Sie die Schnittstelle extrahieren, so um, dass
sie die Schnittstelle implementiert. Dadurch können Sie keinen Fehler einfüh-
ren, weil die Schnittstelle keine Methoden enthält. Aber es ist gut, Ihren Test zu
kompilieren und auszuführen, um dies zu verifizieren.
3. Ändern Sie die Stelle, an der Sie das Objekt verwenden wollen, so, dass sie die
Schnittstelle anstelle der ursprünglichen Klasse verwendet.
4. Kompilieren Sie das System und fügen Sie für jede verwendete Methode, die der
Compiler als Fehler meldet, eine neue Methodendeklaration in die Schnittstelle
ein.
374
25-11
»Introduce Instance Delegatar«
Bezeichnung dafür lautet Static Cling; dt. etwa statisches Anhaften.) In solchen Fäl-
len wäre oft der Einsatz eines Object Seams (4.3) wünschenswert, um das Verhal-
ten zu ändern, wenn die statischen Methoden aufgerufen werden. Was tun Sie in
diesem Fall?
}
}
Diese Klasse enthält nur statische Methoden (nur eine wird gezeigt). Wir können
wie folgt eine Instanzmethode in die Klasse einfügen und darin an die statische
Methode delegieren:
BankingServices.updateAccountBalance(id, sum);
}
}
dieser Aufruf:
375
Kapitel 25
Techniken zur Aufhebung von Dependencies
services.updateBalance(id, sum)
}
}
Dies ist natürlich nur möglich, wenn es Methoden gibt, die das verwendete Ban-
ki ngServi ces-Objekt extern erstellen. Dies erfordert einen zusätzlichen Refacto-
ring-Schritt; aber bei statisch typisierten Sprachen können wir das benötigte
Objekt mit Lean on the Compiler (23.4) identifizieren.
Diese Technik ist bei vielen statischen Methoden einfach genug, löst aber bei Uti-
lity* Klassen vielleicht ein ungutes Gefühl aus. Eine Klasse mit fünf oder zehn sta-
tischen Methoden und nur ein oder zwei Instanzmethoden sieht seltsam aus.
Dieser Eindruck wird verstärkt, wenn diese einfach nur an die statischen Metho-
den delegieren. Aber mit dieser Technik können Sie leicht einen Objekt-Seam ein-
richten und verschiedene Verhaltensweisen unter Testkontrolle bringen. Vielleicht
kommen Sie im Laufe der Zeit an den Punkt, dass jeder Aufruf der Utility-Klasse
von einer der delegierenden Methoden aus erfolgt. Dann können Sie die Bodies
der statischen Methoden in die Instanzmethoden verschieben und die statischen
Methoden löschen.
25.11.1 Schritte
Introduce Instance Delegator funktioniert wie folgt:
376
25-12
in einen Test-Harnisch einzuhüllen. Sie wollen einen Satz von Klassen in einem
Test-Harnisch einfügen, entdecken aber, dass einige nur brauchbar sind, wenn sie
in einem bestimmten Zustand initialisiert werden. Wenn Sie Ihren Harnisch ein-
gerichtet haben, müssen Sie die Liste der Globals prüfen, um dafür zu sorgen,
dass jede den für den Test erforderlichen Zustand hat.
Viele Systeme arbeiten mit Globals. In einigen Systemen werden sie sehr direkt
und unbefangen verwendet; jemand deklariert einfach irgendwo eine Variable. In
anderen Systemen werden sie als Singletons unter strenger Beachtung der Regeln
des Singleton Design Pattern eingeführt. In jedem Fall ist es unkompliziert, zwecks
Überwachung ein Fake einzurichten. Ist die Variable ganz unverhüllt global, befin-
det sie sich außerhalb einer Klasse oder ist sie offen als publ i c stati c deklariert,
können Sie einfach das Objekt ersetzen. Wenn die Referenz const oder fi nal ist,
müssen Sie vielleicht diesen Schutz entfernen. Fügen Sie einen entsprechenden
Kommentar in den Code ein, dass dies nur dem Testen diene und der Zugriff im
Produktions-Code nicht missbraucht werden solle.
Vielleicht fühlen Sie sich bei der Vorstellung, den Zugriffsschutz mit Introduce
Static Setter aufzuheben, ein wenig unwohl; doch vergessen Sie nicht, dass der
Zugriffs schütz Programmierfehler verhindern soll. Mit unseren Tests wollen wir
ebenfalls Fehler verhindern. In diesem Fall zeigt sich, dass wir das stärkere Tool
brauchen.
377
Kapitel 25
Techniken zur Aufhebung von Dependencies
Der folgende Code zeigt die Externa! Router-Klasse, bevor wir den statischen Set-
ter einführen:
class ExternalRouter
{
private:
public:
static ExternalRouter *instance();
};
ExternalRouter ^ExternalRouter::_instance = 0;
ExternalRouter -ExternalRouter::instance()
{
if (_instance == 0) {
_instance = new ExternalRouter;
}
return _instance;
}
Der Router wird beim ersten Aufruf der i nstance-Methode erstellt. Um ihn
durch einen anderen Router zu ersetzen, müssen wir den Rückgabewert von
i nstance ändern. Zunächst führen wir eine neue Methode ein, um die Instanz zu
ersetzen.
378
25-12
»Introduce Static Setter«
Natürlich setzt dies voraus, dass wir eine neue Instanz erstellen können. Bei der
Anwendung des Singleton Patterns wird oft der Konstruktor der Klasse als pri-
vate deklariert, um zu verhindern, dass mehr als eine Instanz erstellt wird. Wenn
Sie den Konstruktor als protected deklarieren, können Sie von dem Singleton
eine Unterklasse ableiten, um den Code zu überwachen oder zu isolieren, und die
neue Instanz an die setTesti nglnstance-Methode übergeben. In dem vorherge-
henden Beispiel erstellen wir eine Unterklasse von ExternalRouter namens
TestingExternal Router und überschreiben die getDi spatcher-Methode so,
dass sie wie gewünscht einen FakeDi spatcher zurückgibt.
Eine Setter-Methode und ein protected Konstruktor eines Singletons sind inva-
siv; aber sie helfen Ihnen, Tests einzurichten. Können Entwickler den public
Konstruktor missbrauchen und in dem Produktionssystem mehr als ein Singleton
erstellen? Ja, aber meiner Meinung nach sollten Sie, wenn es wichtig ist, dass in
einem System nur eine Instanz eines Objekts existiert, am besten dafür sorgen,
dass jedes Mitglied des Teams diese Einschränkung versteht.
379
Kapitel 25
Techniken zur Aufhebung von Dependencies
In dem vorhergehenden Beispiel ersetzen wir ein Singleton mit einem statischen
Setter. Das Singleton war ein Objekt, das ein anderes Objekt, einen Dispatcher, lie-
fert. Gelegentlich enthalten Systeme eine andere Art von Globals: eine globale Fac-
tory. Anstatt dafür zu sorgen, dass nur eine einzige Instanz verwendet wird, liefert
eine Factory bei jedem Aufruf einer ihrer statischen Methoden ein frisches Objekt.
Dieses Objekt bei der Rückgabe gegen ein anderes auszutauschen, kann verzwickt
sein, aber oft können Sie zu diesem Zweck einen Factory-Delegate verwenden, der
eine andere Factory aufruft. Hier ist ein Beispiel in Java:
interface RouterServer
{
Router makeRouter();
}
public class RouterFactory
{
static Router makeRouterf) {
return Server.makeRouterO;
}
static setServer(RouterServer server) {
this.Server = server;
}
static RouterServer server = new RouterServerQ {
public RouterServer makeRouterO {
return new EWNRouterO;
}
};
}
protected void s e t U p O {
RouterServer.setServer(new RouterServerO {
public RouterServer makeRouterO {
return new FakeRouterO;
}
});
}
25-12
Es ist wichtig, dass Sie bei diesen Static-Setter-Patterns immer daran denken, dass
Sie einen Zustand modifizieren, der allen Tests zur Verfügung steht. In xUnit-
Test-Frameworks können Sie die Dinge mit der tearDown-Methode zurück in
einen bekannten Zustand versetzen, bevor die restlichen Tests ausgeführt werden.
Im Allgemeinen tue ich dies nur, wenn die Verwendung des falschen Zustands im
nächsten Test irreführend sein könnte. Wenn ich in allen meinen Tests einen
Fake-MailSender einsetze, ist es nicht sinnvoll, einen anderen zu verwenden.
Gibt es dagegen ein Global, das den Zustand verwaltet und die Ergebnisse des Sys-
tems beeinflusst, tue ich in den setUp- und tearDown-Methoden oft dasselbe,
damit das System in einem sauberen Zustand verlassen wird:
protected void s e t U p O {
Node.count = 0;
}
protected void tearDown() {
Node.count = 0;
}
An diesem Punkt versuche ich mir vorzustellen, wie Sie sich fühlen. Sie sind von
dem Massaker angewidert, das ich in dem System anrichte, nur um einige Tests
einzurichten. Und Sie haben recht: Diese Patterns können Teile eines Systems
erheblich verunstalten. Chirurgische Eingriffe sind niemals schön, besonders
nicht am Anfang. Was können Sie tun, um das System wieder in einen ansehnli-
chen Zustand zu bringen?
Eine Möglichkeit ist die Parameter-Übergabe. Untersuchen Sie die Klassen, die
Zugriff auf Ihr Global brauchen, und überlegen Sie, ob Sie ihnen eine gemein-
same Oberklasse geben können. Ist dies möglich, können Sie das Global bei
Erstellung dieser Klasse an sie übergeben und auf Globals nach und nach ganz
verzichten. Programmierer sind oft irritiert, dass jede Klasse in dem System auf
Globals angewiesen ist. Oft werden Sie überrascht sein. Ich arbeitete einmal an
einem eingebetteten System mit, das die Klassen zur Speicherverwaltung und
Fehlermeldung einkapselte und bei Bedarf ein Memory-Objekt oder einen Error-
Reporter lieferte. Nach und nach wurden die Klassen, die diese Services benötig-
ten, sauber von denen getrennt, die dieses nicht taten. Die Klassen mit diesem
Bedarf hatten einfach eine gemeinsame Oberklasse. Die Objekte, die in dem Sys-
tem weitergereicht wurden, wurden beim Start des Programms erstellt, was kaum
merklich war.
25.12.1 Schritte
1. Verringern Sie den Schutz des Konstruktors so, dass Sie ein Fake erstellen kön-
nen, indem Sie eine Unterklasse des Singletons erstellen.
Kapitel 25
Techniken zur Aufhebung von Dependencies
2. Fügen Sie einen statischen Setter in die Singleton-Klasse ein. Der Setter sollte
eine Referenz der Singleton-Klasse enthalten. Sorgen Sie dafür, dass der Setter
die Singleton-Instanz korrekt zerstört, bevor er das neue Objekt setzt.
3. Wenn Sie in dem Singleton auf private oder protected Methoden zugreifen
müssen, um es für die Tests einzurichten, sollten Sie die Bildung einer Unter-
klasse oder das Extrahieren einer Schnittstelle erwägen und die Instanz in dem
Singleton so ändern, dass ihr Typ dem Typ der Schnittstelle entspricht, die sie
referenziert.
Gibt es andere Alternativen? Ja. Per Link Substitution (Link-Ersetzung) können Sie
eine Funktion durch eine andere ersetzen. Erstellen Sie zu diesem Zweck eine
Dummy-Bibliothek, die Funktionen mit denselben Signaturen wie die Funktionen
enthält, die Sie simulieren wollen. Bei der Überwachung von Code müssen Sie
einen Mechanismus einrichten, um Meldungen zu speichern und abzufragen. Sie
können Dateien, globale Variablen oder andere Techniken verwenden, die beim
Testen leicht anwendbar sind. Hier ist ein Beispiel:
Ich habe Link Substitution nie mit C++-Klassen ausprobiert, aber ich nehme an,
dass dies möglich ist. Ich bin sicher, dass die Auswertung des Ergebnisses durch
382
25.14
Parameterize Constructor«
das Name-Mangling des C++-Compilers recht schwierig sein wird; doch bei Aufru-
fen von C-Funktionen ist die Technik sehr brauchbar. Sie eignet sich am besten,
um Aufrufe von Funktionen in externen Bibliotheken zu simulieren. Am besten
lassen sich Bibliotheken simulieren, die hauptsächlich als Datensenken funktio-
nieren: Sie rufen ihre Funktionen auf, interessieren sich aber kaum für die Rück-
gabewerte. So lassen sich etwa Grafik-Bibliotheken besonders gut mit Link
Substitution simulieren.
Link Substitution kann auch in Java verwendet werden. Erstellen Sie Klassen mit
denselben Namen und Methoden und ändern Sie Ihren Classpath so, dass Aufrufe
an diese Klassen und nicht an die Klassen mit schlechten Dependencies weiterge-
leitet werden.
25.13.1 Schritte
Link Substitution funktioniert wie folgt:
1. Identifizieren Sie die Funktionen oder Klassen, die Sie simulieren wollen.
2. Erstellen Sie dafür alternative Definitionen.
3. Passen Sie Ihren Build so an, dass er die alternativen Definitionen und nicht die
Produktionsversionen einbindet.
383
Kapitel 25
Techniken zur Aufhebung von Dependencies
Diese Technik wird auch deswegen selten benutzt, weil die Entwickler annehmen,
sie müssten immer ein zusätzliches Argument übergeben. Doch wir können
zusätzlich einen Konstruktor schreiben, der die ursprüngliche Signatur erhält:
In diesem Fall können Sie zum Testen andere Objekte übergeben, ohne dass die
Clients der Klasse den Unterschied bemerken.
Gehen wir Schritt für Schritt vor. Hier ist der ursprüngliche Code:
Dann fügen wir einen Parameter für den Mai 1 Recei ver ein:
384
25.14
»Parameterize Constructor«
Als Nächstes weisen wir diesen Parameter der Instanzvariablen zu und beseitigen
den new-Ausdruck.
Jetzt gehen wir zu dem ursprünglichen Konstruktor zurück und entfernen seinen
Body und ersetzen ihn durch einen Aufruf des neuen Konstruktors. Der ursprüng-
liche Konstruktor erstellt mit new den Parameter, den er übergeben muss.
Hat diese Technik Nachteile? Tatsächlich gibt es einen. Wenn wir einen neuen
Parameter zu einem Konstruktor hinzufügen, öffnen wir die Tür für weitere
Dependencies von den Parametern der Klasse. Benutzer der Klasse können den
385
Kapitel 25
Techniken zur Aufhebung von Dependencies
};
In C++ hat dies nur einen Nachteil. Die Header-Datei, die diese Klassendeklara-
tion enthält, muss die Header-Datei für Equi pmentDi spatcher einbinden.
Ohne den Konstruktor-Aufruf hätten wir vielleicht eine Forward-Deklaration
von Equi pmentDi spatcher verwenden können. Deshalb benutze ich Standard-
Argumente in C++ nur selten.
25.14.1 Schritte
1. Identifizieren Sie den Konstruktor, den Sie parametrisieren wollen, und kopie-
ren Sie ihn.
2. Fügen Sie für das Objekt, dessen Erstellung Sie ersetzen wollen, einen Parame-
ter in den Konstruktor ein. Entfernen Sie die Erstellung des Objekts und weisen
Sie den Parameter der Instanzvariablen des Objekts zu.
3. Wenn Sie in Ihrer Sprache von einem Konstruktor aus einen Konstruktor aufru-
fen können, entfernen Sie den Body des alten Konstruktors und ersetzen Sie ihn
durch einen Aufruf des alten Konstruktors. Fügen Sie in den alten Konstruktor
einen neuen Ausdruck in den Aufruf des neuen Konstruktors ein. Wenn Sie in
Ihrer Sprache von einem Konstruktor aus keinen anderen Konstruktor aufrufen
können, müssen Sie möglicherweise duplizierten Code aus den Konstruktoren
in eine neue Methode extrahieren.
386
25.15
»Parameterize Method«
void TestCase::run() {
delete m_result; m_result=0;
m_result = new TestResult;
try {
setUpü;
runTest(m_result);
}
catch (exception& e) {
result->addFailure(e, this);
}
tearDownO;
}
Diese Methode erstellt ein TestResult-Objekt, wenn sie aufgerufen wird. Wollen
wir den einschlägigen Code isolieren, können wir das Objekt als Parameter über-
geben.
void TestCase::run() {
run(new TestResult);
}
In C++, Java, C# und vielen anderen Sprachen können Klassen mehrere gleich-
namige Methoden enthalten, solange deren Signaturen verschieden sind. In
dem Beispiel nutzen wir diese Möglichkeit. Die neue parametrisierte Methode
hat denselben Namen wie die ursprüngliche Methode. Obwohl diese Technik
Arbeit spart, kann sie auch Verwirrung stiften. Alternativ könnte man auch den
Typ des Parameters im Namen der neuen Methode verwenden. So könnten wir
hier etwa run() als Namen der ursprünglichen Methode beibehalten und die
neue Methode runWithTestResult(TestResult) nennen.
Wie bei Parameterize Constructor (25.14) können Clients auch bei Parameterize
Method von neuen Typen abhängig werden, die sich vorher in der Klasse, aber
387
Kapitel 25
Techniken zur Aufhebung von Dependencies
nicht in der Schnittstelle befunden haben. Sollte dies ein Problem sein, erwäge ich
stattdessen den Einsatz von Extract and Override Factory Method (25.7).
25.15.1 Schritte
1. Identifizieren Sie die Methode, die Sie ersetzen wollen, und kopieren Sie sie.
2. Fügen Sie einen Parameter der Methode des Objekts ein, dessen Erstellung Sie
ersetzen wollen. Entfernen Sie die Erstellung des Objekts. Weisen Sie den Wert
des Parameters der Variablen zu, die das Objekt enthält.
3. Löschen Sie den Body der kopierten Methode und rufen Sie die parametrisierte
Methode auf. Verwenden Sie dabei den Ausdruck zur Erstellung des ursprüng-
lichen Objekts.
In einem Tool zum Komponieren von Musik enthält ein Track mehrere Sequenzen
(Folgen) musikalischer Events (Ereignisse). Wir müssen in jeder Sequenz Lücken
finden, die wir mit kleinen wiederkehrenden Musikmustern füllen können und
müssen dafür eine Methode namens bool Sequence: : hasGapFor (Sequence&
pattern) const schreiben. Die Methode sollte einen Wert zurückgeben, der
anzeigt, ob ein Muster in eine Sequenz eingepasst werden kann.
388
25.16
»Primitivize Parameter«
kann. Jede Sequenz besteht aus einem Vektor von Events. Leider leiden Events
unter denselben Problemen wie Sequenzen: Schreckliche Dependencies führen
zu Build-Problemen. Glücklicherweise brauchen wir für diese Berechnung nur die
Dauer jedes Events. Wir können eine andere Methode schreiben, die diese Berech-
nung mit Ganzzahlen (Typ int) ausführt. Danach können wir hasGapFor schrei-
ben und in ihr die Arbeit an die Berechnungsmethode delegieren.
Beginnen wir mit der ersten Methode. Hier ist ein Test dafür:
TEST(hasCapFor, Sequence)
{
vectorcunsigned int> baseSequence;
baseSequence.push_back(l);
baseSequence.push_back(0);
baseSequence.push_back(0);
CHECK(SequenceHasGapFor(baseSequence, pattern));
}
Diese Funktion braucht eine andere Funktion, um ein Array mit der Dauer der
Events abzurufen:
389
Kapitel 25
Techniken zur Aufhebung von Dependencies
An diesem Punkt konnten wir die Funktion zwar einfügen, aber befriedigend ist
diese Lösung nicht. Hier ist eine Liste unserer schrecklichen Vergehen:
Trotz dieser Nachteile konnten wir eine getestete Funktion hinzufügen. Mir gefällt
dieses Refactoring nicht, aber ich werde es benutzen, wenn ich mit dem Rücken
zur Wand stehe. Oft ist diese Technik eine gute Vorbereitung von Sprout Class
(6.2). Stellen Sie sich nur vor, dass Sie SequenceHasGapFor in eine Klasse
namens GapFi nder einhüllen.
25.16.1 Schritte
Primitivize Parameter funktioniert wie folgt:
1. Entwickeln Sie eine ungebundene Funktion, die die Arbeit leistet, die Sie von
der Klasse aus erledigen müssten. Verwenden Sie dabei eine vorläufige Reprä-
sentation, die das Gewünschte leistet.
2. Fügen Sie eine Funktion in die Klasse ein, die die Repräsentation erstellt und sie
an die neue Funktion delegiert.
390
25.17
»Pull Up Feature«
während die Dependencies, die Sie an einer Instanziierung der Klasse hindern,
nichts mit der Gruppe zu tun haben. Mit »nichts zu tun haben« meine ich hier,
dass die Methoden, mit denen Sie arbeiten wollen, weder direkt noch indirekt eine
der hinderlichen Dependencies referenzieren. Sie könnten mehrfach Expose Static
Method (25.5) oder Break Out Method Object (25.2) anwenden, aber das wäre nicht
unbedingt das direkteste Verfahren, die Dependencies aufzuheben.
In dieser Situation können Sie die Gruppe von Methoden, die Funktion, in eine
abstrakte Oberklasse verschieben. Wenn Sie eine solche abstrakte Oberklasse
haben, können Sie in Ihren Tests eine Unterklasse davon ableiten und Instanzen
der Unterklasse erstellen. Hier ist ein Beispiel:
391
Kapitel 25
Techniken zur Aufhebung von Dependencies
Angenommen, wir wollten getDeadTi me ändern, interessieren uns aber nicht für
updateScheduleitern. Es wäre schön, wenn wir uns gar nicht um die Depen-
dency von der Datenbank kümmern müssten. Wir könnten versuchen, getDead-
Ti me statisch zu machen und Expose Static Method (25.3) anzuwenden, aber wir
verwenden in der Scheduler-Klasse viele nicht-statische Funktionen. Break Out
Method Object (23.2) ist eine andere Möglichkeit; aber hier haben wir es mit einer
ziemlich kleinen Methode zu tun; und die Dependencies von anderen Methoden
und Feldern der Klasse machen die Arbeit aufwendiger, als uns lieb ist, nur um die
Methode unter Testkontrolle zu bringen.
Eine weitere Option besteht darin, die betreffende Methode in eine Oberklasse zu
verschieben. Dann können wir die hinderlichen Dependencies in dieser Klasse
stehen lassen, wo sie uns beim Testen nicht im Weg stehen. Danach sieht die
Klasse folgendermaßen aus:
}
private void validateCScheduleltem item)
throws ConflictException {
// Datenbank aufrufen
}
Wir haben getDeadtime (die Funktion, die wir testen wollen) und alle Funktio-
nen, die sie verwendet, in eine abstrakte Klasse verlagert.
392
25.17
»Pull Up Feature«
int result = 0;
for (Iterator it = items.iteratorO ; it.hasNextO ; ) {
Scheduleltem item = (Scheduleltem)it.nextO;
if (item.getTypeO != Scheduleltem.TRANSIENT && notShared(item)) {
result += item.getSetupTimeO + clockTime();
}
if (item.getTypeO != Scheduleltem.TRANSIENT) {
result += item.finishingTimeO;
}
eise {
result += getStandardFinish(item);
>
}
return result;
}
}
Jetzt können wir eine Testing Suhclass erstellen, mit der wir in einem Test-Harnisch
auf diese Methoden zugreifen können:
Hier haben wir die zu testenden Methoden in der Hierarchie nach oben in eine
abstrakte Oberklasse verlagert und eine konkrete Unterklasse für unsere Tests
erstellt.
Ist dies sinnvoll? Vom Standpunkt des Designs aus ist dies nicht ideal. Wir haben
einen Satz von Funktionen auf zwei Klassen verteilt, nur um das Testen zu verein-
fachen. Die Verteilung kann verwirrend sein, wenn die Beziehung zwischen den
Funktionen in den Klassen nicht sehr stark ist, was hier der Fall ist. Schedul er ist
dafür verantwortlich, Items zu planen, und SchedulingServices ist für eine
393
Kapitel 25
Techniken zur Aufhebung von Dependencies
Vielleicht fragen Sie sich, warum wir die Oberklasse abstrakt machen. Ich ziehe
es vor, sie abstrakt zu machen, weil der Code einfacher zu verstehen ist. Es ist
vorteilhaft, wenn man sich den Code einer Anwendung anschauen und sicher
sein kann, dass jede konkrete Klasse verwendet wird. Wenn Sie im Code kon-
krete Klassen finden, die an keiner Stelle instanziiert werden, könnte es sich um
»toten Code« handeln.
25.17.1 Schritte
Pull Up Feature funktioniert wie folgt:
1. Identifizieren Sie die Methoden, die Sie in eine Oberklasse verlagern wollen.
2. Erstellen Sie eine abstrakte Oberklasse der Klasse, die die Methoden enthält.
3. Kopieren Sie die Methoden in die Oberklasse und kompilieren Sie den Code.
4. Kopieren Sie jede fehlende Referenz, die der Compiler meldet, in die neue Ober-
klasse. Wenden Sie dabei Preserve Signatures (23.1) an, um die Gefahr von Feh-
lern zu verringern.
5. Wenn beide Klassen erfolgreich kompiliert werden, erstellen Sie eine Unter-
klasse der abstrakten Klasse und fügen Sie die Methoden in diese Klasse ein, die
Sie zum Testen brauchen.
394
25.18
»Push Down Dependency«
Wenn Sie Push Down Dependency anwenden, machen Sie Ihre gegenwärtige Klasse
abstrakt. Dann erstellen Sie eine Unterklasse, die Sie als neue Produktionsklasse
verwenden, und verlagern alle problematischen Dependencies in diese Klasse. An
diesem Punkt können Sie eine Unterklasse von Ihrer ursprünglichen Klasse ablei-
ten, um ihre Methoden für Tests zur Verfügung zu stellen. Hier ist ein Beispiel in
C++:
void showMessageO {
int status = AfxMessageBox(makeMessage(), MB_ABORTRETRYIGNORE);
if (status == IDRETRY) {
SubmitDialog dlg(this, "Press okay if this is a valid trade");
dlg.DoModal () ;
if (dlg.wasSubmittedO) {
g_di spatcher.undoLastSubmi ssi on();
flag = true;
}
}
el se
if (status == IDABORT) {
flag = false;
}
}
public:
OffMarketTradeValidator(Trade& trade)
: trade(trade), flag(false)
{}
395
Kapitel 25
Techniken zur Aufhebung von Dependencies
Der Code sähe nach Push Down Dependency wie folgt aus:
Virtual void s h o w M e s s a g e O = 0;
public:
OffMarketTradeVali dator(TradeS trade)
: trade(trade), flag(false) {}
bool i s V a l i d O const {
if (inRange(trade.getDateO)
&& validDestination(trade.destination)
&& inHours(trade) {
flag = true;
}
showMessageO;
return flag;
}
};
class Wi ndowsOffMarketTradeVali dator
: public OffMarketTradeValidator
{
protected:
Virtual void s h o w M e s s a g e O {
int status = AfxMessageBox(makeMessage(), MB_ABORTRETRYIGNORE);
if (status == IDRETRY) {
SubmitDialog dlg(this, "Press okay if this is a valid trade");
dlg.DoModal ();
if (dlg.wasSubmitted()) {
g_di spatcher.undoLastSubmissionO;
flag = true;
}
}
el se
if (status == IDABORT) {
flag = false;
}
}
};
Wenn wir die Ul-spezifischen Funktionen in eine neue Unterklasse (Windows-
OffMarketValidator) verlagert haben, können wir zum Testen eine weitere
Unterklasse erstellen. Wir müssen nur das showMessage-Verhalten neutralisie-
ren:
396
25.19
»Replace Function with Function Pointer«
25.18.1 Schritte
Push Down Dependency funktioniert wie folgt:
397
Kapitel 25
Techniken zur Aufhebung von Dependencies
Doch der Reihe nach. Betrachten wir einen Funktionszeiger in seiner natürlichen
Umgebung. Das folgende Beispiel zeigt die Deklaration einiger Funktionszeiger
in C sowie einige Aufrufe mit ihnen:
struct base_operations
{
double (*project) (double,double) ;
double (*maximize)(double,double) ;
};
double default_projection(double first, double second) {
return second;
}
double maximize(double first, double second) {
return first + second;
}
void init_ops(struct base_operations Operations) {
operations->project = default_projection;
operations->maximize = default_maximize;
}
void run_tesselation(struct node *base, struct base_operations O p e r a -
tions) {
double value = operations->project(base.first, base.second);
}
Mit Funktionszeigern können Sie einige sehr einfache objektbasierte Program-
mieraufgaben lösen, aber wie nützlich sind sie, um Dependencies aufzuheben?
Betrachten Sie folgendes Szenarium:
398
25.19
Replace Function with Function Pointer«
Wir könnten mit Link Substitution (23.13) neue Bodies für diese Funktionen erstel-
len, aber manchmal verursacht Link Substitution nicht-triviale Build-Änderungen.
Möglicherweise müssten wir Bibliotheken zerlegen, um die Funktionen zu isolie-
ren, die wir simulieren wollen. Doch was wichtiger ist: Die Seams, die wir mit Link
Substitution erhalten, sind nicht von der Art, die Sie benutzen wollen, um Verhal-
ten im Produktions-Code zu variieren. Wenn Sie Ihren Code unter Testkontrolle
bringen und zugleich flexibel bleiben wollen, um etwa den Typ der Datenbank zu
variieren, mit der Ihr Code kommunizieren kann, kann Replace Function with
Function Pointer nützlich sein. Hier sind die einzelnen Schritte:
Zuerst suchen wir die Deklaration der Funktion, die wir ersetzen wollen:
// db.h
void db_store(struct receive_record *record, struct time_stamp
receive_time);
// db.h
void db_store(struct receive_record -record, struct time_stamp
receive_time);
void (*db_store)(struct receive_record *record, struct time_stamp
receive_time);
// db.h
void db_store_production(struct receive_record *record, struct time_stamp
receive_time);
void (*db_store)(struct receive_record *record, struct time_stamp
receive_time);
// main.c
extern void db_store_production(struct receive_record *record, struct
time_stamp receive_time);
void initializeEnvironmentO {
db_store = db_store_production;
}
int main(int ac, char **av) {
initializeEnvi ronmentO ;
}
399
Kapitel 25
Techniken zur Aufhebung von Dependencies
Jetzt suchen wir die Definition der db_store-Funktion und benennen sie in
db_store_production um.
// db.c
void db_store_production(struct receive_record *record, struct time_stamp
receive_time) {
}
Jetzt können wir kompilieren und testen.
Wenn wir die Funktionszeiger eingeführt haben, können wir in Tests alternative
Definitionen zwecks Überwachung oder Isolierung zur Verfügung stellen.
Replace Function with Function Pointer ist eine gute Methode, um Dependencies
aufzuheben. Diese Technik hat unter anderem den Vorteil, dass alles zur Kompi-
lier-Zeit erfolgt. Deshalb ist die Auswirkung auf Ihr Build-System minimal.
Doch wenn Sie diese Technik in C verwenden, sollten Sie einen Umstieg auf
C++ erwägen, damit Sie all die anderen Seams nutzen können, die Ihnen C++
zur Verfügung stellt. Als ich dies schrieb, boten viele C-Compiler Schalter an,
mit denen Sie die Kompilierung in C und in C++ mischen konnten. Mit dieser
Funktion können Sie ein C-Projekt langsam nach C++ überführen und zunächst
nur die Dateien migrieren, deren Dependencies Sie zuerst aufheben wollen.
25.19.1 Schritte
1. Suchen Sie die Deklarationen der Funktionen, die Sie ersetzen wollen.
2. Erstellen Sie vor jeder Funktionsdeklaration gleichnamige Funktionszeiger.
3. Benennen Sie die ursprünglichen Funktionsdeklarationen so um, dass sie nicht
dieselben Namen wie die gerade deklarierten Funktionszeiger haben.
4. Initialisieren Sie die Zeiger auf die Adressen der alten Funktionen in einer C-
Datei.
5. Führen Sie einen Build durch, um die Bodies der alten Funktionen zu lokalisie-
ren. Geben Sie ihnen die neuen Funktionsnamen.
400
25-20
»Replace Global Reference with Getter«
Hier wird auf die Inventory-Klasse wie auf eine globale Komponente zugegrif-
fen. »Halt!«, höre ich Sie sagen: »Eine globale Komponente? Wird hier nicht nur
eine statische Methode einer Klasse aufgerufen?« Für unsere Zwecke zählt dies als
globale Komponente. In Java ist die Klasse selbst ein globales Objekt; und es
scheint, dass sie einen Zustand referenzieren muss, um ihre Arbeit leisten zu kön-
nen (Artikel-Objekte zurückgeben, deren Barcodes bekannt sind). Können wir
dies mit Replace Global Reference with Getier vermeiden? Versuchen wir es.
Zuerst schreiben wir den Getter. Wir deklarieren ihn als protected, damit wir ihn
beim Testen überschreiben können.
Dann ersetzen wir jeden Zugriff auf die globale Komponente durch den Getter.
401
Kapitel 25
Techniken zur Aufhebung von Dependencies
return Inventory.getlnventoryO;
}
}
Jetzt können wir eine Unterklasse von Inventory erstellen, die wir beim Testen
verwenden können. Weil Inventory ein Singleton ist, müssen wir ihren Kon-
struktor als protected und nicht als private deklarieren. Danach können wir
eine Unterklasse mit einer Logik unserer Wahl von Inventory ableiten, um beim
Testen Barcodes in Artikel umzuwandeln.
2 5 . 2 0 . 1 Schritte
402
25-21
Ihr liegt die Idee zugrunde, dass man in Tests per Vererbung Verhalten selektiv
auswählen oder ausschalten kann, das im jeweiligen Kontext relevant bzw. nicht
relevant ist.
class MessageForwarder
{
private Message createForwardMessage(Session session, Message message)
throws MessagingException, IOException {
MimeMessage forward = new MimeMessage (session);
forward.setFrom (getFromAddress (message));
forward.setReplyTo (new Address [] {
new InternetAddress (1istAddress)
});
forward.addRecipients (Message.RecipientType.TO, 1istAddress);
forward.addReci pi ents (Message.Reci pi entType.BCC,
getMai 1 ListAddresses ());
forward.setSubject (transformedSubject (message.getSubject ()));
forward.setSentDate (message.getSentDate ());
forward.addHeader (L00P_HEADER, listAddress);
buildForwardContent(message, forward);
return forward;
}
}
MessageForwarder enthält zahlreiche weitere Methoden, die hier nicht gezeigt
werden. Eine der öffentlichen Methoden ruft diese private Methode, createFor-
wardMessage, auf, um eine neue Message zu erstellen. Angenommen, wir wollten
beim Testen keine Dependency von der Mi meMessage-Klasse haben. Sie verwendet
eine Variable namens session, und wir wollen beim Testen keine echte Session
(Sitzung) öffnen. Wenn wir die Dependency von Mi meMessage isolieren möchten,
können wir createForwardMessage als protected deklarieren und sie in einer
neuen Unterklasse überschreiben, die wir nur zum Testen erstellen:
In dieser neuen Unterklasse können wir alle Schritte durchführen, für die Über-
wachung oder Isolierung des Codes erforderlich sind. In diesem Fall blockieren
wir im Wesentlichen die meisten Funktionen von createForwardMessage, die
beim Testen nicht benötigt werden, was in Ordnung ist.
403
Kapitel 25
Techniken zur Aufhebung von Dependencies
Im Allgemeinen bestimmt die Zerlegung der Funktionen einer Klasse, wie gut Sie
Dependencies per Vererbung isolieren können. Manchmal ist eine Dependency,
die Sie aufheben möchten, in einer kleinen Methode isoliert. Manchmal müssen
Sie eine größere Methode überschreiben, um eine Dependency zu isolieren.
Subclass and Override Method ist eine leistungsstarke Technik; aber Sie müssen
sorgfältig vorgehen. In dem vorhergehenden Beispiel kann man eine leere Mes-
sage ohne Betreff, von-Adresse usw. zurückgeben; dies wäre aber nur sinnvoll,
wenn man etwa testen will, ob eine Message überhaupt von einer Stelle der Soft-
ware an eine andere übertragen werden kann und ihr eigentlicher Inhalt in die-
sem Kontext keine Rolle spielt.
Für mich ist Programmieren hauptsächlich eine visuelle Tätigkeit. Ich sehe beim
Arbeiten alle möglichen Bilder vor meinem geistigen Auge; sie helfen mir, geeig-
nete Alternativen auszuwählen. Leider sind die Bilder kein reines UML, aber sie
helfen.
Besonders häufig schwebt mir ein Bild vor, das ich als Paper View (Papierdarstel-
lung) bezeichne. Wenn ich mir eine Methode anschaue, sehe ich mögliche Anord-
nungen der Anweisungen und Ausdrücke. Bei fast jedem kleinen Code-Fragment
in einer Methode erkenne ich, ob ich es in eine Methode extrahieren kann oder ob
ich es beim Testen durch etwas anderes ersetzen kann. Es ist vergleichbar damit,
als hätte ich ein durchsichtiges Blatt Papier über das Blatt mit dem Code gelegt.
Das neue Blatt kann anstelle des Fragments, das ich ersetzen will, ein anderes
404
25-22
Code-Fragment zeigen. Der Papierstapel ist das, was ich teste, und die Methoden,
die ich durch das oberste Blatt hindurch sehe, sind diejenigen, die ich beim Testen
ausführen kann. Abbildung 25.6 ist ein Versuch, diese Paper View einer Klasse dar-
zustellen.
Die Paper View hilft mir zu erkennen, was möglich ist; aber wenn ich den Einsatz
von Subclass and Override Method erwäge, versuche ich, Methoden zu überschrei-
ben, die bereits existieren. Schließlich besteht das Ziel darin, Tests einzurichten;
und Methoden ohne zuvor eingerichtete Tests zu extrahieren, kann gefährlich
sein.
25.21.1 Schritte
Subclass and Override Method funktioniert wie folgt:
1. Identifizieren Sie die Dependencies, die Sie isolieren möchten, oder die Stelle,
an der Sie den Code überwachen wollen. Versuchen Sie, den kleinstmöglichen
Satz von Methoden zu finden, die Sie überschreiben können, um Ihre Ziele zu
erreichen.
2. Machen Sie jede Methode überschreibbar. Das entsprechende Verfahren hängt
von der Programmiersprache ab. In C++ müssen die Methoden (falls noch
erforderlich) als vi rtual deklariert werden. In Java müssen die Methoden non-
final sein. In vielen .NET-Sprachen müssen Sie die Methode ebenfalls ausdrück-
lich als überschreibbar deklarieren.
3. Falls Ihre Sprache dies erfordert, passen Sie die Sichtbarkeit der zu überschrei-
benden Methoden so an, dass sie in einer Unterklasse überschrieben werden
können. In Java und C# müssen die Methoden wenigstens die Sichtbarkeit pro-
tected haben, damit dies möglich ist. In C++ können Methoden pri vate blei-
ben und dennoch in Unterklassen überschrieben werden.
4. Erstellen Sie eine Unterklasse, die die Methoden überschreibt. Prüfen Sie, ob
Sie sie in Ihrem Test-Harnisch kompilieren können.
405
Kapitel 25
Techniken zur Aufhebung von Dependencies
Das folgende Beispiel demonstriert das Problem der virtuellen Funktion in C++:
class Pager
{
public:
PagerO {
resetO;
formConnectionO;
}
Virtual void f o r m C o n n e c t i o n O {
assert(state == READY);
// Code, der mit der Hardware kommuniziert
};
Bei diesem Beispiel wird die formConnection-Methode in dem Konstruktor auf-
gerufen. Es ist nichts daran auszusetzen, dass Konstruktoren Arbeit an andere
Funktionen delegieren, aber dieser Code führt in die Irre. Die formConnection-
Methode ist als virtuelle Methode deklariert; deshalb scheint es, als könnten wir
einfach Subclass and Override Method (25.21) einsetzen. Nicht so schnell. Probieren
wir es aus:
};
TEST(messagi ng,Pager)
{
TestingPager pager;
pager.sendMessage("5551212", "Hey, wanna go to a party? XXXOOO");
LONGS_EQUAL(OKAY, pager.getStatus());
}
Wenn wir eine virtuelle Funktion in C++ überschreiben, ersetzen wir erwartungs-
gemäß das Verhalten dieser Funktion in abgeleiteten Klassen, aber mit einer Aus-
nahme: Wird in einem Konstruktor eine virtuelle Funktion aufgerufen, lässt die
Sprache ein Überschreiben nicht zu. In diesem Beispiel bedeutet dies: Wenn
sendMessage aufgerufen wird, wird TestingPager: :formConnection verwen-
det, und das ist großartig: Schließlich wollen wir dem Operator keine flippige Seite
406
25-22
senden, aber leider ist dies bereits passiert. Bei der Konstruktion von Testi ngPa-
ger wurde bei der Initialisierung Page: :formConnection aufgerufen, weil C++
die Überschreibung in dem Konstruktor nicht erlaubt.
class A
{
public:
A() {
someMethodO;
}
V i r t u a l void s o m e M e t h o d O {
}
};
class B : public A
{
C *c;
public:
BO {
c = new C;
}
Virtual void s o m e M e t h o d O {
c.doSomethingO;
}
};
Hier überschreibt someMethod von B die gleichnamige virtuelle Funktion von A.
Doch beachten Sie die Reihenfolge der Konstruktor-Aufrufe: Wenn wir ein B
erstellen, wird der Konstruktor von A vor dem von B aufgerufen. Der Konstruktor
von A ruft die Methode someMethod auf. Da someMethod überschrieben ist, wird
die Methode in B verwendet. Diese versucht, mit einer Referenz von Typ C die
Methode doSomethi ng aufzurufen. Jetzt dürfen Sie raten: C wurde noch nicht ini-
tialisiert, weil der Konstruktor von B noch nicht ausgeführt worden ist.
C++ verhindert, dass dies passiert. Andere Sprachen sind freizügiger. In Java kön-
nen etwa überschriebene Methoden von Konstruktoren aus aufgerufen werden;
ich rate aber von solchen Aufrufen im Produktions-Code ab.
407
Kapitel 25
Techniken zur Aufhebung von Dependencies
BlendingPen: :BlendingPen()
{
setName("BlendingPen");
nuparam = ParameterFactory::createParameter("cm", "Fade", "Aspect Alter");
m_param->addChoice("blend");
m_param->addChoice("add");
m_param->addChoice("filter");
setParamByName("cm", "blend");
}
Hier erstellt ein Konstruktor mit einer Factory einen Parameter. Mit Introduce
Static Setter (25.12) könnten wir eine gewisse Kontrolle über das nächste Objekt
bekommen, das die Factory zurückgibt, aber dies wäre ziemlich tiefgreifend.
Wenn eine zusätzliche Methode in der Klasse keine Rolle spielt, können wir den
Parameter ersetzen, den wir in dem Konstruktor erstellt haben:
In Tests können wir bei Bedarf Pens erstellen und supersedeParameter aufru-
fen, wenn wir ein Überwachungsobjekt einfügen müssen.
Oberflächlich sieht Supersede Instance Variable (25.22) wie eine minderwertige
Methode aus, um ein Überwachungsobjekt einzufügen; doch diese Technik kann
in C++ die beste Wahl sein, wenn Parameterize Constructor (25.14) wegen der ver-
schlungenen Logik in dem Konstruktor zu umständlich ist. In Sprachen, bei
denen virtuelle Aufrufe in Konstruktoren zugelassen sind, ist Extract and Override
Factory Method (25.7) normalerweise eine bessere Wahl.
Das Wort supersede (dt. ersetzen) hat, als Methodenpräfix verwendet, einen Vorteil:
Es ist auffällig und ungebräuchlich. Mit einer schnellen Suche können Sie prüfen,
ob Nutzer im Produktions-Code diese Methoden verwenden.
408
25.23
»Template Redefinition«
25.22.1 Schritte
Supersede Instance Variable funktioniert wie folgt:
1. Identifizieren Sie die Instanzvariable, die Sie ersetzen wollen.
2. Erstellen Sie eine Methode namens supersedeXXX, wobei XXX den Namen der
Variablen bedeutet, die Sie ersetzen wollen.
3. Fügen Sie in die Methode den Code ein, der erforderlich ist, um die vorherge-
hende Instanz der Variablen zu zerstören und auf den neuen Wert zu setzen.
Wenn die Variable eine Referenz ist, prüfen Sie, ob die Klasse keine anderen
Referenzen des Objekts enthält. Gibt es solche Referenzen, müssen Sie mögli-
cherweise die ersetzende Methode zusätzlich bearbeiten, damit das Objekt
sicher und ohne unerwünschte Nebenwirkungen ersetzt wird.
// AsyncReceptionPort.h
class AsyncReceptionPort
{
private:
CSocket m_socket;
Packet m_packet;
int m_segmentSize;
public:
AsyncReceptionPortO;
void Run();
};
// AsynchReceptionPort.cpp
void AsyncReceptionPort::Run() {
for(int n = 0; n < m_segmentSize; ++n) {
int bufferSize = m_bufferMax;
if (n = m_segmentSize - 1)
bufferSize = m_remainingSize;
m_socket.receive(m_receiveBuffer, bufferSize);
m_packet.mark();
409
Kapitel 25
Techniken zur Aufhebung von Dependencies
m _ p a c k e t . a p p e n d ( m _ r e c e i v e B u f f e r , b u f f e r S i ze);
m_packet.pack();
}
m_packet. f i nal i ze() ;
}
Wenn wir in einem solchen Code die Logik in der Methode ändern wollen, stoßen
wir auf folgendes Problem: Wir können die Methode nicht in einem Test-Harnisch
ausführen, ohne etwas über ein Socket zu senden. In C++ können wir dies kom-
plett vermeiden, indem wir AsyncRecepti onPort als Template und nicht als nor-
male Klasse definieren. Nach dieser Änderung sieht der Code wie folgt aus. (Die
Schritte dorthin werden gleich beschrieben.)
// AsynchReceptionPort.h
public:
AsyncReceptionPortlmpl();
voi d Run();
};
templatectypename S0CKET>
void AsyncReceptionPortlmpl<S0CKET>::Run() {
for(int n = 0; n < m_segmentSize; ++n) {
int bufferSize = m_bufferMax;
if (n = m_segmentSize - 1)
bufferSize = m_remainingSize;
m_socket.receive(m_receiveBuffer, bufferSize);
m_packet.mark();
m_packet.append(m_receiveBuffer,bufferSi ze) ;
m_packet.pack();
}
m_packet.fi nali ze();
}
typedef AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;
Nach dieser Änderung können wir das Template in der Testdatei mit einem ande-
ren Typ instanziieren:
// TestAsynchReceptionPort.cpp
#include "AsyncReceptionPort.h"
410
25.23
»Template R e d e f l a t i o n «
class FakeSocket
{
public:
void receive(char *, int size) { ... }
};
TEST(Run.AsyncReceptionPort)
{
AsyncReceptionPortImpl<FakeSocket> port;
}
Das Beste an dieser Technik ist die Tatsache, dass wir mit einer typedef vermei-
den können, Referenzen in unserer ganzen Code-Basis zu ändern. Ohne sie müss-
ten wir jede Referenz von AsyncReceptionPort durch AsyncReception-
Port<CSocket> ersetzen. Dies wäre eine umfangreiche mühsame Arbeit, aber sie
ist einfacher, als es scheint. Mit Lean on the Compiler (23.4) können wir dafür sor-
gen, dass alle einschlägigen Referenzen geändert werden. In Sprachen, die über
Generics, aber keinen Typ-Aliasing-Mechanismus wie etwa typedef verfügen,
müssen Sie Lean on the Compiler verwenden.
In C++ können Sie mit dieser Technik alternative Definitionen von Methoden statt
von Daten zur Verfügung stellen, aber sie ist ein wenig chaotisch. Die Regeln von
C++ zwingen Sie, einen Template-Parameter zu verwenden, damit Sie einer Vari-
ablen einen beliebigen Template-Parameter als Typ zuweisen können, oder eine
neue Variable einzuführen, nur um die Klasse mit einem Typ zu parametrisieren
- aber dies wäre nur meine letzte Maßnahme. Zuerst prüfe ich sehr sorgfältig, ob
ich die vererbungsbasierten Techniken verwenden kann.
Template Redefinition hat in C++ einen wesentlichen Nachteil. Wenn Ihr C++-
Compiler nicht über eine sehr gute Template-Unterstützung verfügt, kann
Code, der diese Technik implementiert, die Dependencies in Systemen verstär-
ken. Nutzer der Template sind dann gezwungen, den Code jedes Mal neu zu
kompilieren, wenn der Template-Code geändert wird.
Im Allgemeinen bevorzuge ich vererbungsbasierte Techniken, um in C++
Dependencies aufzuheben. Doch Template Redefinition kann nützlich sein, wenn
sich die aufzuhebenden Dependencies in Code befinden, der bereits mit Tem-
plates arbeitet. Hier ist ein Beispiel:
template<typename ArcContact> class CollaborationManager
{
ContactManager<ArcContact> m_contactManager;
};
411
Kapitel 25
Techniken zur Aufhebung von Dependencies
ArcContactManager m_contactManager;
};
25.23.1 Schritte
Hier ist eine Beschreibung von Template Redefinition in C++. In anderen Sprachen,
die Generics unterstützen, können die Schritte anders aussehen, aber diese
Beschreibung vermittelt ein Gefühl für die Technik:
1. Identifizieren Sie die Funktionen, die Sie in der zu testenden Klasse ersetzen
wollen.
2. Machen Sie aus der Klasse ein Template, parametrisieren Sie es mit den Varia-
blen, die Sie ersetzen müssen, und kopieren Sie die Methoden-Bodies in die
Header-Datei.
3. Geben Sie dem Template einen anderen Namen. Mechanisch können Sie etwa
ein Suffix wie Impl an den ursprünglichen Namen anhängen.
4. Fügen Sie nach der Template-Definition eine typedef-Anweisung ein, in der
das Template mit seinen ursprünglichen Argumenten mit dem ursprünglichen
Klassennamen definiert wird.
5. Binden Sie die Template-Definition in die Testdatei ein und instanziieren Sie
das Template mit neuen Typen, die in Tests die vorhandenen Typen ersetzen sol-
len.
# Account.rb
class Account
def report_deposit(value)
end
412
25.24
»Text Redefinition«
def deposit(value)
©balance += value
report_deposi t(value)
end
def withdraw(value)
©balance -= value
end
end
# AccountTest.rb
require "runit/testcase"
requi re "Account"
class Account
def report_deposit(value)
end
end
end
Wichtig ist, dass wir hier nicht die gesamte Account-Klasse umdefinieren, sondern
nur die report_deposi t-Methode. Der Ruby-Interpreter interpretiert alle Zeilen
in einer Ruby-Datei als ausführbare Anweisungen. Die cl ass Account-Anweisung
öffnet die Definition der Account-Klasse so, dass zusätzliche Definitionen in sie
eingefügt werden können. Die Anweisung def report_deposi t(val ue) beginnt
den Prozess, mit dem eine Definition zu der open-Klasse hinzugefügt wird. Für
den Ruby-Interpreter spielt es keine Rolle, ob diese Methode bereits definiert ist. Ist
dies der Fall, ersetzt er einfach die vorhandene Definition.
Text Redefinition in Ruby hat einen Nachteil: Die neue Methode ersetzt die alte,
bis das Programm endet. Dies kann Probleme verursachen, wenn Sie vergessen,
dass eine bestimmte Methode von einem vorhergehenden Test umdefiniert wor-
den ist.
Text Redefinition ist auch in C und C++ möglich, wenn der Präprozessor verwen-
det wird. Ein Beispiel für die entsprechende Vorgehensweise finden Sie unter
Preprocessing Seam (4.3) in Kapitel 4, Das Seam-Modell.
413
Kapitel 25
Techniken zur Aufhebung von Dependencies
25.24.1 Schritte
Text Redefinition funktioniert in Ruby wie folgt:
1. Identifizieren Sie eine Klasse mit Definitionen, die Sie ersetzen wollen.
2. Fügen Sie am Anfang der Test-Quelldatei eine requi re-Klausel mit dem
Namen des Moduls hinzu, das diese Klasse enthält.
3. Stellen Sie am Anfang der Test-Quelldatei für jede Methode, die Sie ersetzen
wollen, alternative Definitionen zur Verfügung.
Anhang A
Refactoring
Refactoring ist eine grundlegende Technik zur Verbesserung von Code. Die maß-
gebende Referenz für das Refactoring ist das Buch Refactoring: Improving the
Design of Existing Code (Addison-Wesley, 1999) von Martin Fowler. Wenn Sie
Näheres über verschiedene Arten des Refactorings wissen wollen, sollten Sie die-
ses Buch lesen.
Bei einer schlecht gepflegten Code-Basis werden Methoden im Laufe der Zeit oft
größer, weil immer wieder neue Logik zu vorhandenen Methoden hinzugefügt
wird. Dies führt dazu, dass Methoden oft zwei oder drei, in pathologischen Fäl-
len sogar mehrere Dutzend oder gar Hunderte verschiedene Funktionen für
ihre Aufrufer erledigen. In diesen Fällen ist Extract Method die Lösung des Pro-
blems.
Wenn Sie eine Methode extrahieren wollen, brauchen Sie zunächst einen Satz von
Tests. Wenn Sie über Tests verfügen, die eine Methode gründlich prüfen, können
Sie Methoden wie folgt extrahieren:
1. Identifizieren Sie den Code, den Sie extrahieren wollen, und kommentieren Sie
ihn aus.
2. Wählen Sie einen Namen für die neue Methode und erstellen Sie sie als leere
Methode.
3. Fügen Sie in die alte Methode einen Aufruf der neuen Methode ein.
415
Anhang A
Refactoring
4. Kopieren Sie den Code, den Sie extrahieren wollen, in die neue Methode.
5. Finden Sie mit Lean On the Compiler (23.4) heraus, welche Parameter Sie über-
geben und welche Werte Sie zurückgeben müssen.
6. Passen Sie (falls erforderlich) die Methodendeklaration an die Parameter und
Rückgabewerte an.
7. Führen Sie Ihre Tests aus.
8. Löschen Sie den auskommentierten Code.
Hier ist ein einfaches Beispiel in Java:
416
A.i
Extract Method
Wir wollen die neue Methode getPremi umFee nennen; deshalb fügen wir die
neue Methode und ihren Aufruf wie folgt hinzu:
Als Nächstes kopieren wir den alten Code in die neue Methode und prüfen, ob er
kompiliert wird:
Er wird nicht kompiliert. Der Code verwendet die Variablen resul t und amount,
die nicht deklariert sind. Weil wir nur einen Teil des Ergebnisses berechnen, brau-
chen wir auch nur das Berechnete zurückgeben. Wir können auch auf den amount
417
Anhang A
Refactoring
zugreifen, wenn wir ihn zu einem Parameter der Methode machen und zu dem
Aufruf hinzufügen:
Jetzt können wir unsere Tests ausführen und prüfen, ob der Code noch funktio-
niert. Ist dies der Fall, können wir zurückgehen und den auskommentierten Code
löschen:
418
A.i
Extract Method
Obwohl es nicht unbedingt erforderlich ist, kommentiere ich Code, den ich
extrahieren möchte, vorher zunächst aus. So kann ich, falls ich einen Fehler
mache und ein Test scheitert, leicht zu dem vorhandenen Code zurückgehen,
den Fehler korrigieren und es dann noch einmal versuchen.
Dieses Beispiel ist nur eine Möglichkeit, Extract Method anzuwenden. Wenn Sie
Tests haben, ist diese Operation relativ einfach und sicher. Mit einem Refactoring-
Tool ist sie sogar noch einfacher. Dann müssen Sie nur einen Teil der Methode
auswählen und eine Menüauswahl treffen. Das Tool prüft, ob dieser Code als
Methode extrahiert werden kann, und fordert Sie zur Eingabe des Namens der
neuen Methode auf.
Extract Method ist eine grundlegende Technik der Arbeit mit Legacy Code. Sie kön-
nen damit lange Methoden zerlegen, duplizierten Code extrahieren oder Verant-
wortlichkeiten trennen.
x'9
Anhang B
Glossar
Abfangpunkt (engl, interception point) - Eine Stelle, an der man einen Test schreiben kann,
u m bestimmte Eigenschaften von Software zu überwachen.
Abschnürung (engl, pinch point) - Eine Verengung in einer Effektskizze, die eine ideale Stelle
anzeigt, u m eine Gruppe von Funktionen zu testen.
Änderungspunkt (engl, change point) - Eine Stelle im Code, die Sie ändern müssen,
Charakterisierungstest (engl, characterization test) - Ein Test, der geschrieben wird, u m das
gegenwärtige Verhalten einer Software-Komponente zu dokumentieren und zu erhalten,
wenn m a n ihren Code ändert.
Effektskizze (engl, effect sketch) - Eine kleine Handskizze, die zeigt, welche Variablen und
Methoden-Rückgabewerte von einer Software-Änderung beeinflusst werden. Wirkungsskiz-
zen können nützlich sein, wenn man überlegt, an welchen Stellen man Tests einfügen sollte.
Freie Funktion (engl, free function) - Eine Funktion, die nicht zu einer Klasse gehört. In C und
anderen prozeduralen Sprachen werden sie schlicht als Funktionen bezeichnet. In C++ werden
sie als Non-member-Funktionen bezeichnet. In Java und C # gibt es keine freien Funktionen.
Funktionsskizze (engl, feature sketch) - Eine kleine Handskizze, die zeigt, wie Methoden in
einer Klasse andere Methoden und Instanzvariablen verwenden. Funktionsskizzen können
nützlich sein, wenn Sie Kriterien suchen, u m eine größere Klasse zu zerlegen.
Kopplungszahl (engl, coupling count) - Die Anzahl der Werte, die bei dem Aufruf einer
Methode übergeben und zurückgegeben werden. Wenn es keine Rückgabewerte gibt, ist es die
Anzahl der Parameter. Gibt es einen Rückgabewert, ist es die Anzahl der Parameter plus eins.
Die Berechnung der Kopplungszahl kann bei kleinen Methoden sehr nützlich sein, die man
gerne extrahieren möchte, aber den Code ohne Tests extrahieren muss.
421
Anhang B
Glossar
Link Seam - Eine Stelle, an der man das Verhalten variieren kann, indem man eine Bibliothek
einbindet. In kompilierten Sprachen kann man beim Testen Produktionsbibliotheken, DLLs,
Assemblies oder JAR-Dateien durch andere ersetzen, u m Dependencies aufzuheben oder
bestimmte Eigenschaften zu überwachen.
Mock-Objekt (engl, mock object) - Ein simuliertes Objekt (eine Attrappe), das in Unit-Tests als
Platzhalter für ein echtes Objekt verwendet werden kann und dessen Bedingungen/Funktio-
nen intern zur Verfügung stellt.
Objekt-Seam (engl, object seam) - Eine Stelle, an der man Verhalten variieren kann, indem
man ein Objekt durch ein anderes ersetzt. In objektorientierten Sprachen kann man dies nor-
malerweise machen, indem man eine Unterklasse einer Klasse des Produktionscodes bildet
und verschiedene Methoden der Klasse überschreibt.
Platzhalter-Objekt (engl, fake object) - Ein Objekt, das beim Testen einen Kollaborateur einer
Klasse vertritt.
Seam (dt. wörtl. Naht, Saum) - Eine Stelle, an der man das Verhalten eines Softwaresystems
variieren kann, ohne den Code an dieser Stelle zu bearbeiten. So ist etwa ein Aufruf einer poly-
morphen Funktion eines Objekts eine Naht, weil man eine Unterklasse der Klasse des Objekts
mit einem anderen Verhalten bilden kann.
Testabdeckung (engl, test coverage) - »Eine Klasse durch Tests abdecken« bedeutet, Testfälle
f ü r die Klasse zu schreiben.
Test-Unterklasse (engl, testing subclass) - Eine Unterklasse, die erstellt wird, u m für Testzwe-
cke den Zugriff auf eine Klasse zu ermöglichen.
Unit-Test (engl, unit test, wörtl. Einheiten-Test) - Ein Test, der in weniger als i / i o Sekunde
ausgeführt wird und klein genug ist, u m Ihnen zu helfen, Probleme zu lokalisieren, wenn er
scheitert.
422
Stich wortverzeich n is
A Aufzählungsmethode 302
Abdeckung 37 Ausführungsdauer 37
Abfangpunkt 1 9 4 , 1 9 7 , 421 Automatisiertes Refactoring 70
Abhängigkeit Siehe Dependency
Abhängigkeitsumkehrung 107 B
Abschnürung 421 Beck, Kent 72, 238
Abstraktionsebene 267 Bibliothek
Adapt Parameter 164, 335, 338 Dependency 217
Aktivierungspunkt 60 fremde Bibliothek einsetzen 219
Alias-Parameter 154 Schnittstellen 217
Änderung Break Out Method Object 315, 339
Angst 109 Bug 29
Auswirkungen studieren 230 Build Dependencies 104
Dauer 101 Bulleted method 302
Dependencies aufheben 103
Neukompilierung 107 C
Resignation 1 0 9 C# 76
Verständlichkeit 101 C++
Verzögerungszeit 102 Test-Harnisch 149
von Legacy Code 227 change point 421
Änderungspunkt 4 3 , 1 8 1 , 421 Charakterisierungs-Test 173,179, 206
Angst Heuristik 215
vor Änderungen 3 2 , 1 0 9 Charakterisierungstest 421
Anwendung Class, Responsibility and Collaborations
Struktur erkennen 233 (CRC) 239
API-Aufrufe Code 319
in Legacy-Projekten 219 ändern 81
Arbeit mit Legacy Code Änderungsaufwand 101
Tools 69 Aufgaben identifizieren 223
Arbeitsmoral 331 duplizierten Code entfernen i n , 114
Architektur 233 duplizierter Code 283
von JUnit 235 editieren 319
Aufgabe Funktionsbeschreibung 222
beim Design erkennen 265 kleine Fragmente extrahieren 318
einer Klasse 262 Open/Closed-Prinzip 300
mit Heuristiken identifizieren 278 prozeduraler Code 248
wie erkennen? 264 refaktorisieren 115
Aufgaben Seams 55
trennen 229 Skizzen anfertigen 228
Aufgabenbasierte Extraktion 225 ungenutzten Code löschen 231
Aufruf unklarer 222
extrahieren 356
Stichwortverzeichnis
424
Stichwortverzeichnis
C J
Gamma, Erich 72 Jeffries, Ron 77, 239
Gesprächsbeobachtung 241 JUnit 73
Getter Architektur 235
extrahieren 360
globale Referenz ersetzen 4 0 0 K
Gleaning Dependencies 314 Kernlogik
Gleichnamige Methoden 387 eines Systems 224
Globale Dependency 140 Klasse
Globale Referenz 347 Abstraktionsebenen 267
durch Getter ersetzen 4 0 0 Aufgabe 262
Globals 377 benennen 243
Green-Field-System 329 charakterisieren 209
Große Klassen Dependencies aufheben 127
Refactoring 262 Größe als Testproblem 261
Hauptaufgabe suchen 274
H Hauptzweck 262
Haskell 189 in Test-Harnisch einfügen 127
Hauptzweck
425
Stichwortverzeichnis
426
Stichwortverzeichnis
427
Stichwortverzeichnis
s Software-Zwinge 34
Scheinobjekt 51 Sprout Class 87, 2 6 2
Scheme 1 8 9 Nachteile 91
Schnittstelle Schritte 9 0
benennen 3 6 9 Vorteile 91
extrahieren 104, 368 Sprout Method 83, 2 6 2
und Dependencies 1 0 7 Nachteile 86
Schnittstellenebene 274 Schritte 85
Scratch Refactoring 230, 277, 278 Vorteile 86
Seam 54, 4 2 2 SRP 262, 274
Aktivierungspunkt 6 0 Implementierungsebene 274
Arten 57 Schnittstellenebene 274
Definition 55 Static Cling 375
Enabling Point 6 0 Statische Methode 353, 374
Seam-Modell 53 Statischer Setter 376
sensing 45 Strategie
Separation 45 Refactoring 316
Sequenzen suchen 316 Subclass and Override Method 1 4 5 , 1 5 7 , 1 7 0 ,
Setter 4 0 8 402
Statischer Setter 376 Supersede Instance Variable 138, 405
Shadowing 2 8 0 System
Signatur Architektur 233
bewahren 322 Architektur vereinfachen 235
Single-Goal Editing 321 Geschichte erzählen 234
Single-Responsibility Prinzip (SRP) 2 6 2 , 274 im Team analysieren 235
Singleton 2 1 8 Kernlogik 224
Singleton Design Pattern 141, 377 mit API-Aufrufen verbessern 2 1 9
Skelettierung 316
Skin and Wrap the API 165, 224, 2 2 6 T
Skizze TDD 4 4 , 1 1 0
von Code 228 und Legacy Code 115
von Effekte 175 T D D (Test-Driven Development) 4 2 2
Snarled method 303 TDD-Algorithmus 1 1 6
Software Teamarbeit
ändern 27 Systeme analysieren 235
Änderungen im Überblick 30 Technik der kleinen Schritte 44
Angst vor Änderungen 32 Template
Dependency-Probleme 4 0 umdefinieren 4 0 9
Design verbessern 29 Template Redefinition 4 0 9
Fehler 29 in C++ 4 1 1
Fehler beseitigen 28 Test
Funktion hinzufugen 27 Abdeckung 37
Optimierung 30 Ausführungsdauer 37
Qualität 177 Ausführungsebene 39
Refactoring 29, 6 9 automatisiertes Refactoring 70
Risiken beim Ändern 31 Charakterisierungs-Test 2 0 6
Risikomanagement 32 Failing Test kompilieren 1 1 2 , 1 1 3
Verhalten 28 Failing Test schreiben i n , 112
S oftware- Entwicklung Fehlen in Legacy Code 1 0 9
Dependencies 217 Fehlerlokalisierung 36, 37
Zeit sparen 2 1 9 f ü r eine Klasse schreiben 193
428
Stichwortverzeichnis
429
ISBN 978-3-8266-9023-5 ISBN 978-3-8266-5072-7
• Lesbare, wartbare und zuverlässige • Weiterentwicklung bestehender Systeme
Tests entwickeln ohne vorhandene Tests
• Stubs, Mock-Objekte und automatisierte • Alternde Systeme mit Tests absichern
Frameworks • Methoden zur Verbesserung der
• Einsatz von .NET-Tools inkl. NUnit, Softwarequalität
Rhino Mocks und Typemock Isolator
www.mitp.de