Sie sind auf Seite 1von 434

Effektives Arbeiten mit Legacy Code

Michael Feathers

Effektives Arbeiten mit


Legacy Code
Refactoring und Testen
bestehender Software

Übersetzung aus dem Amerikanischen


von Reinhard Engel

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.

Bei der Herstellung des Werkes haben wir uns zukunftsbewusst f ü r


umweltverträgliche und wiederverwertbare Materialien entschieden.
Der Inhalt ist auf elementar chlorfreiem Papier gedruckt.

ISBN 978-3-8266-9021-1
1. Auflage 2 0 1 1

E-Mail: kundenbetreuung@hjr-verlag.de

Telefon: +49 89/2183-7928


Telefax: +49 89/2183-7620

www.mitp.de

© 2 0 1 1 mitp, eine Marke der Verlagsgruppe Hüthig Jehle R e h m G m b H


Heidelberg, München, Landsberg, Frechen, H a m b u r g

Dieses Werk, einschließlich aller seiner Teile, ist urheberrechtlich geschützt.


Jede Verwertung außerhalb der engen Grenzen des Urheberrechtsgesetzes ist
ohne Z u s t i m m u n g des Verlages unzulässig und strafbar. Dies gilt insbesondere
f ü r Vervielfältigungen, Übersetzungen, Mikroverfilmungen und die
Einspeicherung und Verarbeitung in elektronischen Systemen.

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 .

Lektorat: Sabine Schulz


Sprachkorrektorat: Petra Heubach-Erdmann
Satz: III-satz, Husby, www.drei-satz.de
Druck: Beltz Druckpartner G m b H und Co. KG, H e m s b a c h
Cover: © L11X2008 - fotolia.de
Für Ann, Deborah und Ryan,
die strahlenden Zentren meines Lebens.
- Michael
Inhaltsverzeichnis

Vorwort 13

Geleitwort 15

Danksagungen 21

Einführung - Wie man dieses Buch lesen sollte 23

Teil I Wie Wandel funktioniert 25

1 Software ändern 27
1.1 Vier Gründe, Software zu ändern 27
1.2 Riskante Änderungen 31

2 Mit Feedback arbeiten 33


2.1 Was sind Unit-Tests? 36
2.2 Higher-Level-Tests 39
2.3 Testabdeckung 39
2.4 Der Algorithmus zur Änderung von Legacy Code 42

3 Überwachung und Trennung 45


3.1 Kollaborateure simulieren 47

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

Teil II Software ändern 79

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

7 Änderungen brauchen eine Ewigkeit 101


7.1 Verständlichkeit 101
7.2 Verzögerungszeit 102
7.3 Dependencies aufheben 103
7.4 Zusammenfassung 108

8 Wie fuge ich eine Funktion hinzu? 109


8.1 Test-Driven Development (TDD) 110
8.2 Programming by Difference 116
8.3 Zusammenfassung 125

9 Ich kann diese Klasse nicht in einen Test-Harnisch einfügen 127


9.1 Der Fall des irritierenden Parameters 127
9.2 Der Fall der verborgenen Dependency 134
9.3 Der Fall der verketteten Konstruktionen 138
9.4 Der Fall der irritierenden globalen Dependency 140
9.5 Der Fall der schrecklichen Include-Dependencies 148
9.6 Der Fall der Zwiebel-Parameter 152
9.7 Der Fall des Alias-Parameters 154

10 Ich kann diese Methode nicht in einem Test-Harnisch ausführen . . . 159


10.1 Der Fall der verborgenen Methode 159
10.2 Der Fall der »hilfreichen« Sprachfunktion 163
10.3 Der Fall des nicht erkennbaren Nebeneffekts 166

11 Ich muss eine Änderung vornehmen. Welche Methoden


sollte ich testen? 173
11.1 Effekte analysieren 173
11.2 Vorwärtsanalyse (Reasoning Forward) 179
11.3 Effektfortpflanzung (Effect Propagation) 184
11.4 Tools für Effektanalysen 186
11.5 Von der Effektanalyse lernen 188
11.6 Effektskizzen vereinfachen 189

8
Inhaltsverzeichnis

12 Ich muss in einem Bereich vieles ändern. Muss ich


die Dependencies für alle beteiligten Klassen aufheben? 193
12.1 Abfangpunkte 194
12.2 Ein Design mit Einschnürpunkten beurteilen 201
12.3 Fallen bei Einschnürpunkten 203

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

14 Dependencies von Bibliotheken bringen mich um 217

15 Meine Anwendung besteht nur aus API-Aufrufen 219

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

17 Meine Anwendung hat keine Struktur 233


17.1 Die Geschichte des Systems erzählen 234
17.2 Naked CRC 238
17.3 Conversation Scrutiny 241

18 Der Test-Code ist im Weg 243


18.1 Konventionen für Klassennamen 243
18.2 Der Speicherort für Tests 244

19 Mein Projekt ist nicht objektorientiert. Wie kann ich es


sicher ändern? 247
19.1 Ein einfacher Fall 248
19.2 Ein schwieriger Fall 248
19.3 Neues Verhalten hinzufügen 252
19.4 Die Objektorientierung nutzen 255
19.5 Es ist alles objektorientiert 258

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

20.3 Die nächsten Schritte 278


20.4 Nach dem Extrahieren von Klassen 281

21 Ich ändere im ganzen System denselben Code 283


21.1 Erste Schritte 286

22 Ich muss eine Monster-Methode ändern und kann


keine Tests dafür schreiben 301
22.1 Spielarten von Monstern 301
22.2 Monster mit automatischer Refactoring-Unterstützung zähmen .. . 306
22.3 Die Herausforderung des manuellen Refactorings 308
22.4 Strategie 316

23 Wie erkenne ich, dass ich nichts kaputtmache? 319


23.1 Hyperaware Editing 319
23.2 Single-Goal Editing 321
23.3 Preserve Signatures 322
23.4 Lean on the Compiler 325

24 Wir fühlen uns überwältigt. Es wird nicht besser 329

Teil III Techniken zur Aufhebung von Dependencies 333

25 Techniken zur Aufhebung von Dependencies 335


25.1 Adapt Parameter 335
25.2 Break Out Method Object 339
25.3 Definition Completion 345
25.4 Encapsulate Global References 347
25.5 Expose Static Method 353
25.6 Extract and Override Call 356
25.7 Extract and Override Factory Method 358
25.8 Extract and Override Getter 360
25.9 Extract Implementer 363
25.10 Extract Interface 368
25.11 Introduce Instance Delegatar 374
25.12 Introduce Static Setter 376
25.13 Link Substitution 382
25.14 Parameterize Constructor 383
25.15 Parameterize Method 386

TO
Inhaltsverzeichnis

25.16 Primitivize Parameter 388


25.17 Pull Up Feature 390
25.18 Push Down Dependency 394
25.19 Replace Function with Function Pointer 397
25.20 Replace Global Reference with Getter 400
25.21 Subclass and Override Method 402
25.22 Supersede Instance Variable 405
25.23 Template Redefinition 409
25.24 Text Redefinition 412

A Refactoring 415

A.i Extract Method 415

B Glossar 421

Stichwortverzeichnis 423

n
Vorwort

»... damit fing es an ...«


In der Einführung zu diesem Buch verwendet Michael Feathers diesen Ausdruck,
um den Beginn seiner Leidenschaft für Software zu beschreiben.
»... damit fing es an ...«
Kennen Sie dieses Gefühl? Erinnern Sie sich an den einen Moment in Ihrem
Leben, über den Sie sagen könnten: »... damit fing es an ...«? Gab es einen einzi-
gen Moment, der den Lauf Ihres Lebens änderte und schließlich dazu führte, dass
Sie zu diesem Buch gegriffen haben und begannen, dieses Vorwort zu lesen?
Ich war in der sechsten Klasse, als ich einen solchen Moment erlebte. Ich interes-
sierte mich für Naturwissenschaften, den Weltraum und alles Technische. Meine
Mutter hatte in einem Katalog einen Plastikcomputer entdeckt und für mich
bestellt. Er hieß Digi-Comp I. Vierzig Jahre später hat dieser kleine Plastikcompu-
ter auf meinem Bücherregal einen Ehrenplatz. Er war der Katalysator, an dem sich
meine lebenslange Leidenschaft für Software entzündete. Er vermittelte mir eine
erste Ahnung davon, welche Freude das Schreiben von Programmen machen
kann, die für Menschen Probleme lösen. Er bestand nur aus drei S-R-Flip-Flops
und sechs Und-Gates aus Plastik, aber das reichte aus - er erfüllte seinen Zweck.
Damit begann es ... - für mich.

Aber meine Freude wurde bald getrübt, als ich erkannte, dass Softwaresysteme
fast immer in einem Chaos enden. Was als kristallklares Design im Geist der 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.

Dieses Problem scheint unlösbar schwierig zu sein. Tatsächlich ist es so schwierig,


dass fast jedes jemals produzierte System langsam und kräftezehrend verrottet.
Diese Verrottung ist so weit verbreitet, dass wir für verrottete Programme einen
besonderen Begriff geprägt haben: Legacy Code.

Legacy Code - ein Begriff, der Programmierer abstößt. Er ruft Bilder von einem
unergründlichen Sumpf mit verschlungenem Wurzelwerk, Blutegeln in trüben
Gewässern und sirrenden Stechmücken hervor. Es riecht nach Verfall, Moder,
Schleim und Verwesung. Auch wenn unsere erste Freude am Programmieren
überschäumend gewesen sein mag, reicht das Elend beim Umgang mit Legacy
Code oft aus, um diese Begeisterung zu ersticken.

Viele haben versucht, Methoden zu entwickeln, um zu verhindern, dass aus Code


überhaupt Legacy Code werden kann. Wir haben Bücher über Prinzipien, Patterns
und Verfahren geschrieben, die Programmierern helfen können, ihre Systeme
sauber zu halten. Aber Michael Feathers hat etwas erkannt, das vielen anderen ent-
gangen ist. Vorbeugung ist nicht perfekt. Selbst das disziplinierteste Entwick-
lungsteam, das die besten Prinzipien und Patterns beherrscht und die besten
Verfahren anwendet, produziert immer wieder einmal chaotische Systeme. Der
Morast wächst immer noch. Es reicht nicht aus, Verrottung zu verhindern - Sie
müssen diesen Prozess umkehren können.

Darum geht es in diesem Buch: die Umkehrung der Verrottung. Wie kann man
aus einem verschlungenen, undurchschaubaren, verworrenen System langsam
und allmählich Schritt für Schritt ein einfaches, sauber strukturiertes System mit
einem makellosen Design machen? Wie kann man die Entropie umkehren?

Bevor Sie sich von Ihrer Begeisterung fortreißen lassen, möchte ich Sie warnen:
Verrottung umzukehren, ist nicht leicht und braucht Zeit. Die Techniken, Patterns
und Tools, die Feathers in diesem Buch präsentiert, sind wirksam, aber sie 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_pDi spatcher->regi sterfli stener);

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.

Eine Anmerkung zu den Beispielen: Ich habe Beispiele in mehreren verschiede-


nen Programmiersprachen verwendet. Die meisten Beispiele sind in Java, C++
und C geschrieben. Ich habe Java ausgewählt, weil diese Sprache weit verbreitet
ist, und ich habe C++ eingeschlossen, weil diese Sprache in einer Legacy-Umge-
bung einige besondere Herausforderungen präsentiert. Ich habe C ausgewählt,
weil es viele Probleme in prozeduralem Legacy Code hervorhebt. Zusammen
decken diese Sprachen einen großen Teil des Spektrums der Legacy-Code-Pro-
bleme ab. Doch auch wenn Sie mit anderen Sprachen arbeiten, sollten Sie sich die
Beispiele anschauen. Viele der behandelten Techniken können auch in anderen
Sprachen, wie etwa Delphi, Visual Basic, COBOL oder FORTRAN, verwendet wer-
den.

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.

l.i Vier Gründe, Software zu ändern


Der Einfachheit halber möchte ich vier Hauptgründe unterscheiden, Software zu
ändern:
1. Eine Funktion hinzufügen
2. Einen Fehler beseitigen
3. Das Design verbessern
4. Die Nutzung von Ressourcen optimieren

i.i.i Funktionen hinzufügen und Fehler beseitigen


Eine Funktion hinzuzufügen, scheint mir die unkomplizierteste Art von Änderun-
gen zu sein. Die Software zeigt ein Verhalten, und der Anwender erwartet von
dem System auch noch ein anderes Verhalten.
Angenommen, wir arbeiteten an einer Webanwendung und ein Manager teilte
uns mit, das Unternehmenslogo solle nicht auf der linken, sondern auf der rech-
ten Seite stehen. Wir sprechen mit ihm darüber und stellen fest, dass dies nicht
ganz so einfach ist. Er möchte das Logo verschieben, aber erwartet auch andere
Änderungen. Es soll beim nächsten Release animiert werden. Gehört dies in die
Kategorie »Fehler beseitigen« oder »Neue Funktion hinzufügen«? Das hängt von
Ihrem Standpunkt ab. Aus der Sicht des Kunden handelt es sich definitiv um die
Beseitigung eines Problems. Vielleicht hat er die Webseite gesehen und ein Mee-
ting mit Mitarbeitern seiner Abteilung veranstaltet; und sie haben beschlossen,
das Logo an eine andere Stelle zu setzen und ein wenig mehr Funktionalität zu
fordern. Auf der Sicht eines Entwicklers kann die Änderung als vollkommen neue

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.

Fügen wir in unserem Unternehmenslogo-Beispiel Verhalten hinzu? Ja; denn


nach der Änderung wird das System ein Logo auf der rechten Seite anzeigen.
Beseitigen wir Verhalten? Ja; denn es gibt kein Logo mehr auf der linken Seite.

Betrachten wir einen schwierigeren Fall. Angenommen, ein Kunde wolle ein Logo
rechts auf einer Webseite anzeigen, aber es gäbe kein Logo auf der linken Seite,
mit dem wir anfangen könnten. Ja; wir fügen Verhalten hinzu; aber entfernen wir
auch Verhalten? Wurde an der Stelle, an der das Logo erscheinen soll, irgendetwas
anderes angezeigt?

Ändern wir Verhalten, fügen wir Verhalten hinzu, oder beides?


Wir können eine Unterscheidung treffen, die für uns als Programmierer nützli-
cher ist. Wenn wir Code modifizieren müssen (und HTML zählt in diesem Fall als
Code), könnten wir Verhalten ändern. Wenn wir nur Code hinzufügen und ihn
aufrufen, fügen wir oft Verhalten hinzu. Betrachten wir ein anderes Beispiel, eine
Methode einer Java-Klasse:

public class CDPlayer


{
public void addTrackListing(Track track) {

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:

public class CDPlayer


{
public void addTrackListing(Track track) {

public void replaceTrackListingCString name, Track track) {


}

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.

1.1.2 D a s Design verbessern

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.

1.1.4 Alles im Überblick


Doch ähnelt das Refactoring der Optimierung tatsächlich viel stärker als dem Hin-
zufügen von Funktionen oder der Beseitigung von Fehlern? Refactoring und Opti-
mierung haben gemeinsam, dass die Funktionalität invariant bleibt, während
etwas anderes geändert wird.
Im Allgemeinen können wir bei der Arbeit an einem System drei verschiedene
Aspekte ändern: Struktur, Funktionalität und Ressourcenverbrauch.
Was ändert sich normalerweise und was bleibt im Wesentlichen konstant, wenn
wir vier unserer verschiedenen Arten von Änderungen vornehmen (ja, oft ändern
sich alle drei Aspekte, aber wir wollen das Typische betrachten):

Funktion Fehler Refactoring Optimierung


hinzufügen beseitigen

Struktur Ändert sich Ändert sich Ändert sich

Funktionalität Ändert sich Ändert sich

Ressourcenverbrauch - - - Ändert sich

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.


1.2
Riskante Änderungen

Funktion Fehler Refactoring Optimierung


hinzufügen beseitigen

Struktur Ändert sich Ändert sich Ändert sich -

Neue Funktionalität - Ändert sich - -

Res sourcenverbrauch - - - Ändert sich

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 |

Vorhandenes Verhalten Neues Verhalten

Abb. i . i : Verhalten bewahren

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.

1.2 Riskante Änderungen


Um Risiken zu verändern, müssen wir drei Fragen stellen:
i. Welche Änderungen müssen wir vornehmen?
Kapitel 1
Software ändern

2. Wie erfahren wir, dass wir sie korrekt vorgenommen haben?


3. Wie können wir sicher sein, dass wir nichts beschädigt haben?
Wie viele Änderungen können Sie sich leisten, wenn Änderungen riskant sind?
Die meisten Teams, mit denen ich gearbeitet habe, arbeiten mit einem sehr kon-
servativen Risikomanagement. Sie minimierten die Anzahl der Änderungen ihrer
Code-Basis. Manchmal handeln sie nach der Maxime: »Wenn es nicht kaputt ist,
fass es nicht an.« In anderen Teams werden Änderungen »kleingeredet«. Die Ent-
wickler sind einfach sehr vorsichtig, wenn sie Änderungen vornehmen: »Was? Sie
erstellen dafür eine andere Methode?« Antwort: »Nein, ich füge nur die Code-Zei-
len direkt hier in die Methode ein, wo ich gleichzeitig den Rest des Codes sehen
kann. Ich muss weniger editieren, und es ist sicherer.«

Es ist verlockend zu denken, wir können Software-Probleme minimieren, indem


wir sie ignorieren; aber leider holt uns die Wirklichkeit immer ein. Wenn wir ver-
meiden, neue Klassen und Methoden zu erstellen, werden die vorhandenen
immer größer und unübersichtlicher. Wenn Sie ein umfangreiches System
ändern, müssen Sie damit rechnen, dass es eine Weile dauert, mit dem Arbeits-
kontext vertraut zu werden. Gute und schlechte Systeme unterscheiden sich auch
dadurch, dass Sie bei guten nach dieser Lernphase ein Gefühl der Sicherheit
haben und sich zutrauen, die Änderungen erfolgreich vorzunehmen. Wenn Sie
dagegen schlecht strukturierten Code nach der Lernphase ändern wollen, haben
Sie eher das Gefühl, von einer Klippe zu springen, um einem Tiger zu entkom-
men. Sie zögern diesen Schritt immer weiter hinaus: »Bin ich bereit dafür? Nun,
mir bleibt wohl nichts anderes übrig.«

Änderungen zu vermeiden, hat auch andere negative Konsequenzen. Wer Code


nicht ändert, verliert oft die Fähigkeit dafür. Eine große Klasse in Teile zu zerlegen,
kann ziemlich anstrengend sein, wenn Sie es nicht mehrfach pro Woche tun.
Andernfalls wird es zur Routine. Sie erkennen immer besser, was kaputtgehen
kann und was nicht, und die Arbeit geht viel leichter von der Hand.

Die letzte Konsequenz, Änderungen zu vermeiden, ist Angst. Leider haben viele
Teams eine unglaubliche Angst vor Änderungen; und jeden Tag wird es 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

Mit Feedback arbeiten

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

kationen erfüllt. In einigen sehr traditionellen IT-Abteilungen wird Software so


und nicht anders entwickelt. Das Team kann Feedback bekommen, aber die Feed-
back-Schleife ist lang. Das Team arbeitet einige Wochen oder Monate, und dann
sagen Tester in einer anderen Gruppe, ob alles richtig ist oder nicht.

Solche Tests versuchen eigentlich, die »Korrektheit zu demonstrieren«. Obwohl


dies ein erstrebenswertes Ziel ist, können Tests auch ganz anders eingesetzt wer-
den, nämlich »um Änderungen aufzudecken«.
Solche Tests werden üblicherweise als Regressionstests bezeichnet. Wir führen peri-
odisch Tests aus, die bekanntermaßen richtiges Verhalten prüfen, um festzustel-
len, ob unsere Software immer noch so funktioniert wie vor den Änderungen.
Tests, die Code-Fragmente einschließen, die Sie ändern wollen, sind eine Art Soft-
ware-Zwinge. Sie können das meiste Verhalten konstant halten und sicher sein,
nur das zu ändern, was Sie ändern wollen.

Software-Zwinge
Eine Zwinge (Schraubstock, engl, vise) ist ein Gerät, in das man ein Werkstück
einspannen kann, um es für die Dauer der Bearbeitung in einer bestimmten
Position zu fixieren.
Tests, die Änderungen entdecken, verhalten sich wie eine Zwinge, in die unser
Code eingespannt ist. Sie fixieren das Verhalten des Codes. Bei Änderungen
können wir sicher sein, dass wir immer nur einen Teil des Verhaltens gleichzei-
tig ändern. Kurz gesagt: Wir kontrollieren unsere Arbeit.

Regressionstests sind eine hervorragende Errungenschaft. Warum werden sie von


Entwicklern nicht öfter eingesetzt? Bei Regressionstests gibt es ein kleines Pro-
blem: Sie werden häufig auf die Anwendungsschnittstelle angewendet, egal ob es
sich um eine Webanwendung, eine Befehlszeilenanwendung oder eine GUI-
basierte Anwendung handelt. Regressionstests wurden traditionell der Anwen-
dungsebene zugeordnet. Leider! Denn sie können ein sehr nützliches Feedback
liefern. Deshalb lohnt es sich, sie auf einer feinkörnigeren Ebene einzusetzen.

Angenommen, wir analysierten eine umfangreiche Funktion mit einer kompli-


zierten Logik. Wir denken nach, wir reden mit anderen Entwicklern, die das Code-
Fragment besser kennen als wir, und dann ändern wir es. Wir wollen gewährleis-
ten, dass die Änderung nichts beschädigt hat; doch wie können wir das tun?
Glücklicherweise haben wir ein Qualitätssicherungsteam, das über einen Satz von
Regressionstests verfügt, die wir über Nacht ausführen können. Wir rufen das
Team an und bitten es, einen Testlauf einzuplanen. Das Team sagt zu, dass es die
Tests über Nacht ausführen kann, aber es wäre gut, dass wir früh angerufen hät-
ten. Andere Gruppen versuchen normalerweise, Regressionstests in der Mitte der
Woche durchzuführen, und wenn wir länger gewartet hätten, wäre möglicher-

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?

Unit-Tests zählen zu den wichtigsten Instrumenten beim Arbeiten mit Legacy


Code. Regressionstests auf Systemebene sind großartig, aber kleine, lokalisierte

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.

2.1 Was sind Unit-Tests?


Der Terminus Unit-Test hat bei der Software-Entwicklung eine lange Geschichte.
Den meisten Definitionen ist die Idee gemeinsam, dass es sich um Tests handelt,
bei denen einzelne Software-Komponenten isoliert getestet werden. Was sind
Komponenten? Es gibt verschiedene Definitionen; doch bei Unit-Tests untersu-
chen wir normalerweise die kleinsten Verhaltenseinheiten eines Systems. In pro-
zeduralem Code sind die Units oft Funktionen, bei objektorientiertem Code sind
sie Klassen.

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.

Gute Unit-Tests haben folgende Eigenschaften:


1. Sie werden schnell ausgeführt.
2. Sie helfen uns, Probleme zu lokalisieren.
Entwickler diskutieren oft, ob bestimmte Tests Unit-Tests sind. Ist ein Test wirk-
lich ein Unit-Test, wenn er mehr als eine Produktionsklasse verwendet? Mit rei-
chen die beiden genannten Eigenschaften. Natürlich gibt es Übergänge. Einige
Tests sind umfangreicher und verwenden mehrere Klassen gleichzeitig. Vielleicht
ähneln sie kleinen Integrationstests. Allein laufen sie vielleicht schnell, aber was
passiert, wenn sie alle zusammen ausgeführt werden? Wenn ein Test eine Klasse
zusammen mit mehreren ihrer Kollaborateure prüft, wird er tendenziell immer

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.

Abb. 2.1: Invoice-Update-Klassen

39
Kapitel 2
Mit Feedback arbeiten

Um Tests schreiben und ausführen zu können, müssen wir Instanzen von


Invoi ceUpdateResponder und I n v o i c e in einem Test-Harnisch erstellen kön-
nen. Ist dies möglich? Nun, eine Invoi ce zu erstellen, scheint einfach genug zu
sein; die Klasse hat einen Konstruktor, der keine Argumente übernimmt. Doch
die Klasse Invoi ceUpdateResponder scheint etwas komplizierter zu sein. Sie
akzeptiert eine DBConnection, eine echte Verbindung zu einer aktiven Daten-
bank. Wie können wir dies in einem Test handhaben? Müssen wir eine Daten-
bank mit Daten für unsere Tests einrichten? Das wäre viel Arbeit. Wären die
Tests mit der Datenbank nicht langsam? Außerdem interessieren wir uns im
Moment nicht besonders für die Datenbank, sondern wollen einfach unsere
Änderungen in Invoi ceUpdateResponder und Invoice abdecken. Wir haben
auch ein größeres Problem. Der Konstruktor von Invoi ceUpdateResponder
braucht ein Invoi ceUpdateServlet als Argument. Wie leicht können wir dieses
erstellen? Wir könnten den Code so ändern, dass er kein Servlet mehr entgegen-
nimmt. Wenn der Invoi ceUpdateResponder nur wenige Daten aus Invoi ce-
UpdateServlet benötigt, könnten wir nur diese Daten anstelle des ganzen
Servlets übergeben; aber sollten wir nicht einen Test eingerichtet haben, um zu
prüfen, ob wir diese Änderung korrekt vorgenommen haben?

Alle diese Probleme sind Dependency-Probleme (Abhängigkeitsprobleme). Hän-


gen Belassen direkt von Dingen ab, die in einem Test schwer zu handhaben sind,
ist es schwierig, sie zu ändern und damit zu arbeiten.

Dependency ist eines der gravierendsten Probleme bei der Software-Entwick-


lung. Ein großer Teil der Arbeit mit Legacy Code besteht darin, Dependencies so
aufzuheben, dass der Code leichter geändert werden kann.

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.

Abb. 2.2: Invoice-Update-Klassen nach Aufhebung der Dependencies

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

ber aufheben wie in dem vorhergehenden Beispiel. Vielleicht führen wir in


Methoden Parameter ein, die im Produktions-Code eigentlich überflüssig sind,
oder wir zerlegen Klassen auf obskure Weise, nur um Tests einzurichten. In die-
sem Fall sieht der Code hinterher an der jeweiligen Stelle schlechter aus als vor-
her. Wären wir weniger konservativ, würden wir dies sofort korrigieren. Wir
können dies tun, aber das hängt davon ab, welches Risiko damit verbunden ist.
Wenn Fehler ein großes Problem sind, und normalerweise sind sie das, lohnt es
sich, konservativ zu sein.

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.

2.4 Der Algorithmus zur Änderung von Legacy Code


Wenn Sie eine Änderung in einer Legacy-Code-Basis vornehmen müssen, können
Sie nach folgendem Algorithmus vorgehen:

1. Identifizieren Sie Änderungspunkte.


2. Suchen Sie Testpunkte.
3. Heben Sie Dependencies auf.
4. Schreiben Sie Tests.
5. Machen Sie Änderungen und refaktorisieren Sie.

Ä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.

2.4.2 Suchen Sie Testpunkte


In einigen Fällen ist es leicht, geeignete Stellen für Tests zu finden, aber in Legacy
Code kann dies oft schwierig sein. Werfen Sie einen Blick in Kapitel n, Ich muss
eine Änderung vornehmen. Welche Methoden sollte ich testen?, und Kapitel 12, Ich
muss in einem Bereich vieles ändern. Muss ich die Dependencies aller beteiligten Klassen
auflieben? Diese Kapitel enthalten Techniken, mit denen Sie feststellen können, an
welchen Stellen Sie Ihre Tests für bestimmte Änderungen schreiben müssen.

2.4.3 Heben Sie Dependencies a u f


Dependencies sind oft das offensichtlichste Hindernis, das Tests blockiert. Das
Problem manifestiert sich in zwei Formen: der Schwierigkeit, Objekte in Test-Har-
nischen zu instanziieren, und der Schwierigkeit, Methoden in Test-Harnischen
auszuführen. Oft müssen Sie Dependencies in Legacy Code aufheben, um Tests
einrichten zu können. Idealerweise sollten uns Tests mitteilen, ob unsere Maß-
nahmen zur Aufhebung von Dependencies Probleme machen, aber oft haben wir
solche Tests nicht. Werfen Sie einen Blick in Kapitel 23, Wie erkenne ich, dass ich
nichts kaputtmache? Es beschreibt Verfahren, mit denen Sie die ersten Einschnitte
in ein System sicherer vornehmen können, wenn Sie anfangen, es unter Testkon-
trolle zu bringen. Danach finden Sie in 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 ausführen, Szenarien, die Ihnen zeigen, wie Sie häufige Dependency-Pro-
bleme umgehen können. Diese Abschnitte enthalten umfangreiche Referenzen
auf den Katalog der Techniken zur Aufhebung von Dependencies am Ende des
Buches, behandeln aber nicht alle Techniken. Nehmen Sie sich Zeit, den Katalog
zu studieren und sich inspirieren zu lassen, wie Sie Dependencies aufheben kön-
nen.

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

2.4.4 Schreiben Sie Tests


Meiner Meinung nach unterscheiden sich die Tests, die ich für Legacy Code
schreibe, von denen, die ich für neuen Code schreibe. In Kapitel 13, Ich muss etwas
ändern, weiß aber nicht, welche Tests ich schreiben soll, erfahren Sie mehr über die
Rolle von Tests beim Arbeiten mit Legacy Code.

2.4.5 Machen Sie Änderungen und refaktorisieren Sie


Ich befürworte die Verwendung des Test-Driven Development (TDD), um Funk-
tionen in Legacy Code einzufügen. In Kapitel 8, Wie fuge ich eine Funktion hinzu?,
beschreibe ich TDD und einige anderen Techniken ausführlicher, um Code um
neue Funktionen zu erweitern. Nachdem wir Legacy Code geändert haben, sind
wir oft mit seinen Problemen besser vertraut, und die Tests, die wir geschrieben
haben, um Funktionen hinzuzufügen, geben uns oft eine gewisse Deckung, um
den Code zu refaktorisieren. In Kapitel 20, Diese Klasse ist zu groß und soll nicht
noch größer werden, Kapitel 22, Ich muss eine Monster-Methode ändern und kann keine
Tests dafür schreiben und Kapitel 21, Ich ändere im ganzen System denselben Code,
beschreibe ich viele Techniken, mit denen Sie anfangen können, die Struktur
Ihres Legacy Codes zu verbessern. Denken Sie daran, dass dies »Techniken der
kleinen Schritte« sind. Die Kapitel zeigen nicht, wie Sie das Design in eine ideale
Form bringen oder mit Patterns anreichern. Zu diesem Thema gibt es zahlreiche
gute Bücher, und wenn Sie diese Techniken anwenden können, nur zu! Diese
Kapitel zeigen Ihnen, wie Sie das Design im jeweiligen Kontext verbessern kön-
nen. Oft sind dies einfach einige Schritte, nach denen das Design wartungsfreund-
licher ist als vorher. Sie sollten diese Arbeit nicht gering schätzen. Oft sind es die
einfachsten Dinge, wie etwa das Zerlegen einer umfangreichen Klasse, die das
Arbeiten erleichtern und weitere Fortschritte ermöglichen, auch wenn sie etwas
mechanisch wirken.

2.4.6 Der Rest dieses Buches


Im Rest dieses Buches zeige ich Ihnen, wie Sie Legacy Code ändern können. Die
nächsten beiden Kapitel enthalten Hintergrundmaterial über drei zentrale Kon-
zepte der Legacy-Arbeit: Überwachung, Trennung und Seams.

44
Kapitel 3

Überwachung und Trennung

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.

In Systemen, die nicht mit Unit-Test-Begleitung entwickelt wurden, müssen oft


Dependencies aufgehoben werden, um Klassen in einen Test-Harnisch einzu-
schließen. Doch dies ist nicht der einzige Grund, Dependencies aufzuheben.
Manchmal beeinflusst die Klasse, die wir testen wollen, andere Belassen, und
unsere Tests müssen diese Klasse kennen. Manchmal können wir diese Effekte
über die Schnittstelle der anderen Belasse beobachten, manchmal nicht. Unsere
einzige Möglichkeit besteht darin, die andere Belasse so zu instanziieren, dass wir
die Effekte direkt beobachten können.

Im Allgemeinen gibt es bei der Einrichtung von Tests zwei Gründe, Dependencies
aufzuheben: Überwachung (engl, sensing) und Trennung (engl. Separation):

1. Überwachung - Wir heben Dependencies zwecks Überwachung auf, wenn wir


Werte nicht abrufen können, die der Code berechnet.
2. Trennung - Wir heben Dependencies zwecks Trennung auf, wenn wir es nicht
einmal schaffen, ein Code-Fragment in einem Test-Harnisch auszuführen.

Hier ist ein Beispiel. Wir haben eine Belasse namens NetworkBri dge in einer Net-
work-Management* Anwendung:

public class NetworkBridge


{
public NetworkBridgeCEndPoint [] endpoints) {

45
Kapitel 3
Überwachung und Trennung

public void formRouting(String sourcelD, String destlD) {


}

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 Kollaborateure simulieren


Ein großes Problem beim Arbeiten mit Legacy Code sind Dependencies. Wenn
wir ein Code-Fragment separat ausführen wollen, müssen wir oft Dependencies
von anderem Code aufheben. Dies ist aber selten einfach. Oft ist dieser andere
Code die einzige Stelle, an der wir die Effekte unserer Aktionen leicht beobachten
können. Könnten wir diesen Code durch anderen ersetzen und diesen zum Testen
verwenden, dann könnten wir unsere Tests schreiben. In der objektorientierten
Programmierung werden diese anderen Teile von Code oft als Fake-Objekte
bezeichnet.

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)

Abb. 3.1: S a l e-Klasse

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)

Abb. 3.2: K o m m u n i k a t i o n der S a l e - K l a s s e mit einer Display-Klasse

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

+ scan(barcode : String) + showl_ine(line : String)

ArtR56Display FakeDisplay
• showLine(line : String) - lastLine : String
- getLastüne() : String
• showLine(line : String)

Abb. 3.3: S a l e - K l a s s e mit der Display-Hierarchie

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.

public interface Display


{
void showLine(String line);
}

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:

public class Sale


{
private Display display;

public Sale(Display display) {


this. display = display;
}

public void scan(String barcode) {

String itemLine = i t e m . n a m e O + " " + item.price() .asDisplayText();


display.showLine(itemLine) ;

}
}

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.*;

public class SaleTest extends TestCase


{
public void testDisplayAnltemO {
FakeDi spl ay display = new FakeDi spl ay() ;
Sale sale = new Sale(display);

sale.scan("l");
assertEquals("Mi1k $3.99", display.getLastLineO) ;
}
}

Die FakeDi spl ay-Klasse ist ein wenig ungewöhnlich:

public class FakeDi spl ay implements Display


{
private String lastLine = "";

public void showLine(String line) {


lastLine = line;
}

public String getLastLineO {


return lastLine;
}
}

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.

Fake-Objekte unterstützen echte Tests


Entwickler haben bei Fake-Objekten manchmal den Eindruck, gar keine »ech-
ten Tests« auszuführen. Schließlich zeigen diese Tests nicht, was auf dem ech-
ten Display angezeigt wird. Angenommen, ein Teil der Software des Kassen-
Displays funktionierte nicht richtig; dies würde durch diesen Test nicht
erkannt.

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).

Abb. 3.4: Die beiden Seiten eines Fake-Objekts

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.*;

public class SaleTest extends TestCase


{
public void testDisplayAnltemO {
FakeDisplay display = new FakeDi spl ay() ;


3-i
Kollaborateure simulieren

Sale sale = new Sale(display);

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.

3.1.3 Fakes destilliert

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.*;

public class SaleTest extends TestCase


{
public void testDisplayAnItem() {
MockDisplay display = new MockDisplayC) ;
display.setExpectation("showLine", "Milk $3.99");
Sale sale = new Sale(display);
sale.scan("l");
display.verifyO;
}
}

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.

Mock-Objekte sind ein leistungsstarkes Tool; und es gibt zahlreiche verschiedene


Mock-Objekt-Frameworks, allerdings nicht für alle Sprachen. In den meisten Situ-
ationen reichen einfach Fake-Objekte aus.

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.

4.1 Ein riesiges Blatt mit Text


Ich fing so spät an zu programmieren, dass ich in der glücklichen Lage war, einen
eigenen Rechner und einen Compiler dafür zu haben. Viele meiner Freunde lern-
ten zur Zeit der Lockkarten programmieren. Als ich in der Schule einen Program-
mierkurs belegte, saß ich zunächst an einem Terminal in einem Labor. Wir
konnten unseren Code remote auf einem VAX-Rechner von DEC kompilieren. Es
gab ein kleines Buchhaltungssystem. Jeder Lauf des Compilers kostete Geld, mit
dem unser Konto belastet wurde; und jedem war am Terminal eine bestimmte
Rechnerzeit zugeteilt.

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

war Modularität ein recht akademisches Thema. Bei meinen Schulprojekten


musste ich keine Klassen gegeneinander austauschen. Als ich später mein Geld
mit Software verdiente, wurden diese Dinge sehr wichtig für mich, aber in der
Schule war ein Programm für mich einfach ein Listing, ein langer Satz von Funk-
tionen, die ich nacheinander schreiben und verstehen musste.

Programme als Listings zu betrachten, scheint der Sache zu entsprechen, wenigs-


tens wenn wir uns anschauen, wie Programmierer mit ihren Programmen umge-
hen. Wüssten wir nichts über das Programmieren und sähen wir einen Raum
voller Programmierer bei der Arbeit, könnten wir den Eindruck gewinnen, sie
seien Gelehrte, die umfangreiche wichtige Dokumente untersuchen und bearbei-
ten. Ein Programm scheint danach ein umfangreicher Textausdruck zu sein. Den
Text an einer kleinen Stelle zu modifizieren, kann die Bedeutung des gesamten
Dokuments ändern; deshalb nehmen Programmierer diese Änderungen sorgfäl-
tig vor, um Fehler zu vermeiden.

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);

Wie könnten wir dies tun?

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.

Betrachten wir zuerst seine Definition und dann einige Beispiele.

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

Virtual void PostReceiveError(UINT type, UINT errorcode);

};
In der Implementierungsdatei können wir einen Body für die Methode einfügen:

void CAsyncSslRec::PostReceiveError(UINT type, UINT errorcode)


{
::PostReceiveError(type, errorcode);
}

Diese Änderung soll Verhalten bewahren. Mit dem C++-Scoping-Operator (: :)


delegieren wir mit dieser neuen Methode an die globale PostReceiveError-
Funktion. Zwar ist dies eine kleine Umlenkung, aber letztlich rufen wir dieselbe
globale Funktion auf.

Jetzt können wir eine Unterklasse der CasyncSsl Rec-Klasse bilden und die
PostRecei veError-Methode überschreiben:

class TestingAsyncSslRec : public CAsyncSslRec


{
Virtual void PostReceiveError(UINT type, UINT errorcode)
{

}
};

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ü) ;
}

für den Compiler folgenden Code generieren:

class AccountgetBalanceTest : public Test


{ public: AccountgetBalanceTest () : Test ("getBalance" "Test") {}
void run (TestResult& result_); }
AccountgetBalancelnstance;
void AccountgetBalanceTest::run (TestResult& result_)
{
Account account;
{ result_.countCheck();
long actualTemp = (account.getBalanceO);
long expectedTemp = (0);
if ((expectedTemp) != (actualTemp))
{ result_.addFailure (Failure (name_, "c:\\seamexample.cpp", 24,
StringFrom(expectedTemp),
StringFrom(actualTemp))); return; } }
}

Wir können Code in bedingten Compiler-Anweisungen auch verschachteln, um


das Debugging und verschiedene Plattformen zu unterstützen (aarrrgh!):

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

Eine umfangreiche Präprozessor-Verarbeitung ist in Produktions-Code nicht zu


empfehlen, weil die Klarheit des Codes darunter leidet. Die verschiedenen Compi-
ler-Direktiven für eine bedingte Kompilierung (#i fdef, #i f ndef, #i f usw.) zwin-
gen Sie, verschiedene Programme im selben Quellcode zu pflegen. Mit Makros
(die mit #define definiert werden) können Sie einige sehr gute Sachen machen,
aber sie ersetzen einfach nur Text. Es ist leicht, Makros zu erstellen, in denen sich
schreckliche Bugs verbergen.

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

Dependencies von einer Bibliotheksroutine namens db_update. Die db_update-


Funktion kommuniziert direkt mit einer Datenbank. Wenn wir die Funktion nicht
durch eine andere Implementierung ersetzen können, können wir ihr Verhalten
nicht überwachen.

#include <DFHLItem.h>
#include <DHLSRecord.h>

extern int db_update(int, struct DFHLItem *);

void account_update(int account_no, struct DHLSRecord *record, int activated)


{
if (activated) {
if (record->dateStamped && record->quantity > MAX_ITEMS) {
db_update(account_no, record->item);
} eise {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);
}

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>

extern int db_update(int, struct DFHLItem *);

#include "1ocaldefs.h"

void account_update(int account_no, struct DHLSRecord *record, int activated)


{
if (activated) {
if (record->dateStamped && record->quantity > MAX_ITEMS) {
db_update(account_no, record->item);
} eise {
db_update(account_no, record->backup_item);
}
}
db_update(MASTER_ACCOUNT, record->item);

In ihr definieren wir db_update und einige nützliche Variablen:

#i fdef TESTINC

struct DFHLItem *last_item = NULL;


int last_account_no = -1;

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.

Präprozessor-Seams sind ziemlich leistungsstark. Bei Java und anderen moderne-


ren Sprachen vermisse ich einen Präprozessor nicht, aber in C und C++ ist dieses
Tool nützlich, weil es einige andere Testhindernisse in diesen Sprachen ausgleicht.
Es gibt einen wichtigen Aspekt bei Seams, den ich bis jetzt noch nicht erwähnt
habe: Jeder Seam hat einen so genannten Enabling Point (Aktivierungspunkt)
Betrachten wir erneut die Definition eines Seams:

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.

Aktivierungspunkt (Enabling Point)


Jeder Seam verfügt über einen Aktivierungspunkt, eine Stelle, an der Sie das
gewünschte Verhalten auswählen können.

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

linkt der Compiler Komponenten im Hintergrund. Wenn eine Quelldatei import-


Anweisungen enthält, prüft der Compiler, ob importierte Klassen wirklich kompi-
liert worden sind, und kompiliert bei Bedarf auch diese. Dann prüft er, ob alle Auf-
rufe zur Laufzeit tatsächlich korrekt aufgelöst werden.

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 class FitFilter {

public String input;


public Parse tables;
public Fixture fixture = new F i x t u r e O ;
public PrintWriter Output;

public static void main (String argv[]) {


new FitFilterO . run(argv) ;
}

public void run (String argv[]) {


args(argv);
processO ;
exit();
}

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

Klassen erstellen und in verschiedene Verzeichnisse einfügen und dann Class-


path so ändern, dass verschiedene fit. Parse- und fit. Fixture-Versionen ein-
gebunden werden. Obwohl dieser Trick in Produktions-Code verwirrend wäre, ist
er beim Testen ziemlich nützlich, um Dependencies aufzuheben.

Angenommen, wir wollten beim Testen eine andere Version der Parse-Klasse
zur Verfügung stellen. Wo wäre der Seam?

Der Seam ist der new Parse-Aufrufin der process-Methode.

Wo ist der Aktivierungspunkt?

Der Aktivierungspunkt ist der cl asspath.

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.

Viele C- und C++-Build-Systeme erstellen Executables durch statische Verlinkung.


Oft besteht die einfachste Methode, den Link-Seam zu verwenden, darin, eine
separate Bibliothek für die Klassen oder Funktionen zu erstellen, die Sie ersetzen
wollen, und Ihre Build-Skripts so zu ändern, dass Ihr Code beim Testen mit dieser
Bibliothek verlinkt wird. Dies ist mit Arbeit verbunden, kann sich aber auszahlen,
wenn Ihre Code-Basis mit Aufrufen einer Fremd-Bibliothek durchsetzt ist. Ange-
nommen, eine CAD-Anwendung enthalte zahlreiche eingebettete Aufrufe einer
Grafik-Bibliothek. Hier ist ein Beispiel für typischen Code:

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:

void drawText(int x, int y, char *text, int textLength)


{
}

void drawLine(int firstX, int firstY, int secondX, int secondY)


{
}

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;

void drawLine(int firstX, int firstY, int secondX, int secondY)


{
actions.push_back(GraphicsAction(LINE_DRAW, firstX, firstY, secondX, secondY);
}

Mit diesen Datenstrukturen können wir die Auswirkungen einer Funktion in


einem Test überwachen:

TESTCsimpleRender, Figure)
{
std: :string text = "simple";

63
Kapitel 4
Das Seam-Modell

Figure figure(text, 0, 0);


figure. rerender() ;
L0NGS_EQUAL(5, actions.size());
GraphicsAction action;
action = actions.pop_front();
LONGS_EQUAL(LABEL_DRAW, action.type);
action = actions.pop_front();
L0NGS_EQUAL(0, action.firstX);
L0NGS_EQUAL(0, action.firstY);
LONGS_EQUAL(text.si ze (), acti on.secondX);
}
Solche Überwachungsverfahren können ziemlich kompliziert werden; doch am
besten beginnen Sie sehr einfach und bauen das Verfahren nur nach Bedarf aus.
Der Aktivierungspunkt für einen Link-Seam liegt immer außerhalb des Pro-
grammtextes, manchmal in einem Build- oder in einem Deployment-Skript.
Dadurch wird es schwierig zu erkennen, wo Link-Seams verwendet werden.


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.

Abb. 4.1: Cell-Hierarchie

64
4-3
Seam-Arten

Welche Methode wird in der folgenden Code-Zeile aufgerufen?

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.

In objektorientierten Sprachen sind nicht alle Methodenaufrufe Seams. Ein Bei-


spiel:

public class CustomSpreadsheet extends Spreadsheet


{

public Spreadsheet buildMartSheet() {

Cell cell = new FormulaCell(this, "AI", "=A2+A3");

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.

Was wäre, sähe der Code wie folgt aus?

public class CustomSpreadsheet extends Spreadsheet


{
public Spreadsheet buildMartSheetC Cell cell) {

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.

Wo ist der Aktivierungspunkt?

Im folgenden Beispiel ist der Aktivierungspunkt die Argumente-Liste von bui1d-


MartSheet. Wir können wählen, welche Objekte übergeben werden sollen, und
das Verhalten von Recal cul ate bei Tests beliebig ändern.

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?

public class CustomSpreadsheet extends Spreadsheet


{
public Spreadsheet buildMartSheet(Cell cell) {

Recal cul ate(cell);


}
private static void RecalculateCCell cell) {
}

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:

public class CustomSpreadsheet extends Spreadsheet


{
public Spreadsheet buildMartSheetfCell cell) {

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:

bool CAsyncSslRec: :Init()


{
if (m_bSsHnitia"lized) {
return true;
}
m_smutex.Unlock();
m_nSslRefCount++;

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;

Hier sind die Seams des PostReceiveError-Aufrufs:


i. PostRecei veError ist eine globale Funktion; deshalb können wir hier leicht
den Link-Seam verwenden. Wir können eine Bibliothek mit einer Stub-Funktion
erstellen und sie einbinden, um das Verhalten zu beseitigen. Der Aktivierungs-
punkt würde in unserem Makefile oder den Einstellungen unserer IDE liegen.
Wir müssten unser Build-Skript ändern, damit beim Testen eine Test- und bei
der Erstellung des endgültigen Systems eine Produktionsbibliothek eingebun-
den wird.

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.

5.1 Automatisierte Refactoring-Tools


Refactoring per Hand ist in Ordnung, aber wenn Sie dafür ein Tool verwenden
können, können Sie viel Zeit sparen. In den 1990er Jahren begann Bill Opdyke als
Teil seiner Abschlussarbeit über Refactoring mit der Entwicklung eines C++-Re-
factoring-Tools. Obwohl es meines Wissens nie kommerziell angeboten wurde,
regte seine Arbeit viele andere Entwicklungen in anderen Sprachen an. Eine der
bedeutendsten war der Smalltalk Refactoring Browser, den John Brant und Don
Roberts an der University of Illinois entwickelt haben. Dieser Browser unterstützt
sehr viele Refactorings und galt lange als Referenz-Tool für die Technologie des
automatischen Refactorings. Seit damals wurden zahlreiche Anstrengungen
unternommen, weiter verbreitete Sprachen durch Refactoring zu unterstützen.
Als ich dies schrieb, waren zahlreiche Java-Refactoring-Tools auf dem Markt; die
meisten sind in IDEs integriert, einige andere nicht. Es gibt auch Refactoring-
Tools für Delphi und einige relative neue für C++. Als ich dies schrieb, wurden
auch Refactoring-Tools für C# aktiv entwickelt.

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.

Tests und automatisiertes Refactoring


Wenn Sie Ihren Code mit einem Tool refaktorisieren, könnten Sie versucht sein
zu glauben, Sie müssten keine Tests für den Code schreiben, den Sie refaktori-
sieren wollen. In einigen Fällen stimmt das. Wenn Ihr Tool Refactorings sicher
durchführt und Sie von einem automatischen Refactoring zum nächsten gehen,
ohne den Code anderweitig zu bearbeiten, können Sie davon ausgehen, dass
Ihre Änderungen das Verhalten nicht geändert haben. Doch dies ist nicht
immer der Fall.

Hier ist ein Beispiel:


public class A {
private int alpha = 0;
private int getValueO {
alpha++;
return 12;
}
public void doSomething() {
int v = getValueO;
int total = 0;

70
5-2
Mock-Objekte

for (int n = 0; n < 10; n++) {


total += v;
}
}
}
In wenigstens zwei Java-Refactoring-Tools können wir die Variable v aus
doSomethi ng mit einem Refactoring entfernen. Nach dem Refactoring sieht der
Code wie folgt aus:
public class A {
private int alpha = 0;
private int getValueO {
alpha++;
return 12;
}

public void doSomething() {


int total = 0;
for (int n = 0; n < 10; n++) {
total += g e t V a l u e O ;
}
}
}
Sehen Sie das Problem? Die Variable ist entfernt worden, aber jetzt wird der
Wert von al pha nicht einmal, sondern zehnmal inkrementiert. Diese Änderung
bewahrt das Verhalten also nicht.

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

Es gibt mehrere kostenlose Mock-Objekt-Bibliotheken. Auf der Website www.


mock-obj ects. com finden Sie Referenzen für die meisten dieser Bibliotheken.

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:

• Programmierer können Tests in der Sprache schreiben, in der sie entwickeln.


• Alle Tests werden isoliert ausgeführt.
• Tests können in Suites zusammengefasst werden, damit sie bei Bedarf ausge-
führt und wiederholt werden können.

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

später mit einigen C-Idiomen verkleinern, vereinfachen und portierbarer machen


konnte.

5.3.1 JUnit

In JUnit schreiben Sie Tests, indem Sie von einer Klasse namens TestCase Unter-
klassen ableiten:

import junit.framework.*;

public class FormulaTest extends TestCase {


public void testEmptyO {
assertEquals(0, new FormulaC") .valueO) ;
}

public void testDigitO {


assertEquals(l, new Formula("l") .valueO) ;
}
}

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.

public class EmployeeTest extends TestCase {


rivate Employee employee;

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

public void testOvertime() {


TDate newCardDate = new TDate(ll, 10, 2000);
employee.addTimeCard(new TimeCard(newCardDate, 50));
assertTrue(employee.hasOvertimeFor(newCardDate));
}

public void testNormalPay() {


assertEquals(400, employee.getPayO) ;
}
}

Die EmployeeTest-Klasse enthält eine besondere Methode namens setUp. Die


setUp-Methode wird in TestCase definiert und in jedem Test-Objekt vor der Test-
Methode ausgeführt. Mit der setUp-Methode können wir einen Satz von Objekten
erstellen, die wir in einem Test verwenden werden. Dieser Satz von Objekten wird
vor der Ausführung jedes Tests auf dieselbe Weise erstellt. Das Objekt, das test-
Normal Pay ausführt, prüft, ob ein employee-Objekt, das in setUp erstellt wurde,
den Lohn für ein timecard-Objekt korrekt berechnet, das in setUp hinzugefügt
wurde. In dem Objekt, das testOvertime ausführt, wird für ein employee-
Objekt, das in setUp erstellt wurde, ein zusätzliches timecard-Objekt erstellt;
eine Prüfung verifiziert, ob das zweite timecard-Objekt eine overtime-Bedin-
gung auslöst. Die setUp-Methode wird für jedes Objekt der Klasse EmployeeTest
aufgerufen; und für jedes dieser Objekte wird per setUp ein eigener Satz von
Objekten erstellt. Wenn Sie nach der Ausführung des Tests etwas Besonderes tun
wollen, können Sie eine andere Methode namens tearDown überschreiben, die in
TestCase definiert ist. Sie wird nach der Test-Methode für jedes Objekt ausge-
führt.

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>

using namespace std;


TEST(testNormalPay,Employee)
{
auto_ptr<Employee> employee(new Employee("Fred", 0, 10));
LONGS_EQUALS(400, employee->getPay());
}

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.

Hier ist ein NUnit-Test in VB.NET:

Imports NUnit.Framework

<TestFixture()> Public Class LogOnTest


Inherits Assertion

<Test()> Public Sub TestRunValid()


Dim display As New MockDisplayO
Dim reader As New MockATMReaderO
Dim logon As New LogOn(display, reader)
logon. Run()
AssertEquals("Please Enter Card", display.LastDisplayedText)
AssertEqual s("MainMenu",logon.GetNextTransaction().GetType.Name)
End Sub

End Class

<TestFixture()> und <Test()> sind Attribute, die LogonTest als Test-Klasse


bzw. TestRunVal i d als Test-Methode kennzeichnen.

76
5-4
Allgemeine Test-Harnische

5.3.4 Andere xUnit-Frameworks


Das xUnit-Framework wurde in viele verschiedene Sprachen und Plattformen por-
tiert. Im Allgemeinen unterstützt es die Spezifikation, Gruppierung und Ausfüh-
rung von Unit-Tests. Suchen Sie eine xUnit-Portierung für Ihre Plattform oder
Sprache? Besuchen Sie die Downloads-Seite von www.xProgrammieren.com.
Diese Website wird von Ron Jeffries betrieben; sie ist das De-facto-Repository aller
xUnit-Portierungen.

5.4 Allgemeine Test-Harnische


Die weiter vorne beschriebenen xUnit-Frameworks wurden für Unit-Tests konzi-
piert. Sie können damit auch mehrere Klassen gleichzeitig testen, aber solche
Tests lassen sich mit Fit und Fitnesse besser durchführen.

5.4.1 Framework for Integrated Tests (Fit)


Fit (Framework for Integrated Tests; Framework für integrierte Tests) ist ein
schlankes und elegantes Test-Framework, das von Ward Cunningham entwickelt
wurde. Es basiert auf einem einfachen und leistungsstarken Konzept. Wenn Sie
Dokumente über Ihr System schreiben und Tabellen darin einbetten können, die
die Inputs und Outputs Ihres Systems beschreiben und als HTML gespeichert
werden können, kann das Fit-Framework sie als Tests ausführen.
Fit akzeptiert HTML, führt Tests aus, die in HTML-Tabellen definiert sind, und
erzeugt HTML-Output. Die Outputs sehen genau wie die Inputs aus, und der
gesamte Text und alle Tabellen werden bewahrt. Doch im Output werden die Zel-
len in den Tabellen grün und rot unterlegt, um Werte anzuzeigen, die einen Test
bestanden bzw. nicht bestanden haben. Optional können Sie auch eine Testzusam-
menfassung in den HTML-Output einfügen.

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.

Fitnesse unterstützt hierarchische Webseiten, die fit-Tests definieren. Seiten mit


Test-Tabellen können einzeln oder in Suites ausgeführt werden; und zahlreiche
Optionen erleichtern die Zusammenarbeit in Teams. Näheres über Fitnesse finden
Sie unter http://www.fitnesse.org. Wie alle anderen Test-Tools, die in diesem
Kapitel beschrieben werden, ist es kostenlos und wird von einer Entwickler-Com-
munity unterstützt.

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

Ich habe nicht viel Zeit und ich muss


den Code ändern

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.

Es passiert jeden Tag irgendwo


Ihr Chef kommt rein. Er sagt: »Unsere Kunden fordern diese Funktion. Können
Sie das heute erledigen?«
»Ich weiß nicht.«
Sie schauen sich um. Gibt es Tests? Nein.
Sie fragen: »Wie dringend ist es denn?«
Sie wissen, dass Sie die Änderungen an allen zehn betroffenen Stellen inline
einfügen können; damit wäre die Sache erledigt, sogar bis 17:00 Uhr. Schließ-
lich handelt es sich um einen Notfall, oder? Morgen können wir dies dann korri-
gieren, nicht wahr?
Denken Sie daran: Code ist Ihr Brot, und Sie müssen Ihr Geschäft verstehen.

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!

6.1 Sprout Method


(Neue Methode schreiben; wörtl. »Methode sprießen lassen«) Wenn Sie eine
Funktion in ein System einfügen müssen und dafür vollkommen neuer Code
geschrieben werden kann, sollten Sie eine neue Methode erstellen. Rufen Sie sie
von den Stellen auf, an denen die neue Funktionalität benötigt wird. Vielleicht
können Sie diese Aufrufpunkte nicht leicht unter Testkontrolle bringen, aber
wenigstens können Sie Tests für den neuen Code schreiben. Hier ist ein Beispiel.

public class TransactionGate


{
public void postEntries(List entries) {
for (Iterator it = entries.iteratorQ; it.hasNextQ; ) {

83
Kapitel 6
Ich habe nicht viel Zeit und ich muss den Code ändern

Entry entry = (Entry)it.nextO;


entry.postDate();
}
transactionBundle. getLi stManagerO . add(entries) ;
}

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:

public class TransactionCate


{
public void postEntriesCList entries) {
List entriesToAdd = new LinkedList() ;
for (Iterator it = entries.iteratorO; it.hasNextC) ; ) {
Entry entry = (Entry)it.nextO ;
if ([transactionBundle.getListManager().hasEntry(entry) {
entry.postDate();
entriesToAdd.add(entry);
}
}
transacti onBundl e. getLi stManagerO . add(entri esToAdd) ;
}

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:

public class TransactionCate


{

84
6.1
Sprout Method

List uniqueEntries(List entries) {


List result = new ArrayListO;
for (Iterator it = entries.iteratorO; it.hasNextO; ) {
Entry entry = (Entry)it.nextO;
if (ItransactionBundle.getListManagerO .hasEntry(entry) {
result.add(entry);
}
}
return result;
}
}
Es wäre leicht, für diese Methode Tests zu schreiben. Wenn wir die Methode
erstellt haben, können wir den Aufruf in den ursprünglichen Code einfügen:

public class TransactionCate


{

public void postEntries(List entries) {


List entriesToAdd = uniqueEntries(entries);
for (Iterator it = entriesToAdd.iteratorO ; it.hasNextO; ) {
Entry entry = (Entry)it.nextO ;
entry.postDateO ;
}
transactionBundle.getListManager().add( entriesToAdd);
}

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

4. Stellen Sie fest, ob die Sprout-Methode Werte an die Quellmethode zurückge-


ben muss. Ist dies der Fall, ändern Sie den Aufruf so, dass ihr Rückgabewert
einer Variablen zugewiesen wird.
5. Entwickeln Sie die Sprout-Methode unter Verwendung von Test-Driven Develop-
ment (8.1).
6. Entfernen Sie den Kommentar in der Quellmethode, um den Aufruf zu aktivie-
ren.
Ich empfehle Ihnen, Sprout Method immer anzuwenden, wenn Ihr Code eine
separate Aufgabe erledigt oder Sie eine Methode noch nicht in Tests einschließen
können. Dies ist erheblich vorteilhafter, als Code inline hinzuzufügen.
Wenn Sie Sprout Method verwenden möchten, sind die Dependencies in Ihrer
Klasse manchmal so nachteilig, dass Sie sie nicht instanziieren können, ohne
zahlreiche Konstruktorargumente zu simulieren. Eine Alternative ist die Verwen-
dung von Pass Null (g.i). Wenn das nicht funktioniert, könnten Sie die Sprout-
Methode als publ i c s t a t i c deklarieren. Vielleicht müssen Sie ihr Instanzvariab-
len der Quellklasse als Argumente übergeben, aber Sie könnten dann Ihre Ände-
rung vornehmen. Auch wenn diese Lösung, die Methode als s t a t i c zu
deklarieren, etwas seltsam aussehen mag, kann sie bei Legacy Code weiterhelfen.
Ich betrachte statische Methoden von Klassen oft als Bereitstellungsräume. Wenn
Sie mehrere statische Methoden deklariert und erkannt haben, dass einige diesel-
ben Variablen verwenden, können Sie oft erkennen, dass Sie eine neue Klasse
erstellen und die statischen Methoden als Instanzmethoden in die neue Klasse
verschieben können. Wenn sie tatsächlich als Instanzmethoden in die gegenwär-
tige Klasse gehören, können Sie sie wieder zurück in diese Klasse verschieben,
wenn Sie sie schließlich unter Testkontrolle bringen.

6.1.1 Vorteile und Nachteile


Sprout Method hat einige Vorteile und Nachteile. Betrachten wir zunächst die
Nachteile. Wenn Sie diese Technik einsetzen, gestehen Sie praktisch ein, dass Sie
vor der Quellmethode und ihrer Klasse im Moment kapitulieren. Sie bekommen
sie nicht unter Testkontrolle, und Sie können sie nicht verbessern. Stattdessen
fügen Sie einfach neue Funktionalität in eine neue Methode ein. Vor einer
Methode oder einer Klasse zu kapitulieren, ist manchmal praktisch, aber dennoch
traurig. Ihr Code bleibt im Schwebezustand. Die Quellmethode könnte umfang-
reichen komplizierten Code und eine einzige neue Methode als Ableger enthalten.
Manchmal ist es nicht klar, warum nur diese Arbeit an anderer Stelle geleistet
wird; und die Quellmethode bleibt in einem seltsamen Zustand. Aber wenigstens
weist das auf zusätzliche Arbeit hin, die Sie erledigen können, wenn Sie die Quell-
klasse später unter Testkontrolle bringen.

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.

6.2 Sprout Class


(Neue Klasse schreiben; wörtl. »Klasse sprießen lassen«) Sprout Method ist eine
leistungsstarke Technik, aber in manchen verschlungenen Dependency-Situatio-
nen nicht leistungsstark genug.
Angenommen, Sie müssten eine Klasse ändern, es gäbe aber keine Möglichkeit,
mit einem vernünftigen Aufwand Objekte dieser Klasse in einem Test-Harnisch
zu erstellen, weshalb es auch nicht möglich ist, in dieser Klasse eine Sprout-
Methode zu erstellen und Tests für sie zu schreiben. Vielleicht werden Sie durch
zahlreiche Erstellungs-Dependencies behindert, die das Instanziieren Ihrer Klasse
erschweren. Vielleicht gibt es viele verborgene Dependencies, die Sie nur durch
zahlreiche invasive Refactorings aufheben könnten, um sie gut genug abzutren-
nen, um die Klasse in einem Test-Harnisch zu kompilieren.

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;

pageText += " < h t m l x h e a d x t i t l e > "


"Quarterly Report"
"</ti tl e x / h e a d x b o d y x t a b l e>";
if (results.size() != 0) {
for (std::vector<Result>::iterator it = results.begin();
it != results.end() ; ++it) {
pageText += "<tr>";
pageText += "<td>" + it->department + "</td>";
pageText += "<td>" + it->manager + "</td>";
char buffer [128];
sprintf(buffer, "<td>$%d</td>", it->netProfit / 100);
pageText += std::string(buffer) ;
sprintf(buffer, "<td>$%d</td>", it->operatingExpense / 100);
pageText += std::string(buffer);
pageText += "</tr>";
}

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;
}

Angenommen, wir müssten in der erforderlichen Änderung eine Kopfzeile in eine


HTML-Tabelle einfügen. Die Kopfzeile sollte etwa wie folgt aussehen:

"<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.

Wir könnten die Änderung als kleine Klasse namens QuarterlyReportTable


HeaderProducer per Test-Driven Development (8.1) erstellen.

using namespace std;

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

Was wäre, wenn wir die Klasse QuarterlyReportTableHeaderCenerator nen-


nen und ihr folgende Schnittstelle geben würden?

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:

Virtual string generateO;


};

Vielleicht können wir QuarterlyReportGenerator bei unserer weiteren Arbeit


unter Testkontrolle bringen und ihre Implementierung so ändern, dass sie ihre
Arbeit hauptsächlich mit Generator-Klassen leisten.

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.

Hier sind die Schritte für Sprout Class:


1. 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, denken Sie sich einen guten Namen
für eine Klasse aus, die diese Arbeit leisten könnte. Schreiben Sie danach Code,
der an dieser Stelle ein Objekt dieser Klasse erstellt, und rufen Sie eine Methode
in ihm auf, die die erforderliche Aufgabe erfüllt. Kommentieren Sie dann diese
Zeile aus.
3. Stellen Sie fest, welche lokalen Variablen Sie aus der Quellmethode brauchen,
und fügen Sie sie als Argumente in den Konstruktor der Klasse ein.
4. Stellen Sie fest, ob die Sprout-Klasse Werte an die Quellmethode zurückgeben
muss. Ist dies der Fall, fügen Sie eine Methode in die Klasse ein, die diese Werte
zurückgibt, und fügen Sie in die Quellmethode einen Aufruf ein, um diese
Werte entgegenzunehmen.

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.

6.2.1 Vorteile und Nachteile


Der Hauptvorteil von Sprout Class liegt darin, dass Sie damit Ihre Arbeit zuver-
sichtlicher vorantreiben können, als dies bei invasiveren Änderungen möglich
wäre. In C++ hat Sprout Class außerdem den Vorteil, dass Sie keine vorhandenen
Header-Dateien modifizieren müssen. Sie können die Header-Datei für die neue
Klasse in der Implementierungsdatei der Quellklasse einbinden. Dass Sie eine
neue Header-Datei zu Ihrem Projekt hinzufügen, ist tatsächlich vorteilhaft. Im
Laufe der Zeit können Sie Deklarationen in die neue Header-Datei einfügen, die
sonst in der Header-Datei der Quellklasse hätten landen können. Dadurch wird
das Kompilieren der Quellklasse verkürzt. Zumindest wissen Sie, dass Sie eine
schlechte Situation nicht verschlechtern. Irgendwann später können Sie dann die
Quellklasse vielleicht unter Testkontrolle bringen.

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.

6.3 Wrap Method


(Methode einhüllen) Verhalten zu vorhandenen Methoden hinzuzufügen ist
leicht, oft aber nicht das Richtige. Eine neue Methode erfüllt normalerweise genau
eine Aufgabe. Jeder zusätzliche Code, den Sie später darin einfügen, ist verdächtig.
Möglicherweise landet er nur in der Methode, weil er zur gleichen Zeit wie der vor-
handene Code ausgeführt werden muss. Man bezeichnet dies als zeitliche Kopp-
lung. Sie macht Code sehr unübersichtlich. Wenn Sie Dinge zusammenfassen,
nur weil sie gleichzeitig passieren, ist die Beziehung zwischen ihnen nicht sehr
stark. Später müssen Sie möglicherweise das eine ohne das andere ausführen;
doch dann ist der Code vielleicht schon zusammengewachsen. Ohne Seam kann
es schwer sein, die Aufgaben zu trennen.

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

public 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.nextO ;
if (payPeriod.contains(date)) {
amount.add(card.getHours() * payRate);
}
}
payDispatcher.pay(this, date, amount);
}
}
Hier werden Tageszettel (engl, daily timecards) eines Mitarbeiters addiert und dann
seine Lohnabrechnungsdaten an einen PayDi spatcher gesendet. Angenommen,
aufgrund einer neuen Anforderung müsste bei jeder Abrechnung eine Datei mit
dem Namen des Mitarbeiters aktualisiert werden, die Daten für einen Reportgene-
rator enthält. Am einfachsten ließe sich der betreffende Code in die pay-Methode
einfügen. Schließlich passiert dies zur gleichen Zeit, nicht wahr? Was passiert,
wenn wir stattdessen Folgendes tun?

public class Employee


{
private void dispatchPaymentO {
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.getHours() * payRate);
}
}
payDispatcher.payfthis, date, amount);
}

public void pay() {


logPaymentO;
di spatchPaymentO;
}

private void logPaymentO {


}

Hier habe ich pay() in di spatchPaymentO umbenannt und als p r i v a t e dekla-


riert. Als Nächstes habe ich eine neue pay-Methode erstellt, die sie aufruft. Unsere
neue pay()-Methode protokolliert eine Bezahlung und versendet sie dann. Cli-
ents, die pay() aufgerufen haben, müssen nichts über die Änderung wissen. Sie
rufen einfach die Methode auf, und alles funktioniert korrekt.

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 class Employee


{
public void makeLoggedPaymentO {
"logPaymentO ;
pay();
}

public void pay() {


}

private void logPaymentO {


}

Jetzt können die Anwender so oder so bezahlen.

Wrap Method eignet sich hervorragend, um beim Hinzufügen neuer Funktionen


Seams einzuführen. Es gibt einige Nachteile. Erstens darf die neue Funktion nicht
mit der Logik der alten verquickt sein. Sie muss entweder vor oder nach der alten
Funktion ausgeführt werden können. (Genau genommen ist dies eigentlich kein
Nachteil.) Zweitens müssen Sie einen neuen Namen für den alten Code in der
Methode finden. Hier nenne ich den Code in der payO-Methode dispatchPay-
ment(). Dies ist etwas weit hergeholt, und, offen gesagt, gefällt mir das Ergebnis
nicht. Tatsächlich leistet die di spatchPayment()-Methode mehr, als eine Abrech-
nung weiterzuleiten; sie berechnet auch die Lohnsumme. Würde ich Tests einrich-
ten, würde ich wahrscheinlich den ersten Teil von di spatchPaymentO in eine
eigene Methode namens calculatePayO extrahieren und die payO-Methode
wie folgt ändern:

public void p a y O {
logPaymentO;
Money amount = calculatePayO;
dispatchPayment(amount);
}

Damit scheinen alle Aufgaben sauber getrennt zu sein.

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.

6.3.1 Vorteile und Nachteile


Wrap Method eignet sich, um neue, getestete Funktionalität in eine Anwendung
einzufügen, wenn wir leicht Tests für den aufrufenden Code schreiben können.
Sprout Method und Sprout Class fügen Code zu vorhandenen Methoden hinzu und
machen sie um wenigstens eine Zeile länger; dagegen vergrößert Wrap Method
vorhandene Methoden nicht.

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

6.4 Wrap Class


(Klasse einhüllen) Wrap Class ist das Gegenstück zu Wrap Method auf Klassen-
ebene. Wrap Class basiert im Wesentlichen auf demselben Konzept. Wenn wir Ver-
halten zu einem System hinzufügen müssen, können wir es zu einer
vorhandenen Methode oder einer Komponente hinzufügen, die diese Methode
verwendet. Bei Wrap Class ist diese Komponente eine andere Klasse.

Betrachten wir noch einmal den Code der Empl oyee-Klasse:

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.

class LoggingEmployee implements Employee


{
..public LoggingEmployee(Employee e) {
employee = e;
}

public void pay() {


logPaymentO;
employee.pay();
}

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


Mit einem Decorator können Sie komplexes Verhalten erstellen, indem Sie
Objekte zur Laufzeit zusammenfügen. So könnte etwa ein industrielles Prozess-
kontrollsystem eine Klasse namens Tool Controller mit Methoden wie etwa
raiseO, lower(), stepO, on() oder off() enthalten. Wenn zusätzliche
Funktionen ausgeführt werden müssen, wenn raiseO oder lowerO aufgeru-
fen werden (etwa um ein akustisches Signal auszugeben, um die Mitarbeiter zu
warnen), könnten wir diese Funktionalität direkt in diese Methoden in der
Tool Controller-Klasse einfügen. Doch wahrscheinlich wären dies nicht alle
Erweiterungen. Vielleicht soll die Anzahl der Aktivierungen des Controllers pro-
tokolliert werden. Vielleicht müssen benachbarte Controller benachrichtigt wer-
den, damit sie nicht gleichzeitig in Aktion treten usw. usw. Wir könnten unsere
fünf einfachen Operationen (rai se, 1 ower, step, on und off) endlos erweitern;
und für jede Kombination von Funktionen Unterklassen zu erstellen, wäre
unpraktisch. Die Anzahl der möglichen Kombinationen dieser Verhaltenswei-
sen wüchse exponential.

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 { Controller. s t e p O ; }


public void on() { C o n t r o l l e r . o n ( ) ; }
public void off() { Controller.off(); }
}
Dem Anschein widersprechend ist Klasse sehr nützlich. Sie können eine Unter-
klasse davon ableiten und darin die Methoden überschreiben, um zusätzliches
Verhalten hinzuzufügen. Wenn wir etwa bei einem Schritt andere Controller
benachrichtigen müssen, könnten wir folgenden StepNotifyingController
definieren:
public class StepNotifyingController extends ToolControllerDecorator
{
private List notifyees;
public StepNotifyingControner(ToolController Controller, List
notifyees) {
super(controller) ;
this.notifyees = notifyees;
}

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);

Wird eine Operation wie etwa s t e p O ausgeführt, benachrichtigt der Controller


alle noti fyees (Empfänger), gibt einen Alarm aus und führt dann die Stepping-
Aktion aus. Dieser letzte Schritt, die Stepping-Aktion, erfolgt in ACMEControl-
ler, einer konkreten Unterklasse von ToolController, nicht in ToolControl -
1 erDecorator. Er gibt den Stab nicht an ein anderes Objekt weiter, sondern
leistet nur die Aufgaben eines ToolControllers. Für das Decorator Pattern brau-
chen Sie wenigstens eine dieser »Basis«-Klassen, die Sie einhüllen.

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.

Dieses Verfahren eignet sich, um Funktionalität hinzuzufügen, wenn ein System


viele Aufrufer einer Methode wie pay () enthält. Doch es gibt eine andere Methode
des Wrappings, die nicht so Decorator-artig funktioniert. Angenommen, wir

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.

Hier ist eine kleine Belasse, die dies tut:

class LoggingPayDispatcher
{
private Employee e;
public LoggingPayDispatcher(Employee e) {
this.e = e;
}

public void pay() { employee. pay() ; logPaymentO;


}

private void logPaymentO {


}

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.

Hier sind die Schritte für Wrap Class:

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, erstellen Sie eine Klasse, die die einzuhüllende Klasse
als Konstruktor-Argument akzeptiert. Wenn Sie Schwierigkeiten haben, eine
Klasse zu erstellen, die die ursprüngliche Klasse in einem Test-Harnisch ein-
hüllt, müssen Sie möglicherweise Extract Implementer (25.9) oder Extract Inter-
face (25.10) auf die eingehüllte Klasse anwenden, damit Sie Ihren Wrapper
instanziieren können.

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:

public void pay() {


logPaymentO;
Money amount = calculatePayO;
di spatchPayment(amount);
}

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

Änderungen brauchen eine Ewigkeit

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.

Betrachten wir die Gründe und einige mögliche Lösungen.

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.

7.3 Dependencies aufheben


Dependencies können problematisch sein; aber glücklicherweise können wir sie
aufheben. Bei objektorientiertem Code besteht der erste Schritt oft in dem Ver-
such, die benötigten Klassen in einem Test-Harnisch zu instanziieren. In den ein-
fachsten Fällen müssen wir nur die Deklaration der Klassen, von denen wir
abhängen, importieren oder einbinden. In schwierigeren Fällen sollten Sie die
Techniken aus Kapitel 9, Ich kann diese Klasse nicht in einen Test-Harnisch einfügen,
ausprobieren. Können Sie ein Objekt einer Klasse in einem Test-Harnisch erstel-
len, müssen Sie vielleicht andere Dependencies aufheben, wenn Sie einzelne
Methoden testen wollen. Hier kann Ihnen Kapitel 10, Ich kann diese Methode nicht
in einem Test-Harnisch ausführen, helfen.

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.

Manchmal sind Änderungen unkompliziert, aber manchmal scheitern Entwickler


schon beim ersten Versuch, eine Klasse in einen Test-Harnisch einzufügen. Dies
kann bei einigen Systemen sehr aufwendig sein. Einige Klassen sind riesig;
andere haben so viele Dependencies, dass sie die Funktionalität, mit der Sie arbei-
ten wollen, komplett überlagern. In solchen Fällen lohnt es sich zu prüfen, ob Sie
größere Fragmente des Codes herauslösen und unter Testkontrolle bringen kön-
nen. Kapitel 12, Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für
alle beteiligten Klassen aufheben?, enthält Techniken, mit denen Sie so genannte

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

sultantSchedulerDB und einen AddOpportuni tyXMLGenerator. Diese beiden


Klassen könnten ihrerseits durchaus von anderen Klassen abhängen, die nicht in
dem Diagramm stehen.

Abb. 7.1: Opportuni ty-Klassen

Wenn wir einen AddOpportuni tyFormHandl er instanziieren wollen, wissen wir


nicht, wie viele Klassen wir letztlich brauchen. Wir können dieses Problem lösen,
indem wir die Dependencies aufheben. Die erste Dependency ist Consultant-
Schedul erDB. Wir brauchen ein Objekt dieser Klasse, das an den Konstruktor von
AddOpportuni tyFormHandl er übergeben wird. Es wäre ungeschickt, diese
Klasse zu verwenden, weil sie eine Verbindung mit der Datenbank herstellt, was
wir beim Testen vermeiden wollen. Doch mit Extract Implementer (25.9) könnten
wir die Dependency aufheben (siehe Abbildung 7.2).

Abb. 7.2: Einen Implementer aus ConsultantSchedulerDB extrahieren

105
Kapitel 7
Änderungen brauchen eine Ewigkeit

Mit dieser ConsultantSchedulerDB-Schnittstelle können wir ein Objekt von


AddOpportuni tyFormHandl er erstellen, das ein Fake-Objekt verwendet, das die
ConsultantSchedulerDB-Schnittstelle implementiert. Interessanterweise wird
unser Build durch Aufheben dieser Dependency unter gewissen Bedingungen
schneller. Bei der nächsten Änderung von ConsultantSchedulerDBImpl muss
AddOpportuni tyFormHandl er nicht neu kompiliert werden. Warum? Nun, die
Klasse hängt nicht mehr direkt von dem Code in ConsultantSchedulerDBImpl
ab. Wir können diese Datei so oft ändern, wie wir wollen; solange wir dabei die
ConsultantSchedulerDB-Schnittstelle nicht ändern müssen, muss die Klasse
AddOpportuni tyFormHandl er nicht n e u kompiliert w e r d e n .

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.

Abb. 7.3: Einen Implementer von Opportuni tyltem extrahieren

Jetzt hängt AddOpportuni tyFormHandl er überhaupt nicht von dem ursprüng-


lichen Code in Opportuni tyltem ab. In gewisser Weise haben wir eine »Kompi-
lierungs-Firewall« in den Code eingebaut. Wir können ConsultantScheduler-
DBImpl u n d Opportuni tyltemlmpl so viel ändern, w i e w i r wollen, o h n e dass
A d d O p p o r t u n i t y F o r m H a n d l e r oder Nutzer v o n A d d O p p o r t u n i t y F o r m H a n d l e r
neu kompiliert werden müssen. Wollten wir dies auch in der Package-Struktur der
Anwendung zum Ausdruck bringen, könnten wir unser Design auf separate
Packages verteilen (siehe Abbildung 7.4).

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

Abb. 7.4: Refaktorisierte Package-Struktur

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

Entwickeln mehr Feedback bekommen. Meistens bedeutet dies weniger Fehler


und weniger Ärger. Aber umsonst bekommen Sie dies nicht. Mehr Schnittstellen
und Packages sind nur für einen gewissen konzeptionellen Overhead zu haben.
Ist dies ein fairer Preis, verglichen mit der Alternative? Sicher kann es gelegentlich
etwas länger dauern, in mehr Packages und Schnittstellen etwas zu finden, aber
dann können Sie sehr leicht damit arbeiten.

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.

Abb. 7.5: Package-Struktur

Wenn Sie anfangen, Ihre durchschnittliche Build-Zeit zu optimieren, erhalten Sie


Code-Fragmente, mit denen Sie sehr leicht arbeiten können. Es könnte etwas
schwierig sein, einen kleinen Satz von Klassen separat unter Testkontrolle zu brin-
gen und zu kompilieren, aber wichtig ist, nicht zu vergessen, dass Sie dies für die-
sen Satz von Klassen nur einmal machen müssen; danach können Sie die Vorteile
für immer ernten.

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

Wie füge ich eine Funktion hinzu?

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?

8.1 Test-Driven Development (TDD)


Test-Driven Developement(TDD; dt. testgetriebene Entwicklung, testgesteuerte Entwick-
lung) ist meines Wissens nach die leistungsstärkste Technik, um neue Funktionen
hinzuzufügen. Kurz gesagt funktioniert sie wie folgt: Uns schwebt eine Methode
vor, um den Teil eines Problems zu lösen, und dann schreiben wir einen Fading
Test Case dafür. Die Methode existiert noch nicht, aber wenn wir einen Test dafür
schreiben können, haben wir unser Verständnis über den zu schreibenden Code
fundiert.

Test-Driven Development arbeitet mit folgendem kleinen Algorithmus:


1. Schreiben Sie einen Fading Test Case.
2. Sorgen Sie dafür, dass er kompiliert wird.
3. Sorgen Sie dafür, dass er bestanden wird.
4. Entfernen Sie duplizierten Code.
5. Fangen Sie von vorne an.
Im folgenden Beispiel arbeiten wir mit einer Finanzanwendung. Wir brauchen
eine Klasse, die mit einer aufwendigen Berechnung verifiziert, ob bestimmte
Wertpapiere gehandelt werden sollten, konkret eine Java-Klasse, die das so
genannte erste statistische Moment um einen Punkt berechnet. Wir verfügen
noch nicht über eine entsprechende Methode, aber wir wissen, dass wir einen
Testfall für die Methode schreiben können. Wir kennen die Berechnung; deshalb
wissen wir, dass die Daten, die wir in dem Test-Code verwenden, das Ergebnis
- 0 . 5 liefern sollten.

8.1.1 Schreiben Sie einen Failing Test C a s e


Hier ist ein Testfall für die benötigte Funktionalität.

public void testFirstMomentC) {


InstrumentCalculator calculator = new InstrumentCalculatorO;
calculator.addElement(l.O);
calculator.addElement(2.0) ;
assertEquals(-0.5, calculator.fi rstMomentAbout(2.0), TOLERANCE);
}

8.1.2 Sorgen Sie dafür, d a s s er kompiliert wird


Der gerade geschriebene Test ist brauchbar, wird aber nicht kompiliert. Instru-
mentCal cul ator enthält keine Methode namens f i rstMomentAbout. Wir fügen
sie als leere Methode hinzu. Da der Test scheitern soll, definieren wir als Rück-
gabewert der Methode den doubl e-Wert NaN (der definitiv nicht der erwartete
Wert - 0 . 5 ist).

110
8.1
Test-Driven Development (TDD)

public class InstrumentCalculator


{
double firstMomentAbout(double point) {
return Double.NaN;
}

8.1.3 Sorgen Sie dafür, d a s s er bestanden wird


Wenn der Test eingerichtet ist, schreiben wir den Code, mit dem er bestanden
wird.

public double firstMomentAbout(double point) {


double numerator = 0.0;
for (Iterator it = elements.iteratorO; it.hasNextO; ) {
double element = ((Double)(it. next())) .doubleValueO ;
numerator += element - point;
}
return numerator / elements.size() ;
}

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.

8.1.4 Entfernen Sie duplizierten Code


Gibt es hier duplizierten Code? Nein. Wir können mit dem nächsten Fall fortfah-
ren.

8.1.5 Schreiben Sie einen Failing Test C a s e


Der Code in den voranstehenden Absätzen führt dazu, dass der Test bestanden
wird, aber er eignet sich definitiv nicht für alle Fälle. In der Anweisung zur Berech-
nung des Rückgabewertes könnte aus Versehen eine Division durch null erfolgen.
Was sollten wir in diesem Fall tun? Was geben wir zurück, wenn keine Elemente
vorhanden sind? In diesem Fall wollen wir eine Ausnahme auslösen. Die Ergeb-
nisse sagen uns nur etwas, wenn unsere Elementeliste Daten enthält.
Der folgende Test ist etwas Besonderes. Er scheitert, wenn keine Inval i dBasi s-
Excepti on ausgelöst wird, und er wird bestanden, wenn keine Ausnahmen ausge-
löst werden oder wenn keine andere Ausnahme ausgelöst wird. Wenn wir ihn
ausführen, scheitert er, weil eine Ari thmeti cExcepti on ausgelöst wird, wenn wir
in fi rstMomentAbout durch null dividieren.

public void testFirstMoment() {


try {
new InstrumentCalculatorO .fi rstMomentAbout(O.O) ;

in
Kapitel g
Wie füge ich eine Funktion hinzu?

fail("expected InvalidBasisException");
}
catch (InvalidBasisException e) {
}

8.1.6 S o r g e n S i e dafür, d a s s er kompiliert wird

Z u d i e s e m Zweck m ü s s e n w i r die Deklaration v o n fi rstMomentAbout so ändern,


dass die Methode eine Inval i dBasi sExcepti on auslöst.

public double firstMomentAbout(double point)


throws InvalidBasisException {

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNextO; ) {
double element = ((Double) (it.nextO)) .doubleValueO ;
numerator += element - point;
}
return numerator / elements.sizeO;

A b e r so wird die Methode nicht kompiliert. Die Compiler-Fehler sagen u n s , dass


w i r die A u s n a h m e tatsächlich auslösen m ü s s e n , w e n n sie in der Deklaration auf-
g e f ü h r t ist. Deshalb e r g ä n z e n w i r den Code wie folgt.

public double firstMomentAbout(double point)


throws InvalidBasisException {

if (elements.sizeO == 0)
throw new InvalidBasisException("no elements");

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNextO; ) {
double element = ((Double) (it. next())) .doubleValueO ;
numerator += element - point;
}
return numerator / elements.sizeO;

8.1.7 S o r g e n Sie dafür, d a s s er b e s t a n d e n wird

Jetzt w e r d e n u n s e r e Tests bestanden.

8.1.8 Entfernen Sie duplizierten C o d e

In d i e s e m Fall gibt es k e i n e n duplizierten Code.

8.1.9 Schreiben Sie einen Failing Test C a s e

Als Nächstes m ü s s e n w i r eine Methode schreiben, die das zweite statistische


M o m e n t u m e i n e n Punkt berechnet. Tatsächlich handelt es sich n u r u m eine Vari-

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.

public void testSecondMomentO throws Exception {


InstrumentCalculator calculator = new InstrumentCalculatorO ;
calculator.addElement(l.O);
calculator.addElement(2.0) ;

assertEquals(0.5, calculator.secondMomentAbout(2.0), TOLERANCE);


}

8 . 1 . i o Sorgen Sie dafür, d a s s er kompiliert wird


Damit der Test kompiliert wird, müssen wir eine Definition für secondMoment-
About hinzufügen. Wir können denselben Trick wie für die f i rstMomentAbout-
Methode verwenden; aber es zeigt sich, dass der Code für das zweite Moment nur
eine geringe Variante des Codes für das erste Moment ist.

Die folgende Zeile

numerator += element - point;

aus f i rstMoment muss beim zweiten Moment wie folgt lauten:

numerator += Math.pow(element - point, 2.0);

Solche Berechnungen folgen einem allgemeinen Muster. Das n-te statistische


Moment wird mit folgendem Ausdruck berechnet:

numerator += Math.pow(element - point, N);

Der Code in f i rstMomentAbout funktioniert, weil element - point denselben


Wert ergibt wie Math. pow(el ement - poi nt, 1 . 0 ) .
An diesem Punkt haben wir mehrere Möglichkeiten. Wir können die Allgemein-
heit anerkennen und eine allgemeine Methode schreiben, die einen »about«-
Punkt und einen Wert für N übernimmt. Dann können wir jede Verwendung von
f i rstMomentAbout (doubl e) durch einen Aufruf dieser allgemeinen Methode
ersetzen. Wir könnten dies tun, aber dann müssten die Aufrufer immer einen
N-Wert angeben; und wir wollen Kunden nicht die Möglichkeit geben, beliebige
Werte für N zu übergeben. Wir scheinen hier in die falsche Richtung zu denken.
Deshalb sollten wir dieses Problem zurückstellen und zunächst das Angefangene
beenden. Im Moment müssen wir dafür sorgen, dass der Test kompiliert wird. Wir
können später immer noch verallgemeinern, sollten wir das dann noch wollen.

Damit der Test kompiliert wird, können wir die f i rstMomentAbout-Methode


kopieren und in secondMomentAbout umbenennen:

113
Kapitel g
Wie füge ich eine Funktion hinzu?

public double secondMomentAbout(double point)


throws InvalidBasisException {

if (elements.sizeO == 0)
throw new InvalidBasisException("no elements");

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNextO; ) {
double element = ((Double) (i t.next())) .doubleValueO ;
numerator += element - point;
}
return numerator / elements.sizeO;

8.i.n Sorgen Sie dafür, d a s s er bestanden wird


Dieser Code besteht den Test nicht. Wenn er scheitert, können wir ihn wie folgt
ändern:

public double secondMomentAbout(double point)


throws InvalidBasisException {

if (elements.sizeO == 0)
throw new InvalidBasisException("no elements");

double numerator = 0.0;


for (Iterator it = elements.iterator(); it.hasNextO; ) {
double element = ((Double) (it.next())) .doubleValue() ;
numerator += Math.pow(element - point, 2.0);
}
return numerator / elements.sizeO;
}

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.

8 . 1 . 1 2 Entfernen Sie duplizierten C o d e


Nachdem jetzt beide Tests bestanden werden, müssen wir als Nächstes den dupli-
zierten Code entfernen. Doch wie?

114
8.1
Test-Driven Development (TDD)

Wir könnten etwa den gesamten Body von secondMomentAbout extrahieren, in


nthMomentAbout umbenennen und ihm einen Parameter, N, übergeben:

public double secondMomentAbout(double point)


throws InvalidBasisException {
return nthMomentAbout(point, 2.0);
}
private double nthMomentAbout(double point, double n)
throws InvalidBasisException {

if (elements.size() == 0)
throw new InvalidBasisException("no elements");

double numerator = 0.0;


for (Iterator it = elements.iteratorO; it.hasNextO; ) {
double element = ((Double)(it.next())) .doubleValueO;
numerator += Math.pow(element - point, n);
}
return numerator / elements.sizeO;
}

Jetzt werden unsere Tests bestanden. Wir können zur fi rstMomentAbout-


Methode zurückgehen und deren Body ebenfalls durch einen Aufruf von nthMo-
mentAbout ersetzen:

public double firstMomentAbout(double point)


throws InvalidBasisException {
return nthMomentAbout(point, 1.0);
}

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.

TDD und Legacy Code


Einer der wertvollsten Aspekte von TDD besteht darin, dass wir uns zu einem
Zeitpunkt auf eine Sache konzentrieren können. Entweder schreiben wir Code
oder wir refaktorisieren ihn; doch wir machen niemals beides gleichzeitig.

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.

8.2 Programming by Difference


Test-Driven Development ist nicht an Objektorientierung gebunden. Tatsächlich han-
delt es sich bei dem Beispiel aus dem vorangegangenen Abschnitt eigentlich um
prozeduralen Code, der in eine Klasse eingehüllt wurde. Bei der 0 0 haben wir
eine weitere Option. Wir können Funktionen durch Vererbung einfügen, ohne
eine Klasse direkt zu ändern. Nachdem wir die Funktion hinzugefügt haben, kön-
nen wir genau herausfinden, wie wir sie am besten integrieren sollten.

Die Schlüsseltechnik für dieses Verfahren heißt Programming by Difference (wörtl.


Programmierung durch Unterscheidung). Die Technik ist recht alt und wurde bereits
in den 1980er Jahren intensiv diskutiert. Sie fiel in den 1990er Jahren in
Ungnade, als die OO-Community zu der Einsicht kam, eine übermäßige Anwen-
dung der Vererbung könne ziemlich problematisch sein. Aber die Tatsache, dass
wir zunächst mit Vererbung arbeiten, bedeutet nicht, dass wir dies auch im end-
gültigen Produkt tun müssen. Mit der Hilfe von Tests können wir leicht auf andere
Strukturen übergehen, wenn die Vererbung problematisch werden sollte.

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:

private InternetAddress getFromAddress(Message message)


throws MessagingException {

Address [] from = message.getFrom ();

116
8.2
Programming by Difference

if (from != null && from.length > 0)


return new InternetAddress (from [Oj.toString ());
return new InternetAddress CgetDefaultFromO);
}
Diese Methode soll die »von«-Adresse aus einer empfangenen Mail-Message extra-
hieren und zurückgeben, damit sie als »von«-Adresse der Message verwendet wer-
den kann, die an die Empfänger auf der Liste weitergeleitet werden.
Sie wird nur an einer Stelle verwendet. Hier sind die entsprechenden Zeilen in
einer Methode namens forwardMessage:

MimeMessage forward = new MimeMessage (session);


forward.setFrom (getFromAddress (message));

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):

public void testAnonymous () throws Exception {


MessageForwarder forwarder = new MessageForwarderO;
forwarder.forwardMessage (makeFakeMessageO);
assertEquals ("anon-members@" + forwarder.getDomain() ,
expectedMessage.getFrom ()[0].toStringO);
}

Müssen wir MessageForwarder modifizieren, um diese Funktionalität hinzuzu-


fügen? Nicht wirklich - wir könnten einfach eine Unterklasse von MessageFor-
warder namens AnonymousMessageForwarder bilden und diese in dem Test
verwenden:

public void testAnonymous () throws Exception {


MessageForwarder forwarder = new AnonymousMessageForwarderO;
forwarder.forwardMessage (makeFakeMessageO);
assertEquals ("anon-members@" + forwarder.getDomainO ,
expectedMessage.getFrom () [0] .toStringO) ;
}

Dann bilden wir die Unterklasse (siehe Abbildung 8.i).


Hier wurde die getFromAddress-Methode in MessageForwarder nicht als p r i -
vate, sondern als protected deklariert und dann in AnonymousMessageForwar-
der überschrieben. In dieser Klasse sieht sie wie folgt aus:
Kapitel g
Wie füge ich eine Funktion hinzu?

protected InternetAddress getFromAddress(Message message)


throws MessagingException {
String anonymousAddress = "anon-" + "listAddress;
return new InternetAddress(anonymousAddress);
}

Abb. 8.i: Eine Unterklasse von MessageForwarder

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.

public void testAnonymous () throws Exception {


MessageForwarder forwarder = new AnonymousMessageForwarderO;
forwarder.forwardMessage (makeFakeMessageO);
assertEquals ("anon-members@" + forwarder.getDomain(),
expectedMessage.getFrom ()[0].toStringO) ;
}

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

Abb. 8.2: Unterklassen für zwei Unterschiede

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:

Properties configuration = new Properties();


configuration.setProperty("anonymous", "true");
MessageForwarder forwarder = new MessageForwarder(configuration);

Können wir in diesem Fall erreichen, dass unser Test bestanden wird? Betrachten
wir noch einmal den Test:

public void testAnonymous () throws Exception {


MessageForwarder forwarder = new AnonymousMessageForwarderO ;
forwarder.forwardMessage (makeFakeMessageO);
assertEquals ("anon-members@" + forwarder.getDomain() ,
expectedMessage.getFrom () [0] .toStringO) ;
}

119
Kapitel g
Wie füge ich eine Funktion hinzu?

Gegenwärtig wird dieser Test bestanden. AnonymousMessageForwarder über-


schreibt die getFrom-Methode von MessageForwarder. Was passiert, wenn wir
die getFrom-Methode in MessageForwarder wie folgt ändern?

private InternetAddress getFromAddress(Message message)


throws MessagingException {

String fromAddress = getDefaultFrom();


if (configuration,getProperty("anonymous").equals("true")) {
fromAddress = "anon-members@" + domain;
}
eise {
Address [] from = message.getFrom ();
if (from != null && from.length > 0) {
fromAddress = from [Oj.toString () ;
}
}
return new InternetAddress (fromAddress) ;
}

Jetzt sollte die getFrom-Methode in MessageForwarder den anonymen und den


normalen Fall handhaben können. Wir können dies verifizieren, indem wir die
Überschreibung von getFrom in AnonymousMessageForwarder auskommentie-
ren und prüfen, ob die Tests bestanden werden:

public class AnonymousMessageForwarder extends MessageForwarder


{
/*
protected InternetAddress getFromAddress(Message message)
throws MessagingException {
String anonymousAddress = "anon-" + listAddress;
return new InternetAddress(anonymousAddress);
}
V
}

Und tatsächlich werden sie bestanden.

Da wir die AnonymousMessageForwarder-Klasse nicht mehr brauchen, können


wir sie löschen. Dann müssen wir jede Stelle suchen, an der wir einen Anony-
mousMessageForwarder erstellen, und den Aufruf des Konstruktors durch einen
Aufruf des Konstruktors ersetzen, der eine Properties Collection akzeptiert.

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

private InternetAddress getFromAddress(Message message)


throws MessagingException {

String fromAddress = getDefaultFromO;


if (configuration.getProperty("anonymous").equalsC'true")) {
fromAddress = "anon-members@" + domain;
}
eise {
Address [] from = message.getFrom ();
if (from != null && from.length > 0)
fromAddress = from [0].toString () ;
}
return new InternetAddress (fromAddress);

Nach einigen Refactoring-Schritten sieht er wie folgt aus:

private InternetAddress getFromAddress(Message message)


throws MessagingException {

String fromAddress = getDefaultFromO;


if (configuration,getProperty("anonymous").equals("true")) {
from = getAnonymousFromO;
}
eise {
from = getFrom(Message);
}
return new InternetAddress (from);
}

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)

Abb. 8.3: Delegation an Mai Ii ngConfi guration


Kapitel g
Wie füge ich eine Funktion hinzu?

Sieht gut aus, aber ist dies nicht zu viel des Guten? Macht MailingConfigura-
tion nicht dasselbe wie eine Properties Collection?

Was passiert, wenn wir getFromAddress in die MailingConfiguration-Klasse


verschieben wollten? Die MailingConfiguration-Klasse könnte eine Message
entgegennehmen und die zurückzugebende »von«-Adresse auswählen. Ist laut
Konfiguration die Anonymität ausgewählt, würde sie die »von«-Adresse für ano-
nyme Mails zurückgeben, andernfalls könnte sie die erste Adresse aus der Mes-
sage zurückgeben (siehe Abbildung 8.4).

MessageForwarder

+ MessageForwarderQ
MailingConfiguration
+ processMessage(Message)
- forwardMessage( Message) + getFromAddress(Message)

Abb. 8.4: Das Verhalten in MailingConfiguration verschieben

Wir könnten auch andere Methoden in MailingConfiguration einfügen. Woll-


ten wir etwa die Offlist-Empfänger-Funktion implementieren, könnten wir eine
Methode namens bui 1 dRecipientList zu MailingConfiguration hinzufügen
und sie von MessageForwarder aufrufen lassen (siehe Abbildung 8.5).

MessageForwarder

+ MessageForwarderf)
MailingConfiguration
+ processMessage(Message)
- forwardMessage(Message) + getFromAddress(Message)
+ buildRecipientList(List recipients) : List

Abb. 8.5: Zusätzliches Verhalten in MailingConfiguration einfügen

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.

Es gibt viele leistungsstarke Refactorings, aber Rename C l a s s ist die leistungs-


stärkste. Ein anderer Name ändert die Wahrnehmung des Codes durch die Nut-
zer und lässt sie Möglichkeiten erkennen, an die sie vorher vielleicht nicht
gedacht haben.

122
8.2
Programming by Difference

MessageForwarder

+ MessageForwarder()
MailingList
+ processMessage(Message)
- forwardMessage(Message) + getFromAddress(Message)
+ buildRecipientList(List recipients) : List

Abb. 8.6: Die in MailingList umbenannte Klasse Mail i ngConfi guration

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).

Das Liskov Substitution Principle (Liskov-Ersetzungsprinzip)


Durch Vererbung können wir einige subtile Fehler verursachen. Betrachten Sie
den folgenden Code:
public class Rectangle
{

public Rectangle(int x, int y, int width, int height) { ... }


public void setWidth(int width) { ... }
public void setHeight(int height) { ... }
public int g e t A r e a O { ... }
}
Können wir von dieser Rectangl e-Klasse eine Unterklasse namens Square
ableiten?
public class Square extends Rectangle
{

public Square(int x, int y, int width) { ... }

}
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:

1. Falls möglich, sollten Sie konkrete Methoden nicht überschreiben.


2. Falls Sie es tun, prüfen Sie, ob Sie die Methode, die Sie überschreiben, in der
überschreibenden Methode aufrufen können.

Stopp! Dies haben wir in MessageForwarder nicht gemacht. Tatsächlich machten


wir das Gegenteil. Wir überschrieben eine konkrete Methode in einer Unterklasse
(AnonymousMessageForwarder). Was ist falsch daran?

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

Abb. 8.7: Normalisierte Hierarchie

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

Ich kann diese Klasse nicht in einen


Test-Harnisch einfügen

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.

9.1 Der Fall des irritierenden Parameters


Normalerweise gehe ich Änderungen eines Legacy-Systems optimistisch an.
Warum, weiß ich nicht. Ich versuche, möglichst realistisch zu sein, aber der Opti-
mismus ist immer da: Einfach hier eine Methode hinzufügen, dort eine andere
Methode ändern und dann alles in einen Test-Harnisch einfügen - fertig! Doch
dann regen sich kleine Zweifel. Der einfachste Konstruktor dieser Klasse akzep-
tiert drei Parameter. Doch der Optimismus bleibt: Vielleicht ist es nicht zu schwer,
sie zu konstruieren.
Prüfen wir an einem Beispiel, ob mein Optimismus gerechtfertigt oder nur ein
Schutzmechanismus ist.
Im Code für ein Abrechnungssystem finden wir eine ungetestete Java-Klasse
namens CreditValidator.

127
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen

public class CreditValidator


{
public CreditValidator(RGHConnection connection, CreditMaster master,
String validatorlD) {
}

Certificate validateCustomer(Customer customer)


throws InvalidCredit {

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.

Wir sollen eine neue Methode namens getValidationPercent zu dieser Klasse


hinzufügen. Sie soll uns den Prozentsatz der erfolgreichen validateCustomer-
Aufrufe während der Lebensdauer des val i dator melden.

Wie fangen wir an?

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:

public void testCreateQ {


CreditValidator validator = new CreditValidator() ;
}

Ob das Instanziieren einer Klasse in einem Test-Harnisch schwierig ist, probiert


man am besten aus. Schreiben Sie einen Testfall und versuchen Sie, ein Objekt
darin zu erstellen. Der Compiler wird Ihnen sagen, was Sie machen müssen,
damit es wirklich funktioniert.

Dieser Test ist ein Konstruktionstest. Konstruktionstests sehen etwas ungewöhn-


lich ist. Normalerweise verwende ich in solchen Tests keine Zusicherungen
(Assertionen). Ich versuche einfach, das Objekt zu erstellen. Später, wenn ich
schließlich ein Objekt in dem Test-Harnisch erstellen kann, entferne ich ihn nor-
malerweise oder benenne ihn so um, dass ich mit ihm etwas Substanzielleres tes-
ten kann. Zurück zu unserem Beispiel:

Da wir noch keine Argumente des Konstruktors hinzugefügt haben, beschwert


sich der Compiler. Es teilt uns mit, dass es keinen Standardkonstruktor für Cre-

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:

public class RCHConnection


{
public RCHConnectionCint port, String Name, string passwd)
throws IOException {

}
}

public class CreditMaster


{
public CreditMaster(String filename, boolean isLocal) {
}

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?

public void testCreateO throws Exception {


RCHConnection connection = new RGHConnection(DEFAULT_PORT, "admin", "rii8ii9s");
CreditMaster master = new CreditMaster("crm2.mas", true);
CreditValidator validator = new CreditValidator(connection, master, "a");
}

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

+ RGHConnection(port, name, passward)


+ connect()
+ disconnect()
+ RFDIReportForjid : int) : RFDIReport
+ ACTIOReportFor(customerlD : int) ACTIOReport
- retry()
- formPacket() : RFPacket

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

^-ACr/OFepo/TFor/'ci/stomer/ü ://j/J. -ACr/Otfepost

l
RGHConnection

+ RGHConnection(port, name, passward)


+connect()
+disconnect()
+RFDIReportFor(id : int) : RFDIReport
+ACTIOReportFor(customerlD : int) ACTIOReport
- retry()
- formPacket(): RFPacket

A b b . 9.2: R G H C o n n e c t i o n nach d e m Extrahieren einer Schnittstelle

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:

public class FakeConnection implements IRGHConnection


{
public RFDIReport report;

public void connectO {}


public void disconnectO {}
public RFDIReport RFDIReportFor(int id) { return report; }
public ACTIOReport ACTIOReportFor(int customerlD) { return null; }
}

Diese Klasse ermöglicht uns folgende Tests:

void testNoSuccessO throws Exception {


CreditMaster master = new CreditMaster("crm2.mas", true);
IRGHConnection connection = new FakeConnectionO;

CreditValidator validator = new CreditValidator(connection, master, "a");

connection.report = new RFDIReport(. . .) ;

Certificate result = validator.validateCustomer(new Customer(...)) ;

assertEqualsCCertificate.VALID, result.getStatusO);
}

Die FakeConnection-Klasse ist etwas ungewöhnlich. Wie oft schreiben wir


Methoden, die keinen Body haben oder einfach null an die Aufrufer zurückge-
ben? Schlimmer: Sie hat eine public Variable, die man beliebig setzen kann. Es
scheint, als verstieße die Klasse gegen alle Regeln. Nun, nicht wirklich. Für Klas-
sen, mit denen wir das Testen ermöglichen, gelten andere Regeln. Der Code in
FakeConnection ist kein Produktions-Code. Er ist nur für den Test-Harnisch
bestimmt.

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:

void testAl1PassedlOOPercent() throws Exception {


CreditMaster master = new CreditMaster("crm2.mas", true);
IRGHConnection connection = new FakeConnection("admin", "rii8ii9s");
CreditValidator validator = new CreditValidator(connection, master, "a");

connection.report = new RFDIReport(...) ;


Certificate result = validator.validateCustomer(new Customer(.. .)) ;
assertEquals(100.0, validator.getValidationPercentO , THRESHOLD) ;
}

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

Test-Code im Gegensatz zu Produktions-Code


Test-Code muss nicht dieselben Standards erfüllen wie Produktions-Code. Im
Allgemeinen verzichte ich auf komplizierte Einkapselungen, indem ich Varia-
blen als public deklariere, wenn ich so meine Tests einfacher schreiben kann.
Doch Test-Code sollte sauber sein. Er sollte leicht zu verstehen und änderungs-
freundlich sein.

Die Tests testNoSuccess und testAl 1 PassedlOOPercent enthalten duplizier-


ten Code. Ihre ersten drei Zeilen sollten extrahiert und an einer gemeinsam
genutzten Stelle, der setUpO-Methode für diese Testklasse, eingefügt werden.

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:

CreditValidator validator = new CreditValidator(connection, null, "a") ;

Folgen Sie mir noch?


Wie Entwickler auf solche Code-Zeilen reagieren, sagt oft viel über die Art von Sys-
tem, an dem sie arbeiten. Wenn Sie sich überhaupt nichts dabei denken, einen
null-Wert an den Konstruktor zu übergeben, weil Sie dies in Ihrem System
immer wieder tun, arbeiten Sie mit einem ziemlich widerspenstigen System.
Wahrscheinlich enthält es an vielen Stellen Prüfungen auf null-Werte und viele
konditionale Anweisungen, um herauszufinden, welche Werte vorliegen und was
Sie damit tun können. Haben Sie sich dagegen gefragt, ob ich mein Fach über-
haupt verstünde, weil ich wider alle Regeln der Kunst null-Werte in einem Sys-
tem herumreiche, und trotzdem weitergelesen, möchte ich nur einwenden:
Vergessen Sie nicht, dass wir dies nur in den Tests machen. Schlimmstenfalls
kann anderer Code versuchen, die Variable abzurufen. In diesem Fall wird das Java
Runtime eine Ausnahme auslösen. Weil der Harnisch alle in Tests ausgelösten
Ausnahmen abfängt, werden wir ziemlich schnell feststellen, ob der Parameter
überhaupt verwendet wird.

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:

public void testCreateO {


CreditValidator validator = new CreditValidator(null, null, "a");
}

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.

Null Object Pattern


Das Null Object Pattern ist eine Methode, die Verwendung von null in Program-
men zu vermeiden. Wenn etwa eine Methode einen Mitarbeiter mit einer
bestimmten ID zurückgeben soll, was sollten wir zurückgeben, wenn es keinen
Mitarbeiter mit dieser ID gibt?
for(Iterator it = idList.iteratorO; it.hasNextO; ) {
EmployeelD id = (EmployeelD)it.nextO;
Employee e = finder.getEmployeeForlD(id);
e.payO;
}
Es gibt mehrere Möglichkeiten:
Wir könnten einfach eine Ausnahme auslösen. Dann müssten wir gar nichts
zurückgeben, aber der Client wäre gezwungen, den Fehler ausdrücklich zu bear-
beiten.

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.

Null-Objekte können nützlich sein, um Clients vor einer ausdrücklichen Fehler-


prüfung abzuschotten. Doch Sie müssen Null-Objekte vorsichtig verwenden.
Die folgende Methode, die Anzahl der bezahlten Mitarbeiter zu zählen, ist nicht
zu empfehlen:
int employeesPaid = 0;
for(Iterator it = idList. i t e r a t o r O ; it.hasNextO; ) {
EmployeelD id = (EmployeelD)it.nextO;
Employee e = finder.getEmployeeForlD(id) ;
e.payO;
employeesPaid++; : // Fehler!
}
Wird ein Null-Mitarbeiter zurückgegeben, wird falsch gezählt.
Null-Objekte sind besonders nützlich, wenn sich ein Client nicht um den Erfolg
einer Operation kümmern muss. Oft können wir unser Design so umbauen,
dass dies der Fall ist.

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.

9.2 Der Fall der verborgenen Dependency


Einige Klassen sind irreführend. Sie enthalten einen Konstruktor, den wir verwen-
den wollen, und versuchen, ihn aufzurufen. Dann, rumms! Wir stoßen auf ein
Hindernis. Eines der häufigsten Hindernisse ist die verborgene Dependency; der
Konstruktor verwendet eine Ressource, die in unserem Test-Harnisch nicht leicht

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;

void send_message(const std::string& message);


void add_recipient(const mail_txm_id id, const mail_address& address);

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?

mai1i ng_li st_di spatcher::mai1i ng_li st_di spatcher()


: service(new mail_service), status(MAIL_OKAY)
{
const int client_type = 12;
service->connect();
if (service->get_status() == MS_AVAILABLE) {
service->register(this, client_type, MARK_MESSAGES_OFF);
service->set_param(client_type, MI NOBOUNCE | MI REPEATOFF);
}
el se
status = MAII OFFLINE;

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:

mai 1 ing_list_dispatcher::mai 1 ing_list_dispatcher(mai"l_service *service)


: status(MAIL_0KAY)
{
const int c"lient_type = 12;
service->connect();
if (service->get_status() == MS_AVAILABLE) {
service->register(this, c1ient_type, MARK_MESSAGES_OFF);
service->set_param(client_type, MI NOBOUNCE | MI REPEATOFF);
}
el se
status = MAII OFFLINE;

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.

Parameterize Constructor (25.14) ist sehr praktisch, um Konstruktor-Dependencies


zu externalisieren, aber Entwickler denken nur selten daran, weil sie oft anneh-
men, alle Clients der Klasse müssten geändert werden, um den neuen Parameter
zu übergeben; aber das ist nicht wahr. Wir können dies wie folgt handhaben.
Zuerst extrahieren wir den Body des Konstruktors in eine neue Methode, die wir
i ni t i al i ze nennen können. Im Gegensatz zu den meisten Methoden-Extraktio-
nen ist dies auch ohne Tests ziemlich sicher, weil wir Preserve Signatures (25.1)
anwenden können.

void mailing_1ist_dispatcher: :initia~Iize(mail_service *service)


{
status = MAIL_OKAY;
const int client_type = 12;
Service.connect() ;

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.

mai1i ng_li st_di spatcher::mai1i ng_li st_di spatcher()


{
initialize(new mail_service) ;
}

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:

public class MailingListDispatcher


{
public MailingListDispatcher()
: this(new MailServiceO)
{}

public MailingListDispatcher(MailService Service) {


}

Dependencies, die in Konstruktoren verborgen sind, können mit vielen Techni-


ken aufgehoben werden. Oft können wir Extract and Override Getter (25.8),
Extract and Override Factory Method (25.7) oder Supersede Instance Variable (25.22)
anwenden; aber ich verwende lieber Parameterize Constructor (25.14), falls mög-
lich. Wenn ein Objekt in einem Konstruktor erstellt wird und selbst keine Kon-
struktions-Dependencies hat, ist die Anwendung von Parameterize Constructor
sehr einfach.

137
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen

9.3 Der Fall der verketteten Konstruktionen


Parameterize Constructor (25.14) ist eine der einfachsten Techniken, um verborgene
Dependencies in einem Konstruktor aufzuheben. Ich probiere sie oft zuerst aus.
Leider ist sie nicht immer die beste Wahl. Wenn ein Konstruktor intern zahlreiche
Objekte erstellt oder auf viele Globals zugreift, könnten wir eine sehr umfangrei-
che Parameterliste erhalten. In ungünstigen Situationen erstellt ein Konstruktor
einige Objekte und verwendet sie dann, um andere Objekte zu erstellen:

class WatercolorPane
{
public:
WatercolorPane(Form *border, WashBrush *brush, Pattern *backdrop)
{

anteriorPanel = new Panel(border);


anteriorPanel->setBorderColor(brush->getForeColor());
backgroundPanel = new Panel(border, backdrop);

Cursor = new FocusWidget(brush, backgroundPanel);

Wollen wir das cursor-Objekt überwachen, kommen wir in Schwierigkeiten. Es


ist in ein Construction Bloh, wörtl. etwa Konstruktionsklumpen eingebettet. Wir kön-
nen versuchen, den gesamten Code zur Erstellung des cursor-Objekts aus der
Klasse herauszulösen. Dann kann ein Client das cursor-Objekt erstellen und als
Argument übergeben. Aber dies ist ohne Tests nicht sehr sicher und könnte die
Clients dieser Klasse sehr belasten.

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)
{

anteriorPanel = new Panel(border) ;


anteriorPanel->setBorderColor(brush->getForeColor());
backgroundPanel = new Panel(border, backdrop);
Cursor = new FocusWidget(brush, backgroundPanel);

void supersedeCursor(FocusWidget *newCursor)


{
delete Cursor;
cursor = newCursor;
}
}
In C++ müssen wir bei diesem Refactoring sehr vorsichtig sein. Wenn wir ein
Objekt ersetzen, müssen wir das alte beseitigen. Oft müssen wir zu diesem Zweck
mit dem delete-Operator seinen Destruktor aufrufen und seinen Speicherplatz
freigeben. Doch dann müssen wir genau verstehen, was der Destruktor tut und ob
er alles zerstört, was an den Konstruktor des Objekts übergeben worden ist. Wenn
wir den Speicher nicht sauber freigeben, können wir subtile Fehler einführen.

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.

void supersedeCursor(FocusWidget newCursor) {


cursor = newCursor;
}

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

TestingFocusWidget *widget = new TestingFocusWidget;


WatercolorPane pane(form, border, backdrop);
pane.supersedeCursor(widget);
LONGS_EQUAL(0, pane.getComponentCount());

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.

9.4 Der Fall der irritierenden globalen Dependency


Seit Jahren haben sich Entwickler beklagt, es würde zu wenige wiederverwendbare
Komponenten auf dem Markt geben. Die Situation hat sich im Laufe der Zeit
leicht gebessert; es gibt viele kommerzielle und Open-Source-Frameworks, aber
im Allgemeinen sind sie nicht die Dinge, die wir wirklich nutzen, sondern Dinge,
die unseren Code nutzen. Frameworks verwalten oft den Lebenszyklus einer
Anwendung, und wir schreiben Code, um die Lücken zu füllen. Dieses Phänomen
findet sich in allen möglichen Frameworks, von ASP.NET bis hin zu Java Struts.
Selbst die xUnit-Frameworks folgen diesem Verhaltensmuster. Wir schreiben Test-
klassen; xUnit ruft sie auf und zeigt ihre Ergebnisse an.

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)-

Viele verschiedene Arten von Dependencies können es erschweren, Klassen in


einem Test-Framework zu erstellen und zu nutzen: Eine der schwierigsten ist die
Verwendung globaler Variablen. In einfachen Fällen können wir diese Dependen-
cies mit Parameterize Constructor (25.14), Parameterize Method (25.15) und Extract
and Override Call (25.6) aufheben, aber manchmal sind Dependencies von Globals
so umfangreich, dass es einfacher ist, das Problem an der Wurzel anzupacken.
Diese Situation wird im nächsten Beispiel gezeigt, eine Klasse in einer Java-
Anwendung, die Baugenehmigungen für eine Behörde registriert. Hier ist eine
der Hauptklassen:

140
9-4
Der Fall der irritiererden globalen Dependency

public class Facility


{
private Permit basePermit;

public Facility(int facilityCode, String owner, PermitNotice notice)


throws PermitViolation {
Permit associatedPermit =
Permi tReposi tory.getlnstance(). fi ndAssoci atedPermi t(noti ce);
if (associatedPermit.isValidO && !notice.isValid()) {
basePermit = associatedPermit;
}
eise if (Inotice.isValidO) {
Permit permit = new Permit(notice);
permit.validateO;
basePermit = permit;
}
el se
throw new PermitViolation(permit);
}

}
Wir wollen eine F a c i l i t y in einem Test-Harnisch erstellen. Deshalb beginnen
wir mit einem entsprechenden Versuch:

public void testCreateO {


PermitNotice notice = new PermitNotice(0, "a");
Facility facility = new Facility(Facility.RESIDENCE, "b", notice);
}

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);

Wir könnten dieses Problem durch eine Parametrisierung des Konstruktors


umgehen; aber in dieser Anwendung handelt es sich nicht um einen isolierten
Fall. Zehn weitere Klassen enthalten eine etwa gleichartige Code-Zeile. Der Code
befindet sich in Konstruktoren, normalen Methoden und statischen Methoden.
Wir können uns ausmalen, welchen Aufwand es bedeuten würde, dieses Problem
in der Code-Basis zu beseitigen.
Wenn Sie sich mit Design Patterns beschäftigt haben, haben Sie wahrscheinlich
dies als Beispiel für das Singleton Design Pattern (25.12) erkannt. Die getlnstance-
Methode von Permi tReposi t o r y ist eine statische Methode, die die einzige

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.

Account example = new Account O ;


example.deposit(l);
int balance = example.getBalanceO;

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.

Test-Situationen werden dadurch schwierig, dass wir herausfinden müssen, wel-


che Globals von einer Klasse verwendet werden, und sie für den Test in einen
geeigneten Zustand versetzen müssen; und wir müssen dies vor jedem Test
machen, wenn das Setup verschieden ist. Ich habe diese ziemlich mühsamen Auf-
gaben bei Dutzenden von Systemen ausgeführt, um sie unter Testkontrolle zu
bringen, und es wird nicht angenehmer.

Zurück zu unserem Beispiel:

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

Z u e r s t f ü g e n w i r eine n e u e statische Methode in die Singleton-Klasse ein, mit der


wir die statische Instanz in d e m Singleton ersetzen k ö n n e n . Wir wollen sie als
setTesti nglnstance b e z e i c h n e n .

public class PermitRepository


{

private static PermitRepository instance = null;

private Permi tReposi t o r y O {}

public static= void


instance setTestingInstance(PermitRepository newlnstance) {
newlnstance;
}

public static Permi tReposi tory getlnstanceO {


if (instance == null) {
instance = new PermitRepositoryO;
}
return instance;
}

public Permit findAssociatedPermit(PermitNotice notice) {


}

Mit d i e s e m Setter k ö n n e n w i r eine Test-Instanz einer Permi tReposi tory erstel-


len u n d setzen. Der entsprechende Code in u n s e r e m Test-Setup sähe w i e folgt aus:

public void setllpO {

PermitRepository repository = new PermitRepositoryO;

// Zugriffsrechte auf das Repository hier setzen

} Permi tReposi tory.setTesti ngInstance(reposi tory);

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 :

public class PermitRepository


{

public void resetForTesting() {


instance = null;
}
} "

143
Kapitel 9
Ich kann diese Klasse nicht in einen Test-Harnisch einfügen

Wenn wir diese Methode in unserer Test-setUp-Methode (und sinnvollerweise


auch in tearDown) aufrufen, können wir für jeden Test ein frisches Singleton
erstellen. Das Singleton wird sich selbst für jeden Test neu initialisieren. Dieses
Schema ist brauchbar, wenn Sie beim Testen den Zustand des Singletons mit sei-
nen öffentlichen Methoden nach Ihren Anforderungen einrichten können. Ver-
fügt das Singleton nicht über solche öffentlichen Methoden oder verwendet es
externe Ressourcen, die seinen Zustand beeinflussen, ist Introduce Static Setter
(25.12) die bessere Wahl. Sie können von dem Singleton eine Unterklasse ableiten,
Methoden überschreiben, um Dependencies aufzuheben, und öffentliche Metho-
den in die Unterklasse einfügen, um den gewünschten Zustand zu setzen.

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:

public class PermitRepository


{

public Permit findAssociatedPermitCPermitNotice notice) {


// Permit-Datenbank öffnen

// Werte auswählen

145
// Gültigkeit prüfen; falls ungültig, Fehler ausgeben

// zu dem passenden Permit zurückkehren

}
}

Wenn wir nicht mit der Datenbank kommunizieren können, können wir von Per-
mi tRepository eine Unterklasse ableiten:

public class TestingPermitRepository extends PermitRepository


{
private Map permits = new H a s h M a p O ;

public void addAssociatedPermit(PermitNotice notice, permit) {


permits.put(notice, permit);
}

public Permit findAssociatedPermit(PermitNotice notice) {


return (Permit)permits.get(notice);
}
}

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.

public class PermitRepository


{

private static PermitRepository instance = null;

protected Permi tReposi t o r y O {}

public static void setTestingInstance(PermitRepository newlnstance) {


instance = newlnstance;
}

public static PermitRepository getlnstanceO {


if (instance == null) {
instance = new PermitRepositoryO ;
}
return instance;
}

public Permit findAssociatedPermit(PermitNotice notice) {


}

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:

public class PermitRepository implements IPermitRepository


{
private static IPermitRepository instance = null;

protected PermitRepositoryC) {}

public static void setTestingInstance(IPermitRepository newlnstance)


{
instance = newlnstance;
}

public static IPermitRepository getlnstanceO


{
if (instance == null) {
instance = new PermitRepositoryO;
}
return instance;
}

public Permit findAssociatedPermit(PermitNotice notice)


{

Die IPermi tReposi tory-Schnittstelle wird Signaturen für alle öffentlichen nicht-
statischen Methoden von Permi tReposi tory haben.

public interface IPermitRepository


{
Permit findAssociatedPermit(PermitNotice notice);

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.

Greifen Sie probeweise eine globale Variable einer umfangreichen Anwendung


heraus und suchen Sie nach ihr. In den meisten Fällen werden globale Variablen
gar nicht global, sondern nur an einer relativ kleinen Anzahl von Stellen genutzt.
Überlegen Sie, wie Sie dieses Objekt zu dem Objekt bringen könnten, in dem es
gebraucht wird, wenn es keine globale Variable wäre. Wie würden Sie die Anwen-
dung refaktorisieren? Gibt es andere Aufgaben, die Sie aus Gruppen von Klassen
herauslösen könnten, um den Geltungsbereich der globalen Variablen zu verrin-
gern?

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).

9.5 Der Fall der schrecklichen Include-Dependencies


C++ war meine erste OO-Sprache, und ich muss zugeben, dass ich sehr stolz war,
viele ihrer Details und Komplexitäten zu kennen. Die Sprache dominierte eine
Zeit lang die Branche, weil sie damals viele schwierige Probleme der Software-Ent-
wicklung pragmatisch löste. Waren die Rechner zu langsam? Okay, hier ist eine
Sprache, in der alles optional ist. Sie können die Effizienz von C nutzen, wenn Sie

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.

Ein besonders problematischer Aspekt des C++-Vermächtnisses ist die interne


Kommunikation von Teilen eines Programms untereinander. Wenn eine Klasse in
Java oder C# eine Klasse in einer anderen Datei benutzen muss, wird seine Defini-
tion mit einer import- bzw. usi ng-Anweisung zur Verfügung gestellt. Der Compi-
ler sucht nach dieser Klasse, prüft, ob sie bereits kompiliert worden ist, und
kompiliert sie bei Bedarf. Dann liest er aus der kompilierten Datei eine kurze
Beschreibung der Klasse ein, um zu prüfen, ob sie alle angeforderten Methoden
enthält.

C++-Compiler verfügen im Allgemeinen nicht über diese Optimierung. Wenn


eine Klasse in C++ etwas über eine andere Klasse wissen muss, wird die Deklara-
tion dieser Klasse (in einer anderen Datei) textuell in die Datei eingebunden, die
sie benutzen will. Dieser Prozess kann viel langsamer sein. Der Compiler muss
jedes Mal die Deklaration neu parsen und erneut eine interne Repräsentation
erstellen. Noch schlimmer ist: Der Mechanismus zur Einbindung kann miss-
braucht werden. Eine Datei kann eine Datei einbinden, die eine andere Datei ein-
bindet usw. Bei Projekten, bei denen Entwickler dieses Problem nicht beachten, ist
es nicht ungewöhnlich, kleine Dateien zu finden, die transitiv Zehntausende von
Code-Zeilen einbinden. Die Entwickler fragen sich, warum ihre Builds so lange
dauern; doch weil die i ncl ude-Direktiven über das ganze System verteilt sind, ist
es schwer, eine bestimmte Datei herauszugreifen und zu verstehen, warum es so
lange dauert, sie zu kompilieren.

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.

In Legacy Code kann es schwer sein, eine C++-Klasse in einem Test-Harnisch zu


instanziieren. Eines der offensichtlichsten Probleme ist die Header-Datei-Depen-
dency. Welche Header-Dateien brauchen wir, um eine Klasse isoliert in einem
Test-Harnisch zu erstellen?

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();

void addEvent(Event *event);


bool hasEvents(Date date);
bool performConsistencyCheck(string& message);

};
#endif

Unter anderem verwendet die Scheduler-Klasse Meeting, Mai IDaemon, Event,


Schedul erDi spl ay und Date. Wenn wir einen Test für Scheduler-Objekte erstel-
len wollen, ist es am einfachsten, zu versuchen, ein Objekt im selben Verzeichnis
in einer anderen Datei namens Schedul erTests zu erzeugen. Warum im selben
Verzeichnis? Wegen des Präprozessors ist es oft leichter. Wenn das Projekt
Dateien nicht konsistent mit Pfaden einbindet, könnten wir viel Arbeit haben,
wenn wir versuchen, die Tests in anderen Verzeichnissen zu erstellen.

#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

In einfachen Fällen bindet die Schedul er.h-Datei alles Erforderliche ein, um


einen Scheduler zu erstellen, aber in einigen Fällen schließt die Header-Datei
nicht alles ein. Wir müssen einige zusätzliche i ncl ude-Direktiven erstellen und
ein Objekt verwenden.

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.

In einer idealen Welt wäre es am einfachsten, alle benötigten Dateien einzubauen,


bis keine Build-Fehler mehr gemeldet werden, aber das schüfe keine Klarheit. Bei
einer langen Kette von transitiven Dependencies binden wir vielleicht viel mehr
ein, als wir wirklich brauchen. Selbst bei einer kurzen Kette könnten wir Abhän-
gigkeiten von Dingen einbauen, die das Arbeiten in einem Test-Harnisch erheb-
lich erschweren. Im folgenden Beispiel ist die SchedulerDi splay-Klasse eine
solche Dependency. Ich zeige sie hier nicht; aber sie wird tatsächlich im Konstruk-
tor von Schedul er abgerufen. Wir können die Dependency wie folgt aufheben:

#include "TestHarness.h"
#include "Scheduler.h"

void Schedul erDisplay: :displayEntry(const string& entityDescription)


{
}

TEST(create,Schedul er)
{
Scheduler scheduler("fred");
}

Hier haben wir eine alternative Definition für SchedulerDisplay: :displayEn-


try eingeführt. Wenn wir dies tun, müssen wir leider einen separaten Build für
die Testfälle in dieser Datei ausführen. In einem Programm darf es für jede
Methode von SchedulerDisplay nur eine Definition geben; deshalb brauchen
wir für unsere Scheduler-Tests ein separates Programm.

Glücklicherweise können wir die erstellten Fakes wiederverwenden. Anstatt die


Definitionen von Klassen wie SchedulerDisplay inline in die Testdatei einzufü-
gen, können wir sie in einer separaten i ncl ude-Datei speichern, die von mehre-
ren Testdateien verwendet werden kann:

#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.

9.6 Der Fall der Zwiebel-Parameter


Ich schätze einfache Konstruktoren wirklich sehr. Es ist schön, sich zu entschei-
den, eine Klasse zu erstellen, einen Konstruktor-Aufruf einfach einzutippen und
dann ein echtes funktionsfähiges Objekt nutzen zu können. Aber in vielen Fällen
kann es schwer sein, Objekte zu erstellen. Jedes Objekt muss in einem brauchba-
ren Zustand initialisiert werden, was zusätzliche Arbeit erfordert. In vielen Fällen
müssen wir ihm andere Objekte übergeben, die selbst ebenfalls initialisiert wer-
den müssen. Auch diese Objekte könnten andere Objekte benötigen usw. Um also
einen Parameter für einen Konstruktor der zu testenden Klasse zu erstellen, müs-
sen wir möglicherweise eine ganze Kette voneinander abhängiger Objekte erstel-
len - die Struktur scheint einer großen Zwiebel zu ähneln. Hier ist ein Beispiel für
ein solches Problem:

Wir haben eine Klasse, die ein Schedul i ngTask-Objekt anzeigt:

public class SchedulingTaskPane extends SchedulerPane


{
public SchedulingTaskPane(SchedulingTask task) {
}

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:

public class SchedulingTask extends SerialTask


{
public SchedulingTask(Scheduler scheduler, MeetingResolver resolver)
{
}
}

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.

Abbildung 9.3 zeigt die daraus resultierende Hierarchie.

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() ;
}

};

Abb. 9.3: SchedulingTask

In jeder Sprache, in der wir Schnittstellen oder Klassen erstellen können, die
sich wie Schnittstellen verhalten, können wir damit Dependencies systematisch
aufheben.

9.7 Der Fall des Alias-Parameters


Oft, wenn Konstruktoren hinderliche Parameter erfordern, können wir das Pro-
blem mit Extract Interface (25.10) oder Extract Implementer (25.9J beheben. Aber

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:

public class IndustrialFacility extends Facility


{
Permit basePermit;

public IndustrialFacility(int faci 1 ityCode, String owner,


OriginationPermit permit) throws PermitViolation {

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.

Abb. 9.4: Die Permi t-Hierarchie

155
Kapitel io
Ich kann dieseMethodenicht in einem Test-Harnisch ausführen

Der IndustrialFacility-Konstruktor akzeptiert ein OriginationPermit-


Objekt und wendet sich an das Permi tReposi tory-Objekt, um ein Permi t-Objekt
zu erhalten; wir verwenden eine Methode von Permi tReposi tory, die ein Ori gi -
nationPermi t-Objekt akzeptiert und ein Permi t-Objekt zurückgibt. Wenn das
Repository das zugeordnete Permi t-Objekt findet, speichert sie es in dem permi t-
Feld. Andernfalls speichert sie das Ori gi nationPermi t-Objekt in dem permit-
Feld. Wir könnten eine Schnittstelle für OriginationPermit erstellen, aber das
wäre sinnlos. Wir müssten eine IOrigi nationPermi t einem Permi t-Feld zuwei-
sen, und das würde nicht funktionieren. In Java können Schnittstellen nicht von
Klassen erben. Die offensichtliche Lösung besteht darin, Schnittstellen aller abge-
leiteten Methoden zu erstellen und aus dem Permi t-Feld ein IPermi t-Feld zu
machen. Abbildung 9.5 zeigt, wie dies aussehen würde.

Permit -[> I Permit

I
FacilityPermit IFacility Permit

I
OriginationPermit [>
----- - — (OriginationPermit

Abb. 9.5: Permit-Hierarchie mit extrahierten Schnittstellen

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:

public class OriginationPermit extends FacilityPermit


{

public void validateC) {

// Verbindung zur Datenbank aufbauen

// 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:

public void testHasPermitsO {


class AlwaysValidPermit extends FakeOriginationPermit
{
public void validateO {
// das Validierungs-Flag setzen
becomeValid();
}
} ;

Facility facility = new IndustrialFacility(Facility.HT_l, "b",


new AlwaysVal idPermitO);
assertTrue(faci1i ty.hasPermi ts());
}

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

Ich kann diese Methode nicht in


einem Test-Harnisch ausführen

Tests einzurichten, um Änderungen durchzuführen, kann ein Problem sein.


Wenn Sie Ihre Klasse separat in einem Test-Harnisch instanziieren können, soll-
ten Sie sich glücklich schätzen. Viele Entwickler können dies nicht. Wenn Sie
Schwierigkeiten haben, sollten Sie Kapitel 9, Ich kann diese Klasse nicht in einen
Test-Harnisch einfügen, lesen.
Eine Klasse zu instanziieren ist oft nur der erste Teil der Schlacht. Der zweite Teil
ist das Schreiben von Tests der Methoden, die wir ändern müssen. Manchmal kön-
nen wir dies tun, ohne die Klasse überhaupt zu instanziieren. Benötigt die
Methode nur wenige Instanz-Daten, können wir mit Expose Static Method (25.5)
Zugriff auf den Code bekommen. Bei langen und schwierigen Methoden können
wir den Code mit Break Out Method Object (25.2) in eine Klasse verschieben, die
wir leichter instanziieren können.

Glücklicherweise ist der Aufwand, um Tests für Methoden zu schreiben, nicht


allzu hoch. Hier sind einige Probleme, auf die wir dabei stoßen können.
• Vielleicht ist die Methode für den Test nicht zugänglich. Sie könnte privat sein
oder ein anderes Zugriffsproblem haben.
• Es könnte schwierig sein, die Methode aufzurufen, weil es problematisch ist,
die für den Aufruf benötigten Parameter zu konstruieren.
• Die Methode könnte unerwünschte Nebeneffekte haben (etwa die Änderung
einer Datenbank), weshalb sie nicht in einem Test-Harnisch ausgeführt wer-
den kann.
• Vielleicht müssen wir ein Objekt überwachen, das die Methode verwendet.
Im Rest dieses Kapitels beschreibe ich Szenarien, die verschiedene Methoden zei-
gen, um diese Probleme zu lösen, und diskutiere die damit verbundenen Vor- und
Nachteile.

io.i Der Fall der verborgenen Methode


Wir müssen eine Methode in einer Klasse ändern, aber sie ist privat. Was tun?
Zunächst müssen wir fragen, ob wir sie über den Umweg über eine öffentliche
Methode testen können. Ist dies der Fall, lohnt es sich. Dieser indirekte Weg

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 ;

};

Mit der CCAImage-Klasse werden Bilder in einem Sicherheitssystem erfasst. Viel-


leicht fragen Sie sich, warum eine Image-Klasse Bilder aufnimmt; doch vergessen
Sie nicht: Dies ist Legacy Code. Die Klasse enthält eine snapO-Methode, die mit
einem systemnahen C-API eine Kamera steuert und das Bild »erfasst«; aber es
handelt sich um eine sehr besondere Art von Image. Ein einziger Aufruf von
s n a p O kann mehrere verschiedene Kamera-Aktionen auslösen, die alle jeweils
ein Bild aufnehmen und in einem anderen Teil eines Image-Puffers der Klasse
speichern. Der Speicherort der einzelnen Bilder wird dynamisch zur Laufzeit
bestimmt. Er hängt von der Bewegung des aufgenommenen Subjekts ab. Je nach-
dem, wie sich das Subjekt bewegt, kann die snapO-Methode setSnapRegion
mehrfach aufrufen, um den Speicherort des gegenwärtigen Bildes im Puffer fest-
zulegen. Leider wurde das API der Kamera geändert; deshalb müssen wir set-
SnapRegion ändern. Wie sollten wir vorgehen?

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:

Zuerst wird setSnapRegion von p r i v a t e in protected umdeklariert:

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:

class TestingCCAImage : public CCAImage


{
public:
void setSnapRegion (int x, int y, int dx, int dy) {
// setSnapRegion der Oberklasse aufrufen
CCAImage::setSnapRegion(x, y, dx, dy) ;
}
};

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

berücksichtigen, dass setSnapRegion jetzt in Unterklassen aufgerufen werden


kann, aber dies ist relativ unwichtig. Vielleicht stoßen wir auf eine Kleinigkeit, die
bei der nächsten Bearbeitung der Klasse ein vollständiges Refactoring rechtfertigt.
Wir können die Aufgaben von CCAImage in andere Klassen auslagern und testbar
machen.

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.

10.2 Der Fall der »hilfreichen« Sprachfunktion


Sprach-Designer versuchen oft, uns das Leben zu erleichtern, aber sie haben es
schwer. Sie müssen leichtes Programmieren gegen Sicherheitsüberlegungen
abwägen. Einige Funktionen scheinen anfänglich allen Belangen gerecht zu wer-
den, erweisen sich aber beim Testen als sehr sperrig.
Der folgende C#-Code akzeptiert eine Collection hochgeladener Dateien eines
Web-Clients. Der Code durchläuft die Dateien und gibt eine Liste von Streams
zurück, die Dateien mit bestimmten Eigenschaften zugeordnet sind.

public void IList getKSRStreams(HttpFileCollection files) {


ArrayList list = new ArrayListO;
foreach(string name in files) {
HttpPostedFile file = files[name];
if (fi1e.Fi 1eName.EndsWith(".ksr") || (file.FileName.
EndsWithC.txt") && file.ContentLength > MIN_LEN)) {

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.

Was können wir machen, um Tests für die getKSRStreams-Methode zu schrei-


ben? Die Techniken Extract Interface (25.10) oder Extract Implementer (25. g) sind
unbrauchbar; denn die Klassen HttpPostedFi le und HttpFi leCollection
unterliegen nicht unserer Kontrolle; sie sind Bibliotheksklassen, die wir nicht
ändern können. Auf den ersten Blick scheint es für dieses Problem hier nur eine
geeignete Technik zu geben: Adapt Parameter (25.1).

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.

public void LList getKSRStreams(OurHttpFileCollection files) {


ArrayList Ii st = new ArrayListO;
foreach(string name in files) {
HttpPostedFi le file = files [name];
if (fi 1 e.Fi 1 eName.EndsWith(".ksr") || (file.FileName.EndsWithC.txt")
&& fi 1 e. ContentLength > MAX_LEN)) {

list.Add(file.InputStream);
10.2
Der Fall der »hilfreichen« Sprachfunktion

}
}
return list;
}

Unsere HttpFileCollection ist von NameObjectCollectionBase abgeleitet,


einer abstrakten Klasse, die Strings mit Objekten verknüpft.

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):

public class HttpPostedFileWrapper : IHttpPostedFile


{
public HttpPostedFileWrapper(HttpPostedFile file) {
this.file = file;
}
public int ContentLength {
get { return file.ContentLength; }
}

Weil wir eine Schnittstelle haben, können wir auch eine Klasse für die Tests erstel-
len:

public class FakeHttpPostedFile : IHttpPostedFile


{
public FakeHttpPostedFile(int length, Stream stream, ...) { ... }

public int ContentLength {


get { return length; }
}
}

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.

public IList getKSRStreams(OurHttpFi1eCol1ection) {


ArrayList list = new ArrayList() ;
foreach(string name in files) {

165
Kapitel io
Ich kann diese M e t h o d e nicht in e i n e m Test-Harnisch ausführen

IHttpPostedFile file = files[name] ;


if (file. FileName.EndsWith(".ksr") || (file.FileName.EndsWith(".txt"))
&& fi 1 e.ContentLength > MAX_LEN)) {

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.

10.3 Der Fall des nicht erkennbaren Nebeneffekts


Theoretisch sollte es nicht zu schwierig sein, einen Test für eine isolierte Funktio-
nalität zu schreiben. Wir instanziieren eine Klasse, rufen ihre Methoden auf und
prüfen ihre Ergebnisse. Was könnte schiefgehen? Nun, so leicht kann es sein,
wenn das Objekt, das wir erstellen, nicht mit anderen Objekten kommuniziert.
Wird es von anderen Objekten benutzt und verwendet es selbst nichts anderes,
können unsere Tests dieses Objekt ebenfalls verwenden und sich wie der Rest
unseres Programms verhalten. Aber Objekte, die keine anderen Objekte verwen-
den, sind dünn gesät.

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

Hier ist eine Klasse mit diesem Problem:

public class AccountDetailFrame extends Frame


implements ActionListener, WindowListener
{
private TextField display = new TextField(lO);

public AccountDetailFrame(...) { ... }

public void actionPerformed(ActionEvent event) {


String source = (String)event.getActionCommandO ;
if (source.equals("project activity")) {
Detail Frame detail Display = new Detai 1 Frame () ;
detailDisplay.setDescription(getDetailText() + "" +
getProjectionTextO) ;
detailDisplay.show();
String accountDescription = detai 1 Display.getAccountSymbol ();
accountDescription += ": ";

di splay.setText(accountDescri pti on);

}
}

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.

Wir könnten diese Methode versuchsweise in einem Test-Harnisch ausführen,


aber das würde nichts bringen. Sie würde ein Fenster erstellen, anzeigen, uns zu
einer Eingabe auffordern und dann etwas anderes in einem anderen Fenster
anzeigen. Es gibt keine vernünftige Stelle, um zu beobachten, was dieser Code tut.

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.

Wo sollten wir anfangen?

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.

public class AccountDetailFrame extends Frame


implements ActionListener, WindowListener
{
private TextField display = new TextField(lO);

public AccountDetailFrame(...) { ... }

public void actionPerformed(ActionEvent event) {


String source = (String)event.getActionCommandO;
performCommand(source);
}

public void performCommand(String source) {


if (source.equals("project activity")) {
Detail Frame detailDisplay = new DetailFrame();
detailDisplay.setDescription(getDetailText() + " " +
getProjectionTextO);
detailDisplay. show();
String accountDescription = detail Display.getAccountSymbol();
accountDescription += ": ";

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 class AccountDetai1 Frame extends Frame


implements ActionListener, WindowListener
{
private TextField display = new TextField(lO);
private DetailFrame detailDisplay;

public AccountDetailFrame(...) { .. }

public void actionPerformed(ActionEvent event) {


String source = (String)event.getActionCommand() ;
performCommand(source);
}

public void performCommand(String source) {


if (source.equals("project activity")) {
detailDisplay = new DetailFrameQ ;

168
io-3
Der Fall des nicht erkennbaren Nebeneffekts

detailDisplay.setDescription(getDetailText() + " " +


getProjectionTextO);
detail Di splay.showO ;
String accountDescription = detailDisplay.getAccountSymbol() ;
accountDescription += ": ";

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.

Nach mehreren Extraktionen sieht die performCommand-Methode wie folgt aus:

public class AccountDetai1 Frame extends Frame


implements ActionListener, WindowListener
{
public void performCommand(String source) {
if (source.equals("project activity")) {
setDescription(getDetailText() + " " + getProjectionTextO);

String accountDescription = getAccountSymbol();


accountDescription += ": ";
Kapitel io
Ich kann diese Methode nicht in einem Test-Harnisch ausführen

di splay.setText(accountDescripti on);

}
}

void setDescription(String description) {


detailDisplay = new Detai 1 F r a m e O ;
detai IDi splay. setDescri pti on (description) ;
detailDisplay.show();
}
String getAccountSymbol() {
return detaiIDisplay.getAccountSymbol();
}

Nachdem wir den gesamten Code für das d e t a i l Display-Frame extrahiert


haben, können wir den Code extrahieren, der auf Komponenten von AccountDe-
t a i 1 Frame zugreift.

public class AccountDetailFrame extends Frame


implements ActionListener, WindowListener
{
public void performCommand(String source) {
if (source.equals("project activity")) {
setDescription(getDetailText() + " " + getProjectionTextO);

String accountDescription = detai IDisplay.getAccountSymbol () ;


accountDescription += ": ";

setDi splayText(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

Display bei einer angenommenen "project acti vi ty "-Anweisung den richtigen


Text erhält:

public class TestingAccountDetai1 Frame extends AccountDetai1 Frame


{
String displayText = "";
String accountSymbol = "";

void setDescription(String description) {


}

String getAccountSymbol() {
return accountSymbol;
}

void setDisplayText(String text) {


displayText = text;
}
}

Der folgende Test führt die performCommand-Methode aus:

public void testPerformCommandO {


Testi ngAccountDetai 1 Frame frame = new Testi ngAccountDetai 1 F r a m e O ;
frame.accountSymbol = "SYM";
frame.performCommandC'project activity");
assertEquals("SYM: basic account", frame.displayText);
}

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)

Abb. 10.1: AccountDetailFrame

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).

AccountDetai IFrame SymbolSource


+ performAction(ActionEvent) > - detailDisplay : DetailFrame
+ performCommand(String) + getAccountSymbol : String
+ setDescription(String)

11
AccountDetailDisplay
- display : TextField

+ setDisplayText(String)

Abb. 10.2: AccountDetai 1 Frame, grob refaktorisiert

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

Ich muss eine Änderung


vornehmen. Welche Methoden
sollte ich testen?

Wir müssen einige Änderungen vornehmen, und wir müssen Charakterisierungs-


Tests (lj.i) schreiben, um das bereits vorhandene Verhalten zu fixieren. Wo sollten
wir sie schreiben? Am einfachsten wäre es, Tests für jede zu ändernde Methode zu
schreiben. Aber reicht dies aus? Vielleicht - wenn der Code einfach und verständ-
lich ist. Aber bei Legacy Code ist das oft nicht der Fall. Eine Änderung an einer
Stelle kann Verhalten an anderen Stellen beeinflussen. Solange wir keine Tests
haben, bemerken wir dies vielleicht gar nicht.

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.

n.i Effekte analysieren


Auch wenn es selten thematisiert wird, löst jede funktionale Änderung von Soft-
ware eine Kette von Effekten (Wirkungen) aus. Wenn ich etwa in dem folgenden
C#-Code 3 zu 4 ändere, gibt die aufgerufene Methode einen anderen Wert zurück.
Es könnten auch die Ergebnisse von Methoden geändert werden, die diese
Methode aufrufen, und so weiter, bis hin zu einer Grenze des Systems. Andere

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;

IDE-Unterstützung der Effektanalyse


Manchmal wünsche ich, meine IDE hülfe mir, Effekte in Legacy Code zu erken-
nen. Ich könnte ein Code-Fragment markieren und eine spezielle Taste anschla-
gen. Dann würde mir die IDE eine Liste aller Variablen und Methoden geben,
die durch eine Änderung des ausgewählten Codes beeinflusst werden könnten.
Vielleicht kommt eines Tages ein solches Tool auf den Markt. Bis dahin müssen
wir Effekte ohne Tools analysieren. Man kann es lernen, aber ob man richtig
denkt, ist schwer zu erkennen.

Effektanalyse lernt man am besten an einem Beispiel. Die folgende Java-Klasse


gehört zu einer Anwendung, die C++-Code manipuliert. Für eine Effektanalyse
spielt es keine Rolle, dass dieses Thema sehr Domänen-spezifisch zu sein scheint.
Zunächst eine kleine Übung: Angenommen, wir hätten ein CppClass-Objekt
erstellt. Listen Sie alle Dinge auf, die geändert werden und die Ergebnisse beein-
flussen können, die von seinen Methoden zurückgegeben werden.

public class CppClass {


private String name;
private List declarations;

public CppClassCString name, List declarations) {


this.name = name;
this.declarations = declarations;
}
public int getDeclarationCountO {
return declarations.size();
}
public String getName() {
return name;
}
public Declaration getDeclaration(int index) {

174
11.1
Effekte analysieren

return ((Declaration)declarations.get(index));
}

public String getInterface(String interfaceName, int [] indices) {


String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {
Declaration virtualFunction = (Declaration)(declara-
tions. get (indices [n])) ;
result += "\t" + vi rtual Function. asAbstractO + "\n";
}
result += "};\n" ;
return result;
}
}

Ihre Liste sollte etwa wie folgt aussehen:

1. Jemand könnte zusätzliche Elemente in die Deklarationsliste einfügen, nach-


dem er sie an den Konstruktor übergeben hat. Weil die Liste nur referenziert
wird, können Änderungen dieser Liste die Ergebnisse von getlnterface, get-
Declaration und getDeclarationCount ändern.
2. Jemand kann eines der Objekte in der Deklarationsliste ändern oder eines ihrer
Elemente ersetzen und damit dieselben Methoden beeinflussen.

Einige Entwickler befürchten, die getName-Methode könne einen anderen Wert


zurückgeben, wenn der name-String geändert wird, aber in Java sind String-
Objekte unveränderlich. Nach der Erstellung können Sie ihren Wert nicht
ändern. Nachdem ein CppCl ass-Objekt erstellt worden ist, gibt getName immer
denselben String-Wert zurück.

Wir können eine Skizze erstellen, die die Auswirkungen von Änderungen in
declarations auf getDeclarationCountQ hat (siehe Abbildung ii.i).

Abb. n. l: Einfluss von declarations auf getDeclarationCount

Diese Skizze soll darstellen, dass getDeclarationCountO einen anderen Wert


zurückgeben kann, wenn sich decl arati ons ändert - etwa indem es größer wird.

175
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?

Für getDeclaration(int index) können wir eine entsprechende Skizze erstel-


len (siehe Abbildung 11.2).

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.

Abbildung 11.3 zeigt einen ähnlichen Einfluss auf die getlnterface-Methode.

^^^eclaration^^^-
T C getinterface
3

Abb. 11.3: Einflüsse auf getinterface

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.

public class ClassReader {


private boolean inPublicSection = false;
private CppClass parsedClass;
private List declarations = new ArrayListO;
private Reader reader;

public ClassReader(Reader reader) {


this.reader = reader;
}

public void parse() throws Exception {


TokenReader source = new TokenReader(reader) ;
Token classToken = source.readToken();
Token className = source.readTokenO ;

Token lbrace = source.readTokenO ;


matchBody(source);
Token rbrace = source.readTokenO ;

177
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?

Token semicolon = source.readTokenO ;

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.

private void matchVi rtualDeclaration(TokenReader source)


throws IOException {
if (!source.peekToken().getTypeO == Token.VIRTUAL)
return;
List declarationTokens = new A r r a y L i s t O ;
declarationTokens.add(source.readTokenO);
while(source.peekToken() .getTypeO 1= Token.SEMIC) {
declarati onTokens.add(sou rce.readToken());
}
declarationTokens.add(source.readTokenO);
if (inPublicSection)
declarations.add(new Deelaration(declarationTokens));
}

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.

Betrachten wir die Dinge in der Deklarationsliste. Die readToken-Methode von


TokenReader gibt Token-Objekte zurück, die einfach einen String und eine Ganz-
zahl enthalten, die sich nie ändern. Ich zeige sie hier nicht; aber ein schneller Blick
auf die Deel arati on-Klasse zeigt, dass ihr Zustand nach ihrer Erstellung nicht
geändert werden kann. Deshalb können wir recht zuversichtlich sagen, dass sich
nach der Erstellung eines CppCI ass-Objekts die Deklarationsliste und deren
Inhalt nicht ändern werden.

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)

Im Allgemeinen können wir zuerst die Stellen untersuchen, an denen Unterob-


jekte von CppClass erstellt werden. Wir können den Code auch übersichtlicher
machen, indem wir einige Referenzen in CppClass mit dem Java-Schlüsselwort
fi nal als konstant deklarieren.

In schlechter strukturierten Programmen ist oft kaum zu erkennen, warum sie


ganz bestimmte Ergebnisse liefern. An einem solchen Punkt stehen wir vor einem
Debugging-Problem und müssen von dort zur Quelle des Problems zurückgehen.
Bei Legacy Code müssen wir oft eine andere Frage stellen: Wie könnte eine
bestimmte Änderung die anderen Ergebnisse des Programms beeinflussen?

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.

n.2 Vorwärtsanalyse (Reasoning Forward)


In dem vorhergehenden Beispiel versuchten wir, den Satz von Objekten zu ermit-
teln, die Werte an einem bestimmten Punkt im Code beeinflussen. Wenn wir Cha-
rakterisierungs-Tests (15.1) schreiben, kehren wir diesen Prozess um. Wir schauen
uns einen Satz von Objekten an und versuchen zu analysieren, was sich von ihnen
ausgehend ändern wird, wenn sie ihre Arbeit einstellen. Hier ist ein Beispiel. Die
folgende Klasse gehört zu einem In-Memory-Dateisystem. Wir haben keine Tests
für dieses System, wollen aber einige Änderungen vornehmen.

public class InMemoryDirectory {


private List elements = new A r r a y L i s t O ;

public void addElement(Element newElement) {


elements.add(newElement);
}
public void generatelndex() {
Element index = new Element("index");
for (Iterator it = elements.iteratorO ; it.hasNext(); ) {
Element current = (Element)it.nextO;
index.addText(current.getName() + "\n");
}
addElement(index);
}

public int getElementCount() {


return elements.size();
}

public Element getElement(String name) {


for (Iterator it = elements.iteratorO; it.hasNextO; ) {
Element current = (Element)it.next() ;

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.

InMemoryDi rectory enthält eine ungewöhnliche Funktion: Wir können genera-


telndex nicht zweimal aufrufen; denn dann würden wir zwei i ndex-Elemente
erzeugen (wobei das zweite das erste als eins seiner Elemente enthielte).

Glücklicherweise verwendet unsere Anwendung InMemoryDi rectory sehr einge-


schränkt. Sie erstellt Verzeichnisse, füllt sie mit Elementen, ruft generatelndex
auf und reicht dann die Verzeichnisse herum, damit andere Teile der Anwendung
auf die Elemente zugreifen können. Im Moment funktioniert dies einwandfrei;
aber wir müssen die Software so ändern, dass Nutzer während der Lebensdauer
eines Verzeichnisses jederzeit Elemente zu ihm hinzufügen können.

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)

In diesem Beispiel müssen wir zuerst die richtigen Änderungspunkte herausfin-


den. Wir müssen Funktionalität aus generatelndex herauslösen und Funktiona-
lität zu addEl ement hinzufügen. Haben wir diese Änderungspunkte identifiziert,
können wir eine erste Effektskizze erstellen.

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).

Abb. 11.5: Auswirkung von generatelndex auf el ements

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).

Abb. n.6: Weitere Auswirkungen von Änderungen in generatelndex

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^^^

Abb. n.7: Auswirkung von addEl ement auf el ements

Wir könnten untersuchen, worauf ei ements einwirkt; doch das haben wir bereits
getan, weil generatelndex ebenfalls elements beeinflusst.

Abbildung 11.8 zeigt die komplette Skizze.

^^^ddElemenT^^

^^eneratelnde^^

^^etElementCourv^

Abb. n.8: Effektskizze der InMemoryDi rectory-Klasse

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 class Element {


private String name;
private String text = "";

public Element(String name) {


this.name = name;
}
public String g e t N a m e O {
return name;
}
public void addText(String newText) {
text += newText;
}

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).

Abb. 11.9: Auswirkungen der El ement-Klasse

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.

11.3 Effektfortpflanzung (Effect Propagation)


Einige Wege, auf denen sich Effekte fortpflanzen, sind einfacher zu erkennen als
andere. Im InMemoryDi rectory-Beispiel aus dem letzten Abschnitt fanden wir
letztlich Methoden, die Werte an den Aufrufer zurückgaben. Auch wenn ich
Effekte zunächst von Änderungspunkten aus verfolge, fallen mir normalerweise
Methoden mit Rückgabewerten zuerst auf. Werden diese Rückgabewerte verwen-
det, pflanzen sie Effekte an den Code fort, der diese Methoden aufruft.

184
11.3
Effektfortpflanzung (Effect Propagation)

Effekte können sich auch stillschweigend oder fast unmerklich fortpflanzen.


Wenn ein Objekt ein anderes Objekt als Parameter akzeptiert, kann es dessen
Zustand modifizieren; und die Änderung wirkt auf den Rest der Anwendung
zurück.

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.

Die tückischste Methode, wie ein Code-Fragment anderen Code beeinflussen


kann, ist die Verwendung globaler oder statischer Daten. Hier ist ein Beispiel:

public class Element {


private String name;
private String text = "";

public Element(String name) {


this.name = name;
}
public String g e t N a m e O {
return name;
}

public void addTextCString newText) {


text += newText;
View.getCurrentDisplayO .addText(newText) ;
}

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?

Es gibt drei grundlegende Formen, wie sich Effekte in Code fortpflanzen:

1. Rückgabewerte, die von einem Aufrufer verwendet werden;


2. Änderung von Objekten, die als Parameter übergeben und später verwendet
werden;
3. Änderung von statischen oder globalen Daten, die später verwendet werden.

Einige Sprachen stellen weitere Mechanismen zur Verfügung. So kann man


etwa in aspektorientierten Sprachen Konstrukte, so genannte Aspekte, schreiben,
die das Verhalten von Code in anderen Bereichen des Systems beeinflussen.

Mit folgender Heuristik können Sie Effekte aufspüren:

1. Identifizieren Sie die zu ändernde Methode.


2. Wenn die Methode einen Rückgabewert hat, analysieren Sie ihre Aufrufer.
3. Prüfen Sie, ob die Methode Werte modifiziert. Ist dies der Fall, prüfen Sie die
Methoden, die diese Werte verwenden, sowie die Methoden, die mit diesen
Methoden arbeiten.
4. Prüfen Sie auch etwaige Ober- und Unterklassen, die diese Instanzvariablen
und Methoden ebenfalls nutzen könnten.
5. Prüfen Sie Parameter der Methoden. Werden sie oder etwaige Objekte, die ihre
Methoden zurückgeben, von dem Code verwendet, den Sie ändern wollen?
6. Suchen Sie nach globalen Variablen und statischen Daten, die von den Metho-
den modifiziert werden, die Sie identifiziert haben.

11.4 Tools für Effektanalysen


Das wichtigste Tool in unserem Arsenal ist die Kenntnis unserer Programmier-
sprache. Jede Sprache verfügt über kleine »Firewalls«, die das Propagieren von
Effekte verhindern. Wenn wir sie kennen, können wir unsere Analysen an solchen
Stellen abbrechen.

Angenommen, wir wollten die Repräsentation in der folgenden Coordinate-


Klasse ändern und die x- und y-Werte in einem Vektor speichern, um die Coordi -
nate-Klasse so zu verallgemeinern, dass sie auch drei- und vierdimensionale
Koordinaten repräsentieren kann. Im folgenden Java-Code müssen wir nicht über
die Klasse hinausblicken, um den Effekt dieser Änderung zu verstehen:

public class Coordinate {


private double x = 0;
private double y = 0;

public Coordinate() {}

186
11.4
Tools für Effektanalysen

public Coordinate(double x, double y) {


this.x = x;
this.y = y;
}

public double distance(Coordinate other) {


return Math.sqrt(Math.pow(other.x - x, 2.0) + Math.pow(other.y - y, 2.0));
}
}

Hier ist Code, über den wir hinausblicken müssen:

public class Coordinate {


double x = 0;
double y = 0;

public CoordinateO {}
public Coordinate(double x, double y) {
this.x = y;
this.y = y;
}

public double distance(Coordinate other) {


return Math.sqrt(Math.pow(other.x - x, 2.0) + Math.pow(other.y - y, 2.0));
}
}

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:

class PolarCoordinate : public Coordinate {


public:
PolarCoordi n a t e Q ;

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;
};

Wird in C++ das Schlüsselwort mutable in einer Deklaration verwendet, kann


diese Variable in const-Methoden modifiziert werden. Zugegeben, mutabl e so zu
verwenden, ist etwas seltsam, aber wenn wir herausfinden müssen, was sich in
einem unbekannten Programm ändern kann und was nicht, müssen wir alle mög-
lichen Effekte suchen, so seltsam sie auch zu sein scheinen. Das Schlüsselwort
const in C++ wörtlich zu nehmen, ohne alle betroffenen Stellen zu prüfen, kann
gefährlich sein. Dasselbe gilt für andere Sprachkonstrukte, die umgangen werden
können.

Beherrschen Sie Ihre Sprache.

11.5 Von der Effektanalyse lernen


Versuchen Sie, Effekte in Code an allen möglichen Stellen zu analysieren. Wenn
Sie mit einer Code-Basis vertraut werden, werden Sie manchmal bemerken, dass
Sie bestimmte Dinge nicht mehr suchen müssen. Dann haben Sie eine gewisse
»Grundqualität« in Ihrer Code-Basis entdeckt. Guter Code enthält kaum »Überra-
schungen«. Einige »Regeln«, die ausdrücklich oder stillschweigend in die Code-
Basis eingebettet sind, bewahren Sie davor, paranoid nach möglichen Effekte zu
suchen. Am leichtesten finden Sie diese Regeln, indem Sie überlegen, wie ein Teil
der Software einen anderen beeinflussen könnte, auch wenn Sie diesem Wirkme-
chanismus in der Code-Basis noch nicht begegnet sind. Dann sollten Sie sich
sagen: »Aber das wäre doch dumm!« Enthält Ihre Code-Basis zahlreiche derartige
Regeln, ist sie viel einfacher zu handhaben. In schlechtem Code sind keine
»Regeln« erkennbar oder werden von vielen Ausnahmen gebrochen.

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

public class CppClass {


private String name;
private List declarations;

public CppCIass(String name, List declarations) {


this.name = name;
this.declarations = declarations;
}

}
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.

Im Allgemeinen wird Programmieren einfacher, wenn wir Auswirkungen in


einem Programm einschränken. Wir müssen weniger wissen, um ein Code-Frag-
ment zu verstehen. Im Extremfall nähern wir uns der funktionalen Programmie-
rung in Sprachen wie etwa Scheme oder Haskell. Programme in diesen Sprachen
können tatsächlich sehr leicht verständlich sein; aber diese Sprachen sind nicht
weit verbreitet. Doch auch in OO-Sprachen können wir durch das Einschränken
von Effekte das Testen erheblich vereinfachen, und es gibt keine Hindernisse,
diese Erkenntnis umzusetzen.

n.6 EfFektskizzen vereinfachen


Dieses Buch handelt davon, das Arbeiten mit Legacy Code zu vereinfachen. Mit
Effektskizzen können Sie etwas sehr Nützliches, das Ihren künftigen Stil ändern
kann, Code zu schreiben.
Erinnern Sie sich an die Effektskizze der CppCI ass-Klasse (siehe Abbildung ii.ii)?
Anscheinend fächert sich der Code auf. Zwei Daten-Komponenten, eine Deklara-
tion und die decl a r a t i ons-Collection wirken auf mehrere verschiedene Metho-
den. Wir können auswählen, welche wir für unsere Tests verwenden möchten. Am
geeignetsten scheint g e t l n t e r f a c e zu sein, weil sie Deklarationen etwas
umfangreicher nutzt. Über diese Methode können wir einige Dinge überwachen,
über die wir über getDecl a r a t i on und getDeclarationCount nicht so leicht
herankommen. Ich hätte nichts dagegen, nur Tests für g e t l n t e r f a c e zu schrei-
ben, wenn ich CppCI a s s charakterisieren müsste, aber es wäre eine Schande, wür-
den getDecl a r a t i on und get-Decl a r a t i onCount nicht abgedeckt. Doch was
wäre, sähe g e t l n t e r f a c e wie folgt aus?

public String getInterface(String interfaceName, int [] indices) {


String result = "class " + interfaceName + " {\npublic:\n";
for (int n = 0; n < indices.length; n++) {

189
Kapitel 11
Ich muss eine Änderung vornehmen. Welche Methoden sollte ich testen?

Declaration vi rtual Function = getDeclaration(indices[n]) ;


result += "\t" + vi rtual Function. asAbstractO + "\n";
}
result += "};\n";
return result;

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.

Effekte und Einkapselung


Einkapselung ist einer der häufig genannten Vorteile der Objektorientierung.
Wenn ich Entwicklern die Techniken zur Aufhebung von Dependencies in die-
sem Buch zeige, weisen sie oft darauf hin, dass viele dieser Techniken die Ein-
kapselung durchbrechen. Das stimmt.
Einkapselung ist wichtig, aber warum sie wichtig ist, ist wichtiger. Einkapselung
hilft uns, über unseren Code nachzudenken. Gut eingekapselter Code enthält
weniger Pfade, denen Sie folgen müssen, um ihn zu verstehen. Wenn wir etwa
einen weiteren Parameter zu einem Konstruktor hinzufügen, um eine Depen-
dency aufheben (siehe das Refactoring mit Parameterize Constructor (25.14)),
müssen wir bei der Effektanalyse einen zusätzlichen Pfad verfolgen.
Die Einkapselung zu durchbrechen, kann die Analyse des Codes erschweren.
Sie kann aber auch einfacher werden, wenn wir danach aussagekräftige Tests
schreiben können. Mit Testfällen für eine Klasse können wir den Code direkter
analysieren. Wir können auch neue Tests für etwaige Fragen über das Verhalten
des Codes schreiben. Einkapselung und Testabdeckung hindern sich nicht
immer gegenseitig; doch wenn sie dies tun, ziehe ich die Testabdeckung vor. Sie
ermöglicht mir oft später eine bessere Einkapselung.

Einkapselung ist kein Selbstzweck, sondern ein Mittel, um die Verständlichkeit


und Sicherheit von Code zu verbessern.

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

Ich muss in einem Bereich vieles


ändern. Muss ich die Dependencies
für alle beteiligten Klassen
aufheben?

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?

Wie können wir solche »Abdeckungstests« einrichten? Zunächst müssen wir


geeignete Stellen für die Tests finden. Falls Sie dies nicht bereits getan haben, wer-
fen Sie einen Blick in Kapitel 11, Ich muss eine Änderung vornehmen. Welche Metho-
den sollte ich testen? Dieses Kapitel beschreibt Wirkungsskizzen (Einschuh in 11.1),
ein leistungsstarkes Tool, mit dem Sie geeignete Stellen identifizieren können. In
diesem Kapitel beschreibe ich ein weiteres Konzept, den Abfangpunkt (engl, inter-
ception point), und zeige Ihnen, wie Sie solche Punkte finden können. Ich
beschreibe auch die beste Art von Abfangpunkten, die Sie in Code finden können:
Einschnürpunkte (engl, pinch points). Ich zeige Ihnen, wie Sie sie finden und wie sie
Ihnen helfen können, wenn Sie Tests schreiben wollen, um Code abzudecken, den
Sie ändern wollen.

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.

12.1.1 Der einfache Fall


Angenommen, wir müssten eine Java-Klasse namens Invoice modifizieren, um
die Methode zu Berechnung der Kosten, getVal ue zu ändern.

public class Invoice


{

public Money getValueO {


Money total = itemsSumO;
if (billingDate.after(Date.yearEnd(openingDate))) {
if (originator.getStateO .equals("FL") ||
originator ,getState() . equal s("NY"))
total .add(getLocalShippingO);
eise
total .add(getDefaultShippingO);
}
eise
total .add(getSpanningShippingO);

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:

public class Invoice


{
public Money getValueO {
Money total = itemsSum();
total .add(shippingPricer.getPriceO);
total.add(getTax());
return total;
}
}

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.

Um unsere Abfangpunkte zu finden, müssen wir, von unseren Änderungspunk-


ten ausgehend, die Wirkungen studieren. Die getVal ue-Methode wird ein ande-
res Ergebnis liefern. Es zeigt sich, dass keine Methode von Invoi ce auf getVal ue
zugreift; aber getVal ue wird in einer anderen Klasse verwendet, und zwar in der
makeStatement-Methode einer Klasse namens Bi 11 i ngStatement (siehe Abbil-
dung 12.i).

(^^^etValue^^

Abb. 12.1: Einfluss der getVal ue-Methode auf die Methode


BiIii ngStatement.makeStatement

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?

Abb. 12.2: Auswirkungen auf getVal ue

Wir können die Skizzen kombinieren (siehe Abbildung 12.3).

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

Im Allgemeinen ist es sinnvoll, Abfangpunkte auszuwählen, die sehr nahe bei


Ihren Änderungspunkten liegen. Dafür gibt es mehrere Gründe. Der erste
Grund ist die Sicherheit. Jeder Schritt zwischen einem Änderungspunkt und
einem Abfangpunkt ähnelt einem Schritt in einer logischen Schlusskette. Im
Wesentlichen sagen wir: »Wir können hier testen, weil dieses jenes beeinflusst
und jenes wiederum ein anderes Ding beeinflusst usw.« Je mehr Schritte eine
Schlusskette enthält, desto schwieriger können Sie feststellen, ob Sie richtiglie-
gen. Manchmal können Sie Sicherheit nur gewinnen, wenn Sie Tests am
Abfangpunkt schreiben, dann zurück zu dem Änderungspunkt gehen, den Code
etwas ändern und prüfen, ob der Test scheitert. Manchmal müssen Sie auf diese
Technik zurückgreifen, Sie sollten sie aber nicht laufend einsetzen. Ein weiterer
Grund, warum entferntere Abfangpunkte weniger geeignet sind, liegt darin,
dass es oft schwieriger ist, dort Tests einzurichten. Dies gilt nicht immer, son-
dern hängt von dem Code ab. Auch hier hängt die Schwierigkeit von der Anzahl
der Schritte zwischen dem Änderungs- und dem Abfangpunkt ab. Oft müssen
Sie die Arbeit des Computers im Kopf nachvollziehen, um zu erkennen, dass
ein Test eine entferntere Funktionalität abdeckt.

In dem Beispiel werden die gewünschten Änderungen von Invoice wahr-


scheinlich am besten auch in dieser Klasse getestet. Wir können ein Invoice-
Objekt in einem Test-Harnisch erstellen, es nach Bedarf einrichten und getVa-
1 ue aufrufen, um sein Verhalten festzuhalten, während wir unsere Änderungen
machen.

12.1.2 Abfangpunkte a u f höherer Ebene


In den meisten Fällen ist der bestmögliche Abfangpunkt für eine Änderung eine
öffentliche Methode der geänderten Belasse. Diese Änderungspunkte sind leicht zu
finden und zu nutzen; aber manchmal sind sie nicht die beste Wahl. Dies wird
klar, wenn wir das Invoi ce-Beispiel etwas erweitern.
Angenommen, zusätzlich zur Berechnung der Versandkosten für Invoice-
Objekte müssten wir in eine Klasse namens Item ein neues Feld zur Speicherung
des Versenders einfügen. Außerdem brauchen wir im Bi 1 1 i ngStatement einen
separaten Nachweis pro Versender. Abbildung 12.4 zeigt unser gegenwärtiges
Design in UML.
Wenn keine dieser Belassen über Tests verfügt, könnten wir damit beginnen, Tests
für jede Klasse einzeln zu schreiben und die benötigten Änderungen durchfüh-
ren. Dies würde funktionieren; aber es kann effizienter sein, zunächst zu versu-
chen, einen Abfangpunkt auf höherer Ebene zu finden, mit dem wir diesen Bereich
des Codes charakterisieren können. Dies böte zwei Vorteile: Wir müssten weniger
Dependencies aufheben, und wir bringen auch ein größeres Code-Fragment unter
Kontrolle.

197
Kapitel 12
Ich muss in einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen aufheben?

Abb. 12.4: Erweitertes Abrechnungssystem

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.

Wodurch wird Bi 1 1 i ngStatement hier zu einem idealen Abfangpunkt? Es ist ein


einziger Punkt, an dem wir Wirkungen von Änderungen in einem Cluster von
Klassen beobachten können. Abbildung 12.5 zeigt die Wirkungsskizze der geplan-
ten Änderungen.

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

<- ' >

Abb. 12.5: Wirkungsskizze des Abrechnungssystems

Doch wichtig ist, dass Einschnürpunkte durch Änderungspunkte bestimmt wer-


den. Ein Satz von Änderungen einer Klasse könnte einen guten Einschnürpunkt
haben, selbst wenn die Klasse über mehrere Clients verfügt. Dies wird klar, wenn
wir den Blick auf das Abrechnungssystem erweitern (siehe Abbildung 12.6).

*
BillingStatement Invoice

+ makeStatement() : string + addltem(item)

\! *
InventoryControl Item

+ run() + ltem(id : int, price : Money)


+ needsReorder () : boolean

Abb. 12.6: Abrechnungssystem mit Lagerverwaltung

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.

Abb. 12.7: Szenarium des kompletten Abrechnungssystems

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.

12.2 Ein Design mit Einschnürpunkten beurteilen


Im vorhergehenden Abschnitt habe ich beschrieben, wie nützlich Einschnürpunkte
beim Testen sein können; aber sie haben noch andere Verwendungen. Ihre Posi-
tion kann Ihnen Hinweise zur Verbesserung Ihres Codes liefern.

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.

Mit Wirkungsskizzen verborgene Klassen finden


Manchmal können Sie mit einer Wirkungsskizze einer umfangreichen Klasse
entdecken, wie Sie die Klasse in Komponenten zerlegen könnten. Hier ist ein
kleines Beispiel in Java. Eine Klasse namens P a r s e r enthält eine öffentliche
Methode namens parseExpression.
public class Parser
{
private Node root;
private int currentPosition;
private String stringToParse;
public void parseExpression(String expression) { .. }
private Token getTokenO { . . }
private boolean hasMoreTokensO { .. }
}
Zeichneten wir eine Wirkungsskizze dieser Klasse, stellten wir fest, dass parse-
Expression von getToken und hasMoreTokens, aber nicht direkt von stri ng-
ToParse oder currentPosition abhängt, auch wenn dies für getToken und
hasMoreTokens der Fall ist. Hier liegt eine natürliche Einkapselungsgrenze vor,
auch wenn sie nicht wirklich schmal ist (zwei Methoden verbergen zwei
bestimmte Informationen). Wir können diese Methoden und Felder in eine
Klasse namens Tokenizer extrahieren und erhalten eine einfachere Parser-
Klasse.

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.

12.3 Fallen bei Einschnürpunkten


Beim Schreiben von Unit-Tests gibt es mehrere mögliche Stolpersteine. So könn-
ten sich etwa Unit-Tests langsam zu Mini-Integrationstests auswachsen. Wir müs-
sen eine Klasse testen; deshalb instanziieren wir mehrere ihrer Kollaborateure
und übergeben sie an die Klasse. Wir prüfen einige Werte und sind ziemlich
sicher, dass die ganze Gruppe von Objekten gut zusammenarbeitet. Doch wenn
wir dies zu oft machen, erhalten wir viele große, sperrige Unit-Tests, die endlos
lange laufen.

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

Ich muss etwas ändern, weiß aber


nicht, welche Tests ich schreiben soll

Wenn Entwickler über Tests diskutieren, meinen sie normalerweise Tests, um


Bugs zu finden. Oft werden diese Tests manuell durchgeführt. Automatisierte
Tests zur Suche von Bugs in Legacy Code zu schreiben, scheint oft weniger effizi-
ent zu sein, als den Code einfach auszuprobieren. Wenn Sie Legacy Code manuell
ausführen können, können Sie Bugs normalerweise sehr schnell lokalisieren. Der
Nachteil liegt darin, dass Sie diese manuelle Arbeit nach jeder Änderung des
Codes erneut ausführen müssen. Und offen gestanden: Entwickler tun dies ein-
fach nicht. Fast alle Teams, mit denen ich gearbeitet habe und die ihre Änderun-
gen manuell getestet haben, kamen nur schlecht voran. Das Selbstvertrauen in
diesen Teams war viel schlechter, als es hätte sein können.

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.

Der folgende kleine Algorithmus hilft Ihnen, Charakterisierungs-Tests zu schrei-


ben:

1. Verwenden Sie ein Code-Fragment in einem Test-Harnisch.


2. Schreiben Sie eine Zusicherung, von der Sie wissen, dass sie scheitern wird.
3. Lesen Sie am Scheitern das zugehörige Verhalten ab.
4. Ändern Sie den Test so, dass er das Verhalten erwartet, das der Code zeigt.
5. Wiederholen Sie.

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

at sun.ref1ect.NativeMethodAccessorlmpl.invokeO (Native Method)


at sun.ref1ect.Nati veMethodAccessorlmpl.i nvoke
(NativeMethodAccessorlmpl.java:39)
at sun.ref 1 ect.Delegati ngMethodAccessorlmpl.i nvoke
(Delegati ngMethodAccessorlmpl.j ava:2 5)

FAILURES!!!
Tests run: 1, Failures: 1, Errors: 0

Wir können den Test so ändern, dass er bestanden wird:

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.

Charakterisierungs-Tests halten das tatsächliche Verhalten eines Code-Frag-


ments fest. Wenn wir beim Schreiben der Tests etwas Unerwartetes finden,
lohnt es sich, Klarheit zu schaffen. Es könnte ein Bug sein. Dies bedeutet, dass
wir den Test nicht in unsere Test-Suite aufnehmen; stattdessen sollten wir ihn
als verdächtig markieren und feststellen, welche Auswirkungen eine Korrektur
hätte.

Über Charakterisierungs-Tests gibt es noch viel mehr zu sagen. In dem PageGene-


rator-Beispiel schien es, als erhielten wir blind Testwerte, indem wir Werte an den
Code übergeben und sie in den Zusicherungen zurückbekommen. Wir können
dies tun, wenn wir ziemlich genau wissen, was der Code tun soll. Einige Fälle,
etwa ein Objekt nicht zu beeinflussen und zu beobachten, was seine Methoden
generieren, liegen auf der Hand und sind lohnenswerte Charakterisierungskandi-
daten; aber was tun wir danach? Wie viele Tests können wir auf Objekte wie einen
PageGenerator anwenden? Unbegrenzt viele. Wir könnten einen größeren Teil
unseres Lebens mit dem Schreiben von Testfällen allein für diese Klasse verbrin-
gen. Wann hören wir auf? Können wir wichtige Fälle von weniger wichtigen unter-
scheiden? Wenn ja, wie?

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.

Methoden und Tests


Bevor Sie eine Methode in einem Legacy-System verwenden, sollten Sie prüfen,
ob es für sie Tests gibt. Ist dies nicht der Fall, schreiben Sie sie. Wenn Sie konsis-
tent so vorgehen, verwenden Sie Tests als Medium der Kommunikation. Ent-
wickler können an den Tests ablesen, was sie von der Methode erwarten dürfen.
Eine Klasse testbar zu machen, steigert an sich bereits die Code-Qualität. Ent-
wickler können feststellen, was wie funktioniert; sie können die Methode
ändern, Fehler korrigieren und zu anderen Aufgaben fortschreiten.

13.2 Klassen charakterisieren


Wie können wir feststellen, was wir in einer Klasse testen sollten? Zuerst müssen
wir die Grundfunktion der Klasse herausfinden. Wir können Tests für die ein-
fachste Funktion schreiben, die die Klasse unserem Verständnis nach ausführen
kann, und uns dann von dort von unserer Neugier leiten lassen. Folgende Heuris-
tiken können helfen:
1. Suchen Sie nach verzwickten Teilen der Logik. Wenn Sie einen Bereich des
Codes nicht verstehen, können Sie ihn vielleicht mit Sensing Variable (22.3) cha-
rakterisieren. Verwenden Sie Überwachungsvariablen, um zu prüfen, ob
bestimmte Bereiche des Codes ausgeführt werden.
2. Wenn Sie die Aufgaben einer Klasse oder Methode entdecken, legen Sie eine
Liste der Dinge an, die schiefgehen können. Können Sie Tests formulieren, um
sie auszulösen?
3. Welche Ihrer Inputs können Sie unter Testkontrolle bringen. Was passiert bei
extremen Werten?
4. Sollten über die Lebensdauer der Klasse hinweg immer alle Bedingungen wahr
sein? Oft werden solche Bedingungen als Invarianten bezeichnet. Versuchen
Sie, Tests zu schreiben, um die Bedingungen zu verifizieren. Oft müssen Sie
den Code vielleicht refaktorisieren, um diese Bedingungen zu entdecken. Dann
führen die Refactorings oft zu neuen Einsichten über die Verbesserung des
Codes.

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.

Wenn Sie Fehler finden


Wenn Sie Legacy Code charakterisieren, werden Sie immer wieder Fehler fin-
den. Es gibt keinen Legacy Code ohne Bugs; normalerweise hat er umso mehr,
je schwieriger er zu verstehen ist. Was sollten Sie tun, wenn Sie einen Fehler fin-
den?

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.

13.3 Gezielt testen


Nachdem wir Tests geschrieben haben, um einen Code-Abschnitt zu verstehen,
müssen wir prüfen, ob die Dinge, die wir ändern wollen, von unseren Tests tat-
sächlich abgedeckt werden. Das folgende Beispiel zeigt eine Methode einer Java-
Klasse, die den Wert von Treibstoff in geleasten Fahrzeugen berechnet:

public class FuelShare


{
private long cost = 0;
private double corpBase = 12.0;

210
13-3
Gezielt testen

private ZonedHawthorneLease lease;

public void addReading(int gallons, Date readingDate){


if (lease.isMonthlyO) {
if (gallons < Lease.C0RP_MIN)
cost += corpBase;
eise
cost += 1.2 * priceForGallons(ganons);
}
lease.postReading(readingDate, gallons);
}

}
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.

Nach dem Refactoring sollte der Code wie folgt aussehen:

public class FuelShare


{
public void addReading(int gallons, Date readingDate){
cost += lease.computeValue(gallons, priceForGallons(gallons)) ;

lease.postReading(readingDate, gallons);
}

public class ZonedHawthorneLease extends Lease


{
public long computeValue(int gallons, long totalPrice) {
long cost = 0;
if (lease.isMonthlyO) {
if (gallons < Lease.C0RP_MIN)
cost += corpBase;
eise
cost += 1.2 * totalPrice;
}
return cost;
}

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

if (gallons < Lease.CORP_MIN)


cost += corpBase;

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

cost += 1.2 * priceForCallons(gallons);

Wird dieser Code in die neue Methode verschoben, wird daraus:

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:

public class FuelShare


{
public void addReading(int gallons, Date readingDate){
if (lease.isMonthlyO) {
if (gallons < C0RP_MIN)
cost += corpBase;
eise
cost += 1.2 * priceForCallons(gallons);
}

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:

public void testValueForCallonsMoreThanCorpMinO {


StandardLease lease = new StandardLease(Lease.MONTHLY);
FuelShare share = new FuelShare(lease);

share.addReading(FuelShare.C0RP_MIN +1, new Date());


assertEquals(12, share.getCostO) ;
}

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:

public class FuelShare


{
private double cost = 0.0;

public void addReading(int gallons, Date readingDate){


if (lease. isMonthlyO) {
if (gallons < C0RP_MIN)
cost += corpBase;
el se
cost += 1.2 * priceForCallons(gallons);
}

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?

public void testValue () {


StandardLease lease = new StandardLease(Lease.MONTHLY);
FuelShare share = new FuelShare(lease);

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?

public class ZonedHawthorneLease


{
public long computeValue(int gallons, long totalPrice) {
long cost = 0;
if (lease.isMonthlyO) {
if (gallons < C0RP_MIN)
cost += corpBase;
el se
cost += 1.2 * totalPrice;
}
return cost;
}
}

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?

Viele Charakterisierungs-Tests sehen wie »Schönwetter«-Tests aus. Sie testen


selten Sonderfälle, sondern verifizieren nur, ob bestimmte Verhaltensweisen
vorhanden sind. Aus ihrem Vorhandensein können wir schließen, dass unsere
Refactorings das Verhalten bewahrt haben.

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

Die wertvollsten Charakterisierungs-Tests prüfen einen besonderen Pfad und


prüfen jede Umwandlung auf diesem Pfad.

Und viertens könnten wir einfach beschließen, ein kleineres Code-Fragment zu


charakterisieren. Wenn wir Methoden sicher mit einem Refactoring-Tool extrahie-
ren können, können wir kleinere Methoden als computeValue extrahieren und
Tests für ihre Teile schreiben. Leider verfügen nicht alle Sprachen über Refacto-
ring-Tools - und manchmal extrahieren auch vorhandene Tools Methoden nicht
so, wie Sie wollen.

Mängel von Refactoring-Tools


Ein gutes Refactoring-Tool ist unschätzbar, aber oft müssen Entwickler, die über
diese Tools verfügen, auf ein manuelles Refactoring zurückgreifen. Hier ist ein
verbreiteter Fall. Eine Klasse A enthält in ihrer b()-Methode Code, den wir extra-
hieren wollen:
public class A
{
int x = 1;
public void b() {
int y = 0;
int c = x + y;
}
};
Wenn wir den Ausdruck x + y in Methode b extrahieren und eine Methode
namens add erstellen wollen, extrahiert wenigstens ein Tool auf dem Markt sie
als add (y) und nicht als add (x, y). Warum? Weil x eine Instanzvariable ist und
für alle Methoden zur Verfügung steht, die wir extrahieren.

13.4 Eine Heuristik für das Schreiben von


Charakterisieru ngs-Tests
1. Schreiben Sie Tests für den Bereich des Codes, in dem Sie Änderungen vorneh-
men wollen. Schreiben Sie so viele Testfälle, wie Sie brauchen, um das Verhalten
des Codes zu verstehen.
2. Analysieren Sie danach besonders die Dinge, die Sie ändern werden, und versu-
chen Sie, dafür Tests zu schreiben.
3. Wenn Sie versuchen, Funktionalität zu extrahieren oder zu verschieben, schrei-
ben Sie für jeden Testfall Tests, die die Existenz und Verknüpfung dieser Verhal-
tensweisen bestätigen. Verifizieren Sie, ob der zu verschiebende Code geprüft
wird und korrekt verknüpft ist. Prüfen Sie die Umwandlungen.

215
Kapitel 14

Dependencies von Bibliotheken


bringen mich um

Software-Entwicklung kann durch die Wiederverwendung von Code wirklich


unterstützt werden. Wenn wir eine Bibliothek kaufen können, die ein Problem für
uns löst (und herausfinden, wie sie verwendet wird), können wir oft erheblich Zeit
bei einem Projekt einsparen. Das einzige Problem besteht darin, dass wir uns sehr
leicht von einer Bibliothek abhängig machen können. Wenn Sie eine Bibliothek
freizügig überall in Ihrem Code verwenden, sind Sie an sie gebunden. Einige
Teams, mit denen ich gearbeitet habe, wurden durch ihr übermäßiges Vertrauen
in Bibliotheken nachhaltig geschädigt. In einem Fall erhöhte ein Anbieter die
Lizenzgebühren so drastisch, dass die Anwendung auf dem Markt keinen Gewinn
mehr erzielte. Das Team konnte nicht einfach die Bibliothek gegen die eines ande-
ren Anbieters austauschen, weil die Entfernung aller Aufrufe des Codes des
ursprünglichen Anbieters auf eine Neuerstellung der Software hinausgelaufen
wäre.

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.

Bibliotheksdesigner, die Design Constraints (Design-Einschränkungen) durch


Sprachfunktionen erzwingen wollen, machen oft einen Fehler: Sie vergessen,
dass guter Code sowohl in Produktions- als auch in Testumgebungen läuft.
Constraints für Produktionsumgebungen können das Arbeiten in Testumge-
bungen fast unmöglich machen.

Zwischen Sprachfunktionen, die versuchen, ein gutes Design zu erzwingen, und


den Dingen, die Sie beim Testen von Code machen müssen, existiert eine grundle-
gende Spannung. Eine der häufigsten Spannungen ist das Once-Dilemma (Einmal-
Dilemma). Wenn eine Bibliothek annimmt, dass in einem System nur eine
Instanz einer Klasse existiert, kann sie die Verwendung von Fake-Objekten
erschweren. Möglicherweise können Sie Introduce Static Setter (25.12) oder viele
der anderen Dependency-lösenden Techniken für den Umgang mit Singletons
nicht einsetzen. Manchmal können Sie dann das Singleton nur in einen Wrapper
packen.

Ein verwandtes Problem ist das Restricted-Override-Dilemma (Eingeschränktes-


Überschreiben-Dilemma). In einigen OO-Sprachen sind alle Methoden virtuell. In
anderen sind sie standardmäßig virtuell, können aber als nicht-virtuell deklariert
werden. In wieder anderen müssen Sie sie ausdrücklich als virtuell deklarieren.
Aus der Sicht des Designs hat es Vorteile, einige Methoden als nicht-virtuell zu
deklarieren. Gelegentlich empfehlen Software-Gurus, so viele Methoden wie mög-
lich als nicht-virtuell zu deklarieren. Manche ihrer Gründe sind gut; aber es ist
schwer zu widerlegen, dass ein solches Vorgehen die Überwachung und Struktu-
rierung einer Code-Basis nicht gerade erleichtert. Außerdem lässt sich nicht
abstreiten, dass Entwickler oft sehr guten Code in Smalltalk schreiben, wo eine
solche Praxis unmöglich ist; in Java ist diese Praxis unüblich; und sogar in C++
wurde sehr viel Code ohne diese Technik geschrieben. Sie können sehr gut
zurechtkommen, indem Sie einfach vorgeben, eine öffentliche Methode sei im
Produktionscode nicht-virtuell. Mit dieser Technik können Sie sie beim Testen
selektiv überschreiben und das Beste aus beiden Welten bekommen.

Manchmal ist die Verwendung einer Programmierkonvention genauso gut wie


die Verwendung einer restriktiven Sprachfunktion. Überlegen Sie, was Ihre
Tests erfordern.

218
Kapitel 15

Meine Anwendung besteht nur aus


API-Aufrufen

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.*;

public class MailingListServer


{
public static final String SUBJECT_MARKER = "[list]";
public static final String LOOP_HEADER = "X-Loop";

public static void main (String [] args) {


if (args.length != 8) {
System.err.printin ("Usage: java MailingList <popHost> " +
"<smtpHost> <popBuser> <pop3password> " +
"<smtpuser> <smtppassword> <listname> " +
"<relayinterval>") ;
return;
}
Hostinformation host = new Hostinformation (args [0], args [1], args
[2], args [3], args [4], args [5]);
String listAddress = args [6];
int interval = new Integer (args [7]) .intValue ();
Roster roster = null;
try {
roster = new FileRosterCroster.txt");
} catch (Exception e) {
System.err.printin ("unable to open roster.txt");
return;
}
try {
do {
try {
Properties properties = System.getProperties ();
Session session = Session.getDefaultlnstance (properties, null);
Store störe = session.getStore ("pop3");
störe.connect (host.pop3Host, -1, host.pop3User,
host.pop3Password);
Folder defaultFolder = störe.getDefaultFolderO;
if (defaultFolder == null) {
System.err.println("Unable to open default folder");
return;
}
Folder folder = defaultFolder.getFolder ("INBOX");
if (folder == null) {
System.err.println("Unable to get: " + defaultFolder);
return;
}
folder.open (Folder.READ_WRITE);
process(host, listAddress, roster, session, störe, folder);
} catch (Exception e) {
System.err.println(e);

220
Meine Anwendung besteht nur aus API-Aufrufen

System.err.printin ("(retrying mail check)");


}
System.err.print (".");
try { Thread.sleep (interval * 1000); }
catch (InterruptedException e) {}
} while (true);
}
catch (Exception e) {
e.printStackTrace ();
}

private static void process(


Hostinformation host, String listAddress, Roster roster,
Session session,Store störe, Folder folder)
throws MessagingException {
try {
if (folder.getMessageCount() != 0) {
Message[] messages = folder.getMessages ();
doMessage(host, listAddress, roster, session, folder, messages);
}
} catch (Exception e) {
System.err.printin ("message handling error");
e.printStackTrace (System.err);
}
finally {
folder.close (true);
störe.close ();
}
}
private static void doMessage(
Hostinformation host,
String listAddress,
Roster roster,
Session session,
Folder folder,
Message[] messages)
throws MessagingException, AddressException,
IOException, NoSuchProviderException {
FetchProfile fp = new FetchProfile ();
fp.add (FetchProfi1e.Item.ENVELOPE);
fp.add (FetchProfi1e.Item.FLAGS);
fp.add ("X-Mailer");
folder.fetch (messages, fp);
for (int i = 0; i < messages.length; i++) {
Message message = messages [i];
if (message.getFlags O.contains (Flags.Flag.DELETED))
continue;
System.out.printin("message received: " + message.getSubject ());
if (!roster.containsOneOf (message.getFrom ()))
continue;
MimeMessage forward = new MimeMessage (session);
InternetAddress result = null;
Add ress [] fromAddress = message.getFrom ();
if (fromAddress != null && fromAddress.length > 0)
result = new InternetAddress (fromAddress [Oj.toString ());

221
Kapitel 15
Meine A n w e n d u n g besteht nur aus API-Aufrufen

InternetAddress from = result;


forward.setFrom (from);
forward.setReplyTo (new Address [] { new InternetAddress
(listAddress) });
forward.addRecipients (Message.RecipientType.TO, listAddress);
forward.addReci pi ents (Message.Reci pi entType.BCC,
roster.getAddresses ());
String subject = message.getSubjectQ;
if (-1 == message.getSubjectO .indexOf (SUBDECT_MARKER))
subject = SUBJECT_MARKER + " " + message.getSubjectO;
forward.setSubject (subject);
forward.setSentDate (message.getSentDate ());
forward.addHeader (L00P_HEADER, listAddress);
Object content = message.getContent ();
if (content instanceof Multipart)
forward.setContent ((Multipart)content);
el se
forward.setText ((String)content);

Properties props = new Properties ();


props.put ("mail.smtp.host", host.smtpHost);

Session smtpSession = Session.getDefaultlnstance (props, null);


Transport transport = smtpSession.getTransport ("smtp");
transport.connect (host.smtpHost, host.smtpUser, host.smtpPassword);
transport.sendMessage (forward, roster.getAddresses ());
message.setFlag (Flags.Flag.DELETED, true);
}
}
}
Dieser Code ist ziemlich kurz; trotzdem ist er nicht sehr klar. Er sind kaum Zeilen
ohne API-Aufrufe zu finden. Könnte dieser Code besser strukturiert werden?
Könnte er so strukturiert werden, dass Änderungen einfacher werden?

Ja, das ist möglich.


Der erste Schritt besteht darin, den »rechnerischen Kern« des Codes zu finden:
Was macht dieser Code wirklich für uns?

Es kann hilfreich sein, seine Funktion kurz zu beschreiben:


Dieser Code liest Konfigurationsdaten von der Befehlszeile und eine Liste mit
E-Mail-Adressen aus einer Datei ein. Er prüft periodisch, ob neue Mail vor-
handen ist. Wenn er eine Mail findet, leitet er sie an alle E-Mail-Adressen in
der Datei weiter.

Bei diesem Programm scheint es hauptsächlich um Input und Output zu gehen,


aber es macht noch etwas mehr. In dem Code wird ein Thread ausgeführt. Er
schläft und wacht dann periodisch auf, um zu prüfen, ob Mail vorhanden ist.
Außerdem leiten wir die eingehenden Mail-Messages nicht einfach weiter, son-
dern leiten aus ihnen neue Messages ab. Wir müssen alle Felder setzen und dann

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.

A b b . 15.1: Ein b e s s e r e r Mailing-List-Server

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

API-Code auflösen. Im Produktions-Code können unsere Wrapper Aufrufe an das


echte API delegieren; beim Testen können wir Fakes verwenden.

Können wir diese Technik auf den Mailing-List-Code anwenden?

Dies ist der Code in dem Mailing-List-Server, der die Mail-Messages tatsächlich
versendet:

Session smtpSession = Session.getDefaultlnstance (props, null);


Transport transport = smtpSession.getTransport ("smtp");
transport.connect (host.smtpHost, host.smtpUser, host.smtpPassword);
transport.sendMessage (forward, roster.getAddresses ());

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.

Dieser Mailing-List-Code ist wirklich ein schlechter Skinning-Kandidat. Das API


ist relativ kompliziert. Aber wenn wir keine Refactoring-Tools zur Hand haben,
könnte dies die sicherste Methode sein.

Glücklicherweise stehen für Java Refactoring-Tools zur Verfügung. Deshalb kön-


nen wir eine andere Methode anwenden, die als Responsibility-Based Extraction
(Aufgabenbasierte Extraktion) bezeichnet wird. Dabei identifizieren wir in dem
Code Aufgaben und extrahieren die zugehörigen Methoden.

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;

public class MailSender


{
private Hostinformation host;
private Roster roster;

public MailSender (Hostinformation host, Roster roster) {


this.host = host;
this.roster = roster;
}

225
Kapitel 15
Meine A n w e n d u n g besteht nur aus API-Aufrufen

public void sendMessage (Message message) throws Exception {


Transport transport = getSMTPSession ().getTransport ("smtp");
transport.connect (host.smtpHost, host.smtpUser, host.smtpPassword);
transport.sendMessage (message, roster.getAddresses ());
}

private Session getSMTPSession () {


Properties props = new Properties ();
props.put ("mail.smtp.host", host.smtpHost);
return Session.getDefaultlnstance (props, null);
}
}
Wie wählen wir zwischen Skin and Wrap the API und Responsibility-Based Extrac-
tion? Hier sind die Vor- und Nachteile:
Skin and Wrap the API eignet sich unter folgenden Umständen:
• Das API ist relativ klein.
• Sie wollen die Dependencies von einer fremden Bibliothek vollständig heraus-
lösen.
• Sie haben keine Tests und Sie können keine schreiben, weil Sie nicht durch das
API testen können.
Wenn wir die Methode Skin and Wrap the API anwenden, können wir fast unseren
gesamten Code unter Testkontrolle bringen, abgesehen von einer dünnen Delega-
tionsschicht zwischen dem Wrapper und den echten API-Klassen.
Responsibility-Based Extraction eignet sich unter folgenden Umständen:
• Das API ist komplizierter.
• Sie verfügen über ein Tool, mit dem Sie Methoden sicher extrahieren können,
oder Sie trauen es sich zu, Code sicher manuell zu extrahieren.
Die Vorteile und Nachteile dieser Techniken gegeneinander abzuwägen, ist nicht
leicht. Skin and Wrap the API bedeutet mehr Arbeit, kann aber sehr nützlich sein,
um sich von fremden Bibliotheken abzukoppeln, was häufig erforderlich ist.
Details finden Sie in Kapitel 14, Dependencies von Bibliotheken bringen mich um. Bei
der Responsibility-Based Extraction extrahieren wir zusammen mit dem API-Code
möglicherweise einen Teil unserer eigenen Logik, nur damit wir eine Methode mit
einem auf höherer Ebene angesiedelten Namen extrahieren können. Dann könnte
unser Code von höher angesiedelten Schnittstellen anstatt von niedrig angesiedel-
ten API-Aufrufen abhängig sein; aber möglicherweise gelingt es uns nicht, den
extrahierten Code einer Testkontrolle zu unterwerfen.

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

Ich verstehe den Code nicht gut


genug, um ihn zu ändern

In unbekannten Code, insbesondere in Legacy Code einzugreifen, kann Angst


machen. Im Laufe der Zeit werden Menschen relativ immun gegen Angst. Sie ent-
wickeln Selbstvertrauen im Kampf mit Monster-Code; aber man muss abgebrüht
sein, um keine Angst zu bekommen. Jeder stößt von Zeit zu Zeit auf Dämonen,
die er nicht bezwingen kann. Grübelt man darüber nach, bevor man sich dem
Code zuwendet, wird alles nur schlimmer. Man weiß nie, ob eine Änderung ein-
fach sein wird oder ob sie haarsträubende wochenlange Mühen nach sich ziehen
wird, in denen man das System, die eigene Situation und auch die Kollegen am
liebsten verfluchen möchte. Würden wir alles verstehen, was wir für unsere Ände-
rungen wissen müssen, wäre alles leichter. Wie können wir uns dieses Verständ-
nis erarbeiten?

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 Listing Markup


Listing Markup (Listings markieren) ist eine weitere Technik, um Code, vor allem
lange Methoden besser zu verstehen. Die Idee ist einfach; und fast jeder hat sie
schon einmal angewendet; doch meiner Meinung nach wird sie zu wenig einge-
setzt.
Wie Sie Listings am besten markieren, hängt davon ab, was Sie verstehen wollen.
Doch zunächst müssen Sie den Code ausdrucken. Danach können Sie ihn wie
folgt markieren.

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.

16.2.2 Die Struktur einer Methode verstehen


Wenn Sie eine umfangreiche Methode verstehen wollen, richten Sie Blöcke aus.
Oft machen es die Einrückungen in langen Methoden unmöglich, sie zu lesen. Sie
können die Blöcke ausrichten, indem Sie Linien von ihrem Anfang zu ihrem Ende
ziehen oder indem Sie am Ende eines Blocks seine erste Zeile (den Anfangstext
einer Schleife oder Bedingung) in einem Kommentar einfügen.
Am besten gehen Sie von innen nach außen vor. So können Sie etwa bei Sprachen
der C-Familie (C, C++, Java, C#) vom Anfang des Listings über alle öffnenden
geschweiften Klammern bis zur ersten schließenden geschweiften Klammer
gehen. Markieren Sie diese, gehen Sie dann zurück zu der zugehörigen öffnenden
Klammer und markieren Sie diese ebenfalls. Gehen Sie dann weiter zur nächsten
schließenden geschweiften Klammer und tun Sie dort dasselbe. Gehen Sie zurück
bis zur zugehörigen öffnenden Klammer.

22g
Kapitel 16
Ich verstehe den Code nicht gut genug, um ihn zu ändern

16.2.3 Methoden extrahieren


Wenn Sie eine umfangreiche Methode zerlegen wollen, kreisen Sie den Code ein,
den Sie extrahieren möchten. Schreiben Sie ihren Coupling Count daneben (siehe
Kapitel 22, Ich muss eine Monster-Methode ändern und kann keine Tests dafür schrei-
ben).

16.2.4 Auswirkungen einer Änderung studieren


Wenn Sie Auswirkungen einer geplanten Änderung verstehen wollen, können Sie
anstelle einer Auswirkungsskizze (11.1) eine Markierung neben die zu ändernden
Code-Zeilen setzen. Markieren Sie dann alle Variablen, deren Wert durch diese
Änderung modifiziert werden kann, und alle Methodenaufrufe, die von ihr betrof-
fen sein können. Markieren Sie als Nächstes die Variablen und Methoden, die von
den Dingen beeinflusst werden, die Sie gerade markiert haben. Wiederholen Sie
dies so oft wie nötig, um zu erkennen, wie sich die Auswirkungen der Änderung
in dem Code fortpflanzen. Danach werden Sie ein klareres Bild der Dinge haben,
die Sie testen müssen.

16.3 Scratch Refactoring


Refactoring zählt zu den besten Techniken, Code gründlich kennen zu lernen.
Greifen Sie sich einen Teil des Codes heraus und beginnen Sie, Fragmente zu ver-
schieben und den Code zu säubern. Allerdings kann dies ohne Tests ziemlich ris-
kant sein. Woran erkennen Sie, dass Sie den Code nicht beschädigen, wenn Sie
versuchen, ihn per Refactoring zu verstehen? Tatsächlich gibt es ein Verfahren, bei
dem Sie sich keine Sorgen machen müssen - und es ist ziemlich einfach. Arbeiten
Sie mit Ihrem Versionskontrollsystem; checken Sie den Code aus. Vergessen Sie
die Tests. Extrahieren Sie Methoden, verschieben Sie Variablen, refaktorisieren Sie
Methoden, die Sie besser verstehen wollen. Nur eins dürfen Sie nicht: den Code
wieder einchecken. Werfen Sie den Code danach einfach weg. Diese Technik heißt
Scratch Refactoring (dt. sinngemäß Schmierzettel-Refactoring).

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.

16.4 Ungenutzten Code löschen


Ist der Code, den Sie studieren, verwirrend und enthält ungenutzte Fragmente,
löschen Sie sie. Sie beseitigen damit nur ein nutzloses Hindernis.
Löschen von Code ist für manche Entwickler Verschwendung. Schließlich hat
jemand Zeit aufgewendet, um diesen Code zu schreiben - vielleicht kann er in
Zukunft noch verwendet werden. Nun, zu diesem Zweck gibt es Versionskontroll-
systeme. Dieser Code bleibt in früheren Versionen erhalten. Falls Sie ihn je benö-
tigen, können Sie ihn von dort abrufen.

86
Kapitel 17

Meine Anwendung hat


keine Struktur

Langlebige Anwendungen haben die Tendenz, zu wuchern. Vielleicht hatten sie


ursprünglich eine gut durchdachte Architektur; doch im Laufe der Jahre können
sie unter Zeitdruck an einen Punkt kommen, an dem niemand mehr ihre Struktur
vollkommen versteht. Entwickler können jahrelang an einem Projekt arbeiten,
ohne zu wissen, wo neue Funktionen eingefügt werden müssten; sie kennen nur
die Hacks, die sie vor Kurzem in das System eingefügt haben. Wenn sie neue
Funktionen hinzufügen, gehen sie zu den »Hack-Punkten«, weil sie diese Berei-
che am besten kennen.

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.

Es gibt mehrere Möglichkeiten, sich ein Gesamtbild eines umfangreichen Sys-


tems zu machen. Das Buch Object-Oriented Reengineering Pattems von Serge
Demeyer, Stephane Ducasse und Oscar M. Nierstrasz (Morgan Kaufmann Publi-
shers, 2002) enthält einen Katalog von Techniken für genau dieses Problem. Hier
beschreibe ich mehrere andere, ziemlich leistungsstarke Verfahren. Wenn Sie sie
oft in einem Team einsetzen, schaffen und nähren Sie das »Architekturbewusst-
sein« des Teams - und das ist vielleicht das Wichtigste, was Sie tun können, um
die Integrität der Architektur zu bewahren. Es ist schwierig, auf Dinge zu achten,
an die man nur selten denkt.

17.1 Die Geschichte des Systems erzählen


Bei meiner Arbeit mit Teams verwenden ich oft eine Technik, die ich als »Die
Geschichte des Systems erzählen« bezeichne. Dafür brauchen Sie wenigstens
zwei Personen. Der Prozess beginnt mit einer Frage der ersten Person: »Welche
Architektur hat das System?«; dann versucht die andere, die Architektur des Sys-
tems mit wenigen, vielleicht nur zwei oder drei, Konzepten zu erklären. Wer
erklärt, muss vorgeben, dass die andere Person nichts über das System weiß. In
wenigen Sätzen müssen Sie die Komponenten des Designs und ihre Interaktion
beschreiben. Mit diesen wenigen Sätzen drücken Sie die Essenz des Systems aus.
Als Nächstes teilen Sie die nächstwichtigen Dinge über das System mit. So fahren
Sie fort, bis Sie alles Wichtige über das Kern-Design des Systems gesagt haben.

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?

Pragmatische Überlegungen verhindern oft eine Vereinfachung, aber es hat Vor-


teile, die einfachere Sicht darzulegen. Wenigstens hilft sie jedem zu verstehen, wie
die ideale Lösung aussehen könnte und welche Dinge nur zweckdienlich sind. Der
andere wichtige Aspekt dieser Technik liegt darin, dass sie Sie wirklich zwingt, das
wirklich Wichtige des Systems herauszuarbeiten und zu kommunizieren.

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.

Welche Architektur hat 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.

235
Kapitel 17
Meine Anwendung hat keine Struktur

Hier ist eine Liste der Vereinfachungen:

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.

Zurück zu unserer Geschichte.

Ist das alles?

Nein. Tests können in Objekten zu Gruppen zusammengefasst werden, die als


Suites bezeichnet werden. Wir können eine Suite mit einem Testergebnis wie
einen einzigen Test ausführen. Alle Tests in einer Suite werden ausgeführt und
melden das Testergebnis, wenn sie scheitern.

Wie haben wir hier vereinfacht?

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.

Ist das alles?

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?

Eine Möglichkeit besteht darin, in die TestCase-Klasse eine Methode namens


buildUsageReport einzufügen, die jede Methode ausführt und dann einen
Bericht über alle Methoden erstellt, die keine assert-Methode aufrufen. Wäre
dies eine gute Methode, diese Funktion hinzuzufügen? Wie würde dies unsere
Geschichte ändern? Nun, wir würden damit eine weitere kleine »Lüge durch Aus-
lassung« in unsere kürzeste Beschreibung des Systems einfügen:

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.

17.2 Naked CRC


In den Anfangstagen der objektorientierten Programmierung kämpften viele Ent-
wickler mit dem Problem des Designs. Er ist schwer, objektorientiert denken zu
lernen, wenn man die meisten Programmiererfahrungen mit prozeduralen Spra-
chen gesammelt hat. Einfach ausgedrückt: Man denkt anders über Code nach. Als
jemand zum ersten Mal versuchte, mir objektorientiertes Design auf einem Blatt
Papier zu erklären, schaute ich mir all diese Figuren und Linien an und hörte die
Beschreibung; aber mir ging die Frage nicht aus dem Kopf, wo denn die mai n()-
Funktion wäre und wo sich der Einstiegspunkt für alle diese neuen Objekt-Dinge
befinden würde. Ich war eine kurze Zeit lang verwirrt, aber dann klickte es. Doch
ich war mit diesem Problem nicht allein. Es schien, als würden die meisten Ent-
wickler etwa zur selben Zeit mit denselben Problemen kämpfen. Doch es passiert
immer noch. Jeder Branchenneuling wird mit diesen Problemen konfrontiert,
wenn er zum ersten Mal objektorientierten Code sieht.

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.

Hier ist ein Beispiel für eine Beschreibung eines Online-Abstimmungssystems:

»Das Abstimmungssystem funktioniert in Echtzeit folgendermaßen. Hier ist eine


Client-Session« (zeigt auf die Karte).

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).

»Server-Sessions verfügen ebenfalls jeweils über zwei Verbindungen« (legt zwei


Karten, die die Verbindungen repräsentieren, auf die rechte Karte).

»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.

Bei Naked CRC müssen Sie nur zwei Richtlinien beachten:


1. Karten repräsentieren Instanzen, keine Klassen.
2. Collections werden durch überlappte Karten repräsentiert.

17.3 Conversation Scrutiny


Conversation Scrutiny heißt so viel wie Gesprächsbeobachtung. Bei Legacy Code
ist die Versuchung groß, möglichst keine Abstraktionen zu erstellen. Wenn ich
mir vier oder fünf Klassen anschaue, die jeweils über tausend Zeilen Code enthal-
ten, denke ich weniger daran, neue Klassen hinzuzufügen, sondern will herausfin-
den, was sich ändern muss. Weil wir so abgelenkt sind, wenn wir versuchen, diese
Dinge herauszufinden, übersehen wir aber oft Dinge, die uns zusätzliche Ideen
vermitteln könnten. Hier ist ein Beispiel.

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

walten, um sie zu implementieren. Als einer der anderen Programmierer anfing,


den Policy Code inline zu schreiben, sagte ich: »Halt; wir reden hier doch über
eine Locking Policy. Warum erstellen wir nicht eine Klasse namens LockingPo-
1 i cy, um die Counts dort zu verwalten? Wir können Methodennamen verwenden,
die wirklich ausdrücken, was wir tun wollen; und das wird klarer sein als Code, der
Counts in einem Array verändert.«

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.

In diesem Kapitel habe ich einige Techniken beschrieben, um die Architektur


umfangreicher vorhandener Systeme aufzudecken und zu kommunizieren. Viele
dieser Techniken eignen sich auch für das Design neuer Systeme. Design ist
Design, unabhängig davon, wann es im Entwicklungszyklus erfolgt. Einer der
schlimmsten Fehler, die ein Team machen kann, ist die Annahme, das Design sei
an einem bestimmten Punkt der Entwicklung vorbei. Wenn das Design »vorbei«
ist und Entwickler den Code immer noch ändern, ist die Gefahr groß, dass neuer
Code an ungeeigneten Stellen eingefügt wird und dass Klassen aufgebläht wer-
den, weil sich niemand zutraut, neue Abstraktionen einzuführen. Es gibt keine
sicherere Methode, ein Legacy-System zu verschlechtern.

242
Kapitel 18

Der Test-Code ist im Weg

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.

18.1 Konventionen für Klassennamen


Eine Ihrer wichtigsten Konventionen betrifft die Klassennamen. Im Allgemeinen
richten Sie für jede Ihrer Klassen wenigstens eine Unit-Test-Klasse ein. Der Name
der Testklasse wird sinnvollerweise vom Namen der Klasse abgeleitet. Dafür gibt
es mehrere Konventionen. Am häufigsten wird das Wort Test als Präfix oder Suf-
fix des Klassennamens verwendet. So erhält etwa die Testklasse einer Klasse
namens DBEngi ne den Namen TestDBEngi ne oder DBEngi neTest. Ist dies wich-
tig? Eigentlich nicht. Persönlich ziehe ich die Test-Suffix-Konvention vor, weil
dann Klassen und Testklassen in einer alphabetischen Liste, etwa in einer IDE,
hintereinander stehen und das Navigieren erleichtert wird.

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

Hier ein Beispiel aus einem kleinen Buchhaltungspaket:

• 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?

18.2 Der Speicherort für Tests


Bis jetzt habe ich in diesem Kapitel angenommen, dass Test- und Produktions-
Code im selben Verzeichnis gespeichert werden. Dies ist im Allgemeinen die ein-
fachste Struktur für ein Projekt; Sie müssen dabei allerdings einige wichtige
Dinge beachten.

Hauptsächlich müssen Sie prüfen, ob die Größe des Deployment-Codes Ihrer


Anwendung eingeschränkt ist. Bei einer Anwendung, die auf einem Server läuft,
der Ihrer Kontrolle untersteht, gibt es wahrscheinlich nicht viele Constraints
(Beschränkungen). Wenn für das Deployment der doppelte Speicherplatz (die
Binaries des Produktions-Codes und der Tests) zur Verfügung steht, ist es einfach
genug, den Code und die Tests in denselben Verzeichnissen zu speichern und alle
Binaries zu deployen.

Handelt es sich dagegen um ein kommerzielles Produkt, das auf Computern


anderer Anwender laufen soll, kann die Größe des Deployments ein Problem sein.
Sie können versuchen, Test- und Produktions-Code zu trennen, müssen dann aber
den Einfluss auf die Navigation in Ihrem Code berücksichtigen.

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.

Alternativ kann man Produktions- und Test-Code im selben Verzeichnis spei-


chern, aber den Test-Code mit Scripts oder Build-Einstellungen aus dem Deploy-
ment heraushalten. Mit guten Namenskonventionen für Ihre Klassen kann dies
ein brauchbarer Ansatz sein.

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

Mein Projekt ist nicht objektorien-


tiert. Wie kann ich es sicher ändern?

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.

Prozedurale Sprachen stellen in einer Legacy-Umgebung eine besondere Heraus-


forderung dar. Er ist wichtig, den Code unter Testkontrolle zu bringen, bevor Sie
ihn modifizieren; aber die Möglichkeiten, Unit-Tests in prozedurale Sprachen ein-
zuführen, sind recht begrenzt. Oft ist es am einfachsten, gründlich nachzuden-
ken, das System zu patchen und zu hoffen, dass Ihre Änderungen korrekt waren.
Dieses Test-Dilemma ist bei prozeduralem Legacy Code weit verbreitet. Prozedu-
rale Sprachen haben oft einfach nicht die Seams, auf die OO-Sprachen und viele
funktionale Programmiersprachen zurückgreifen können. Erfahrene Entwickler
können dieses Problem eindämmen, indem sie ihre Dependencies sorgfältig ver-
walten (so gibt es etwa sehr viel hervorragenden Code, der in C geschrieben ist);
aber es ist auch leicht möglich, den Code so zu verfilzen, dass man ihn nur noch
schwer inkrementell ändern und verifizieren kann.
Weil es so schwer ist, Dependencies in prozeduralem Code aufzuheben, besteht
die beste Strategie darin, zu versuchen, einen umfangreichen Teil des Codes unter
Testkontrolle zu bringen, bevor Sie etwas anderes tun, und dann mit diesen Tests
bei der weiteren Entwicklung Feedback zu bekommen. Die Techniken in Kapitel
12, Ich muss in einem Bereich vieles ändern. Muss ich die Dependenciesfiiralle beteilig-
ten Klassen aufheben?, können helfen. Sie lassen sich sowohl auf prozeduralen als
auch auf objektorientierten Code anwenden. Kurz gesagt: Es lohnt sich, nach
einem Pinch Point (12.1) zu suchen und dann Dependencies mit dem Link Seam
(4.3) so weit aufzuheben, dass der Code in einem Test-Harnisch eingeschlossen
werden kann. Wenn Ihre Sprache über einen Makro-Präprozessor verfügt, können
Sie auch Preprocessing Seam (4.2) verwenden.

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.

19.1 Ein einfacher Fall


Prozeduraler Code ist nicht immer ein Problem. Das folgende Beispiel zeigt eine
C-Funktion aus dem Betriebssystem Linux. Wäre es schwer, für diese Funktion
Tests zu schreiben, wenn wir sie ändern müssten?

void set_writetime(struct buffer_head * buf, int flag)


{
int newtime;

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.«

19.2 Ein schwieriger Fall


Hier ist eine C-Funktion, die wir ändern wollen. Es wäre schön, wenn wir sie vor-
her unter Testkontrolle bringen könnten:

#include "ksrlib.h"

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;
while(current) {
scan_result = loc_scan(current->body, flag);

248
19-2
Ein schwieriger Fall

if(scan_result & INVALID_PORT) {


ksr_notify(scan_result, current);
}

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:

void ksr_notify(int scan_code, struct rnode_packet *packet)


{
}

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.

In C gibt es eine andere Alternative. C verfügt über einen Makro-Präprozessor, mit


dem wir einfacher Tests für die scan_packets-Funktion schreiben können. Hier
ist die Datei für scan_packets, nachdem wir den Test-Code hinzugefügt haben:

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

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

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 = ...

int err = scan packets(&packet, DUP_SCAN);


assert(err & INVALID_P0RT);

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"

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

while(current) {

250
19-2
Ein schwieriger Fall

scan_resu"lt = loc_scan(current->body, flag);


if(scan_resu"lt & INVALID_PORT) {
ksr_notify(scan_result, current);
}

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 = .. .

int err = scan_packets(&packet, DUP_SCAN);


assertCerr & INVALID_PORT);
}

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;
}

Wir können sogar noch weiter gehen und Registrierungsfunktionen hinzufügen,


die das Gruppieren von Tests vereinfachen. Details finden Sie in den verschiede-
nen C-Unit-Test-Frameworks unterwww.xprogramming.com.

Obwohl sich Makro-Präprozessoren leicht missbrauchen lassen, sind sie hier


nützlich. Dateien einzubinden und Makros zu ersetzen, kann uns helfen, Depen-
dencies in dem widerspenstigsten Code aufzuheben. Solange wir den großzügi-
gen Einsatz von Makros auf Code beschränken, der unter Testkontrolle läuft,
müssen wir nicht zu besorgt sein, Makros so zu verwenden, dass der Produktions-
Code beeinflusst wird.

C gehört zu den wenigen Mainstream-Sprachen, die über einen Makro-Präprozes-


sor verfügen. Wollen wir Dependencies in anderen prozeduralen Sprachen aufhe-
ben, müssen wir im Allgemeinen Link Seam (4.3) benutzen und versuchen,
größere Code-Fragmente unter Testkontrolle zu bringen.

19.3 Neues Verhalten hinzufügen


Bei prozeduralem Legacy Code lohnt es sich, eher neue Funktionen einzuführen,
als Code in alte einzufügen. Wenigstens können wir für die neuen Funktionen
Tests schreiben.

Wie vermeiden wir es, Dependency-Fallen in prozeduralen Code einzufügen? Eine


Methode (siehe in Kapitel 8, Wie füge ich eine Funktion hinzu?) arbeitet mit Test-Dri-
ven Development (8.1) (TDD). TDD funktioniert sowohl bei objektorientiertem als
auch prozeduralem Code. Oft führt uns die Entwicklung der Tests für Code-Frag-
mente zu einem besseren Design. Wir schreiben einige Funktionen und integrie-
ren sie dann in den Rest der Anwendung.

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:

void send_command(int id, char *name, char *command_string) {


char -message, *header, *footer;
if (id == KEY_TRUM) {
message = ra11oc(sizeof(int) + HEADER_LEN + ...

} 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:

char *command = form_command(l, "Mike Ratledge", "56:78:cusp-:78");


assert(!strcmp("<-rsp-Mi ke Ratledge><56:78:cusp-:78><-rspr>", command));

Dann können wir eine form_command-Funktion schreiben, die eine Anweisung


zurückgibt:

char *form_command(int id, char -name, char *command_string)


{
char *message, *header;
if (id == KEY_TRUM) {
message = ralloc(sizeof (int) + HEADER_LEN + ...

} eise {
}

sprintf(message, "%s%s%s", header, command_string, footer);


return message;
}

Danach können wir die benötigte einfache send_command-Funktion schreiben:

void send_command(int id, char *name, char *command_string) {


char *command = form_command(id, name, command_string);
mart_key_send(command);

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:

void ca"lculate_1oan_interest(struct temper_loan *loan, int calc_type)


{

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.

Bei älteren Compilern müssen wir möglicherweise die alte Funktionszeiger-Syn-


tax verwenden:

extern struct database db;


(*db.update)(load->id, 1oan->record);

Aber bei anderen können wir diese Funktionen sehr natürlich in einem objektori-
entierten Stil aufrufen:

extern struct database db;


db.update(1oad->id, 1oan->record);

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.

19.4 Die Objektorientierung nutzen


In objektorientierten Sprachen verfügen wir über Object Seams (4.3). Sie haben
einige nützliche Eigenschaften:

• Sie sind in Code leicht zu erkennen.


• Mit ihnen kann Code in kleinere, verständlichere Teile zerlegt werden.
• Sie machen ein System flexibler. Seams, die Sie zum Testen einführen, können
nützlich sein, wenn Sie Ihre Software erweitern müssen.
Leider kann nicht alle Software in objektorientierte Form umgewandelt werden,
aber in einigen Fällen ist es erheblich einfacher als in anderen. Viele prozedurale
Sprachen wurden zu objektorientierten Sprachen weiterentwickelt. Microsoft
Visual Basic wurde erst vor Kurzem voll objektorientiert; es gibt OO-Erweiterun-
gen von COBOL und Fortran; und die meisten C-Compiler können C und C++
kompilieren.

In Sprachen, die Ihnen einen Übergang zur Objektorientierung ermöglichen,


haben Sie mehr Optionen. Der erste Schritt besteht normalerweise darin, Teile des
Codes mit Encapsulate Global References (25.4) unter Testkontrolle zu bringen.
Damit können wir Dependencies aufheben, die uns in der scan_packets-Funk-
tion weiter vorne in dem Kapitel behindert haben. Dort hatten wir ein Problem mit
der ksr_noti fy-Funktion, da wir bei Tests keine Benachrichtigungen versenden
wollten.

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_resu1t, err = 0;

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:

extern "C" void ksr_notify(int scan_result, struct rnode_packet *packet);

void ResultNotifier::ksr_notify(int scan_result, struct rnode_packet ^packet)


{
::ksr_notify(scan_result, packet);
}

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:

ResultNoti fi er globalResultNoti 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.

Hier ist die ursprüngliche Funktion:

#include "ksrlib.h"

int scan_packets(struct rnode_packet -packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

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"

extern ResultNotifier globalResultNotifier;

int scan_packets(struct rnode_packet *packet, int flag)


{
struct rnode_packet *current = packet;
int scan_result, err = 0;

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);

int scan_packets(struct rnode_packet *packet, int flag);


};
/ / i n der Quelldatei
Scanner::Scanner()
: noti fi er(globalResultNoti fi er)
{}

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.

19.5 Es ist alles objektorientiert


Einige prozedurale Programmierer lehnen Objektorientierung ab, weil sie den
Code ohne erkennbaren Nutzen komplexer mache. Doch genau betrachtet, sind
alle prozeduralen Programme objektorientiert. Es ist eine Schande, dass viele nur
ein Objekt enthalten. Um dies zu verstehen, stellen Sie sich ein Programm mit
über hundert Funktionen vor. Hier sind ihre Deklarationen:

int db_find(char *id, unsigned int mnemonic_id, struct db_rec **rec);

void process_run(struct gfh_task **tasks, int task_count);

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:

int db_find(char *id, unsigned int mnemonic_id, struct db_rec **rec);

258
19-5
Es ist alles objektorientiert

void process_run(struct gfh_task **tasks, int task_count);

};
Jetzt können wir zu jeder Funktionsdefinition gehen (hier ist eine):

int db_find(char *id, unsigned int mnemonic_id, struct db_rec: **rec);


{

und den Namen der Klasse vor den der Funktion setzen:

int program::db_find(char *id, unsigned int mnemonic_id, struct db_rec **rec)


{

Jetzt müssen wir eine neue mai n ( )-Funktion des Programms schreiben:

int main(int ac, char **av)


{
program the_program;

return the_program.main(ac, av);


}

Ä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.

Wenn prozedurale Sprachen über objektorientierte Erweiterungen verfügen, kön-


nen wir diese Richtung einschlagen. Wir arbeiten damit nicht grundlegend objekt-
orientiert, sondern benutzen Objekte nur, um ein Programm zum Testen zu
zerlegen.

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

Diese Klasse ist zu groß und soll


nicht noch größer werden

Viele Funktionen, die zu Systemen hinzugefügt werden, sind kleine Anpassun-


gen. Sie erfordern wenig zusätzlichen Code und vielleicht einige Methoden. Es ist
verlockend, diese Änderungen einfach in eine vorhandene Klasse einzufügen.
Wahrscheinlich muss der neue Code Daten einer solchen Klasse nutzen; und es
wäre am einfachsten, ihn in die Klasse einzufügen. Leider kann dies zu ernsten
Problemen führen. Durch den neuen Code werden die vorhandenen Klassen
immer umfangreicher und die Methoden immer länger. Unsere Software entwi-
ckelt sich zu einem Sumpf, und wir brauchen immer länger, um zu verstehen, wo
wir neue Funktionen hinzufügen können, oder selbst, wie die alten Funktionen
arbeiten.

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.

Das Single-Responsibility-Prinzip ist nicht leicht zu beschreiben, weil das Konzept


der Verantwortlichkeit/Zuständigkeit/Aufgabe nicht klar definiert ist. Naiv könn-
ten wir etwa meinen, jede Klasse solle nur eine einzige Methode enthalten. Nun,
Methoden können als Aufgaben betrachtet werden. Ein Task ist dafür verantwort-
lich, ihre run-Methode auszuführen und uns etwa mit einer taskCount-Methode
zu sagen, wie viele sie hat, usw. Doch was wir mit Aufgabe meinen, wird wirklich
klar, wenn wir über ihren Hauptzweck sprechen. Abbildung 20.1 zeigt ein Beispiel.

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)

Abb. 20.1: Rule Parser

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.

Zusammengefasst scheint RuleParser folgende Aufgaben zu haben:

• 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.

Abb. 20.2: Rule-Klassen mit getrennten Aufgaben

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.

20.1 Aufgaben erkennen


Das Rul eParser-Beispiel aus dem letzten Abschnitt zeigt eine bestimmte Zerle-
gung einer Klasse in kleinere Klassen. Ich habe die Klassen im Wesentlichen aus
dem Kopf zerlegt und erst alle Methoden aufgelistet und mir dann ihren Zweck
überlegt. Meine beiden Kernfragen waren: »Warum ist diese Methode hier?« und
»Was macht sie in der Klasse?« Dann fasste ich die Methoden mit verwandten
oder ähnlichen Aufgaben zu Gruppen zusammen.

264
20.1
Aufgaben erkennen

Dieses Vorgehen, Aufgaben zu erkennen, bezeichne ich als Methodengruppierung.


Es ist nur eine von vielen Methoden, Aufgaben in vorhandenem Code zu erken-
nen.
Aufgaben zu erkennen, ist eine Kernfähigkeit des Designs, die man sich durch
Übung aneignen muss. Es mag seltsam erscheinen, im Kontext der Arbeit mit
Legacy Code über eine Design-Fähigkeit zu sprechen, aber tatsächlich ist der
Unterschied zwischen dem Erkennen von Aufgaben in vorhandenem Code und
dem Konzipieren neuen Codes nur gering. In beiden Fällen müssen Sie im
Wesentlichen Aufgaben erkennen und sauber trennen lernen. Dabei bietet Legacy
Code bei Weitem mehr Möglichkeiten für die Anwendung von Design-Fähigkeiten
als neue Funktionen. Die Vor- und Nachteile eines Designs lassen sich leichter
abwägen, wenn man den betroffenen Code sehen kann; und es ist auch leichter zu
beurteilen, ob eine Struktur einem gegebenen Kontext angemessen ist, weil dieser
sich real direkt vor unseren Augen befindet.

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.

Heuristik #i: Gruppieren Sie Methoden.


Suchen Sie nach Methoden mit ähnlichen Namen. Schreiben Sie alle Methoden
einer Klasse sowie ihre Zugriffstypen (öffentlich, privat usw.) auf und versuchen
Sie, diejenigen zu finden, die verwandt zu sein scheinen.

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.

Heuristik #2: Suchen Sie nach verborgenen Methoden.


Achten Sie auf private und geschützte Methoden. Enthält eine Klasse viele derar-
tige Methoden, ist dies oft ein Zeichen dafür, dass sie eine andere Klasse enthält,
die von ihr getrennt werden sollte.

Große Klassen können zu viel verbergen. Unit-Test-Anfänger fragen immer wie-


der, wie sie private Methoden testen können. Sie verbringen viel Zeit damit, dieses
Problem zu umgehen. Aber wie ich in einem früheren Kapitel erwähnt habe, liegt
die eigentliche Antwort darin, dass die Methode nicht privat sein sollte, wenn Sie
sie unbedingt testen wollen. Wenn Sie Bedenken haben, die Methode öffentlich zu
machen, kann dies auch daran liegen, dass sie zu einer separaten Aufgabe gehört
und in einer anderen Klasse stehen sollte.

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

Abb. 20.3: RuleParser und TermTokeni zer

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.

Heuristik #4: Suchen Sie interne Beziehungen.


Suchen Sie Beziehungen zwischen Instanzvariablen und Methoden. Werden
bestimmte Instanzvariablen von einigen Methoden und nicht von anderen
benutzt?

Es gibt kaum Klassen, in denen alle Methoden alle Instanzvariablen benutzen.


Normalerweise gibt es in einer Klasse eine Art »Häufung«. Vielleicht verwenden
nur zwei oder drei Methoden einen Satz von drei Variablen. Oft können Sie dies an
den Namen erkennen. So enthält etwa die RulerParser-Klasse eine Collection
namens variables und eine Methode namens addVariable. Dies deutet auf
eine Beziehung zwischen dieser Methode und dieser Variablen hin. Wir erfahren
zwar nicht, ob andere Methoden ebenfalls auf diese Variable zugreifen, aber
wenigstens haben wir einen Ausgangspunkt für unsere Suche.

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;

public Reservation(Customer customer, int duration, int dailyRate, Date date) {


this.customer = customer;
this.duration = duration;
this.dailyRate = dailyRate;
this.date = date;
}

public void extend(int additionalDays) {


duration += additionalDays;
}

public void extendForWeek() {


int weekRemainder = RentalCalendar.weekRemainderFor(date);
final int DAYS_PER_WEEK = 7;
extend(weekRemainder);
dailyRate = RateCalculator.computeWeekly(customer.getRateCodeO) /
DAYS_PER_WEEK;
}

public void addFee(FeeRider rider) {


fees.add(rider);
}

int getAdditionalFees() {
int total = 0;
for(Iterator it = fees.iteratorO ; it.hasNextO; ) {
total += ((FeeRider) (i t.nextO)) .getAmount() ;
}
return total;
}

int getPrincipal Fee() {


return dailyRate * RateCalculator.rateBase(customer) * duration;
}

public int getTotalFee() {


return getPrincipalFee() + getAdditionalFeesO ;
}
}
Zunächst zeichnen Sie für jede Variable einen Kreis (siehe Abbildung 20.4).
Als Nächstes fügen wir für jede Methode einen Kreis hinzu. Dann ziehen wir von
jedem Methodenkreis eine Linie zu den Kreisen der Instanzvariablen und Metho-
den, die sie abruft oder modifiziert. Es ist normalerweise in Ordnung, die Kon-

268
20.1
Aufgaben erkennen

struktoren zu überspringen. Im Allgemeinen modifizieren sie jede


Instanzvariable.
Abbildung 20.5 zeigt das Diagramm, nachdem wir einen Kreis für die extend-
Methode hinzugefügt haben:

^uratio^

^ustome^ ^customer^

Abb. 20.4: Variablen in der Reservation-Klasse Abb. 20.5: extend verwendet


duration.

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.

Dies sind zwei verschiedene, vollständig legitime Arten, die Interaktionen in


einem System zu betrachten. Funktionsskizzen eignen sich hervorragend, um
die interne Struktur von Klassen darzustellen. Effektskizzen (11.1) eignen sich
hervorragend, um die Konsequenzen einer Änderung an einem Punkt zu ver-
folgen.

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

Abb. 20.S: Reservation bei Verwendung einer neuen Klasse

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 class Reservation


{

private FeeCalculator calculator = new FeeCalculatorO ;


private int getPrincipalFee() {
}

public Reservation(Customer customer, int duration, int dailyRate, Date


date) {
this.customer = customer; this.duration = duration; this.dailyRate =
dailyRate; this.date = date;
}

public void addFee(FeeRider fee) {


calculator.addFee(fee);
}

public getTotalFee() {
int baseFee = getPrincipalFeeO;
return calculator.getTotalFee(baseFee);
}
}

Abbildung 20.10 zeigt die entsprechende Struktur.

FeeCalculator
Reservation
+ addFee(FeeRider)
+ extend (days)
+ getTotalFeeQ
+ extendForWeekQ
- getAdditionalFeeQ
+ addFee(FeeRider) «delegates»
+ getTotalFee()
- getPrincipalFeeO

Abb. 20.10: Reservation mit Verwendung von FeeCalculator

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.

Funktionsskizzen eignen sich hervorragend dazu, separate Aufgaben in Belassen


zu finden. Wir können versuchen, die Funktionen zu gruppieren und herauszu-
finden, welche Klassen wir anhand der Namen extrahieren können. Zusätzlich
können wir mit diesen Skizzen auch die Dependency-Struktur erkennen; dies
kann oft genau so wichtig sein wie die Aufgabe, um zu entscheiden, was wir extra-
hieren sollten. In diesem Beispiel gab es zwei starke Gruppen von Variablen und
Methoden. Die einzige Verbindung zwischen ihnen ist der Aufruf von getPri n-
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden

ci pal Fee in getTotal Fee. In Funktionsskizzen erscheinen solche Verbindungen


als kleiner Satz von Linien zwischen größeren Gruppen. Ich bezeichne diese als
einen Pinch Point (12.1). Näheres darüber finden Sie in Kapitel 12, Ich muss in
einem Bereich vieles ändern. Muss ich die Dependencies für alle beteiligten Klassen auf-
heben?

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.

Heuristik #5: Suchen Sie die Hauptaufgabe.


Versuchen Sie, die Aufgabe der Klasse in einem einzigen Satz zu beschreiben.

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.

Es gibt zwei Arten, gegen das Single-Responsibility-Prinzip zu verstoßen: auf


Schnittstellenebene und auf Implementierungsebene. Das SRP wird auf Schnitt-
stellenebene verletzt, wenn eine Klasse eine Schnittstelle präsentiert, die zum Aus-
druck bringt, dass sie für zahlreiche Aufgaben zuständig ist. So lässt etwa die
Schnittstelle der folgenden Klasse (siehe Abbildung 20.11) schließen, dass sie in
drei oder vier Klassen zerlegt werden könnte.
Wichtiger ist jedoch ein SRP-Verstoß auf Implementierungsebene. Einfach ausge-
drückt: Wir wollen wissen, ob die Klasse wirklich alle in der Schnittstelle veröffent-
lichten Aufgaben selbst erledigt oder ob sie sie einfach an andere Klassen
delegiert. Denn bei einer Delegation haben wir es nicht mit einer umfangreichen
monolithischen Klasse, sondern einer so genannten Fassade zu tun, einem Front-
end für eine Reihe kleinerer Klassen, das deren Handhabung vereinfacht.

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

Abb. 20.11: Die ScheduledJob-Klasse

Abbildung 20.12 zeigt, wie die ScheduledJob-Klasse Aufgaben an einige andere


Klassen delegiert.

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

Abb. 20.12: ScheduledJob mit extrahierten Klassen

275
Kapitel 2 0
Diese Klasse ist zu groß und soll nicht noch größer werden

Das Single-Responsibility-Prinzip wird auf Schnittstellenebene zwar immer noch


verletzt, aber auf Implementierungsebene liegen die Dinge etwas besser.
Auf Schnittstellenebene ist das Problem etwas schwieriger zu lösen. Allgemein
prüfen wir, ob einige der Klassen, an die delegiert wird, auch direkt von Clients
benutzt werden können. Sind etwa nur einige Clients daran interessiert,
ScheduledJob-Objekte auszuführen, könnten wir den Code etwa wie folgt
refaktorisieren:

Abb. 20.13: Eine Client-spezifische Schnittstelle für Schedul edJob

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.

Interface-Segregation-Prinzip, ISP (Prinzip der Interface-Trennung)


Selten nutzen alle Clients alle Methoden einer umfangreichen Klasse. Je nach
Sachlage bevorzugen die Clients verschiedene Gruppen von Methoden. Wenn
wir für jede dieser Gruppen eine Schnittstelle erstellen und diese mit der
umfangreichen Klasse implementieren, sieht jeder Client die große Klasse
durch seine jeweilige Schnittstelle. So können wir Informationen verbergen und
Dependencies in dem System verringern. Die Clients müssen nicht jedes Mal
neu kompilieren, wenn die umfangreiche Klasse neu erstellt wird.

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).

Abb. 20.14: Die Schnittstelle von Schedul edDob herauslösen

Anstatt von Schedul edDob an JobControl 1 e r zu delegieren, delegiert jetzt Job-


Controller an ScheduledJob. Wenn jetzt ein Client einen ScheduledJob aus-
führen will, erstellt er einen JobControl 1 er, übergibt ihm einen ScheduledJob
und führt ihn mit dem JobControl 1er aus.

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.

Heuristik #7: Konzentrieren Sie sich auf die gegenwärtige Arbeit.


Achten Sie auf das, was Sie hier und jetzt tun müssen. Wenn Sie eine andere
Methode einführen, etwas zu tun, haben Sie möglicherweise eine Aufgabe iden-
tifiziert, die Sie extrahieren und dann ersetzbar machen sollten.

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.2 Andere Techniken


Die Heuristiken zur Identifizierung von Aufgaben können Ihnen wirklich helfen,
alte Klassen zu analysieren und neue Abstraktionen zu finden, aber sie sind nur
Tricks. Wer seine diesbezüglichen Fähigkeiten wirklich verbessern will, muss
mehr lesen: Bücher über Design Patterns und, noch wichtiger, den Code anderer
Programmierer. Studieren Sie Open-Source-Projekte und beobachten Sie, wie
andere Probleme gelöst haben. Achten Sie darauf, wie Klassen aufgerufen werden
und wie Klassen- und Methodennamen zusammenhängen. Im Laufe der Zeit wer-
den Sie verborgene Aufgaben besser identifizieren und auch in unbekanntem
Code schneller erkennen können.

20.3 Die nächsten Schritte


Nachdem Sie verschiedene Aufgaben in einer umfangreichen Belasse identifiziert
haben, müssen Sie sich nur noch mit zwei weiteren Problemen befassen: Strategie
und Taktik.

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.

Um diese Probleme zu lösen, verschieben wir die ursprünglichen Methoden über-


haupt nicht. Wir erstellen neue Methoden, indem wir die Bodies der alten extra-
hieren. Das Präfix ist nur ein mechanisches Verfahren, einen neuen Namen zu
generieren und vor dem Verschieben dafür zu sorgen, dass keine Namenskonf-
likte auftreten. Instanzvariablen sind etwas komplizierter: Wir suchen vorher
manuell nach Verwendungen der Variablen, bevor wir sie benutzen. Dabei kön-
nen Fehler passieren. Arbeiten Sie sehr sorgfältig, am besten mit einem Partner.

20.4 Nach dem Extrahieren von Klassen


Aus einer großen Klasse kleinere Klassen zu extrahieren, ist oft ein guter erster
Schritt. In der Praxis besteht die größte Gefahr für Teams darin, zu ehrgeizig vor-
zugehen. Vielleicht haben Sie Scratch Refactoring (16.3) angewendet oder ein ande-
res Konzept entwickelt, wie das System aussehen sollte. Doch vergessen Sie nicht,
dass Ihre Anwendung mit ihrer jetzigen Struktur funktioniert. Sie erfüllt die
gewünschten Funktionen und kann möglicherweise nur nicht optimal weiterent-
wickelt werden. Manchmal ist es dann am besten, ein neues Konzept zu entwi-
ckeln, wie eine umfangreiche Klasse nach dem Refactoring aussehen würde, und
es dann einfach zu vergessen. Sie haben entdeckt, was möglich ist. Um das Sys-
tem weiterzuentwickeln, müssen Sie Rücksicht auf das Vorhandene nehmen und
dieses nicht unbedingt auf das ideale Design hin, sondern wenigstens in eine bes-
sere Richtung lenken.

281
Kapitel 21

Ich ändere im ganzen System


denselben Code

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.

In einem kleinen Java-basierten Netzwerk-System müssen wir zwei Anweisungen


an einen Server senden: AddEmployeeCmd und LogonCommand. Wenn wir eine
Anweisung senden müssen, instanziieren wir ein Anweisungsobjekt und überge-
ben einen Output-Stream an seine wri te-Methode.

Hier sind die Listings der beiden Anweisungsklassen. Können Sie hier duplizier-
ten Code entdecken?

Die Klasse AddEmployeeCmd:

import java.io.OutputStream;

public class AddEmployeeCmd {


String name;
String address;
String city;
String State;
String yearlySalary;

283
Kapitel 21
Ich ändere im ganzen System denselben Code

private static final byte[] header = {(byte)Oxde, (byte)Oxad};


private static final byte[] commandChar = {0x02};
private static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENCTH = 1;

private int getSizeO {


return header.length +
SIZE_LENGTH +
CMD_BYTE_LENGTH +
footer.length +
name.getBytes().length + 1 +
address.getBytes().length + 1 +
ci ty. getBytesO . length + 1 +
State.getBytesO.length + 1 +
yearlySalary.getBytesO.length + 1;

public AddEmployeeCmd(String name, String address, String city, String


State, int yearlySalary) {
this.name = name;
this.address = address;
this.city = city;
this.State = S t a t e ;
this.yearlySalary = Integer. toStri ng(yearlySalary) ;

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSizeO);
outputStream.write(commandChar);
outputStream.write(name.getBytesO) ;
outputStream.write(OxOO) ;
outputStream.write(address .getBytesO);
outputStream.write(OxOO);
outputStream.wri te (city. getBytesO) ;
outputStream.write(OxOO);
outputStream.wri te (state. getBytesO) ;
outputStream.write(OxOO);
outputStream.write(yearlySalary.getBytesO) ;
outputStream.write(OxOO);
outputStream.write(footer);
}

Die Klasse Logi nCommand:

import java.io.OutputStream;

public class Logi nCommand {


private String userName;

284
Ich ändere im ganzen System denselben Code

private String passwd;


private static final byte[] header = {(byte)Oxde, (byte)Oxad};
private static final byte[] commandChar = {0x01};
private static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENCTH = 1;

public LoginCommand(String userName, String passwd) {


this.userName = userName;
this.passwd = passwd;
}
private int getSizeO {
return header.length +
SIZE_LENGTH +
CMD_BYTE_LENGTH +
footer.length +
userName.getBytesO.length + 1 +
passwd.getBytesO.length + 1;

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSizeO) ;
outputStream.write(commandChar);
outputStream.write(userName.getBytesO)I
outputStream.write(OxOO);
outputStream.write(passwd.getBytesO);
outputStream.write(OxOO);
outputStream.write(footer);
}

Abbildung 21.i zeigt die Klassen in UML.

AddEmployeeCmd LoginCommand

name : String - userName : String


address : String - passwd : String
city : String - h e a d e r : byte[]
stat: String - commandChar : byte []
yearlySalary : String - footer : byte []
- h e a d e r : byte [] - S I Z E L E N G T H : int
- c o m m a n d C h a r : byte [] • C M D B Y T E L E N G T H : int
- footer: byte []
- S I Z E _ L E N G T H : int
- C M D 3 Y T E „ L E N G T H : int

- getSize() : int - getSize() : int


+ AddEmployeeCmd(...) + LoginCommand(...)
+ write(OutputStream) + write(OutputStream)

Abb. 21.1: AddEmployeeCmd und 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.

Versuchen wir, Fragmente mit dupliziertem Code zu identifizieren und zu entfer-


nen, um zu sehen, wohin uns das führt. Dann können wir entscheiden, ob diese
Maßnahme wirklich hilfreich war.
Zunächst brauchen wir Tests, die wir nach jedem Refactoring ausführen. Glückli-
cherweise haben wir sie bereits. Ich verzichte hier der Kürze halber auf die
Beschreibung, aber denken Sie daran, dass die Tests vorhanden sind.

2i.i Erste Schritte


Wenn ich mit dupliziertem Code konfrontiert werde, besteht meine erste Reaktion
darin, einen Schritt zurückzugehen, um ein Gefühl für seinen Umfang zu bekom-
men. Ich überlege, welche Art von Klassen ich bilden könnte und wie die extra-
hierten Fragmente des duplizierten Codes aussehen könnten. Dann erkenne ich,
dass ich damit eigentlich ins Grübeln gerate. Kleine Teile von dupliziertem Code
zu entfernen, ist hilfreich. Es macht es einfacher, später größere Fragmente von
dupliziertem Code zu erkennen. So enthält etwa die write-Methode von Login-
Command den folgenden Code:

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.

void writeField(OutputStream outputStream, String field) {


outputStream.wri te (fiel d. getBytesO) ;
outputStream.write(0x00);
}

rien Anfang finden


Wenn wir eine Reihe von Refactorings durchführen, um duplizierten Code zu
entfernen, können wir je nach Ausgangspunkt zu verschiedenen Strukturen
kommen. So könnten wir etwa die Methode
void c O { a(); a(); b(); a(); b(); b(); }

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:

public void write(OutputStream outputStream)


throws Exception {
outputStream. writeCheader);
outputStream.write(getSizeO);
outputStream.write(commandChar);
writeField(outputstream, username);
writeField(outputStream, passwd);
outputStream.write(footer);
}

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).

Jetzt können wir in AddEmployeeCmd die Ausgaben der String/Null-Sequenzen


durch Aufrufe von writeField ersetzen. Danach sieht die wri te-Methode von
AddEmpl oyeeCmd wie folgt aus:

287
Kapitel 21
Ich ändere im ganzen System denselben Code

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);
writeField(outputStream, name);
writeField(outputStream, address);
writeField(outputStream, city);
writeField(outputStream, State);
writeField(outputStream, yearlySalary);
outputStream.write(footer);

Abb. 21.2: Command-Hierarchie

Die wri te-Methode für Logi nCommand sieht wie folgt aus:

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSizeO);
outputStream.write(commandChar);
writeField(outputstream, userName);
writeField(outputStream, passwd);
outputStream.write(footer);
}

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:

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.write(getSize());
outputStream.write(commandChar);

288
21.1
Erste Schritte

writeBody(outputStream);
outputStream.write(footer);
}

Hier ist der extrahierte wri teBody:

private void writeBody(OutputStream outputStream)


throws Exception {
writeField(outputstream, userName);
writeField(outputStream, passwd);
}

AddEmpl oyeeCmd hat dieselbe wri te-, aber eine andere wri teBody-Methode:

private void writeBody(OutputStream outputStream)


throws Exception {
writeField(outputStream, name);
writeField(outputStream, address);
writeField(outputStream, city);
writeField(outputStream, State);
writeField(outputStream, yearlySalary);
}

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:

public class AddEmployeeCmd extends Command {


String name;
String address;
String city;
String State;
String yearlySalary;

private static final byte[] header = {(byte)Oxde, (byte)Oxad};


private static final byte[] commandChar = {0x02};
private static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;

289
Kapitel 21
Ich ändere im ganzen System denselben Code

public class LoginCommand extends Command {


private String userName;
private String passwd;

private static final byte[] header = {(byte)Oxde, (byte)Oxad};


private static final byte[] commandChar = {0x01};
private static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;

Beide K l a s s e n enthalten m e h r e r e identische Daten. Wir k ö n n e n header, footer,


SIZE_LENGTH u n d C M D _ B Y T E _ L E N G T H nach oben in die Command-Klasse verschie-
ben, weil sie dieselben Werte haben. Ich deklariere sie t e m p o r ä r als protected,
damit w i r den Code k o m p i l i e r e n u n d testen k ö n n e n :

public class Command {


protected static final byte[] header = {(byte)Oxde, (byte)Oxad};
protected static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
protected static final int SIZE_LENGTH = 1;
protected static final int CMD_BYTE_LENGTH = 1;

Jetzt enthalten beide U n t e r k l a s s e n n u r noch die commandChar-Variable m i t jeweils


unterschiedlichen Werten. Wir k ö n n e n diese m i t e i n e m abstrakten Getter in der
Command-Klasse abrufen:

public class Command {


protected static final byte[] header = {(byte)Oxde, (byte)Oxad};
protected static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
protected static final int SIZE_LENGTH = 1;
protected static final int CMD_BYTE_LENGTH = 1;

protected abstract char • getCommandCharO;

Jetzt k ö n n e n wir die commandChar-Variablen in den U n t e r k l a s s e n ersetzen, i n d e m


wir g e t C o m m a n d C h a r überschreiben:

public class AddEmployeeCmd extends Command {


protected char [] getCommandCharO {
return new char [] { 0x02};
}

public class LoginCommand extends Command {


21.1
Erste Schritte

protected char [] getCommandChar() {


return new char [] { 0x01};
}

Jetzt ist es sicher, die wri te-Methode in die Oberklasse zu verschieben. Danach
sieht die Command-Klasse wie folgt aus:

public class Command {


protected static final byte[] header = {(byte)Oxde, (byte)Oxad};
protected static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
protected static final int SIZE_LENGTH = 1;
protected static final int CMD_BYTE_LENCTH = 1;

protected abstract char [] getCommandCharO;

protected abstract void writeBody(OutputStream outputStream);

protected void writeField(OutputStream outputStream, String field) {


outputStream.wri te(fi eld.getBytes());
outputStream.write(OxOO);
}

public void write(OutputStream outputStream)


throws Exception {
outputStream.write(header);
outputStream.wri te(getSi ze());
outputStream.wri te(commandChar);
writeBody(outputstream);
outputStream.write(footer);
}
}

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

N a c h d e m w i r die wri te-Methode i n die Oberklasse v e r s c h o b e n h a b e n , enthalten


die U n t e r k l a s s e n n u r noch die Methoden getSi ze u n d g e t C o m m a n d C h a r sowie
die Konstruktoren. Hier n o c h e i n m a l die Logi nCommand-Klasse:

public class LoginCommand extends Command {


private String userName;
private String passwd;

public LoginCommandCString userName, String passwd) {


this.userName = userName;
this.passwd = passwd;
}

protected char [] getCommandCharO {


return new char [] { 0x01};
}

protected int getSizeO {


return header.length + SIZE_LENGTH + CMD_BYTE_LENGTH +
footer.length + userName.getBytesO.length + 1 +
passwd.getBytesO.length + 1;
}
}

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:

Hier ist die getSi ze-Methode von Logi nCommand:

protected int getSizeO {


return header.length + SIZE_LENGTH +
CMD_BYTE_L ENGTH + footer.1ength +
userName.getBytesO.length + 1 +
passwd.getBytesO.length + 1;

U n d hier ist die getSi ze-Methode von AddEmpl oyeeCmd:

private int getSizeO {


return header.length + SIZE_LENGTH +
CMD_BYTE_LENGTH + footer.1ength +
name.getBytesO.length + 1 +
address.getBytesO.length + 1 +
city.getBytesO .length + 1 +
State.getBytesO.length + 1 +
yearlySalary.getBytesO.length + 1;
}

Was ist gleich u n d w a s ist verschieden? A n s c h e i n e n d addieren beide hea-


der. length, SIZE_LENGTH, C M D _ B Y T E _ L E N G T H u n d footer. 1 ength. D a n n addie-

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:

private int getSizeO {


return header.length + SIZE_LENGTH +
CMD_BYTE_LENGTH + footer.length + getBodySize() ;
}

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).

Abb. 21.4: getSize in der Oberklasse

Wo stehen wir jetzt? Die Methode getBodySi ze ist in AddEmployeeCmd wie folgt
implementiert:

protected int getBodySizeO {


return name.getBytesO.length + 1 +
address.getBytesO.length + 1 +
city .getBytesO .length + 1 +
state.getBytesO.length + 1 +
yearlySalary.getBytesO.length + 1;
}

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

protected int getFieldSize(String field) {


return field.getBytesO.length + 1;
}

protected int getBodySizeO {


return getFieldSize(name) +
getFieldSize(address) +
getFieldSize(city) +
getFieldSize(state) +
getFieldSize(yearlySalary);
}

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:

protected int getBodySizeO {


return getFieldSize(name) + getFieldSize(password) ;
}

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:

class LoginCommand extends Command


{

public LoginCommand(String name, String password) {


fields.add(name);
fi elds.add(password);
}

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;
}

Ähnlich kann die wri teBody-Methode wie folgt aussehen:

294
21.1
Erste Schritte

void writeBody(Outputstream outputStream) {


for(Iterator it = fields.iteratorO; it.hasNextO; ) {
String field = (String)it.nextO ;
writeField(outputStream, field);
}
}
Wir können diese Methoden in die Oberklasse verschieben. Danach haben wir
wirklich den gesamten duplizierten Code entfernt. Die Command-Klasse sieht jetzt
folgendermaßen aus. Alle Methoden, auf die in den Unterklassen nicht mehr
zugegriffen wird, wurden als pri v a t e deklariert:

public class Command {


private static final byte[] header = {(byte)Oxde, (byte)Oxad);
private static final byte[] footer = {(byte)Oxbe, (byte)Oxef};
private static final int SIZE_LENGTH = 1;
private static final int CMD_BYTE_LENGTH = 1;

protected List fields = new ArrayListO;


protected abstract char [] getCommandChar();

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;
}

private int getBodySizeO {


int result = 0;
for(Iterator it = fields.iteratorO; it.hasNextO; ) {
String field = (String)it.nextf) ;
result += getFieldSize(field);
}
return result;
}

private int getSizeO {


return header.length + SIZE_LENGTH
+ CMD_BYTE_L ENGTH + footer.1ength
+ getBodySizeO;
}
private void writeField(OutputStream outputStream, String field) {
outputStream.wri te(fi eld.getBytes());
outputStream.write(0x00);
}
public void write(OutputStream outputStream)

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:

public class LoginCommand extends Command {


public LoginCommand(String userName, String passwd) {
fields.add(username);
fields.add(passwd);
}

protected char [] getCommandCharO {


return new char [] { 0x01};
}
}

public class AddEmployeeCmd extends Command {


public AddEmpl oyeeCmd(String name, String address, String city, String
State, int yearlySalary) {
fields.add(name);
fields.add(address);
fields.add(city) ;
fields.add(state);
fi elds.add(Integer.toStri ng(yearlySalary));
}

protected char [] getCommandCharO {


return new char [] { 0x02 };
}
}

Abbildung 21.5 zeigt das Ergebnis als UML-Diagramm.

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:

List arguments = new ArrayListO;


arguments.add("Mi ke");
arguments.add("asdsad");
Command.send(stream, 0x01, arguments);

296
21.1
Erste Schritte

Abb. 21.5: Command-Hierarchie mit dem duplizierten Code in der Oberklasse

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:

Command.SendAddEmployee(stream, "Mike", "122 Elm St", "Miami", "FL",


10000);
Command.SendLogin(stream, "Mike", "asdsad");

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:

public class AggregateCommand extends Command


{
private List commands = new ArrayListO;
protected char [] getCommandCharO {
return new char [] { 0x03 };
}

public void appendCommand(Command newCommand) {


commands.add(newCommand);
}

protected void writeBody(OutputStream out) {


out .wri te (commands. getSizeO) ;

298
21.1
Erste Schritte

for(Iterator it = commands.iterator() ; it.hasNext() ; ) {


Command innerCommand = (Command)it.next() ;
innerCommand.write(out);
}
}
}

Alles andere funktioniert wie gehabt.

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.

Orthogonalität ist (hier) ein beeindruckendes Synonym für Unabhängigkeit. Ein


System ist orthogonal, wenn Sie vorhandenes Verhalten in Ihrem Code ändern
wollen und es genau eine Stelle gibt, an der Sie diese Änderung vornehmen müs-
sen. Ihre Anwendung ist dann mit einem großen Kasten vergleichbar, der von
außen viele Knöpfe hat. Wenn es für jedes Verhalten Ihres Systems nur einen
Knopf gibt, können Sie leicht Änderungen vornehmen. Befindet sich jedoch an
vielen Stellen duplizierter Code, gibt es für jedes Verhalten mehr als einen Knopf.
Betrachten Sie etwa das Schreiben der Felder. Müssten wir in dem ursprünglichen
Design für Felder etwa den Terminator 0 x 0 1 anstelle des Terminators 0x00 ver-
wenden, müssten wir den Code an vielen Stellen ändern. Stellen Sie sich vor, Sie
würden aufgefordert, jedes Feld mit zwei OxOO-Terminatoren auszugeben. Auch
Knöpfe mit mehr als einer Funktion wären ziemlich nachteilig. Aber in dem refak-
torisierten Code können wir wri t e F i el d editieren oder überschreiben, wenn wir
die Ausgabe der Felder ändern wollen; und für Sonderfälle, wie etwa aggregierte
Anweisungen, können wir writeBody überschreiben. Verhalten, das in einzelne
Methoden eingekapselt und lokalisiert ist, kann leicht modifiziert, ersetzt oder
erweitert werden.

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.

Wenn Sie anfangen, gewissenhaft duplizierten Code zu entfernen, machen Sie


eine überraschende Entdeckung: Es kristallisieren sich Designs heraus. Die meis-
ten Knöpfe Ihrer Anwendung müssen nicht geplant werden; sie treten einfach

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

public void write(OutputStream outputStream)


throws Exception {
outputStream.writeCheader);
outputStream.write(getSize());
outputStream.write(commandChar);
writeBody(outputstream);
outputStream.write(footer);
}

folgendermaßen aussähe:

public void write(OutputStream outputStream)


throws Exception {
writeHeader(outputStream);
writeBody(outputstream);
writeFooter(outputStream);
}

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

Ich muss eine Monster-Methode


ändern und kann keine Tests dafür
schreiben

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.

22.1 Spielarten von Monstern


Es gibt mehrere Varianten von Monster-Methoden. Es handelt sich nicht unbe-
dingt um verschiedene Typen. Die Methoden in der Praxis ähneln Schnabeltieren
- sie sehen aus wie Mixturen mehrerer Typen.

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.

void Reservation::extend(int additionalDays)


{
int status = RIXInterface::checkAvailable(type, location, startingDate);

int identCookie = -1;


switch(status) {
case NOT_AVAILABLE_UPGRADE_LUXURY:
identCookie = RIXInterface::holdReservation(Luxury,
1ocati on,starti ngDate,
additionalDays +additionalDays);
break;
case NOT_AVAILABLE_UPGRADE_SUV:
{
int theDays = additionalDays + additionalDays;
if (RIXInterface: :getOpCode(customerID) 1=0)
theDays++;
identCookie = RIXInterface::holdReservation(SUV,location,
starti ngDate, theDays);
}
break;
case NOT_JAVAILABL£_UPGRADE_VAN:
identCookie = RIXInterface::holdReservation(Van,
location,startingDate, additionalDays + additionalDays);
break;
case AVAILABLE:
default:
RIXInterface::holdReservati on(type,1ocati on,starti ngDate);
break;
}
if (identCookie != -1 && State == Initial) {
RIXInterface::wai tli stReservati on(type,1ocati on,starti ngDate);
}
Customer c = res_db.getCustomer(customerID) ;

if (c.vipProgramStatus == VIP_DIAM0ND) {
upgradeQuery = true;
}
if (!upgradeQuery)
RIXInterface::extend(lastCookie, days + additionalDays);
eise {

302
22.1
Spielarten von Monstern

RIXInterface::wai tli stReservati on(type,1ocati on,starti ngDate);


RIXInterface::extend(lastCookie, days + additionalDays +1);
}

}
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.

22.1.2 Tief verschachtelte Methoden


Eine tief verschachtelte Methode (engl, snarled method) ist eine Methode, die von
einem einzigen umfangreichen eingerückten Abschnitt dominiert wird. Der ein-
fachste Fall ist eine Methode mit einer umfangreichen bedingten Anweisung
(siehe Listing 22.2).

Reservation::Reservation(VehicleType type, int customerlD, long


startingDate, int days, XLocation 1)
: type(type), customerlD(customerlD), startingDate(startingDate) ,
days(days), lastCookie(-l),
State(Initial), tempTotal(0)
{
1ocation = 1;
upgradeQuery = false;

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;
}
}

Listing 22.2: Einfache tief verschachtelte Methode

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.

Reservation::Reservation(VehicleType type, int customerlD, long


startingDate, int days, XLocation 1)
: type(type), customerlD(customerlD), startingDate(startingDate),
days(days), lastCookie(-l),
State(Initial), tempTotal(0)
{
location = 1;
upgradeQuery = false;

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

Um zu erkennen, ob eine Methode wirklich tief verschachtelt ist, versuchen Sie


am besten, die Blöcke aufzureihen. Wenn Ihnen schwindlig wird, haben Sie es mit
einer tief verschachtelten Methode zu tun.
Die meisten Methoden kombinieren Aufzählungen mit einer tiefen Einschachte-
lung. Viele tief verschachtelte Methoden enthalten lange Aufzählungsabschnitte;
aber weil diese tief verschachtelt sind, ist es schwer, Tests zu schreiben, die ihr Ver-
halten fixieren. Tief verschachtelte Methoden stellen Sie vor einzigartige Heraus-
forderungen.

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

22.2 Monster mit automatischer Refactoring-Unterstützung


zähmen
Wenn Sie über ein Tool verfügen, das Methoden für Sie extrahiert, müssen Sie wis-
sen, was es für Sie tun kann und was nicht. Die meisten heutigen Refactoring-
Tools können einfache Methoden extrahieren und verschiedene andere Refacto-
rings ausführen; aber sie erledigen nicht alle Hilfs-Refactorings, die man beim
Zerlegen umfangreicher Methoden vornehmen möchte. Beispielsweise sind wir
oft versucht, die Reihenfolge von Anweisungen zu ändern, um sie für das Extra-
hieren zu gruppieren. Ich kenne kein Tool, mit dem ich vorher prüfen kann, ob die
Reihenfolge sicher geändert werden kann. Dies ist eine Schande, weil eine solche
Maßnahme eine Fehlerquelle sein kann.

Wenn Sie umfangreiche Methoden wirksam mit Refactoring-Tools bearbeiten wol-


len, zahlt es sich aus, eine Reihe von Änderungen nur mit dem Tool auszuführen
und auf alle anderen Bearbeitungen des Quellcodes zu verzichten. So können Sie
Änderungen, die nachweislich sicher sind, sauber von Änderungen trennen, die
dies nicht sind. Wenn Sie Ihren Code so refaktorisieren, sollten Sie sogar einfache
Änderungen wie etwa die Umstellung von Anweisungen oder die Zerlegung von
Ausdrücken vermeiden. Es ist schön, wenn Ihr Tool die Umbenennung von Vari-
ablen unterstützt; doch andernfalls sollten Sie dies auf später verschieben.

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.

Wenn Sie Code extrahieren, sollten Sie zwei Hauptziele verfolgen:


x. Die Logik von störenden Dependencies isolieren
2. Seams einführen, die das Einrichten von Tests für weiteres Refactoring erleich-
tern
Hier ist ein Beispiel:

class CommoditySelectionPanel
{

public void update() {


if (commodities.sizeO > 0 && commodities.CetSourceO .equals("local")) {
listbox.clear();
for (Iterator it = commoditi es. iteratorO; it.hasNextO; ) {
Commodity commodity = (Commodity)it.nextO;
if (commodity.isTwilightO && !commodity.match(broker))
1i stbox.add(commodi ty.getVi ew());
}

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.

Mit Tool-basierter Refactoring-Unterstützung können wir anfangen, High-Level-


Fragmente der Methode umzubenennen und gleichzeitig Dependencies aufzuge-
ben. Nach einer Reihe von Extraktionen sähe der Code wie folgt aus.

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") ;
}

private void clearDisplayO {


listbox.clearO;
}

private void updateCommoditiesO {


for (Iterator it = commodities.iterator(); it.hasNext(); ) {
Commodity commodity = (Commodity)it.nextO;
if (singleBrokerCommodity(commodity)) {
di splayCommodi ty(commodity.getVi ew());
}
}
}

private boolean singleBrokerCommodity(Commodity commodity) {


return commodi ty.isTwilightO && ! commodi ty. match (broker) ;
}

private void displ ayCommodity(CommodityView view) {


1i stbox.add(vi ew);
}

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)

Abb. 22.1: Commodi t y S e l e c t i onPanel mit extrahierter Logik-Klasse

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.

22.3 Die Herausforderung des manuellen Refactorings


Wenn Sie den Code automatisch refaktorisieren können, müssen Sie nichts
Besonderes tun, um umfangreiche Methoden zu zerlegen. Gute Tools prüfen
jedes geplante Refactoring und verhindern unsichere Refactorings. Doch ohne
Refactoring-Tool müssen Sie sich mit Ihren Tests selbst um die Korrektheit küm-
mern.

308
22.3
Die Herausforderung des manuellen Refactorings

Monster-Methoden machen es erheblich schwerer, Code zu testen, zu refaktorisie-


ren oder zu erweitern. Wenn Sie in einem Test-Harnisch Instanzen der Klasse
erstellen können, die die Methode enthält, können Sie versuchen, einen Satz von
Testfällen zu entwickeln, die Ihnen Sicherheit geben, wenn Sie die Methode zerle-
gen. Wenn die Logik der Methode besonders komplex ist, kann dies ein Albtraum
sein. Glücklicherweise gibt es dafür einige Techniken. Bevor ich sie beschreibe,
möchte ich erläutern, was schiefgehen kann, wenn wir Methoden extrahieren.

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.

22.3.1 »Introduce Sensing Variable«


Vielleicht wollen wir beim Refactoring keine Funktionen zu dem Produktions-
Code hinzufügen, aber das bedeutet nicht, dass wir gar keinen Code hinzufügen
können. Manchmal hilft es, eine Variable in eine Klasse einzufügen und mit
bestimmten Bedingungen in der Methode zu überwachen, die wir refaktorisieren
wollen. Nach dem Refactoring können wir diese Variable wieder entfernen und
unseren Code in einem sauberen Zustand hinterlassen. Dieses Verfahren heißt
Introduce Sensing Variable (Überwachungsvariable einführen). Hier ist ein Bei-
spiel. Wir beginnen mit einer Methode einer Java-Klasse namens DOMBui 1 der. Wir
wollen sie säubern, aber leider haben wir kein Refactoring-Tool:

public class DOMBui1 der


{

void processNode(XDOMNSnippet root, List childNodes)


{
if (root != null) {
if (childNodes != null)
root. addNode(new XDOMNSnippet(childNodes)) ;
root.addChi1d(XDOMNSni ppet.Nu!1Sni ppet);
}

309
Kapitel 22
Ich muss eine Monster-Methode ändern und kann keine Tests dafür schreiben

List paraList = new A r r a y L i s t O ;


XDOMNSnippet snippet = new XDOMNReSni ppet();
snippet.setSource(m_state);
for (Iterator it = childNodes.iteratorO; it.hasNextO;) {
XDOMNNode node = (XDOMNNode) i t . n e x t O ;
if (node.typeO == TF_C || node.type() == TF_H ||
(node.typeC) == TF_CL0T && node.isChildü)) {
paraList.addNode(node);
}
}
}
}
In diesem Beispiel scheint in der Methode ein XDOMNSnippet intensiv bearbeitet
zu werden. Dies bedeutet, dass wir die erforderlichen Tests schreiben können soll-
ten, indem wir verschiedene Werte als Argumente an diese Methode übergeben.
Aber tatsächlich passieren viele Dinge am Rande und können nur indirekt beob-
achtet werden. In einer solchen Situation können wir unsere Arbeit mit zusätz-
lichen Überwachungsvariablen unterstützen; so könnten wir etwa eine
Instanzvariable einfügen, um zu prüfen, ob ein Node (Knoten) in die paraList
eingefügt wird, wenn er den geeigneten Node-Typ hat.

public class DOMBuilder


{
public boolean nodeAdded = false;

void processNode(XDOMNSnippet root, List childNodes)


{
if (root != null) {
if (childNodes != null)
root.addNode(new XDOMNSnippet(childNodes));
root.addChi1d(XDOMNSnippet.NullSni ppet);
}
L ist paraList = new A r r a y L i s t O ;
XDOMNSnippet snippet = new XDOMNReSnippet() ;
sni ppet. setSource(m_state);
for (Iterator it = childNodes.iterator(); it.hasNextO; ) {
XDOMNNode node = (XDOMNNode)it.next();
if (node.typeO == TF_G | | node.typeO == TF_H | |
(node.typeO == TF_GLOT && node.isChildO)) {
paraList.add(node);
nodeAdded = true;
}
}
}
}

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:

voi d testNoAddNodeOnNonBasi cChi1d()


{
DOMBuilder builder = new DomBuilderO;
List children = new A r r a y L i s t O ;
children.addCnew XDOMNNode(XDOMNNode.TF_A));
builder.processNode(new XDOMNSnippetO, children);
assertTrue(!builder.nodeAdded);
}

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.

public class DOMBuilder


{
void processNode(XDOMNSnippet root, List childNodes) {
if (root != null) {
if (childNodes != null)
root.addNode(new XDOMNSnippet(childNodes)) ;
root.addChi1d(XDOMNSni ppet.Nul1Sni ppet);
}
List paraList = new ArrayListO;
XOMNSnippet snippet = new XDOMNReSnippet() ;
snippet.setSource(m_state);
for (Iterator it = childNodes,iteratorO; it.hasNextO;) {
XDOMNNode node = (XDOMNNode)it.next() ;
if (isBasicChild(node)) {
paraLi st.addNode(node);
nodeAdded = true;

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.

Überwachungsvariablen sind ein Schlüsselinstrument, um Monster-Methoden zu


zerlegen. Sie können damit tief verschachtelte Methoden refaktorisieren oder auch
ihre Verschachtelung schrittweise aufheben. Wenn etwa der größte Teil des Codes
einer Methode tief in einen Satz bedingter Anweisungen eingebettet ist, können
wir mit Überwachungsvariablen entweder die obersten Bedingungsanweisungen
oder die Bodies dieser Bedingungsanweisungen in neue Methoden extrahieren.
Wir können mit den Überwachungsvariablen diese neuen Methoden bearbeiten,
bis wir die Verschachtelung des Codes beseitigt haben.

22.3.2 »Extract What You Know«


Extract what you know (Extrahieren, was man kennt) ist eine weitere Strategie für
die Arbeit mit Monster-Methoden besteht darin, klein anzufangen und kleine Teile
von Code zu suchen, die wir auch ohne Tests extrahieren und dann mit Tests ab-

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:

void processCint a, int b, int c) {


int maximum;
if (a > b)
maximum = a;
eise
maximum = b;

Hier ist der Code nach dem Extrahieren:

void processCint a, int b, int c) {


int maximum = maxCa.b);

}
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:

void addEntryCEntry entry) {


if (view != null && DISPLAY == true) {
view.show(entry);
}
if (entry. categoryO .equalsC'single") || entry. categoryC'dual ")) {
entries.add(entry);

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.

In gewisser Weise wirkt Gleaning Dependencies wie Drückebergerei. Sie bewahren


einen Satz von Verhaltensweisen und arbeiten mit einem anderen in einer unge-
schützten Methode. Aber in einer Anwendung sind nicht alle Verhaltensweisen
gleich. Beim Arbeiten erkennen wir, dass einige wichtiger sind.
Gleaning Dependencies ist besonders leistungsstark, wenn das kritische Verhalten
mit anderem Verhalten vermischt ist. Wenn Sie solide Tests für das kritische Ver-
halten eingerichtet haben, können Sie zahlreiche Änderungen vornehmen, die
technisch nicht alle durch Tests abgedeckt sind, Ihnen aber helfen, Schlüsselver-
halten zu bewahren.

22.3.4 »Break O u t Method O b j e c t «


Sensing Variables ist ein sehr leistungsstarkes Instrument in unserem Arsenal,
aber manchmal gibt es bereits Variablen, die sich ideal zur Überwachung eignen,
aber lokal in der Methode definiert sind. Wären sie jedoch Instanzvariablen, könn-
ten Sie sie zur Überwachung einer Methode verwenden. Sie können lokale Variab-
len in Instanzvariablen umwandeln, doch dies stiftet oft Verwirrung.
Zustandsinformationen, die sie dort speichern, sind nur in der Monster-Methode
und den Methoden zugänglich, die Sie daraus extrahieren. Obwohl sie bei jedem
Aufruf der Monster-Methode reinitialisiert werden, kann es schwierig sein, den
Inhalt der Variablen zu interpretieren, wenn Sie Methoden aufrufen wollen, die
Sie unabhängig extrahiert haben.

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.

22.4.1 Methoden skelettieren


Wenn Sie in einer Bedingungsanweisung nach Stellen suchen, um eine Methode
zu extrahieren, können Sie die Bedingung und den Body zusammen oder separat
extrahieren. Hier ist ein Beispiel:

if (marginalRateO > 2 && order.hasLimitO) {


order.readjust(rateCalculator.rateForTodayO);
order. recalculateO ;
}

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.

22.4.2 Sequenzen suchen


Wenn Sie in einer Bedingungsanweisung nach Stellen suchen, um eine Methode
zu extrahieren, können Sie die Bedingung und den Body zusammen oder separat
extrahieren. Ein weiteres Beispiel:

if (marginalRateO > 2 && order.hasLimitO) {


order. readjust(rateCa"lcu1ator. rateForTodayO) ;

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);

void recalculateOrder(Order order, RateCalculator rateCalculator) {


if (marginalRateO > 2 && order.hasLimitO) {
order. readjust(rateCal cul ator. rateForTodayO); order. recalculateO;
}
}

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.

22.4.3 Zuerst in die gegenwärtige Klasse extrahieren


Wenn Sie Ihre ersten Methoden aus einer Monster-Methode extrahieren, stellen
Sie wahrscheinlich fest, dass einige der extrahierten Code-Fragmente eigentlich in
andere Klassen gehören. Ein starkes Indiz dafür ist der Name, der Ihnen am pas-
sendsten erscheint. Wenn Sie einem Code-Fragment den Namen einer seiner Vari-
ablen geben wollen, gehört der Code wahrscheinlich in die Klasse dieser
Variablen. Ein Beispiel:

if (marginalRateO > 2 && order.hasLimitO) {


order.readjust(rateCalculator.rateForTodayO);
order. recalculateO;
}

Anscheinend könnten wir dieses Code-Fragment recal cul ateOrder nennen.


Der Name wäre passend; aber das Wort Order im Namen der Methode zeigt an,
dass dieses Code-Fragment vielleicht in die Order-Klasse verschoben und dort als
recal cul ate bezeichnet werden sollte. Ja, dort gibt es bereits eine Methode

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.

22.4.4 Kleine Fragmente extrahieren


Ich habe dies zwar bereits erwähnt, möchte es aber noch einmal unterstreichen:
Extrahieren Sie zuerst kleine Fragmente. Bevor Sie dieses kleine Fragment einer
Monster-Methode extrahieren, sieht es so aus, als würde dies überhaupt keinen
Unterschied machen. Doch wenn Sie mehr Fragmente extrahieren, werden Sie die
ursprüngliche Methode wahrscheinlich mit anderen Augen sehen. Vielleicht
erkennen Sie eine Sequenz, die vorher verdeckt war, oder entdecken eine bessere
Möglichkeit, die Methode zu strukturieren. Solche Möglichkeiten können Sie aus-
nutzen. Diese Strategie ist weit besser, als eine Methode von Anfang an in
umfangreiche Fragmente zu zerlegen. Dies ist zu oft nicht so einfach, wie es aus-
sieht; es ist nicht so sicher. Es ist einfacher, Details zu übersehen; und die Details
sorgen dafür, dass der Code funktioniert.

22.4.5 Rechnen Sie damit, Extraktionen zu wiederholen


Es gibt viele Möglichkeiten, einen Kuchen zu zerschneiden oder eine Monster-
Methode zu zerlegen. Nach einigen Extraktionen entdecken Sie normalerweise
bessere Methoden, neue Funktionen leichter zu integrieren. Manchmal machen
Sie den besten Fortschritt, wenn Sie einige Extraktionen rückgängig machen und
eine andere Variante wählen. Das bedeutet nicht, dass die erste Extraktion ver-
schwendete Mühe war. Sie hat Ihnen etwas sehr Wichtiges vermittelt: Einsichten
in das alte Design und eine bessere Methode, weiterzuarbeiten.

318
Kapitel 23

Wie erkenne ich, dass ich nichts


kaputtmache?

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.

23.1 »Hyperaware Editing«


Hyperaware Editing steht für eine erhöhte Aufmerksamkeit beim Editieren. Was
machen wir wirklich, wenn wir Code editieren? Was wollen wir erreichen? Norma-
lerweise verfolgen wir große Ziele. Wir wollen eine Funktion hinzufügen oder
einen Fehler beseitigen. Es ist großartig, seine Ziele zu kennen, aber wie überset-
zen wir sie in Handlungen?

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.

Es muss einfach passieren. Es scheint unvermeidlich zu sein. Es gibt bereits


IDEs, die die Syntax bei jedem Tastenanschlag prüfen und bei einem Fehler die
Farbe des Codes ändern. Beim Bearbeiten ausgelöstes Testen ist der nächste
Schritt.

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.

23.2 »Single-Goal Editing«


Single-Goal Editing steht für das Editieren eines einzigen Ziels. Ich erwarte nicht,
dass jeder denselben ersten Eindruck von der Computer-Branche hat; aber als ich
zum ersten Mal daran dachte, Programmierer zu werden, war ich wirklich von den
Geschichten über super-smarte Programmierer gefesselt, die die Zustände ganzer
Systeme im Kopf haben können und die korrekten Code im Vorübergehen schrei-
ben können und sofort wissen, ob eine Änderung richtig oder falsch war. Es ist
wahr, dass sich Menschen erheblich in ihren Fähigkeiten unterscheiden, umfang-
reiche Mengen von Detaildaten im Kopf zu behalten. Ich kann dies bis zu einem
gewissen Grad. Früher kannte ich viele der obskuren Bereiche von C++ und war
auch einmal so weit, dass ich die Details des UML-Metamodells einigermaßen
korrekt aus dem Gedächtnis abrufen konnte, bevor ich erkannte, dass Program-
mierer zu sein und derart viele Details über UML zu kennen, wirklich sinnlos und
ein wenig traurig war.

In Wahrheit gibt es verschiedene Arten, »smart« zu sein. Umfangreiches Detail-


wissen aus dem Gedächtnis abrufen zu können, kann nützlich sein, aber es ver-
bessert unsere Entscheidungsfähigkeit nicht wirklich. An diesem Punkt meiner
Karriere halte ich mich für einen viel besseren Programmierer als früher, obwohl
ich weniger über die Details der Sprachen weiß, mit denen ich arbeite. Urteilsver-
mögen ist eine Schlüsselfähigkeit guter Programmierer; und wir können in
Schwierigkeiten geraten, wenn wir versuchen, uns wie super-smarte Programmie-
rer zu verhalten.

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.

Lohn es sich? Betrachten wir einen anderen Ansatz.


Sie müssen eine Methode ändern. Die Klasse befindet sich bereits in einem Test-
Harnisch, und Sie beginnen mit der Änderung. Aber dann denken Sie: »Hoppla,
ich muss diese andere Methode da drüben auch ändern.« Deshalb unterbrechen
Sie und navigieren zu der anderen Methode. Sie sieht chaotisch aus; deshalb for-
matieren Sie einige Zeilen neu, um die Struktur zu durchschauen. Ihr Partner
schaut Sie an und fragt: »Was machst du da?« Sie sagen: »Oh, ich prüfe nur, ob wir
Methode X ändern müssen.« Ihr Partner sagt: »Hey, nicht alles gleichzeitig.« Ihr
Partner schreibt den Namen von Methode X auf einen Notizzettel neben dem
Computer; und Sie gehen zurück und beenden das Editieren. Sie führen Ihre Tests
aus und stellen fest, dass alle bestanden werden. Dann nehmen Sie sich die andere
Methode vor. Und wie vermutet, muss sie geändert werden. Sie schreiben
zunächst einen weiteren Test. Dann bearbeiten Sie die Methode, führen Ihre Tests
aus und beginnen mit der Integration. Sie und Ihr Partner blicken auf die andere
Seite des Tisches. Dort sitzen zwei andere Programmierer. Einer schreit: »Ja, ja, ja!
Korrigiere das und dann tun wir jenes.« Sie sitzen seit Stunden an dieser Aufgabe
und sehen ziemlich erschöpft aus. Wenn man der Geschichte glauben darf, wer-
den sie mit der Integration scheitern und einige weitere Stunden zusammenarbei-
ten müssen.

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.

23.3 »Preserve Signatures«


Preserve Signatures steht für das Bewahren von Signaturen. Beim Editieren von
Code gibt es viele Möglichkeiten, Fehler zu machen, etwa indem wir Wörter falsch
schreiben, falsche Datentypen verwenden, Kommas vergessen usw. Refactoring ist
besonders fehleranfällig. Es erfordert oft sehr tief greifende Änderungen. Wir
fügen nicht nur einfach neue Code-Zeilen hinzu, sondern kopieren Code-Frag-
mente und erstellen daraus neue Klassen und Methoden.

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):

public void process(List orders,


int dailyTarget,
double interestRate,
int compensationPercent) {

// komplizierter Code hier

Dann extrahierte ich sie wie folgt und erstellte neben der Methode zusätzlich eine
Reihe von Hilfsklassen:

public void process(List orders,


int dailyTarget,
double interestRate,
int compensationPercent) {
processOrders(new OrderBatch(orders),
new CompensationTarget(dai 1 yTarget,
interestRate * 100,
compensationPercent));
}
Ich hatte gute Absichten. Als ich Dependencies aufhob, wollte ich gleich das
Design verbessern, war damit aber nicht sehr erfolgreich. Ich machte dumme Feh-
ler; und da ich sie nicht mit Tests entdecken konnte, wurden sie oft erst sehr viel
später entdeckt, als notwendig gewesen wäre.
Wenn Sie Dependencies für Tests aufheben, müssen Sie besonders vorsichtig vor-
gehen. Wann immer es möglich ist, wende ich deshalb die Technik Preserve Signa-
tures an. Wenn Sie komplette Methodensignaturen per Cut/Copy/Paste von einer
Stelle an eine andere verschieben, minimieren Sie die Gefahr von Fehlern.

323
Kapitel 23
Wie erkenne ich, dass ich nichts kaputtmache?

In dem vorhergehenden Beispiel würde mein Code zuletzt wie folgt aussehen:

public void process(List orders,


int dailyTarget,
double interestRate,
int compensationPercent) {
processOrders(orders, dailyTarget, interestRate, compensationPercent);
}
private static void processOrders(List orders,
int dailyTarget,
double interestRate,
int compensationPercent) {

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

2. Dann gab ich die neue Methodendeklaration ein:

private void processOrdersO {


}

3. Dann fügte ich den Inhalt der Zwischenablage in die neue Methodendeklaration
ein:

private void processOrders( List orders,


int dailyTarget,
double interestRate,
int compensationPercent) {
}
4. Dann gab ich den Aufruf der neuen Methode ein:

processOrdersO;

5. Ich fügte den Inhalt der Zwischenablage in den Aufruf ein:

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.

Für Preserve Signatures gibt es verschiedene Einsatzszenarien. Sie können damit


neue Methodendeklarationen erstellen. Sie können auch einen Satz von Instanz-
methoden für alle Argumente einer Methode erstellen, wenn Sie Ihren Code mit
Break out Method Object refaktorisieren (siehe Break out Method Object (25.2) für
Details).

23.4 »Lean on the Compiler«


Der Hauptzweck eines Compilers besteht darin, Quellcode in eine andere Form zu
übersetzen; aber in statisch typisierten Sprachen können Sie mit einem Compiler
viel mehr tun. So können Sie etwa seine Typüberprüfung nutzen und damit Stel-
len in Ihrem Code lokalisieren, an denen Sie Änderungen vornehmen müssen.
Ich bezeichne diese Technik als Leaning on the Compiler (den Compiler als Stütze
verwenden). Hier ist ein Beispiel, wie ich diese Technik einsetze.

Angenommen, ein C-i-i-Programm habe einige globale Variablen.

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:

total = domestic_exchange_rate * instrument_shares;

Daraus wird:

total = exchange.domestic_exchange_rate * instrument_shares;

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.

Lean on the Compiler umfasst zwei Schritte:

1. Sie müssen eine Deklaration ändern, um Fehler beim Kompilieren auszulösen.


2. Sie müssen zu diesem Fehler und den Code ändern.

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«

Gegeben sei eine Klassenmethode namens getXO in einer Java-Klasse:

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;
} */

Jetzt kompilieren wir den Code neu.


Und was passiert? Der Compiler meldet keinen Fehler. Bedeutet das, dass die
Methode getXO nicht verwendet wird? Nicht unbedingt. Wenn getXO als kon-
krete Methode in einer Oberklasse deklariert ist, führt das Auskommentieren von
getX in unserer gegenwärtigen Klasse nur dazu, dass die entsprechende Funktion
in der Oberklasse verwendet wird. Eine ähnliche Situation kann bei Variablen und
Vererbung eintreten.
Lean on the Compiler ist eine leistungsstarke Technik, aber Sie müssen ihre Gren-
zen kennen; anderenfalls laufen Sie Gefahr, schwere Fehler zu machen.

23.4.1 Pair Programming


Wahrscheinlich haben Sie bereits vom Pair Programming (Paarprogrammierung)
gehört. Wenn Sie Extreme Programming (XP) praktizieren, wenden Sie wahr-
scheinlich auch Pair Programming an. Gut. Es handelt sich um eine bemerkens-
wert wirksame Methode, um die Qualität von Programmen zu verbessern und
Wissen in einem Team zu verbreiten.
Wenn Sie Pair Programming gegenwärtig nicht praktizieren, empfehle ich Ihnen,
es auszuprobieren, insbesondere wenn Sie die Techniken zur Aufhebung von
Dependencies aus diesem Buch anwenden.
Es ist leicht, Fehler zu machen und dies nicht zu bemerken. Ein weiteres Augen-
paar ist definitiv hilfreich. Mit Legacy Code zu arbeiten, ist Chirurgie, und Chirur-
gen operieren nie alleine.
Weitere Informationen über Pair Programming finden Sie in dem Buch Pair Pro-
gramming Illuminated von Laurie Williams und Robert Kessler (Addison-Wesley
2002) und auf der Website www. pai rprogrammi ng. com.

327
Kapitel 24

Wir fühlen uns überwältigt.


Es wird nicht besser.

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.

von Fehlerkorrekturen und gelegentlich neuen Funktionen an sie gerichtet. Die


Geschäftsleitung bewertet nüchtern jede neue Funktion und entscheidet, ob sie
in das alte System eingebaut werden muss oder ob der Kunde auf das neue Sys-
tem warten kann. In vielen Fällen kann der Kunde nicht warten. Deshalb fließen
die Änderungen in beide Systeme ein. Das Green-Field-Team trägt eine doppelte
Verantwortung, indem es versuchen muss, ein System zu ersetzen, das sich lau-
fend ändert. Im Laufe der Monate wird immer klarer, dass das Team es nicht
schaffen wird, das alte, noch gewartete System zu ersetzen. Der Druck wächst.
Man arbeitet Tag und Nacht und an den Wochenenden. Oft begreifen jetzt auch
die anderen Mitarbeiter des Unternehmens, wie geschäftskritisch diese Arbeit
ist und dass es hier um eine Investition geht, von der die Zukunft aller Mitarbei-
ter abhängt.

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

Techniken zur Aufhebung von


Dependencies

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.

25.1 »Adapt Parameter«


Adapt Parameter steht für das Anpassen von Parametern. Bei der Änderung von
Methoden gibt es oft Dependency-Probleme aufgrund der Methodenparameter.
Manchmal finde ich es schwer, den benötigten Parameter zu erstellen; manchmal

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.

Hier ist ein Beispiel:

public class ARMDispatcher


{
public void populate(HttpServletRequest request) {
String [] values = request.getParameterValues(pageStateName);
if (values != null && values.length > 0)
{
marketBindings.put(pageStateName + getDateStampO , values[0]);
}
}
}

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«

Für J2EE gibt es mehrere Mock-Objekt-Bibliotheken. Wenn wir eine herunterla-


den, können wir beim Testen für HttpServl etRequest ein Mock-Objekt verwen-
den. So könnten wir viel Zeit sparen. Mit diesem Weg müssten wir keine Zeit
darauf verwenden, eine simulierte Servlet-Anfrage manuell zu programmieren. Es
sieht also so aus, als hätten wir eine Lösung - oder etwa doch nicht?

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:

public class ARMDispatcher


public void populate(ParameterSource source) {
String values = source.getParameterForName(pageStateName);
if (value != null) {
marketBindings. put(pageStateName + getDateStampO , value);
}
}
}

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.

Verwenden Sie Schnittstellen, die Aufgaben anstelle von Implementierungsde-


tails beschreiben. So wird Ihr Code lesbarer und pflegeleichter.

Hier ist eine Fake-Klasse, die Parameter Source implementiert. Wir können sie in
unserem Test verwenden:

class FakeParameterSource implements ParameterSource


{
public String value;

public String getParameterForName(String name) {


return value;
}
}

337
Kapitel 25
Techniken zur Aufhebung von Dependencies

Und die Produktions-Klasse sieht wie folgt aus:

class ServletParameterSource implements ParameterSource


{
private HttpServletRequest request;

public ServletParameterSourceCHttpServletRequest request) {


this.request = request;
}
String getParameterValue(String name) {
String [] values = request.getParameterValues(name);
if (values == null || values.length < 1)
return nul1;
return values[0];
}
}

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.

Wenn wir in dem Beispiel ParameterSource verwenden, entkoppeln wir die


Zuweisungslogik von bestimmten Quellen. Wir werden nicht mehr an besondere
J2EE-Schnittstellen gebunden sein.

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.

25.2 »Break Out Method Object«


Lange Methoden erfordern in vielen Anwendungen harte Arbeit. Wenn Sie die
zugehörige Klasse instanziieren und sie in einen Test-Harnisch einfügen können,
können Sie oft die ersten Tests schreiben. In einigen Fällen ist viel Arbeit erforder-
lich, um eine Klasse separat instanziierbar zu machen. Vielleicht ist der Aufwand
für Ihre erforderlichen Änderungen zu groß. Ist die benötigte Methode klein und
verwendet sie keine Instanzdaten, sollten Sie Ihre Änderungen mit Expose Static
Method (23.5) unter Testkontrolle bringen. Ist die Methode dagegen umfangreich
oder verwendet sie Instanzdaten und -methoden, sollten Sie Break Out Method
Ohject (Methodenobjekt herauslösen) in Betracht ziehen. Im Wesentlichen wird
bei diesem Refactoring eine lange Methode in eine neue Klasse überführt.
Objekte, die Sie mit dieser neuen Klasse erstellen, werden als Methodenobjekte
bezeichnet, weil sie den Code einer einzelnen Methode enthalten. Nach der
Anwendung von Break Out Method Object können Sie Tests für die neue Klasse oft
einfacher schreiben, als dies für die alte Methode möglich gewesen wäre. Lokale
Variablen in der alten Methode können in der neuen Klasse zu Instanzvariablen
werden. Oft können Sie so Dependencies einfacher aufheben und den Code ver-
bessern.

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;

drawPoint(p.x, p.y, colors[n]);


}
}
Die GDIBrush-Klasse enthält eine lange Methode namens draw. Es ist nicht
leicht, Tests für sie zu schreiben; und es wird sehr schwierig sein, eine Instanz von
GDIBrush in einem Test-Harnisch zu erstellen. Deshalb wollen wir draw mit Break
Out Method Object in eine neue Klasse überführen.

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;

drawPoint(p.x, p.y, colors[n]);


}
}

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.

void GDIBrush::draw(vector<point>& renderingRoots,


ColorMatrix Scolors,
vector<point>& selection)
{
Renderer rendererfthis, renderingRoots, colors, selection);
renderer.draw();
}

Nun zurück zu der GDIBrush-Dependency. Wenn wir GDIBrush nicht in einem


Test-Harnisch instanziieren können, können wir die Dependency von GDIBrush
mit Extract Interface vollständig aufheben. Details finden Sie im Abschnitt Extract
Interface (25.10), aber kurz gesagt, erstellen wir eine leere Schnittstellenklasse, die
dann von GDIBrush implementiert wird. In diesem Fall können wir sie Point-
Renderer nennen; denn drawPoi nt ist die Methode von GDIBrush, die wir wirk-
lich für den Zugriff auf den Renderer brauchen. Dann ändern wir die Referenz in
Renderer von GDIBrush in PointRenderer, kompilieren den Code und lassen
uns vom Compiler sagen, welche Methoden die Schnittstelle enthalten muss. Hier
ist der Code in seiner endgültigen Form:

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

Abb. 25.1: GDIBrush nach Break Out Method Object

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:

1. Erstellen Sie eine Klasse für den Code der Methode.


2. Erstellen Sie einen Konstruktor für die Klasse und wenden Sie Preserve Signa-
tures (23.1) an, um die Argumente der Methode genau zu kopieren. Wenn die

344
25-3
»Definition Completion«

Methode Instanzdaten oder -methoden der ursprünglichen Klasse verwendet,


fügen Sie eine Referenz der ursprünglichen Klasse als erstes Argument in den
Konstruktor ein.
3. Deklarieren Sie für jedes Argument in dem Konstruktor eine Instanzvariable
und weisen Sie ihr den Typ des Arguments zu. Kopieren Sie mit Preserve Signa-
tures (23.1) alle Argumente direkt in die Klasse und formatieren Sie sie als Dekla-
rationen von Instanzvariablen. Weisen Sie alle Argumente den Instanzvariablen
in dem Konstruktor zu.
4. Erstellen Sie in der neuen Klasse eine leere Ausführungsmethode. Oft wird
diese Methode als run () bezeichnet. In diesem Beispiel haben wir den Namen
d raw verwendet.
5. Kopieren Sie den Body der alten Methode in die Ausführungsmethode, kompi-
lieren Sie den Code und nutzen Sie Lean on the Compiler (23.4).
6. Die Fehlermeldungen des Compilers sollten anzeigen, wo die Methode immer
noch Methoden oder Variablen der alten Klasse verwendet. Tun Sie in jedem
einzelnen Fall, was erforderlich ist, um die Methode zu kompilieren. In einigen
Fällen müssen Sie nur einen Aufruf so ändern, dass er eine Referenz der
ursprünglichen Klasse verwendet. In anderen Fällen müssen Sie vielleicht
Methoden der ursprünglichen Klasse als public deklarieren oder Getter ein-
führen, damit Sie die Instanzvariablen nicht als publ i c deklarieren müssen.
7. Nachdem die neue Klasse kompiliert wird, gehen Sie zu der ursprünglichen
Methode zurück und ändern sie so, dass sie eine Instanz der neuen Klasse
erstellt und ihre Arbeit an sie delegiert.
8. Heben Sie die Dependency von der ursprünglichen Klasse bei Bedarf mit
Extract Interface (23.10) auf.

25.3 »Definition Completion«


In einigen Sprachen können wir einen Typ an einer Stelle deklarieren und an
einer anderen definieren. Die bekanntesten Sprachen mit dieser Möglichkeit sind
C und C++. In beiden können wir eine Funktion oder Methode in einer Header-
Datei deklarieren und sie dann normalerweise in einer Implementierungsdatei
definieren. Mit dieser Fähigkeit können wir auch Dependencies aufheben.

Hier ist ein Beispiel:

class CLateBindingDispatchDriver : public CDispatchDriver


{
publi c:
CLateBindingDispatchDriver ();
Virtual -CLateBindingDispatchDriver ();

ROOTID GetROOTID (int id) const;

345
Kapitel 25
Techniken zur Aufhebung von Dependencies

void: BindName (int id, OLECHAR FAR -name);

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.

#i nclude "LateBi ndi ngDi spatchDriver.h"

CLateBindingDispatchDriver::CLateBindingDispatchDriver() {}

CLateBindingDispatchDriver::~CLateBindingDispatchDriver() {}

ROOTID GetROOTID (int id) const { return ROOTID(-l); }

void BindName(int id, OLECHAR FAR *name) {}

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«

deshalb, Definition Completion nur bei gravierenden Dependency-Problemen zu


verwenden. Selbst dann sollten Sie damit nur die anfänglichen Dependencies auf-
heben. Danach sollten Sie die Klasse schnell unter Testkontrolle bringen, damit
die duplizierten Definitionen entfernt werden können.

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.

25.4 »Encapsulate Global References«


Wenn Sie Code testen wollen, der problematische Dependencies von Globals ent-
hält, haben Sie im Wesentlichen drei Optionen: Sie können versuchen, das Verhal-
ten der Globals beim Testen zu ändern; Sie können mit anderen Globals
verlinken; oder Sie können die Globals so einkapseln, dass Sie einzelne Kompo-
nenten weiter entkoppeln können. Die letzte Option wird als Encapsulate Global
References (Globale Referenzen einkapseln) bezeichnet. Hier ist ein Beispiel in
C++:

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.

Alternativ könnten wir beide Frames als Konstruktor-Argumente an AGGCont rol -


ler übergeben. Doch vorher sollten wir die anderen Stellen untersuchen, an
denen sie verwendet werden. Es scheint, dass beide Frames immer zusammen
verwendet werden und deshalb gebündelt werden könnten.

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.

In dem vorhergehenden Beispiel würde ich erwarten, dass die Methoden


f rame_copy und cl ear im Laufe der Zeit in die neue Klasse verschoben werden.
Gibt es Aufgaben, die für beide Frames erfüllt werden müssen? Wahrscheinlich.
Die suspend_f rame-Funktion von AGGControl 1 er könnte wahrscheinlich in eine
neue Klasse verschoben werden, solange sie sowohl das suspended_frame- als
auch das acti ve_f rame-Array enthält. Wir könnten diese neue Klasse einfach
Frame nennen und sagen, jedes Frame verfüge über einen aktiven und einen
gesperrten Puffer. Zu diesem Zweck müssten wir unsere Konzepte ändern und
einige Variablen umbenennen, aber dafür bekämen wir eine intelligentere Klasse,
die mehr Details verbirgt.

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.

Und so gehen wir Schritt für Schritt vor:

Zuerst erstellen wir eine Klasse, die wie folgt aussieht:

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];

An diesem Punkt meldet der Compiler zahlreiche Fehler, darunter, dass


AGG_acti veframe und AGG230_suspendedf rame nicht existieren, und droht uns
ernste Konsequenzen an.

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.

In dem vorhergehenden Beispiel wird Encapsulate Global References auf globale


Daten angewendet. Dasselbe können Sie in C++-Programmen auch mit Non-
Member-Funktionen tun. Oft müssen Sie beim Arbeiten mit einem C-API globale
Funktionen aufrufen, die über den Code verstreut sind. Die einzige Nahtstelle ist
die Verknüpfung der Aufrufe mit den entsprechenden Funktionen. Mit Link Sub-
stitution (23.13) können Sie eine Trennung herbeiführen, aber wenn Sie mit Encap-
sulate Global References eine andere Nahtstelle erstellen, erhalten Sie besser
strukturierten Code. Hier ist ein Beispiel.

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

Virtual Option GetOption(const string& optionName) = 0;


Virtual void SetOption(const string& optionName, const Option&
newOption) = 0;
};
Die Klasse enthält für alle benötigten ungebundenen Funktionen abstrakte Metho-
den. Als Nächstes erstellen wir eine Unterklasse, die als Fake der Klasse verwendet
wird. Hier könnten wir in der Fake-Klasse eine Map oder einen Vector verwenden,
um einen Satz von Optionen zu speichern, die beim Testen benutzt werden. Wir
könnten eine add-Methode in die Fake-Klasse einfügen; wir könnten auch nur
einen Konstruktor definieren, der eine Map übernimmt - je nachdem, was beim
Testen gebraucht wird. Nachdem wir die Fake-Klasse erstellt haben, können wir
den eigentlichen Option-Code erstellen:

class ProductionOptionSource : public OptionSource


{
public:
Option GetOptionCconst string& optionName);
void SetOption(const string& optionName, const 0ption& newOption) ;
};
Option ProductionOptionSource::GetOption(const string& optionName)
{
::GetOption(optionName);
}
void ProductionOptionSource::SetOption(const string& optionName, const
0ption& newOption)
{
::SetOpti on(opti onName, newOpti on);
}

Um Referenzen von ungebundenen Funktionen einzukapseln, erstellen Sie eine


Schnittstellenklasse mit Fake- und Produktions-Unterklassen. Jede Funktion im
Produktions-Code sollte nichts anderes tun, als an eine globale Funktion zu
delegieren.

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«

cessing Seam (4.3) verwenden müssen, um eine Implementierung durch eine


andere zu ersetzen. Bei dem jetzigen Ansatz (Klasse plus virtuelle Funktionen
plus Parametrisierung der Klasse) sind die Nahtstellen klar erkennbar und leicht
handhabbar.

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).

25.5 »Expose Static Method«


Expose Static Method steht für die Veröffentlichung statischer Methoden. Mit Klas-
sen zu arbeiten, die in einem Test-Harnisch nicht instanziiert werden können, ist
recht verzwickt. In einigen Fällen verwende ich die folgende Technik. Eine
Methode, die keine Instanzdaten oder -methoden verwendet, können Sie in eine
statische Methode umwandeln. Wenn sie statisch ist, können Sie sie unter Test-
kontrolle bringen, ohne dass Sie die Klasse instanziieren müssen. Hier ist ein Bei-
spiel in Java.

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
{

public void validate(Packet packet)


throws InvalidFlowException {
if (packet.getOriginatorC).equalsC "MIA")

353
Kapitel 25
Techniken zur Aufhebung von Dependencies

II packet.getLengthO > MAX_LENGTH


II !packet.hasValidCheckSumO) {
throw new InvalidFlowExceptionO;
}
}
}
Wie können wir diese Methode unter Testkontrolle bringen? Die Methode verwen-
det mehrere Methoden der Packet-Klasse. Tatsächlich wäre es sinnvoll, die val i -
date-Methode in die Packet-Klasse zu verschieben; aber das Verschieben der
Methode ist nicht die risikoloseste Maßnahme, die wir im Moment ausführen
können. Wir werden Preserve Signatures (23.1) definitiv nicht befolgen können.
Wird das Verschieben nicht automatisch unterstützt, ist es oft besser, zuerst einige
Tests einzurichten. Expose Static Method kann Ihnen dabei helfen. Wenn Tests ein-
gerichtet sind, können Sie die benötigte Änderung durchführen und danach die
Methode mit größerer Sicherheit verschieben.

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.

public class RSCWorkflow {


public void validate(Packet packet)
throws InvalidFlowException {
validatePacket(packet)j
}
public static void validatePacket(Packet packet)
throws InvalidFlowException {
if (packet.getOriginator() == "MIA"
II packet.getLengthO <= MAX_LENGTH
II packet.hasValidCheckSumO) {
throw new InvalidFlowExceptionO;
}
}
}

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:

RSCWorkflow workflow = new RCSWorkflow();

// statischer Aufruf, der wie ein nicht-statischer Aufruf aussieht


workflow.validatePacket(packet);

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.

25.6 »Extract and Override Call«


Gelegentlich sind die Dependencies, die beim Testen stören, lokal eng begrenzt.
Vielleicht müssen wir nur einen einzigen Methodenaufruf ersetzen. Wenn wir die
Dependency von einem Methodenaufruf aufheben können, können wir seltsame
Nebeneffekte in Test- oder Kontrollwerten vermeiden, die an den Aufruf überge-
ben werden.

Betrachten wir ein Beispiel:

public class PageLayout


{
private int id = 0;
private List styles;
private StyleTemplate template;

protected void rebindStylesO {


styles = StyleMaster.formStyles(template, id);

}
}
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.

Hier ist der Code nach dem Extrahieren:

public class PageLayout


{
private int id = 0;
private List styles;
private StyleTemplate template;

protected void rebindStylesO {


styles = formStyles(template, id);

}
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:

public class TestingPageLayout extends PageLayout {


protected List formStyles(StyleTemplate template, int id) {
return new ArrayListO;
}
}
Wenn wir die Tests entwickeln, die verschiedene Stile benötigen, können wir diese
Methode so ändern, dass ihr Rückgabewert konfigurierbar ist.

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.

25.7 »Extract and Override Factory Method«


Objekte in Konstruktoren zu erstellen, kann verzwickt sein, wenn Sie eine Klasse
unter Testkontrolle bringen wollen. Manchmal sollte die Arbeit, die in diesen
Objekten geleistet wird, in einem Test-Harnisch nicht ausgeführt werden. Manch-
mal wollen Sie auch nur ein Überwachungsobjekt (engl, sensing object) erstellen,
können dies aber nicht, weil die Erstellung des Objekts fest in einen Konstruktor
eincodiert ist.

Es kann sehr schwer sein, fest in Konstruktoren eincodierte Initialisierungsar-


beit in Tests zu umgehen.

Betrachten wir ein Beispiel:

public class WorkflowEngine


{
public WorkflowEngine () {

Reader reader = new ModelReaderf AppConfig.getDryConfigurationO);

Persister persister = new XMLStore(AppConfiguration.getDryConfigurationO);

this.tm = new TransactionManager(reader, persister);


}
}

Die Klasse WorkflowEngi ne erstellt in ihrem Konstruktor ein TransactionMana-


ger-Objekt. Erfolgte die Erstellung an anderer Stelle, könnten wir den Code leich-
ter trennen. Eine unserer Optionen ist die Extract and Override Factory Method
(Factory-Methode extrahieren und überschreiben).

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.

public class WorkflowEngine


{
public WorkflowEngine () {
this.tm = makeTransactionManagerO;

}
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:

public class TestWorkflowEngine extends WorkflowEngine


{
protected TransactionManager makeTransactionManagerO {
return new FakeTransactionManagerO;
}
}

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

25.8 »Extract and Override Getter«


Extract and Override Factory Method (25.7) (Getter extrahieren und überschreiben)
ist eine leistungsstarke Methode, Dependencies von Typen zu isolieren, aber sie
funktioniert nicht in allen Fällen. Die große »Lücke« ihrer Anwendung ist C++. In
C++ können Sie vom Konstruktor einer Basisklasse keine virtuelle Funktion in
einer abgeleiteten Klasse aufrufen. Glücklicherweise gibt es eine Behelfslösung
für den Fall, dass Sie das Objekt in einem Konstruktor nur erstellen und keine
zusätzliche Arbeit damit leisten.

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.

Im folgenden Beispiel erstellen wir in einem Konstruktor einen Transaktionsma-


nager. Wir wollen die Dinge so einrichten, dass die Klasse diesen Transaktionsma-
nager in der Produktion und einen anderen zwecks Beobachtung beim Testen
verwenden kann.
Wir beginnen mit folgendem Code:

// WorkflowEngine.h class WorkflowEngine


{
private:
TransactionManager *tm;
public:
WorkflowEngi ne ();

// 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);

Hier ist unser Zielcode:

// 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)

TransactionManager ^WorkflowEngine::getTransactionManager() const

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:

class TestWorkflowEngine : public WorkflowEngine


{
public:
TransactionManager *getTransactionManagerO {

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«

25.9 »Extract Implementer«


Extract Interface (23.10) (Implemeter extrahieren) ist eine brauchbare Technik,
aber einer ihrer Aspekte ist schwierig: die Namensvergabe. Ich stoße oft auf Fälle,
in denen ich eine Schnittstelle extrahieren möchte, aber der Name, den ich ver-
wenden möchte, ist bereits für eine Klasse vergeben. Wenn ich mit einer IDE
arbeite, die das Umbenennen von Klassen und Extract Interface unterstützt, kann
ich das Problem leicht lösen. Andernfalls habe ich einige Optionen:

• Ich kann einen unpassenden Namen wählen.


• Ich kann prüfen, ob die benötigten Methoden eine Teilmenge der publ i c Me-
thoden der Belasse sind. Ist dies der Fall, sollte vielleicht ein anderer Name für
die neue Schnittstelle verwendet werden.

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()
{}

Als Nächstes machen wir die ProductionModel Node-Klasse zu einer Unterklasse


der neuen Schnittstellenklasse:

#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

Extract Implementer funktioniert wie folgt:

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.

8. Kompilieren und testen Sie den Code.

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:

Abb. 25.2: Model Node mit Oberklasse und 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

Abb. 25.3: Anwendung von Extract Implementer auf 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).

Abb. 25.4: Anwendungvon Extract Implementer auf Model Node

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

25.10 »Extract Interface«


In vielen Sprachen ist Extract Interface (Schnittstelle extrahieren) einer der sichers-
ten Techniken zur Dependency-Aufhebung. Wenn Sie bei einem Schritt einen
Fehler machen, bekommen Sie vom Compiler sofort ein Feedback; deshalb ist die
Gefahr gering, einen Bug einzuführen. Im Wesentlichen erstellen Sie eine
Schnittstelle für eine Klasse mit Deklarationen für alle Methoden, die Sie in einem
Kontext verwenden wollen. Danach können Sie die Schnittstelle implementieren
und ein Fake-Objekt an die zu testende Klasse übergeben, um sie isoliert zu beob-
achten.

Es gibt wenigstens drei Methoden, Extract Interface auszuführen, sowie einige klei-
nere Stolpersteine, auf die Sie achten müssen:

Erstens: Falls vorhanden, können Sie automatisiertes Refactoring einsetzen. Tools,


die dieses Verfahren unterstützen, bieten normalerweise Möglichkeiten an,
Methoden einer Klasse auszuwählen und den Namen der neuen Schnittstelle ein-
zugeben. Wirklich gute Tools fragen Sie, ob sie den Code durchsuchen und alle
einschlägigen Referenzen auf die neue Schnittstelle ändern sollen. Mit solchen
Tools können Sie viel Arbeit sparen.

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.

Wir müssen eine Schnittstelle extrahieren, um eine Klasse namens Payday-


Transacti on unter Testkontrolle zu bringen. Abbildung 25.5 zeigt PaydayTrans-
acti on und eine Dependency, eine Klasse namens Transacti onLog.

Hier ist unser erster Versuch, einen Testfall zu konstruieren:

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)

Abb. 25.5: PaydayTransaction hängt von TransactionLog ab.

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();

assertEqua1s(getSampleCheck(12) , getTestingDatabaseO .findCheck(12)) ;


}

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.

public class TransactionLog implements TransactionRecorder


{

370
25-10
»Extract Interface«

Als Nächstes erstellen wir FakeTransactionLog als leere Klasse.

public class FakeTransactionLog implements TransactionRecorder


{
}

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.

Hier ist ein Beispiel:

public class PaydayTransaction extends Transaction


{
public PaydayTransaction(PayrollDatabase db, TransactionRecorder log)
{
super(db, log);
}
public void run() {
for(Iterator it = db.getEmployeesO; it.hasNextO; ) {
Employee e = (Employee)it.next() ;
if (e.isPayday(date)) {
e.payO;
}
}
log.saveTransaction(this);
}
}

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

public class FakeTransactionLog implements TransactionRecorder


{
void saveTransaction(Transaction transaction) {
}
}
Damit sind wir fertig. Wir müssen in unseren Tests kein echtes TransactionLog
erstellen. Vielleicht meinen Sie, wir seien noch nicht wirklich fertig, weil noch die
recordError-Methode in TransactionLog und FakeTransactionLog fehlt.
Wohl wahr. Müssten wir die ganze Schnittstelle extrahieren, müssten wir auch für
diese Methode eine Signatur hinzufügen. Doch für den Test brauchen wir sie
nicht. Doch wollten wir immer alle publ i c Methoden einer Klasse in eine Schnitt-
stelle einfügen, leisten wir vielleicht viel mehr Arbeit, als erforderlich ist, um
einen Teil einer Anwendung unter Testkontrolle zu bringen. Wenn Ihnen ein
Design vorschwebt, in dem bestimmte Schlüsselabstraktionen über Schnittstellen
verfügen, die einen Satz von p u b l i c Methoden ihrer Klassen vollständig abde-
cken, sollten Sie immer daran denken, dass Sie inkrementell dorthin gelangen
können. Gelegentlich ist es besser, sich zurückzuhalten, bis Sie die Testabdeckung
erweitern können, bevor Sie umfassende Änderungen vornehmen.

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.

In meisten Fällen ist es leicht, eine Schnittstelle zu extrahieren. Bei nicht-virtuel-


len Methoden ist es etwas schwieriger. In Java könnten dies static Methoden
sein. Sprachen wie etwa C# und C++ erlauben auch nicht-virtuelle Instanzmetho-
den. Weitere Details über den Umgang mit diesen Fällen finden Sie im folgenden
Einschub.

Extract Interface und nicht-virtuelle Funktionen


Angenommen, Ihr Code enthielte etwa folgenden Aufruf: bondRegistry.new-
Fi xedYi el d (cl i ent). In vielen Sprachen ist mit einem Blick auf die Methode
schwer zu erkennen, ob es sich um eine stati c Methode oder um eine virtuelle
oder nicht-virtuelle Instanzmethode handelt. In Sprachen, in denen nicht-virtu-
elle Instanzmethoden zugelassen sind, können Sie Probleme bekommen, wenn
Sie eine Schnittstelle extrahieren und die Signatur einer der nicht-virtuellen
Methoden der Klasse in die Schnittstelle einfügen. Im Allgemeinen können Sie,
wenn Ihre Klasse keine Unterklassen hat, die Methode virtuell machen und
dann die Schnittstelle extrahieren. Es gibt keine Probleme. Hat Ihre Klasse aber
Unterklassen, kann der Code funktionsunfähig werden, wenn Sie die Metho-
densignatur in eine Schnittstelle verschieben. Hier ist ein Beispiel in C++. Es
zeigt eine Klasse mit einer nicht-virtuellen Methode:

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:

void disperse(BondRegistry *registry) {

Bond *bond = registry->newFixedYield(existingClient);

}
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.

Im Allgemeinen ist es in C++ keine gute Vorgehensweise, in einer abgeleiteten


Klasse eine Methode zu erstellen, deren Signatur mit der einer nicht-virtuellen
Methode in der Basisklasse übereinstimmt, weil dies zu Missverständnissen füh-
ren kann. Wenn Sie über eine Schnittstelle auf eine nicht-virtuelle Funktion
zugreifen wollen und diese nicht zu einer Klasse ohne Unterklassen gehört, ist es
am besten, eine neue virtuelle Methode mit einem neuen Namen hinzuzufügen.

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.

25.11 »Introduce Instance Delegator«


Statische Methoden werden in Klassen aus vielen Gründen eingesetzt. Einer der
häufigsten Gründe ist die Implementierung von Singleton Design Pattern (23.12).
Ein weiterer häufiger Grund ist die Verwendung statischer Methoden, um Utility-
Klassen zu erstellen.
Utility-Klassen sind in vielen Designs ziemlich leicht zu finden. Es sind Klassen
ohne Instanzvariablen oder Instanzmethoden. Sie bestehen nur aus einem Satz
statischer Methoden und Konstanten.
Meistens werden Utility-Klassen erstellt, wenn es schwierig ist, für einen Satz von
Methoden eine gemeinsame Abstraktion zu finden. So enthält etwa die Math-
Klasse im Java JDK statische Methoden für trigonometrische (cos, sin, tan) und
viele andere Funktionen. Sprachdesigner, die ihre Sprachen durchgängig objekt-
orientiert entwerfen, sorgen dafür, dass primitive numerische Typen mit diesen
Dingen umgehen können. So sollten Sie etwa die Methode si n () von dem Objekt
1 (die Zahl eins) oder einem anderen numerischen Objekt aus aufrufen können
und das richtige Ergebnis erhalten. Als ich dies schrieb, hatten die primitiven
Typen in Java keine mathematischen Methoden; deshalb ist die Utility-Klasse ein
brauchbarer Ersatz; dennoch ist sie ein Sonderfall. In fast allen Fällen können Sie
für Ihre Arbeit einfache alte Klassen mit Instanzdaten und -methoden einsetzen.

Statische Methoden machen in einem Projekt normalerweise nur Probleme, wenn


sie Dependencies enthalten, die in Tests schwer zu bedienen sind. (Die technische

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?

Sie könnten etwa delegierende Instanzmethoden in die Klasse einfügen. Dann


müssten Sie die Aufrufe der statischen Methoden durch Aufrufe von Methoden
eines Objekts ersetzen. Ein Beispiel:

public class BankingServices


{
public static void updateAccountBalance(int userlD, Money amount) {

}
}

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:

public class BankingServices


{
public static void updateAccountBalance(int userlD, Money amount) {
}

public void updateBalance(int userlD, Money amount) {


updateAccountBalance(userID, amount);
}
}

Hier haben wir eine Instanzmethode namens updateBalance hinzugefügt, die


an die statische Methode updateAccountBal ance delegiert.
Jetzt können wir Referenzen in dem aufrufenden Code ersetzen. So wird aus:

public class SomeClass


{
public void someMethodO {

BankingServices.updateAccountBalance(id, sum);
}
}

dieser Aufruf:

public class SomeClass


{
public void someMethod( BankingServices services) {

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:

1. Identifizieren Sie eine statische Methode, die schwer unter Testkontrolle zu


bringen ist.
2. Erstellen Sie für die Methode in der Klasse eine Instanzmethode (einen Delega-
tor). Wenden Sie Preserve Signatures (23.1) an. Delegieren Sie in der Instanzme-
thode an die statische Methode.
3. Suchen Sie die Stellen in der zu testenden Klasse, an denen die statischen
Methoden verwendet werden. Wenden Sie Parameterize Method (25.15) oder eine
andere Dependency-aufhebende Technik an, um jeweils an der Stelle, an der die
statische Methode aufgerufen wird, eine Instanz zur Verfügung zu stellen.
4. Ersetzen Sie den problematischen Aufruf der ursprünglichen statischen
Methode durch einen Aufruf des Delegators der Instanz, die in Schritt 3 einge-
führt wurde.

25.12 »Introduce Static Setter«


Introduce Static Setter steht für die Einführung des statischen Setters. Vielleicht bin
ich Purist, aber ich mag keine global änderbaren Daten. Bei meiner Arbeit mit
Teams ist dies normalerweise die offensichtlichste Hürde, um Teile ihrer Systeme

376
25-12

»Introduce Static Setter«

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.

Das Singleton Design Pattern


Das Singleton Design Pattern stellt sicher, dass es in einem Programm nur eine
Instanz einer bestimmten Klasse geben kann. Die meisten Singletons haben
drei Eigenschaften gemeinsam:

1. Konstruktoren einer Singleton-Klasse werden normalerweise als private


deklariert.
2. Ein statisches Mitglied der Klasse enthält die einzige Instanz der Klasse, die
überhaupt in dem Programm erstellt wird.
3. Der Zugriff auf die Instanz erfolgt mit einer statischen Methode, die norma-
lerweise als i nstance oder geti nstance bezeichnet wird.

Singletons verhindern nicht nur im Produktions-Code, sondern auch in einem


Test-Harnisch, dass mehr als eine Instanz einer Klasse erstellt wird.

Singletons zu ersetzen, bedeutet einen geringen Mehraufwand. Fügen Sie einen


statischen Setter in das Singleton ein, um die Instanz zu ersetzen, und deklarieren
Sie den Konstruktor als protected. Dann können Sie eine Unterklasse von dem
Singleton ableiten, ein frisches Objekt erstellen und es an den Setter übergeben.

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

Hier ist ein Beispiel für Introduce Static Setter in C++:

void MessageRouter::route(Message *message) {

Dispatcher *dispatcher = ExternalRouter::instance()->getDispatcher();


if (dispatcher != NULL)
dispatcher->sendMessage(message);
}

In der MessageRouter-Klasse erstellen wir an einigen Stellen Dispatcher mit Sin-


gletons. Die Externa! Router-Klasse ist eins dieser Singletons. Sie verwendet eine
statische Methode namens instance, um den Zugriff auf die eine und einzige
Instanz von ExternalRouter zu ermöglichen. Die Externa!Router-Klasse ver-
fügt über einen Getter für einen Dispatcher. Wir können den Dispatcher durch
einen anderen ersetzen, indem wir den externen Router ersetzen, der ihn bedient.

Der folgende Code zeigt die Externa! Router-Klasse, bevor wir den statischen Set-
ter einführen:

class ExternalRouter
{
private:

static ExternalRouter *_instance;

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.

void ExternalRouter::setTestingInstance(ExternalRouter *newlnstance)


{
delete _instance;
_instance = newlnstance;
}

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.

class TestingExternalRouter : public ExternalRouter


{
public:
Virtual void Dispatcher '-getDispatcher() const {
return new FakeDispatcher;
}
};
Doch führen wir hier nicht nur etwas umständlich einen neuen Dispatcher ein?
Schließlich erstellen wir einen neuen External Router, nur um den Dispatcher
zu ersetzen. Wir können einige Abkürzungen nehmen, aber diese haben andere
Nebenwirkungen. Wir können auch ein boolesches Flag in External Router ein-
fügen, damit dieser je nach Wert des Flags einen anderen Dispatcher zurückgibt.
In C++ oder C# können wir den Code auch bedingt kompilieren, um den Dispat-
cher auszuwählen. Diese Techniken können gut funktionieren, aber sie sind inva-
siv und können umständlich sein, wenn sie an vielen Stellen einer Anwendung
eingesetzt werden. Im Allgemeinen ziehe ich es vor, Produktions- und Test-Code
zu trennen.

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.

Eine Alternative zur Verringerung des Konstruktorschutzes und der Unterklassen-


bildung besteht darin, Extract Interface (23.10) auf die Singleton-Klasse anzuwen-
den und einen Setter zur Verfügung zu stellen, der ein Objekt mit dieser Schnitt-
stelle übernimmt. Dieses Verfahren hat folgenden Nachteil: Sie müssen den Typ der
verwendeten Referenzen ändern, damit sie das Singleton referenzieren; und Sie
müssen den Typ des Rückgabewertes der i nstance-Methode ändern. Diese Ände-
rungen können aufwendig sein; und sie verbessern das System nicht grundsätzlich.
Der letztlich »bessere Zustand« besteht darin, die globalen Referenzen des Single-
tons so weit zu reduzieren, dass es einfach eine normale Klasse werden kann.

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:

public class RouterFactory


{
static Router makeRouterf) {
return new EWNRouterO;
}
}

RouterFactory ist eine unkomplizierte globale Factory. In dieser Form können


wir die Router, die sie zurückgibt, beim Testen nicht ersetzen; doch eine Änderung
der Klasse macht dies möglich:

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;
}
};
}

In einen Test können wir Folgendes tun:

protected void s e t U p O {
RouterServer.setServer(new RouterServerO {
public RouterServer makeRouterO {
return new FakeRouterO;
}
});
}
25-12

»Introduce Static Setter«

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

Introduce Static Setter funktioniert wie folgt:

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.

25.13 »Link Substitution«


Objektorientierung bietet viele gute Gelegenheiten, um ein Objekt durch ein ande-
res zu ersetzen. Wenn zwei Klassen dieselbe Schnittstelle implementieren oder
dieselbe Oberklasse haben, können Sie ziemlich leicht ein Objekt der einen Klasse
durch eins der anderen ersetzen. Leider gibt es in prozeduralen Sprachen wie etwa
C diese Option nicht. Bei der folgenden Funktion gibt es, abgesehen vom Präpro-
zessor, kein einfaches Verfahren, beim Kompilieren eine Funktion gegen eine
andere auszutauschen:

void account_deposit(int amount);

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:

void account_deposit(int amount)


{
struct Call *ca"ll = (struct Call *)calloc(l, sizeof (struct Call));
caTI->type = ACC_DEPOSIT;
ca!1->arg0 = amount;
append(g_ca"lls, call);
}
In diesem Fall wollen wir den Code überwachen. Deshalb erstellen wir eine glo-
bale Liste der Aufrufe, die bei jedem Aufruf dieser (oder einer anderen simulier-
ten) Funktion protokolliert werden sollen. In einem Test könnten wir nach dem
Einsatz mehrerer Objekte anhand der Liste prüfen, ob die simulierten Funktionen
in der korrekten Reihenfolge aufgerufen wurden.

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.

25.14 »Parameterize Constructor«


Wenn Sie in einem Konstruktor ein Objekt erstellen, lässt sich dieses oft am ein-
fachsten ersetzen, indem Sie seine Erstellung externalisieren, das Objekt außer-
halb der Klasse erstellen und es als Parameter an den Konstruktor übergeben. Hier
ist ein Beispiel.
Wir beginnen mit folgendem Code:

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
this.receiver = new MailReceiverO ;
this. checkPeriodSeconds = checkPeriodSeconds;
}

Dann führen wir wie folgt einen neuen Parameter ein:

public class MailChecker


{
public MailChecker (MailReceiver receiver, int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;

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:

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
this(new MailReceiverO, checkPeriodSeconds);
}
public MailChecker (Mai 1Receiver receiver, int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}

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:

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
this. receiver = new MailReceiverO;
this.checkPeriodSeconds = checkPeriodSeconds;
}
}

Wir kopieren den Konstruktor:

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
this. receiver = new MailReceiverO;
this.checkPeriodSeconds = checkPeriodSeconds;
}
public MailChecker (int checkPeriodSeconds) {
this. receiver = new MailReceiverO;
this.checkPeriodSeconds = checkPeriodSeconds;
}
}

Dann fügen wir einen Parameter für den Mai 1 Recei ver ein:

384
25.14
»Parameterize Constructor«

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
this. receiver = new MailReceiverO;
this.checkPeriodSeconds = checkPeriodSeconds;
}
public MailChecker ( MailReceiver receiver, int checkPeriodSeconds) {
this.receiver = new MailReceiverO;
this.checkPeriodSeconds = checkPeriodSeconds;
}

Als Nächstes weisen wir diesen Parameter der Instanzvariablen zu und beseitigen
den new-Ausdruck.

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
thi s. recei ver = new MailReceiverO;
this.checkPeriodSeconds = checkPeriodSeconds;
}
public MailChecker (MailReceiver receiver, int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}

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.

public class MailChecker


{
public MailChecker (int checkPeriodSeconds) {
this(new MailReceiverO, checkPeriodSeconds);
}
public MailChecker ( MailReceiver receiver, int checkPeriodSeconds) {
this.receiver = receiver;
this.checkPeriodSeconds = checkPeriodSeconds;
}

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

neuen Konstruktor im Produktions-Code verwenden und die Dependencies sys-


temweit verstärken. Doch im Allgemeinen ist dies eher eine kleine Sorge. Parame-
terize Constructor ist eine sehr leichte Refactoring-Technik, die ich häufig einsetze.

In Sprachen, die Standard-Argumente zulassen, kann Parameterize Constructor


einfach realisiert werden. Wir können einfach ein Standard-Argument zu dem
vorhandenen Konstruktor hinzufügen:

Das folgende C++-Beispiel zeigt einen entsprechenden Konstruktor:


class AssemblyPoint
{
public:
AssemblyPoint(Equi pmentDispatcher *di spatcher
= new EquipmentDispatcher);

};
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

Parameterize Constructor funktioniert wie folgt:

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.

25.15 »Parameterize Method«


Sie verfügen über eine Methode, die intern ein Objekt erstellt, und wollen das
Objekt ersetzen. Oft besteht die einfachste Methode darin, das Objekt von außen
zu übergeben. Hier ist ein Beispiel in C++:

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(TestResult *result) {


delete m_result;
m_result = result;
try {
setUp();
runTest(m_result);
}
catch (exception& e) {
result->addFailure(e, this);
}
tearDownO;
}

Wir können eine kleine Weiterleitungsmethode verwenden, die die ursprüngliche


Signatur bewahrt:

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

Parameterize Method funktioniert wie folgt:

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.

25.16 »Primitivize Parameter«


Primitivize Parameter steht für die Vereinfachung des Parametertyps. Im Allgemei-
nen besteht die beste Methode, eine Klasse zu ändern, darin, eine Instanz in
einem Test-Harnisch zu erstellen, einen Test der gewünschten Änderung zu
schreiben und dann dafür zu sorgen, dass die Änderung den Test besteht. Aber
manchmal ist es lächerlich aufwendig, eine Klasse unter Testkontrolle zu bringen.
Ich arbeitete einmal mit einem Team, das ein Legacy-System mit Domain-Klassen
geerbt hatte, die transitiv von fast jeder anderen Klasse in dem System abhingen.
Und als wäre dies nicht schon schlimm genug gewesen, waren sie alle auch noch
in ein Persistence Framework eingebunden. Zwar wäre es machbar gewesen, diese
Klassen in ein Test-Framework einzufügen, aber das hätte für längere Zeit die
Arbeit an wichtigen Funktionen blockiert. Das folgende Beispiel beschreibt die
Strategie, mit der wir einzelne Komponenten zu isolieren versuchten. Es wurde
zum Schutz der Unschuldigen geändert.

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.

Idealerweise wäre diese Methode in einer Klasse namens Sequence enthalten;


aber Sequence gehört zu den schrecklichen Klassen, die versuchen würden, die
ganze Welt in unseren Test-Harnisch zu saugen, wenn wir sie instanziieren woll-
ten. Damit wir diese Methode schreiben können, müssen wir zunächst herausfin-
den, wie wir einen Test dafür schreiben können. Möglich wird dies dadurch, dass
Sequenzen über eine interne Repräsentation verfügen, die vereinfacht werden

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);

vectorcunsigned int> pattern;


pattern.push_back(l);
pattern.push_back(2);

CHECK(SequenceHasGapFor(baseSequence, pattern));
}

Die Funktion SequenceHasGapFor ist einfach eine ungebundene Funktion, die zu


keiner Klasse gehört. Sie arbeitet mit einer Repräsentation, die aus primitiven
Typen zusammengesetzt ist - hier Variablen vom Typ unsigned int. Wenn wir
die Funktionalität für SequenceHasGapFor in einem Test-Harnisch entwickeln,
können wir eine recht einfache Funktion in Sequence schreiben, die an die neue
Funktionalität delegiert:

bool Sequence::hasGapFor(Sequence& pattern) const


{
vectorcunsigned int> baseRepresentation = getDurationsCopyC);
vectorcunsigned int> patternRepresentation = pattern.getDurationsCopyC);
return SequenceHasGapFor(baseRepresentation, patternRepresentation);
}

Diese Funktion braucht eine andere Funktion, um ein Array mit der Dauer der
Events abzurufen:

vectorcunsigned int> Sequence::getDurationsCopyC) const


{
vectorcunsigned int> result;
for (vectorcEvent>: :iterator it = events.begin(); it != events.endO; ++it) {
result.push_back(it->duration);
}
return result;

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:

1. Wir haben die interne Repräsentation von Sequence enthüllt.


2. Wir haben es etwas schwerer gemacht, die Implementierung von Sequence zu
verstehen, indem wir einen Teil in eine ungebundene Funktion verlagert haben.
3. Wir haben ungetesteten Code geschrieben (einen Test für getDurations-
Copy () zu schreiben, wäre nicht möglich gewesen).
4. Wir haben Daten in dem System dupliziert.
5. Wir haben das Problem verlängert. Wir haben die harte Arbeit, Dependencies
zwischen unseren Domain-Klassen und der Infrastruktur aufzuheben, noch gar
nicht in Angriff genommen. (Dies ist der zentrale Aspekt, der unsere Arbeit
nennenswert voranbringen wird; und er liegt noch vor uns.)

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.

Primitivize Parameter (23.16) hinterlässt Code in einem ziemlich schlechten


Zustand. Insgesamt ist es besser, den neuen Code in die ursprüngliche Klasse
einzufügen oder Sprout Class (6.2) zu verwenden, um neue Abstraktionen ein-
zufügen, die der weiteren Arbeit als Basis dienen können. Ich nutze Primitivize
Parameter nur, wenn ich meine, Zeit zu haben, die Klasse später unter Testkon-
trolle zu bringen. An diesem Punkt kann die Funktion als echte Methode in die
Klasse eingefügt werden.

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.

25.17 »Pull Up Feature«


Pull Up Feature steht für die Verschiebung einer Funktion in eine Oberklasse.
Manchmal müssen Sie mit einer Gruppe von Methoden einer Klasse arbeiten,

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:

public class Scheduler


{
private List items;

public void updateScheduleItem(ScheduleItem item)


throws SchedulingException {
try {
validate(item);
}
catch (ConflictException e) {
throw new SchedulingException(e);
}
}
private void validate(Scheduleitern item)
throws ConflictException {
// Aufrufe der Datenbank

public int getDeadtimeO {


int result = 0;
for (Iterator it = items.iterator(); it.hasNextO; ) {
Scheduleltem item = (Scheduleltem)it.nextC);
if (item.getTypeO != Scheduleltem.TRANSIENT && notShared(item)) {
result += item.getSetupTimeC) + clockTime();
}
if (item.getTypeO != Scheduleltem.TRANSIENT) {
result += item.finishingTimeO;
}
eise {
result += getStandardFinish(item);
}
}
return result;
}
}

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:

public class Scheduler extends SchedulingServices


{
public void updateScheduleltemCScheduleltem item)
throws SchedulingException {

}
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.

public abstract class SchedulingServices


{
protected List items;

protected boolean notShared(ScheduleItem item) {


}

protected int getClockTimeO {


}

protected int getStandardFinish(ScheduleItem item) {


}

public int getDeadtimeO {

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:

public class TestingSchedulingServices extends SchedulingServices


{
public TestingSchedulingServicesO {
}

public void addItem(ScheduleItem item) {


items.add(item);
}
}
import junit.frarnework.*;

class SchedulingServicesTest extends TestCase


{
public void testGetDeadTimeO {
TestingSchedulingServices services = new TestingSchedulingServicesO;
services.addltem(new Scheduleltem("a", 10, 20, Scheduleitern.BASIC));
assertEquals(2, servi ces .getDeadtimeO) ;
}
}

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

Reihe anderer Aufgaben zuständig, darunter Standardzeiten abzurufen und Leer-


standszeiten zu berechnen. Ein besseres Refactoring wäre es gewesen, Funktionen
von Schedul er an ein Validator-Objekt zu delegieren, das mit der Datenbank
kommunizieren kann; aber wenn dieser Schritt am Anfang zu riskant scheint
oder es andere hinderliche Dependencies gibt, ist Pulling up Function ein guter ers-
ter Schritt. Wenn Sie Preserve Signatures (23.1) und Lean on the Compiler (23.4) ein-
setzen, ist er erheblich weniger riskant. Wir können später immer noch
delegieren, wenn wir mehr Tests eingerichtet haben.

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.

25.18 »Push Down Dependency«


Einige Klassen enthalten nur wenige problematische Dependencies. Sind diese in
wenigen Methodenaufrufen enthalten, können Sie sie mit Subclass and Override
Method (23.21) aus der Methode herauslösen, wenn Sie Tests schreiben. Aber wenn
die Dependencies überall verwendet werden, reicht Subclass and Override Method
vielleicht nicht aus. Möglicherweise müssten Sie Extract Interface (23.10) mehrfach
anwenden, um Dependencies von bestimmten Typen aufzuheben. Push Down
Dependency (Dependency in Unterklasse verschieben) ist eine weitere Option.
Diese Technik hilft Ihnen, problematische Dependencies vom Rest der Klasse zu
isolieren und die Arbeit mit ihr in einem Test-Harnisch zu erleichtern.

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++:

class OffMarketTradeValidator : public TradeValidator


{
private:
Trade& trade;
bool flag;

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)
{}

bool isValidO const {


if (inRange(trade.getDateO)
&& validDestination(trade.destination)
&& inHours(trade) {
flag = true;
}
showMessageO;
return flag;
}
};
Wenn wir unsere Validierungslogik ändern müssen, könnten wir Probleme
bekommen, wenn wir Ul-spezifische Funktionen und Klassen nicht in unseren
Test-Harnisch einbinden wollen. In diesem Fall ist Push Down Dependency eine
brauchbare Option.

395
Kapitel 25
Techniken zur Aufhebung von Dependencies

Der Code sähe nach Push Down Dependency wie folgt aus:

class OffMarketTradeValidator : public TradeValidator


{
protected:
Trade& trade;
bool flag;

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«

class Testi ngOffMarketTradeVali dator


: public OffMarketTradeValidator
{
protected:
Virtual void showMessageO {}
};
Jetzt verfügen wir über eine Klasse, die wir testen können und die keine Depen-
dencies von dem UI hat. Ist die Verwendung der Vererbung für diesen Zweck
ideal? Nein; aber sie hilft uns, einen Teil der Logik einer Klasse unter Testkontrolle
zu bringen. Wenn wir Tests für OffMarketTradeVal idator eingerichtet haben,
können wir anfangen, die Retry-Logik zu säubern und sie aus dem Wi ndowsOff-
MarketTradeVal idator herauszuziehen. Wenn nur die UI-Aufrufe übrig sind,
können wir sie an eine neue Klasse delegieren. Diese neue Klasse enthält dann
allein die UI-Dependencies.

25.18.1 Schritte
Push Down Dependency funktioniert wie folgt:

1. Versuchen Sie, die Klasse mit den Dependency-Problemen in Ihrem Test-Har-


nisch zu kompilieren.
2. Identifizieren Sie die Dependencies, die bei dem Build Probleme gemacht
haben.
3. Erstellen Sie eine neue Unterklasse mit einem Namen, der die besondere
Umgebung dieser Dependencies ausdrückt.
4. Kopieren Sie die Instanzvariablen und Methoden, die die hinderlichen Depen-
dencies enthalten, in die neue Unterklasse. Achten Sie auf die Erhaltung der
Signaturen. Deklarieren Sie Methoden in Ihrer ursprünglichen Klasse. Machen
Sie Methoden protected und abstract und machen Sie Ihre ursprüngliche
Klasse abstrakt.
5. Erstellen Sie eine Test-Unterklasse und ändern Sie Ihren Test so, dass Sie versu-
chen, sie zu instanziieren.
6. Kompilieren Sie Ihre Tests, um zu prüfen, ob Sie die neue Klasse instanziieren
können.

25.19 »Replace Function with Function Pointer«


Wenn Sie Dependencies in prozeduralen Sprachen aufheben müssen, haben Sie
nicht so viele Optionen wie in objektorientierten Sprachen. Sie können Encapsu-
late Global References (25.4) oder Subclass and Override Method (23.21) nicht benut-
zen. Dagegen können Sie Link Substitution (23.13) oder Definition Completion (23.3)

397
Kapitel 25
Techniken zur Aufhebung von Dependencies

verwenden, aber sie sind für kleinere Dependency-Aufhebungen zu umfangreich.


Replace Function with Function Pointer (Funktion durch Funktionszeiger ersetzen)
ist eine Alternative in Sprachen, die Funktionszeiger unterstützen. Die bekann-
teste Sprache dieser Art ist C.

Verschiedene Teams vertreten unterschiedliche Einstellungen zu Funktionszei-


gern. In einigen Teams gelten sie als schrecklich unsicher, weil ihr Inhalt beschä-
digt werden kann und die Aufrufe an Undefinierten Speicherorten landen. In
anderen Teams werden sie als nützliche Tools betrachtet, die vorsichtig gehand-
habt werden müssen. Wenn Sie eher zur zweiten Auffassung neigen, können Sie
mit Funktionszeigern Dependencies aufheben, die mit anderen Techniken nur
schwer oder gar nicht beseitigt werden können.

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:

Sie haben eine Netzwerk-Anwendung, die Paket-Daten in einer Online-Daten-


bank speichert. Sie interagieren mit der Datenbank über Aufrufe, die wie folgt
aussehen:

398
25.19
Replace Function with Function Pointer«

void db_store( struct receive_record *record,


struct time_stamp receive_time);
struct receive_record * db_retrieve(time_stamp search_time);

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);

Dann deklarieren wir einen gleichnamigen Funktionszeiger:

// 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);

Jetzt benennen wir die ursprüngliche Deklaration um:

// 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);

Dann initialisieren wir den Zeiger in einer C-Quelldatei:

// 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

Replace Function with Function Pointer funktioniert wie folgt:

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.

25.20 »Replace Global Reference with Getter«


Globale Variablen können eine echte Qual sein, wenn Sie mit Teilen von Code
unabhängig arbeiten wollen. Dies ist alles, was ich an dieser Stelle darüber sagen
möchte. Ausführlicher lasse ich mich über dieses Thema in der Einführung zu
Introduce Static Setter (25.12) aus. Eine Wiederholung ist hier überflüssig.

400
25-20
»Replace Global Reference with Getter«

Eine Methode, um Dependencies von Globals in einer Klasse aufzuheben, besteht


darin, für jede globale Komponente in der Klasse einen Getter einzuführen. Wenn
Sie den Getter haben, können Sie damit per Subclass and Override Method (23.21)
etwas Geeignetes zurückgeben. In einigen Fällen könnten Sie mit Extract Interface
(23.10) extremer vorgehen, um Dependencies von der Klasse des Globals aufzuhe-
ben. Hier ist ein Beispiel in Java:

public class RegisterSale


{
public void addItem(Barcode code) {
Item newltem = Inventory.getlnventoryO .itemForBarcode(code) ;
i tems.add(newltem);
}
}

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.

public class RegisterSale


{
public void addItem(Barcode code) {
Item newltem = Inventory.getlnventoryO .itemForBarcode(code) ;
items.add(newltem);
}
protected Inventory getlnventoryO {
return Inventory.getlnventoryO;
}
}

Dann ersetzen wir jeden Zugriff auf die globale Komponente durch den Getter.

public class RegisterSale


{
public void addItem(Barcode code) {
Item newltem = getlnventoryO•itemForBarcode(code);
items.add(newltem);
}
protected Inventory getlnventoryO {

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.

public class Fakelnventory extends Inventory


{
public Item itemForBarcode(Barcode Code) {
}

Jetzt können wir die Klasse für unsere Tests schreiben.

class TestingRegisterSale extends RegisterSale


{
Inventory inventory = new FakelnventoryQ;
protected Inventory getlnventoryO {
return inventory;
}
}

2 5 . 2 0 . 1 Schritte

Replace Global Reference with Getter funktioniert wie folgt:

1. Identifizieren Sie die globale Referenz, die Sie ersetzen wollen.


2. Schreiben Sie einen Getter für die globale Referenz. Lockern Sie bei Bedarf den
Zugriffsschutz der Methode so weit, dass Sie den Getter in einer Unterklasse
überschreiben können.
3. Ersetzen Sie Referenzen der globalen Komponente durch Aufrufe des Getters.
4. Erstellen Sie eine Test-Unterklasse und überschreiben Sie den Getter.

25.21 Subdass and Override Method


Subclass and Override Method (Methode ableiten und überschreiben) ist eine Kern-
technik, um in objektorientierten Programmen Dependencies aufzuheben. Tat-
sächlich sind viele andere Techniken in diesem Kapitel Varianten dieser Technik.

402
25-21

Subclass and Override Method

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.

Betrachten wir eine Methode in einer kleinen Anwendung:

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:

class TestingMessageForwarder extends MessageForwarder


{
protected Message createForwardMessage(Session session, Message message) {
Message forward = new FakeMessage(message);
return forward;
}
}

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 Produktions-Code instanziieren wir MessageForwarders; in Tests instanziie-


ren wir TestingMessageForwarders. Wir konnten diese Trennung mit einer
minimalen Änderung des Produktions-Codes durchführen. Wir mussten nur den
Gültigkeitsbereich einer Methode von private in protected ändern.

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.

public class Acount


{
public void deposit(int value) {
balance + = value:
/off.neH'loffMessagefo'a/e. va/c/ej;
/off./7i/s/>{J/

public class TestingAccount extends Account


{
protected void logDeposit(Date date, int value) {
}

Abb. 25.6: Überlagerung von Account mit Testi ngAccount

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

Supersede Instance Variable

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.

25.22 Supersede Instance Variable


(Instanzvariable ersetzen) Objekte in Konstruktoren zu erstellen, kann problema-
tisch sein, insbesondere wenn man sich in einem Test kaum auf diese Objekte ver-
lassen kann. In den meisten Fällen können wir dieses Problem mit Extract and
Override Factory Method (25.7) lösen. Doch in Sprachen, bei denen Aufrufe von vir-
tuellen Funktionen in Konstruktoren nicht zulässig sind, müssen wir andere
Lösungen finden. Eine davon ist Supersede Instance Variable.

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

void sendMessage(const std::string& address, const std::string& message) {


formConnectionO ;

};
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:

class TestingPager : public Pager


{
public:
Virtual void f o r m C o n n e c t i o n O {
}

};
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

Supersede Instance Variable

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.

C++ enthält diese Regel, weil Konstruktor-Aufrufe von überschriebenen virtuellen


Funktionen unsicher sein können. Betrachten Sie folgendes Szenarium:

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.

In C++ hindert uns dieser kleine Schutzmechanismus daran, Verhalten in Kon-


struktoren zu ersetzen. Glücklicherweise stehen uns einige andere Methoden zur
Lösung dieses Problems zur Verfügung. Wenn das Objekt, das Sie ersetzen, in
dem Konstruktor nicht verwendet wird, können Sie die Dependency mit Extract
and Override Getter (23.8) aufheben. Wenn Sie das Objekt benutzen, aber dafür sor-
gen müssen, dass Sie es ersetzen können, bevor eine andere Methode aufgerufen
wird, können Sie Supersede Instance Variable verwenden. Hier ist ein Beispiel:

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:

void BlendingPen::supersedeParameter(Parameter *newParameter)


{
delete m_param;
nuparam = newParameter;
}

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.

Im Allgemeinen ist es unvorteilhaft, Setter zur Verfügung zu stellen, die Basis-


objekte ändern, die von einem Objekt verwendet werden. Mit solchen Settern
können Clients das Verhalten eines Objekts während seiner Existenz tiefgrei-
fend ändern. Wenn jemand solche Änderungen vornehmen kann, müssen Sie
die Geschichte dieses Objekts kennen, wenn Sie verstehen wollen, was passiert,
wenn Sie eine seiner Methoden aufrufen wollen. Code ohne Setter ist einfacher
zu verstehen.

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.

25.23 »Template Redefinition«


Viele Techniken zur Aufhebung von Dependencies in diesem Kapitel stützen sich
auf zentrale objektorientierte Mechanismen wie etwa die Schnittstellen- und Imp-
lementierungsvererbung. Einige neuere Sprachfunktionen stellen zusätzliche
Optionen zur Verfügung. Wenn etwa eine Sprache Generics und eine Methode
zum Aliasing von Typen zur Verfügung stellt, können Sie Dependencies mit einer
Technik namens Template Redefinition (Template umdefinieren) aufheben. Hier ist
ein Beispiel in C++:

// 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

template<typename SOCKET> class AsyncReceptionPortlmpl


{
private:
SOCKET m_socket;
Packet m_packet;
int m_segmentSize;

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

Wollen wir die Dependency von m_contactManager aufheben, können wir


nicht ohne Weiteres Extract Interface (25.10) verwenden, weil die Methode Tem-
plates verwendet, fedoch können wir das Template auf andere Weise parametri-
sieren:
template<typename ArcContactManager> class CollaborationManager
{

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.

25.24 »Text Redefinition«


Einige neuere interpretierte Sprachen stellen Ihnen eine sehr schöne Methode zur
Verfügung, um Dependencies aufzuheben. Wenn die Methoden interpretiert wer-
den, können sie zur Laufzeit umdefiniert werden. Hier ist ein Beispiel in der Spra-
che Ruby:

# 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

Wenn report_deposi t nicht unter Testkontrolle ausgeführt werden soll, können


wir die Methode in der Testdatei umdefinieren und die Tests nach der Umdefini-
tion einfügen:

# AccountTest.rb
require "runit/testcase"
requi re "Account"

class Account
def report_deposit(value)
end
end

# tests Start here


class AccountTest < RUNIT::TestCase

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.

In diesem Anhang beschreibe ich ein grundlegendes Refactoring-Verfahren:


Extract Method (Methode extrahieren). Die Beschreibung soll Ihnen ein Gefühl für
die Vorgehensweise beim Refactoring mit Tests vermitteln.

A.i Extract Method


Von allen Refactoring-Verfahren ist Extract Method das vielleicht nützlichste. Es
basiert auf der Grundidee, große vorhandene Methoden systematisch in kleinere
zu zerlegen. Dies hilft uns, Code leichter zu verstehen. Zusätzlich können wir oft
einzelne Teile wiederverwenden und die Duplizierung von Logik in anderen Berei-
chen unseres System vermeiden.

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:

public class Reservation


{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getßaseFee(amount);
}
eise {
result += (amount * PREMIUM RATE_AD3) + SURCHARGE;
}
return result;
}
}

Die Logik in der el se-Anweisung berechnet die Bearbeitungsgebühren für Pre-


mium-Reservierungen. Wir brauchen diese Logik auch noch an anderen Stellen
unseres Systems. Anstatt den Code zu duplizieren, können wir ihn von hier extra-
hieren und dann an anderen Stellen verwenden.

Hier ist der erste Schritt:

public class Reservation


{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
eise {
// result += (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}
return result;
}
}

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:

public class Reservation


{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
eise {
// result += (amount * PREMIUM_RATE_AD3) + SURCHARGE;
result += getPremiumFeeO;
}
return result;
}
int getPremiumFeeO {
}

Als Nächstes kopieren wir den alten Code in die neue Methode und prüfen, ob er
kompiliert wird:

public class Reservation


{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
eise {
// result += (amount - PREMIUM_RATE_AD3) + SURCHARGE;
result += getPremiumFeeO;
}
return result;
}
int getPremiumFeeO {
result += (amount * PREMIUM RATE_AD3) + SURCHARGE;
}

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:

public class Reservation


{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
eise {
// result += (amount - PREMIUM_RATE_ADJ) + SURCHARGE;
result += getPremiumFee(amount);
}
return result;
}
int getPremiumFee(int amount) {
return (amount * PREMIUM_RATE_ADJ) + SURCHARGE;
}

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:

public class Reservation


{
public int calculateHandlingFee(int amount) {
int result = 0;

if (amount < 100) {


result += getBaseFee(amount);
}
eise {
result += getPremiumFee(amount) ;
}
return result;
}
int getPremiumFee(int amount) {
return (amount * PREMIUM_RATE_ADD) + SURCHARGE;
}

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,

change point (dt. Änderungspunkt)

characterization test (dt. Charakterisierungstest)

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.

coupling count (dt. Kopplungszahl)

effect sketch (dt. Effektskizze)

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.

fake objekt (dt. Fake-Objekt, Platzhalter-Objekt)

Fake-Objekt (engl, fake object, s. Platzhalter-Objekt)

feature sketch (dt. Funktionsskizze)

free function (dt. freie Funktion)

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.

interception point (dt. Abfangpunkt)

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.

pinch point (dt. Engpass, Abschnürung)

Platzhalter-Objekt (engl, fake object) - Ein Objekt, das beim Testen einen Kollaborateur einer
Klasse vertritt.

Programming by Difference (wörtl. etwa Unterschiede programmieren) - Eine Methode, per


Vererbung Funktionen zu objektorientierten Systemen hinzufügen. Mit dieser Methode kann
man oft schnell neue Funktionen zu einem System hinzufügen. Die Tests, mit denen die
neuen Funktionen aktiviert werden, können später dazu verwendet werden, u m den Code in
einen besseren Zustand zu refaktorisieren.

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.

TDD (s. Test-Driven Development)

test coverage (dt. Testabdeckung)

test harness (dt. Testharnisch)

Testabdeckung (engl, test coverage) - »Eine Klasse durch Tests abdecken« bedeutet, Testfälle
f ü r die Klasse zu schreiben.

Test-Driven Development (TDD; dt. wörtl. testgesteuerte Entwicklung) - Ein Entwicklungspro-


zess, der daraus besteht, scheiternde Testfälle zu schreiben und sie zugleich zu erfüllen. Dabei
refaktorisieren Sie den Code, u m ihn so einfach wie möglich zu halten. Code, der per T D D ent-
wickelt wurde, ist standardmäßig durch Tests abgedeckt.

Testharnisch (engl, test harness) - Eine Software-Komponente, die Unit-Tests ermöglicht,

testing subclass (dt. Test-Unterklasse)

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 (dt. Unit-Test)

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

Verhalten hinzufügen 252 Duplizierter Code 283


Verständlichkeit 101 entfernen i n , 114
Code Siehe Software Refactoring 283
Command/Query Separation 1 6 9
Compiler E
als Stütze verwenden 325 Edit and Pray 33
Construction Blob 138 Effect propagation 184
Conversation Scrutiny 241 Effect sketch 176, 421
Coupling Count 313, 421 Effekt
Cover and Modify 33 erkennen 185
CppUnit 74 Effektanalyse 173
CppUnitLite 72, 74 Einkapselung 191
CRC 239 Lehren 188
Naked CRC 238 Tools 186
C-Sprache 249 und IDE-Unterstützung 174
Cunningham, Ward 238 Vorwärtsanalyse 179
Effektfortpflanzung 184
D Effektskizze 175,176, 421
Decorator 96 Änderungspunkte 181
Decorator Pattern 96 Funktionsskizze 269
Wrap Class 98 Software-Qualität 177
Definition Completion 345 vereinfachen 189
in C oder C++ 346 Eingeschränktes-Überschreiben-Dilemma
Delegatar 374 218
Dependency 40, 217 Einkapselung
aufheben 41, 4 3 , 1 0 3 , 335 Effektanalyse 191
Build Dependencies 104 Einschnürpunkte 201
einer Klasse aufheben 127 und Testabdeckung 191
globale 140 Einmal-Dilemma 218
in Legacy Code aufheben 42 Einschnürpunkt 1 9 4 , 1 9 8 , 2 0 0
in Unterklasse verschieben 394 Einkapselungen 201
Include-Dependencies 148 Probleme 203
irritierende Parameter 127 und Design 201
Techniken zur Aufhebung 335 Enabling Point 60
umkehren 107 Encapsulate Global References 347
und Packages 107 Expose Static Method 353
verborgene 134 Extract and Override Call 356
verkettete Konstruktoren 138 Extract and Override Factory Method 138, 358
von Schnittstellen 107 Extract and Override Getter 360
Warum? 45 Extract Implementer 363
zusammentragen 314 Beispiel 366
Dependency-Inversion-Prinzip 107 Extract Interface 1 3 6 , 1 5 3 , 1 5 6 , 368
Design nicht-virtuelle Funktionen 372
Aufgaben erkennen 265 Extract Method 415, 419
Einschnürpunkte 201 Extract What You Know 312
Namensvergabe 363 Extrahieren 312
Refactoring 29
Testbarkeit 1 6 0 F
über Design reden 242 Factory-Methode
verbessern 29 extrahieren 358

424
Stichwortverzeichnis

Failing Test einer Klasse 262


Bestehen garantieren i i i , 114 Heuristik
kompilieren 1 1 0 , 1 1 2 , 113 um Aufgaben zu erkennen 265
schreiben 110, i n , 112 zur Identifizierung von Aufgaben 278
Fake 249 Hill, Mike 76
Fake-Objekt 47, 421 HttpServletRequest 336
Ambivalenz 50 Hyperaware Editing 320
und Tests 49 Hypercard 238
Fassade 274
Feature sketch 267, 421
Feedback 33,103 IDE
Fehler 29 Effektanalyse 174
in Legacy Code finden 2 1 0 Implementierungsebene 274
lokalisieren 37 Implemeter
Fehlerlokalisierung 36, 37 extrahieren 363
Fit 77 Include-Dependencies 148
Fitnesse 78 Instanz-Delegator 374
Flow-Zustand 320 Instanzvariable
Fowler, Martin 69, 335, 415 ersetzen 405
Framework for Integrated Tests 77 Integrierte Tests 77
free function 421 interception point 421
Freie Funktion 421 Interface
Funktion extrahieren 368
durch Funktionszeiger ersetzen 397 Interface-Benennung 369
hinzufügen 27 Interface-Segregation-Prinzip (ISP) 276
in alten Code integrieren 116 Introduce Instance Delegatar 374
in Oberklasse verschieben 390 Introduce Sensing Variable 309
Methodenaufruf extrahieren 356 Introduce Static Setter 143,147, 376
Funktionsskizze 267, 421 Irritierender Parameter 127
Effektskizze 269 Isoliertes Testen 36
Funktionszeiger 397 ISP 276

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

interne Beziehungen suchen 267 Listing Markup 229


konsistente Namen 297 LSP 123
nach dem Extrahieren 281 Programming by Difference 123
Name 243
parallel bearbeiten 261 M
Produktionsklasse 244 Makro-Präprozessor 57, 249
Sprout Class 87 Manuelles Refactoring 308
Strategie der Zerlegung 278 Martin, Robert C. 108
Taktik der Zerlegung 279 Methode
Testklasse 244 ableiten 402
Utility-Klasse 374 Aufruf extrahieren 356
verborgene Entscheidungen suchen 266 einhüllen 91
verborgene Klasse finden 202 extrahieren 230, 415
verkleinern 261 Factory-Methode 358
Wrap Class 95 gleichnamige Methoden 387
Kollaborateur in einem Test-Harnisch ausführen 159
simulieren 47 konsistente Namen 297
testen 47 Methoden gruppieren 265
Konstruktor Monster-Methode 301
parametrisieren 383 Parameter 336
verketteter 138 parametrisieren 386
Kopplung private 159
zeitliche 91 Sequenzen suchen 316
Kopplungszahl 313, 421 skelettieren 316
Sprout Method 83
L statische 353, 374
Lag time 102 Struktur verstehen 229
Lazy Getter 361 testen 159, 209
Lean on the Compiler 165, 280, 325 überschreiben 402
Legacy Code verborgene Methoden suchen 266
ändern 42, 227 Wrap Method 91
Arbeitsmoral 331 zwecks Test auswählen 173
Dependency aufheben 42 Methodengruppierung 265
Erfolgsfaktoren 330 Methodenobjekt
Fehlen von Tests 109 herauslösen 315
Fehler finden 210 Meyer, Bertrand 169, 300
Schwierigkeiten 329 Mock-Objekt 51, 71, 422
Test-Dilemma 247 Monster-Methode 301
und TDD 115 Arten 301
vs. Green-Field-Systeme 329 Aufzählungsmethode 302
Legacy-Code-Dilemma 40 Refactoring 306
Legacy-Projekt tief verschachtelte Methode 303
API-Aufrufe 219 Motivation
schleichendes Wachstum 219 als Erfolgsfaktor 330
Link Seam 422
Link Substitution 382 N
Linker 60 Naked CRC 238, 239
Link-Seam 60 Namensvergabe 363
Liskov Substitution Principle (LSP) 123 Nebeneffekte 1 6 6
Vererbung 124 .NET-Sprache 76

426
Stichwortverzeichnis

Neukompilierung Klasse umbenennen 122


bei Änderungen 107 LSP 123
Nicht-virtuelle Methode Vererbung 1 1 9
vs. virtuelle Methode 218 Projekt
Null Object Pattern 133 Objektorientierung 247
NUnit 76 Prozeduraler Code 248
Pull Up Feature 390
O Push Down Dependency 394
Object Seam 255
Objektorientierung 116 Q
Für und Wider 258 Qualität
von Projekten 247 von Software 177
Objekt-Seam 64, 422
Once-Dilemma 218 R
OO 116 Reasoning Forward 179
und Test-Driven Development 116 Refactoring 120, 415
Opdyke, Bill 69 Anfang finden 286
Open/Closed-Prinzip 300 automatisiertes 70
Optimierung Definition 29, 69
und Refactoring 30 duplizierter Code 283
Orthogonalität 299 Extract Method 415
große Klassen 262
P Katalog der Methoden 335
Package Klasse umbenennen 122
und Dependenies 107 manuelles 308
Pair Programming 320, 327 Scratch Refactoring 230, 277
Parallelarbeit 261 Strategie 316
Parameter 336 Tools 69
Alias-Parameter 154 und Optimierung 30
Zwiebel-Parameter 152 von Monster-Methoden 306
Parameterize Constructor 1 3 6 , 1 4 8 , 383 Vorgehensweise 415
Parameterize Method 148, 386 Refactoring-Tools
Parametertyp Mängel 215
vereinfachen 388 Refaktorisieren 44
Pass Null 132,153 Referenz
Pinch point 198, 422 durch Getter ersetzen 4 0 0
Platzhalter-Objekt 422 einkapseln 347, 352
Pointer 397 Regressionstest 34
Präprozessor 57 Probleme 34
Präprozessor-Direktive 250 Replace Function with Function Pointer 397
Präprozessor-Seam 57 Replace Global Reference with Getter 4 0 0
Preserve Signatures 322 Resignation
Primitivize Parameter 388
bei Änderungen 109
Produktionsklasse 244
Responsibility-Based Extraction 225, 226
Programm
Restricted-Override-Dilemma 218
als Text 53
Risiko
Programmieren
Software-Änderung 31
als Beruf 329
Risikomanagement 32
Programming by Difference 116, 422
Ruby 412
Beispiel 116

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

für Methoden 209 Trennung 45


Harnische 77
integrierte Tests 77 U
Isoliertes Testen 36 Überwachung 45
Methode auswählen 173 Überwachungsvariable 309
mögliche Probleme 36 UML239
schreiben 44 Unit-Test 36, 422
Sinn und Zweck 34 Ausführungsebene 39
Testklassen 243 gute Eigenschaften 37
und Fake-Objekte 49 maximale Dauer 38
Unit-Test 36 Unit-Test-Harnisch 72
Vorgehen mit Unit-Tests 35 Utility-Klasse 374
welche schreiben? 205
wo speichern? 244 V
test coverage 422 Variable
test harness 36, 422 Überwachungsvariable 309
Testabdeckung 34, 39, 422 Variablen-Management 263
und Einkapselung 191 VB.NET 76
Test-Code 243 Verborgene Dependency 134
Test-Dilemma Verborgene Klasse 202
bei prozeduralem Legacy Code 247 Vererbung 326
Test-Driven Development (TDD) 4 4 , 1 1 0 , 422 LSP 124
Algorithmus 11 o Programming by Difference 119
und 0 0 116 Verhalten
Testfall hinzufügen 252
schreiben 110 von Software 28
Test-Framework Verkettete Konstruktoren 138
NUnit 76 Verständlichkeit
Testgesteuerte Entwicklung 110 von Code 101
Testgetriebene Entwicklung 110 Verzögerungszeit 102
Test-Harnisch 36 Virtuelle Methode
C++ 149 vs. nicht-virtuelle Methode 218
Klasse einfügen 127 Vorwärtsanalyse 179
Methode ausführen 159
Testharnisch 422 W
testing subclass 422 Wiederverwendung 217
TestKit 76 Wrap Class 95
Testklasse 244 Decorator Pattern 98
Testpunkt 43 Schritte 98
Test-Tool 72 Wrap Method 91
Test-Unterklasse 422 Nachteile 94
Text Schritte 94
umdefmieren 412 Vorteile 94
Text Redefinition 412
in C und C++ 413
X
in Ruby 413
Tool xUnit-Test-Framework 72
für Effektanalysen 186
Tools Z
für Refactoring 69 Zeitliche Kopplung 91
für Tests 72 Zugriffsschutz 163
Legacy Code 69 Zwiebel-Parameter 152

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

ISBN 978-3-8266-9080-8 / erscheint Anfang 2011 ISBN 978-3-8266-1355-5


Oer Autor des Bestsellers „Vom Mythos des Mann- Nur wenige Bücher über das Projektmanagement
Monats" beschreibt in diesem Buch die Bedeutung bei Software haben sich als so einflussreich und
eines durchdachten Designprozesses, indem er wie- zeitlos gültig erwiesen wie „Vom Mythos des
derkehrende Konstanten in allen Design-Disziplinen Mann-Monats": Fred Brooks bietet hier mit einem
herausstellt und erklärt, was einen guten Designer Mix aus harten Fakten und provokanten Ideen
ausmacht. jedem tiefe Einsichten, der komplexe Projekte zu
managen hat.
Weitere Infos und Probekapitel unter: www.mitp.de
ISBN 978-3-8266-5888-4 ISBN 978-3-8266-5548-7
• Die Ziele und Erwartungen Ihrer User • Kommentare, Formatierung, Strukturierung
untersuchen und verstehen • Fehler-Handling und Unit-Tests
• Die Methode des Goal Directed Designs • Zahlreiche Fallstudien, Best Practices,
anwenden Heuristiken und Code Smells
• Produkte entwickeln, mit den Ihre User
optimal interagieren können

ISBN 978-3-8266-5603-3 ISBN 978-3-8266-5915-7


• Trainieren Sie Ihre Java-Kenntnisse • Gezielter Lernerfolg durch
• Learning by Döing anhand praktischer überschaubare Kapiteleinheiten
Übungen • Vollständige Darstellung -
• Mit vollständigen und kommentierten Schritt für Schritt
Lösungen • Konsequent objektorientiert
programmieren

Weitere Infos und Probekapitel unter: www.mitp.de


ISBN 978-3-8266-5936-2 ISBN 978-3-8266-5885-3
• Grundlagen für das Erstellen von Layouts „Das Großartige an diesem Buch ist, dass es zahlrei-
• Eventhandling, Grafik und Animation, che Handlungsanweisungen enthält - Dinge, die ich
Patterns und Deployment tun kann. Es macht deutlich, dass die Verantwortung
• Vollständiges Beispielprojekt für meine Situation dort liegt, wo sie hingehört - bei
mir. Dieses Buch arbeitet heraus, was ich heute tun
kann. Und morgen. Und im Rest meiner beruflichen
Laufbahn." Kent Beck

ISBN 978-3-8266-9046-4 ISBN 978-3-8266-5898-3


»Ausgehend von ihren jahrelangen Erfahrungen Mike Cohns jahrelange Erfahrungen mit
geben Rachel und Liz neuen Trainern das Vertrauen, User Stories machen dieses Buch zu einer
das sie brauchen, während sie uns alten Hasen noch wertvollen Anleitung, in der er Ihnen
ein paar neue Tricks beibringen.« praxisnah zeigt, wie Sie User Stories in
Russ Rufer, Silicon Volley Patterns Group Ihrem Entwicklungsteam effektiv einsetzen
können.
Deutsche Übersetzung des Klassikers Außerdem zum Thema
von Michael C. Feathers bei mitp:

Mit e i n e m Vorwort von Robert C. Martin,


Autor von Clean Code

Holen Sie mehr aus Ihren Legacy-Systemen heraus:


mehr Performance, Funktionalität, Zuverlässigkeit und Handhabbarkeit
Aus d e m Inhalt: Können Sie Ihren Code leicht ändern? Können Sie
fast unmittelbar Feedback bekommen, wenn Sie
• Die M e c h a n i k v o n
ihn ändern? Verstehen Sie ihn? Wenn Sie eine die-
Software-Änderungen
ser Fragen mit Nein beantworten, arbeiten Sie mit
verstehen:
Legacy Code, der Geld und wertvolle Entwicklungs- ISBN 978-3-8266-5072-7
Features h i n z u f ü g e n ,
zeit kostet.
Fehler b e h e b e n ,
Design verbessern, Feathers erläutert in diesem Buch Strategien für
Performance den gesamten Entwicklungsprozess, um effizient
optimieren mit großen, ungetesteten Code-Basen zu arbeiten.
• Legacy Code in e i n e n Dabei greift er auf erprobtes Material zurück, das
Test-Harnisch b r i n g e n er für seine angesehenen Object-Mentor-Seminare
• Tests s c h r e i b e n , d i e Sie entwickelt hat. Damit hat er bereits zahlreichen
davor schützen, neue Entwicklern, technischen Managern und Testern
Probleme einzuführen geholfen, ihre Legacy-Systeme unter Kontrolle zu
• Genau die Stellen
bringen.
identifizieren, an Darüber hinaus finden Sie auch einen Katalog mit
denen Änderungen 24 Techniken zur Aufhebung von Dependencies,
vorgenommen werden die Ihnen zeigen, wie Sie isoliert mit Programm- ISBN 978-3-8266-5548-7
müssen e l e m e n t e n arbeiten und Code sicherer ändern
• Mit Legacy-Systemen können.
u m g e h e n , die nicht
o b j e k t o r i e n t i e r t sind Über den Autor:
• Mit A n w e n d u n g e n
Michael C. Feathers arbeitet für Object Mentor,
u m g e h e n , die keine
Inc., einem der weltweit führenden Unternehmen
offensichtliche Struktur
für Mentoring, Wissenstransfer und Leadership-
haben
Services bei der Software-Entwicklung. Gegen-
wärtig bietet er weltweit Trainings für Test-Driven
• T e c h n i k e n zur D e v e l o p m e n t (TDD), Refactoring, OO-Design,
Aufhebung von Java, C#, C++ und Extreme Programming (XP) an.
Dependencies Feathers ist der ursprüngliche Autor von CppUnit,
• Techniken, die mit einer C++-Portierung des JUnit-Test-Frameworks,
j e d e r Sprache und und FitCpp, einer C++-Portierung des integrierten
auf jeder Plattform Test-Frameworks FIT. Er ist Mitglied der ACM und
eingesetzt werden des IEEE und war Vorsitzender von CodeFest auf
können - mit drei OOPSLA-Konferenzen.
B e i s p i e l e n in Java, C++,
C u n d C#

www.mitp.de

Das könnte Ihnen auch gefallen