Sie sind auf Seite 1von 480

mitp Deutsche Ausgabe

Robert c. Martin
Robert C. Martin

Clean Code

Refactori n g, Patterns, Testen u nd Tech n i ke n


für sau bere n Cod e

Unter Mitarbeit von:

Michael C. Feathers, Timothy R. Ottinger,


Jefftey J. Langr, Brett L. Schuchert ,
James W. Grenning, Kevin Dean Warnpier
Object Mentor, Inc.

Übersetzt 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
im Internet über http:/ jdnb.d-nb.de abrufbar.

ISBN 978-3-8 266-5548-7


I. Auflage 2009

Alle Rechte, auch die der Übersetzung, vorbehalten. Kein Teil des Werkes darf in irgendei­
ner Form (Druck, Fotokopie, Mikrofilm oder einem anderen Verfahren) ohne schriftliche
Genehmigung des Verlages reproduziert oder unter Verwendung elektronischer Systeme
verarbeitet, vervielfältigt oder verbreitet werden. Der Verlag übernimmt keine Gewähr für
die Funktion einzelner Programme oder von Teilen derselben. Insbesondere übernimmt er
keinerlei Haftung für eventuelle aus dem Gebrauch resultierende Folgeschäden.

Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in die­


sem Werk berechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass sol­
che Namen im Sinne der Warenzeichen- und Markenschutz-Gesetzgebung als frei zu
betrachten wären und daher von jedermann benutzt werden dürften.

Authorized translation from the English language edition, entitled CLEAN CODE: A HAND­
BOOK OF AGILE SOFTWARE CRAFT SMANSHIP, rst Edition, or3235o882 by MARTIN,
ROBERT C., published by Pearson Education, Inc, publishing as Prentice Hall, Copyright ©
2009 Pearson Education, Inc.

All rights reserved. No part ofithis book may be reproduced or transmitted in any form or by
any means, electronic or mechanical, including photocopying, recording or by any information
storage retrieval system, without permission from Pearson Education, Inc. GERMAN language
edition published by mitp-Verlag, VERLAGSGRUPPE HÜTHIG JEHLE REHM GMBH, Copy­
right © 2009.

Printed in Austria
© Copyright 2009 by mitp-Verlag
Verlagsgruppe Hüthig Jehle Rehm GmbH
Heidelberg, München, Landsberg, Frechen, Harnburg
www.it-fachportal.de

Lektorat: Sabine Schulz


Korrektorat Petra Heubach-Erdmann
Satz: III-satz, Husby, www.drei-satz.de
Inhaltsverzeichnis

Vorwort................................................... 15

Einführung................................................ 21

1 Sauberer Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.1 Code, Code und nochmals Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.2 Schlechter Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.3 Die Lebenszykluskosten eines Chaos . . . . . . . . . . . . . . . . . . . . . . . . . . 28
Das große Redesign in den Wolken . . . . . ....................... 29
Einstellung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
Das grundlegende Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
S auberen Code schreiben- eine Kunst? . . . . . . . . . . . . . . . . . . . . . . . . 31
Was ist sauberer Code? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
1 .4 Denkschulen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
I.5 Wir sind Autoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
1.6 Die Pfadf inder-Regel . . . . . . . . . . . . . . . . . ....................... 43
1 .7 Vorläufer und Prinzipien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
1 .8 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

2 Aussagekräftige Namen ........... . . . . . . . . . . . . . . . . . . . . . . . . . . 45


2.1 Einführung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.2 Zweckbeschreibende Namen wählen . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.3 Fehlinformationen vermeiden . . . . . . .......................... 47
2 -4 Unterschiede deutlich machen. . . . . . .......................... 49
2 .5 Aussprechbare Namen verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.6 Suchbare Namen verwenden . . . . . . . .......................... 51
2 .7 Codierungen vermeiden . . . . . . . . . . . .......................... 52
Ungarische Notation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Member-Präfi xe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
Interfaces und Implementierungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.8 M entale M appings vermeiden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2 .9 Klassennamen . . . . . . . . . . . . . . . . . . . .......................... 55
2.10 Methodennamen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55

5
I n h a ltsverzeich n i s

2 .II Vermeiden Sie humorige Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55


2.12 Wählen Sie ein Wort pro Konzept . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.13 Keine Wortspiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2 .J4 Namen der Lösungsdomäne verwenden . . . . . . . . . . . . . . . . . . . . . . . 57
2 . 15 Namen der Problemdomäne verwenden . . . . . . . . . . . . . . . . . . . . . . . 57
2.16 Bedeutungsvollen Kontext hinzufügen . . . . . . . . . . . . . . . . . . . . . . . . 57
2 . 17 Keinen überflüssigen Kontext hinzufügen . . . . . . . . . . . . . . . . . . . . . 60
2.18 Abschließende Worte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60

3 Funktionen .............................................. . 61
3-1 Klein! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
Blöcke und Einrückungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Eine Aufgabe erfüllen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
Abschnitte innerhalb von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . 66
3 ·3 Eine Abstraktionsebene pro Funktion . . . . . . . . . . . . . . . . . . . . . . . . . 67
Code Top-down lesen: die Stepdown-Regel . . . . . . . . . . . . . . . . . . . . . 67
3 -4 Switch-Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3 ·5 Beschreibende Namen verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
3·6 Funktionsargumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
Gebräuchliche monadische Formen . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Flag-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
Dyadische Funktionen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
Triaden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .................... 73
Argument-Objekte . . . . . . . . . . . . . . . . . . . . . .................... 74
Argument-Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
Verben und Schlüsselwörter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
3 ·7 Nebeneffekte vermeiden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
Output-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3·8 Anweisung und Abfrage trennen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
3 ·9 Ausnahmen sind besser als Fehler-Codes . . . . . . . . . . . . . . . . . . . . . . 77
TryJ Catch-Blöck e extrahieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Fehler-Verarbeitung ist eine Aufgabe . . . . . . . . . . . . . . . . . . . . . . . . . 79
Der Abhängigkeitsmagnet Error.java . . . . . . . . . . . . . . . . . . . . . . . . . 79
3 · 10 Don't Repeat Yourself . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3 · II Strukturierte Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
3.12 Wie schreibt man solche Funktionen? . . . . . . . . . . . . . . . . . . . . . . . . . 81
3-13 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
3 - 14 SetupTeardownl ncluder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

6
I n h a ltsverzeich n i s

4 Kommentare ................................. . . . . . . . . . . . . . 8S
4-1 Kommentare sind kein Ersatz für schlechten Code . . . . . . . . . . . . . . . 86
4-2 Erklären Sie im und durch den Code . . . . . . . . . . . . . . . . . . . . . . . . . . 87
4-3 Gute Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ............. 87
Juristische Kommentare . . . . . . . . . . . . . . . . . . . . . . . . ............. 87
Informierende Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Erklärung der Absicht . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
Klarstellungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
Warnungen vor Konsequenzen . . . . . . . . . . . . . . . . . . ............. 90
TODO-Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . ............. 91
Verstärkung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
J avadocs in öffentlichen APi s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4-4 Schlechte Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
C eraune . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ............. 92
Redundante Kommentare . . . . . . . . . . . . . . . . . . . . . . . ............. 93
Irreführende Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9S
Vorgeschriebene Kommentare . . . . . . . . . . . . . . . . . . . ............. 96
Tagebuch-Kommentare. . . . . . . . . . . . . . . . . . . . . . . . . ............. 96
Geschwätz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
Beängstigendes Geschwätz . . . . . . . . . . . . . . . . . . . . . . ............. 99
Verwenden Sie keinen Kommentar, wenn Sie
eine Funktion oder eine Variable verwenden können . . . . . . . . . . . . . 1 00
Positions bezeichner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 00
Kommentare hinter schließenden Klammern . . . . . . . . . . . . . . . . . . . 1 01
Zuschreibungen und Nebenbemerkungen . . . . . . . . . . . . . . . . . . . . . . 1 01
Auskommentierter Code. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 02
HTM L-Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 02
Nicht-lokale Informationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 03
Zu viele Informationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 04
Unklarer Zusammenhang . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 04
Funktions-Header . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 04
J avadocs in nicht -öffentlichem Code . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 OS
Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 OS

5 Formatierung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 09
5.1 Der Zweck der Formatierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 09
5.2 Vertikale Formatierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
Die Zeitungs-Metapher . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
Vertikale Offenheit zwischen Konzepten . . . . . . . . . . . . . . . . . . . . . . . 112

7
I n h a ltsverzeich n i s

Vertik ale Dich te . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113


Vertik aler Abstand . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
Vertik ale Anordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
5·3 Horizontale Formatierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
Horizontale Offenh eit und Dich te . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Horizontale Ausrich tung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
Einrück ung . . . . . . . . . . . . . . . . . . . ............................ 123
Dummy-Bereich e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124
5 ·4 Team-Regeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125
5 ·5 Uncle Bobs Formatierungsregeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

6 Objekte und Datenstrukturen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129


6.r Datenabstrak tion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
6.2 DatenjObj ekt-Anti-Symmetrie ............................... 131
6.3 Das Law of Demeter . . . . . . . . ............................... 133
Z ugk atastroph e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
Hybride . . . . . . . . . . . . . . . . . . . ............................... 135
Struk tur verbergen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
6 -4 Datentransfer-Obj ek te . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
Active Record . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
6 .5 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138

7 Fehler-Handling. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
7. 1 Ausnah men statt Rück gabe-Codes . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
7.2 Try-Catch - Finally-Anweisungen zuerst sch reiben . . . . . . . . . . . . . . . 141
7·3 Unch eck ed Exceptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
7·4 Ausnah men mit Kontext auslösen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
7·5 Defi nieren Sie Exception-Klassen mit Blick auf die
Anforderungen des Aufrufers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
7. 6 Den normalen Ablauf defi nieren . . . . . . . . . . . . . . . .............. 146
7·7 Keine Null zurück geben . . . . . . . . . . . . . . . . . . . . . . .............. 147
7.8 Keine Null übergeben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
7· 9 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150

8 Grenzen .......................... . . . . . . . . . . . . . . . . . . . . . . . 151


8.1 Mit Drittanbieter-Code arbeiten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
8.2 Grenzen erforsch en und k ennen lernen . . . . . . . . . . . . . . . . . . . . . . . 154
8.3 log4j k ennen lernen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
8 .4 Lern-Tests sind besser als kostenlos . . . . . . . . . . . . . . . . . . . . . . . . . . . 156

8
I n h a ltsverzeich n i s

8.5 Code verwenden, der noch nicht existiert . . . . . . . . . . . . . . . . . . . . . . . 1 57


8.6 S aubere Grenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 58

9 Unit-Tests. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 59
9.1 Die drei Gesetze der TDD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 60
9 .2 Tests sauber h alten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 61
Tests ermöglich en die -h eiten und -k eiten . . . . . . . . . . . . . . . . . . . . . . 1 62
9 ·3 S aubere Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 63
Domänenspezifi sch e Testsprach e . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 66
Ein Doppelstandard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 66
9 ·4 Ein assert pro Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 68
Ein Konzept pro Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 70
9 ·5 F .I . R. S .T. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 71
9. 6 Z usammenfassung......................................... 1 72

10 Klassen ....................................... . . . . . . . . . . . . 1 73
10.1 Klassenaufb au . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 73
Eink apselung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 74
10.2 Klassen sollten klein sein! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 74
Fünf Meth oden sind nich t zu viel, oder? . . . . . . . . . . . . . . . . . . . . . . . . 1 76
Das Single-Responsibility-P rinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 76
Koh äsion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 78
Koh äsion zu erh alten, füh rt zu vielen kleinen Klassen . . . . . . . . . . . . 1 79
10.3 Änderungen einplanen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 85
Änderungen isolieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 88

11 Systeme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 91
11.1 Wie baut man eine Stadt? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 91
11.2 Konstruktion und Anwendung eines Systems trennen . . . . . . . . . . . . 1 92
Trennung in main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 93
Facta ries . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 94
Dependency I nj ection . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 95
11.3 Aufwärtssk alierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 96
Cross-Cutting Concerns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 99
11.4 Java-P roxies . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
11.5 Reine Java-AOP -Framework s . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
11.6 AspectJ -Aspekte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
11.7 Die Systemarch itek tur testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205
11.8 Die Entsch eidungst indung optimieren . . . . . . . . . . . . . . . . . . . . . . . . . 207

9
I n h a ltsverzeich n i s

rr. 9 Standards weise anwenden, wenn sie nachweisbar


einen Meh rwert bieten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
rr.ro Systeme brauch en domänenspezifi sch e Sprach en . . . . . . . . . . . . . . . 208
II.II Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 208

rz Emergenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
rz. r Saubere Software durch emergentes Design . . .................. 209
r2.2 Einfach e Design-Regel r : Alle Tests besteh en . . . . . . . . . . . . . . . . . . . 210
r2.3 Einfach e Design-Regeln 2-4: Refactoring . . . . .................. 211
r2.4 Keine Duplizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 211
r2.5 Ausdruck sstärke. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
rz. 6 Minimale Klassen und Meth oden . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215
r2.7 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 215

I3 Nebenläufigkeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 217
rp Warum Nebenläufi gk eit? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
Myth en und falsch e Vorstellungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 219
r3.2 Herausforderungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
r3.3 P rinzipien einer defensiven Nebenläufi gk eitsprogrammierung . ... 221
Single-Responsibility-P rinzip . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Korollar: Besch ränk en Sie den Gültigk eitsbereich
von Daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
Korollar: Arbeiten Sie mit Kopien der Daten. . . . . . . . . . . . . . . . . . . . 222
Korollar: Th reads sollten voneinander so unabh ängig
wie möglich sein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222
r3.4 Lernen Sie Ih re Library k ennen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
Th read-sich ere Collections . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223
r3.5 Lernen Sie Ih re Ausfüh rungsmodelle k ennen . . . . . . . . . . . . . . . . . . 224
Erzeuger-Verbrauch er . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 224
Leser- Sch reiber . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
Ph ilosoph enproblem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 225
r3.6 Ach ten Sie auf Abh ängigk eiten zwisch en
synch ronisierten Meth oden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 226
r3.7 Halten Sie synch ronisierte Absch nitte klein . . . . . . . . . . . . . . . . . . . . 226
r3.8 Korrekten Shutdown-Code zu sch reiben, ist schwer . . . . . . . . . . . . . 227
r3.9 Th readed-Code testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 227
Beh andeln Sie gelegentlich auftretende Feh ler als
potenzielle Th reading-P robleme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228
Bringen Sie erst den Nonth readed-Code zum Laufen . . . . . . . . . . . . 228

10
I n h a ltsverzeich n i s

Mach en Sie Ih ren Th readed-Code pluggable . . . . . . . . . . . . . . . . . . . . 229


Sch reiben Sie anpassbaren Th readed-Code . . . . . . . . . . . . . . . . . . . . . 229
Den Code mit meh r Th reads als P rozessoren ausfüh ren . . . . . . . . . . 229
Den Code auf versch iedenen P lattformen ausfüh ren . . . . . . . . . . . . . 229
Code- Sch eitern durch I nstrumentierung provozieren . . . . . . . . . . . . . 230
Manuelle Codierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 230
Automatisiert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
I3 . IO Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232

I4 Schrittweise Verfeinerung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235


I4-I Args-I mplementierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
Wie h abe ich dies gemacht? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
I4-2 Args: der Roh entwurf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
Desh alb h örte ich auf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
Über ink rementeH e Entwick lung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
I4-3 String-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 258
I4-4 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 300

15 JUnit im Detail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301


15 . 1 D a s J Unit-Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301
I5.2 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 6

16 Refactoring von SerialDate .... . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 7


16.1 Z unäch st bring es zum Laufen! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31 8
I6 .2 Dann mach es rich tig! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 320
1 6 .3 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 336

17 Smells und Heuristiken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337


I7. I Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
CI: Ungeeignete I nformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
C2: Überh olte Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
C} Redundante Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
C+ Sch lech t gesch riebene Kommentare . . . . . . . . . . . . . . . . . . . . . . . . 338
Cs : Ausk ommentierter Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
I7.2 Umgebung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
E I: Ein Build erfordert meh r als einen Sch ritt . . . . . . . . . . . . . . . . . . . 339
E2: Tests erfordern meh r als einen Sch ritt . . . . . . . . . . . . . . . . . . . . . . 339
I7 ·3 Funktionen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
F I: Z u viele Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
F2: Output-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340

11
I n h a ltsverzeich n i s

F3: Plag-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340


F4: Tote Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
17.4 Allgemein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
Gr: Meh rere Sprach en in einer Q uelldatei . . . . . . . . . . . . . . . . . . . . . 340
G2: Offensich tlich es Verh alten ist nich t implementiert. . . . . . . . . . . 341
G3: Falsch es Verh alten an den Grenzen . . . . . . . . . . . . . . . . . . ..... 341
G4 : übergangene Sich erungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341
Gs : Duplizierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
G 6 : Auf der falsch en Abstraktionsebene codieren . . . . . . . . . . . . . . . 342
GT Basisld asse h ängt von abgeleiteten Klassen ab . . . . . . . . . . . . . . . 344
G8: Z u viele I nformationen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 344
G 9 : Toter Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 345
Gro: Vertik ale Trennung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 345
Gn : I nk onsistenz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 345
Gr2: Müll . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
G13: Künstlich e Kopplung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 346
GI+ Funktionsneid . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 346
C rs : Selektor-Argumente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Gr6: Verdeckte Absich t . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 348
GrT Falsch e Z uständigk eit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 349
Gr8: Fälsch lich als statisch deklarierte Meth oden . . . . . . . . . . . . . . . 350
G1 9 : Aussagek räft ige Variablen verwenden . . . . . . . . . . . . . . . . . . . . 350
G2o: Funktionsname sollte die Aktion ausdrück en . . . . . . . . . . . . . . 351
G21: Den Algorith mus versteh en . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
G22: Logisch e Abh ängigk eiten in physisch e umwandeln . . . . . . . . . 352
G23: P olymorph ismus statt I f/Else oder Switchj Case
verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
G24: Konventionen beach ten . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 354
G25 : Magisch e Z ah len durch benannte Konstanten ersetzen . ..... 354
G26: P räzise sein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ..... 355
G2T Struk tur ist wich tiger als Konvention . . . . . . . . . . . . . . . . . . . . . 356
G28: Bedingungen eink apseln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
G2 9 : N egative Bedingungen vermeiden . . . . . . . . . . . . . . . . . . . . . . . 356
G3o: Eine Aufgabe pro Funk tion! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 356
G31: Verborgene zeitlich e Kopplungen . . . . . . . . . . . . . . . . . . . ..... 357
G32: Keine Willkür . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
G33: Grenzbedingungen eink apseln . . . . . . . . . . . . . . . . . . . . . . . . . . 359
G34: I n Funktionen nur eine Abstrak tionsebene tiefer geh en . . . . . 359

12
I n h a ltsverzeich n i s

G35 : Konfi gurierbare Daten h och ansiedeln . . . . . . . . . . . . . . . . . . . . . 361


G3 6 : Transitive Navigation vermeiden . . . . . . . . . . . . . . . . . . . . . . . . . 362
17.5 Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 362
Jl : Lange I mportlisten durch Platzh alter vermeiden . . . . . . . . . . . . . . 362
J2: Keine Konstanten vererben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 363
J3: Konstanten im Gegensatz zu Enums . . . . . . . . . . . . . . . . . . . . . . . . 364
17.6 Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
Nr: Deskriptive Namen wählen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
N2: Namen sollten der Abstraktionsebene entsprech en . . . . . . . . . . . 367
N3: Möglich st die Standardnomenklatur verwenden . . . . . . . . . . . . . . 368
N+ Eindeutige Namen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 368
Ns : Lange Namen fü r große Geltungsbereich e . . . . . . . . . . . . . . . . . . 369
N 6 : Codierungen vermeiden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 369
NT Namen sollten Nebeneff ekte besch reiben . . . . . . . . . . . . . . . . . . . 370
17.7 Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
TI: Unzureich ende Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
T2 : Ein Coverage-Tool verwenden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 370
T3 : Triviale Tests nich t überspringen . . . . . . . . . . . . . . . . . . . . . . . . . . 370
T4 : Ein ignorierter Test zeigt eine Meh rdeutigk eit auf . . . . . . . . . . . . 371
Ts : Grenzbedingungen testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
T6: Bei Bugs die Nach barsch aft gründlich testen . . . . . . . . . . . . . . . . . 371
TT Das Muster des Sch eiterns zur Diagnose nutzen . . . . . . . . . . . . . . 371
T8 : Hinweise durch Coverage-P atterns . . . . . . . . . . . . . . . . . . . . . . . . . 371
T 9 : Tests sollten sch nell sein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
17.8 Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372

A Nebenläufigkeit II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
A.r ClientJ Server- Beispiel . . . . . . . . . .............................. 373
Der Server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
Threading h inzufü gen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Server- Beobach tungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
A.2 Möglich e Ausfüh rungspfade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 377
Anzah l der P fade . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 378
Tiefer graben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
Z usammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
A.3 Lernen Sie Ih re Library k ennen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
Executor Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
I n h a ltsverzeich n i s

Nich t block ierende Lösungen . . . . . . . . . . . . . . .................. 384


Nich t th read-sich ere Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
A.4 Abh ängigk eiten zwisch en Meth oden k önnen
nebenläufi gen Code besch ädigen . . . . . . . . . . . .................. 387
Das Sch eitern tolerieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
Clientbasiertes Lock ing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
S erverbasiertes Lock ing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
A.s Den Durch satz verbessern . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
Single-Th read-Berech nung des Durch satzes . . .................. 392
Multith read-Berech nung des Durch satzes . . . . . . . . . . . . . . . . . . . . . 393
A.6 Deadlock . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 393
Gegenseitiger Aussch luss . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
Sperren & warten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39 5
Keine präemptive Aktion. . . . . . . . . . . . . . . . . . .................. 395
Zirk uläres Warten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
Den gegenseitigen Ausschluss aufh eben . . . . . . . . . . . . . . . . . . . . . . 396
Das Sperren & Warten aufh eben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
Die P räemption umgeh en . . . . . . . . . . . . . . . . . .................. 397
Das zirk uläre Warten umgeh en . . . . . . . . . . . . .................. 397
A.7 Multith readed-Code testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 398
A.8 Th readbasierten Code mit Tools testen . . . . . . . . . . . . . . . . . . . . . . . 401
A. 9 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . .................. 402
A.Io Tutorial: k ompletter Beispielcode . . . . . . . . . . . .................. 402
Clientj Server oh ne Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
Clientj Server mit Threads . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406

B org.jfree.date.SerialDate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 407

C Literaturverweise . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 463

Epilog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 465

Stichwortverzeichnis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467

14
Vorwort

Bei uns in Dänemark zäh lt Ga- J ol zu den beliebtesten Süßigk eiten. Sein stark er Lak­
ritzduft ist ein perfek tes Mittel gegen unser feuch tes und oft k üh les Wetter. Der
Ch arme, den Ga-Jol für uns Dänen entfaltet, liegt auch an den weisen oder geist­
reich en Sprüch en, die auf dem Deck el j eder P ackung abgedruckt sind. I ch h abe mir
h eute Morgen eine Doppelpack ung dieser Köstlichk eit gek auft und fand darauf das
folgende alte dänisch e Sprich wort:
JErlighed i sma ting er ikke nagen lille ting.
Ehrlichkeit in kleinen Dingen ist kein kleines Ding. Dies war ein gutes Omen. Es passte
zu dem, was ich h ier sagen wollte. Kleine Dinge spielen eine Rolle. I n diesem Buch
geh t es um besch eidene Belange, deren Wert dennoch beträch tlich ist.
Gott steckt in den Details, sagte der Arch itekt Ludwig Mies van der Roh e. Dieses Zitat
erinnert an zeitgenössisch e Auseinandersetzungen über die Rolle der Arch itek tur
bei der Software-Entwicklung und insbesondere in der Agilen Welt. Bob und ich
füh ren gelegentlich h eiße Disk ussionen über dieses Th ema. Und ja, Mies van der
Roh e war seh r an der Nützlichk eit und der Z eitlosigk eit der Formen des Bauens
interessiert, auf denen großartige Arch itek tur basiert. Andererseits wäh lte er auch
persönlich j eden Türk nopffür j edes Haus aus, das er entworfen h atte. Wa rum? Weil
k leine Aufg aben eine Rolle spielen.
Bei unserer laufenden » Debatte« über T D D h aben Bob und ich festgestellt, dass wir
beide der Auffassung sind, dass die Software-Arch itektur eine wich tige Rolle bei der
Entwicklung spielt, obwoh l wir wah rsch einlich versch iedene Vorstellungen davon
h aben, was gerr au das bedeutet. Doch solch e Diff erenzen sind relativ unwich tig,
weil wir davon ausgeh en k önnen, dass verantwortungsbewusste P rofis am Anfang
eines P roj ekts eine gewisse Z eit über seinen Ablauf und seine Planung nach denk en.
Die Vorstellungen der späten 1 9 9 oer-Jah re, dass Design nur durch Tests und den
Code vorangetrieben werden k önnte, sind längst passe . Doch die Aufmerk samk eit
im Detail ist ein noch wesentlich erer Aspekt der P rofessionalität als Visionen im
Großen. Erstens: Es ist die Übung im Kleinen, mit der P rofis ih r Können und ih r
Selbstvertrauen entwick eln, sich an Größeres h eranzuwagen. Zweitens: Die
k leinste Nach lässigk eit bei der Konstruktion, die Tür, die nich t rich tig sch ließt, die
missratene Kach el auf dem Fußboden oder sogar ein unordentlich er Sch reibtisch
k önnen den Ch arme des größeren Ganzen mit einem Sch lag ruinieren. Darum
geh t es bei sauberem Code.
Vorwo rt

Dennoch bleibt die Arch itek tur nur eine Metaph er für die Software-Entwicklung
und insbesondere die Ph ase der Software, in der das anfänglich e P rodukt etwa in
derselben Weise entsteh t, wie ein Arch itek t ein neues Gebäude h ervorbringt. I n der
h eutigen Z eit von Serum und Agile geh t es h auptsächlich darum, ein P roduk t
sch nell auf den Markt z u bringen. Die Fabrik soll mit h öch ster Kapazität Software
produzieren. Nur: Hier geh t es um menschlich e Fabrik en, denk ende und füh rende
P rogrammierer, die eine Aufg abenliste abarbeiten oder sich bemüh en, anh and von
Benutzer- Stories ein brauchbares P roduk t zu erstellen. Ein solch es Denk en wird
von der Metaph er der Fabrik dominiert. Die P roduk tionsaspekte der Herstellung
von Autos in Japan, also einer von Fließbändern dominierten Welt, h aben viele
I deen von Serum inspiriert.
Doch selbst in der Automobilindustrie wird der Hauptteil der Arbeit nich t bei der
P roduktion, sondern bei der Wartung geleistet- oder bei dem Bemüh en, sie zu ver­
meiden. Bei der Software werden die 8o oder meh r P rozent unserer Tätigk eit anh ei­
melnd als »Wartung« bezeich net, ein besch önigendes Wort f ür »Reparatur«. Statt
uns also auf typisch e westlich e Weise auf die Produktion guter Software zu k onzen­
trieren, sollten wir anfangen, eh er wie ein Hausmeister bei der Gebäudeinstand­
h altung oder wie ein Kfz -Mech anik er bei der Reparatur von Autos zu denk en. Was
h aben j apanisch e Managementweish eiten dazu zu sagen?
Etwa 1 95 1 ersch ien ein Q ualitätsansatz namens Total P roductive Maintenance
(TP M ; »Umfassendes Management des P roduktionsprozesses«, der Ausdruck wird
nicht ins Deutsch e übersetzt) in der j apanisch en Szene. Er k onzentrierte sich weni­
ger auf die P roduktion, sondern meh r auf die Wartung. Eine der fünf Säulen des
TP M ist der Satz der so genannten 5 S -P rinzipien. 5 S bezieh t sich auf einen Satz von
Disziplinen oder Tätigk eitsbereich en. Tatsächlich verk örpern diese 5 S -P rinzipien
die Bedeutung von Lean - einem anderen Modewort in westlich en P roduk tions­
k reisen, das zuneh mend auch in der Software-Entwicklung verwendet wird. Diese
P rinzipien sind nich t optional. Wie Uncle Bob auf der Titelseite sagt, erfordert die
Software-P raxis eine gewisse Disziplin: Fokus, Aufmerk samk eit und Denk en. Es
geh t nich t immer nur darum, ak tiv zu sein und die Fabrik ausrüstung mit der opti­
malen Geschwindigk eit ZU betreiben. Die s S -Ph ilosoph ie umfasst die folgenden
Konzepte:
• Seiri oder Organisation (Eselsbrück e: »sortieren«) . Zu wissen, wo sich Dinge
befinden, ist erfolgsentsch eidend. Dazu zählen zum B eispiel Ansätze wie das
Vergeben geeigneter Namen. Wenn Sie meinen, die B enennung Ih rer B ezeich ­
ner sei nich t wich tig, sollten Sie aufj eden Fall die folgenden Kapitel lesen.
• Seiton oder Ordentlichk eit (Eselsbrück e : »aufräumen«) . Ein altes Sprichwort
sagt: Ein Platz für alles, und alles an seinem Platz. Ein Code-Ab sch nitt sollte da
steh en, wo Sie ih n zu finden erwarten; und falls er nich t da steh t, sollten Sie ein
Refactoring von Ih rem Code vorneh men, so dass er danach an der erwarteten
Stelle steh t.
• Seiso oder Sauberk eit (Eselsbrücke: »wienern«) . Sorgen Sie dafür, dass der Ar­
beitsplatz frei von h erumh ängenden Dräh ten, Müllspuren, Einzelteilen und
Abfall ist. Was sagen die Autoren h ier über die Vermüllung Ih res Codes mit
Kommentaren und ausk ommentierten Codezeilen, die den Verlaufi der Ent­
wicklung oder Wünsch e für die Zukunft wiedergeben? Weg damit.
• Seiketsu oder Standardisierung. Die k ommt zu einem Konsens darüber, wie der
Arbeitsplatz sauber geh alten werden soll. Hat dieses Buch etwas über einen
k onsistenten Codierstil und gemeinsam in der Gruppe befolgte P raktik en zu
sagen? Wo k ommen diese Standards h er? Lesen Sie weiter.
• Shutsuke oder Disziplin ( Selbst disziplin) . Dies bedeutet, die Disziplin aufz u­
-

bringen, den P raktik en zu folgen, seine Arbeit regelmäßig zu überdenk en und


bereit zu sein, sich zu ändern.
Wenn Sie die Herausforderung- j awohl, die Herausforderung- anneh men, dieses
Buch zu lesen und anzuwenden, werden Sie den letzten Punkt versteh en und sch ät­
zen lernen. Hier k ommen wir sch ließlich zu den Wurzeln einer verantwortlich en
professionellen Einstellung in einem Beruf, der sich um den Lebenszyklus eines
P roduktes kümmern sollte. So wie Automobile und andere Masch inen unter TP M
vorbeugend gewartet werden, sollte eine Wartung im Falle eines Zusammenbruch s
- also darauf zu warten, dass die Bugs sich zeigen- die Ausnah me sein. Stattdessen
sollten Sie eine Ebene h öh er ansetzen: I nspizieren Sie die Masch inen j eden Tag und
tausch en Sie Versch leißteile aus, bevor sie k aputtgeh en, oder lassen Sie den sprich­
wörtlich en Ölwech sel vorneh men, um die Abnutzung des Motors möglich st zu ver­
ringern. Für Ih ren Code bedeutet das : Neh men Sie ein gnadenloses Refa ctoring vor.
Sie k önnen mit der Verbesserung noch eine Ebene h öh er ansetzen, wie es die TP M ­
Bewegung innovativ vor über 50 Jah ren vorgemach t h at: Bauen Sie Masch inen, die
von vornh erein wartungsfreundlich er sind. Code lesbar zu mach en, ist genauso
wich tig, wie ih n ausfüh rbar zu mach en. Die ultimative P raxis, die in TP M-Kreisen
etwa 1 9 6 0 eingefüh rt wurde, besteht darin, die alten Masch inen vorbeugend durch
vollk ommen neue zu ersetzen. Wie Fred Brook s anmah nt, sollten wir wah rsch ein­
lich Hauptteile unserer Software etwa alle sieben Jah re von Grund auf erneuern, um
die schleich ende Ansammlung von Cruft (Jargon: Müll, Staub, Unrat) zu beseitigen.
V:ielleich t sollten wir die von Brook s genannte Z eitspanne drastisch reduzieren und
nich t von Jah ren, sondern von Woch en, Tagen oder gar Stunden sprech en. Denn
dort fi nden sich die Details.
Details h aben eine mäch tige Wirkungsk raft. Dennoch h at dieser Ansatz etwas
Besch eidenes und Grundsätzlich es, so wie wir es vielleich t stereotyp von einem
Ansatz erwarten, der j apanisch e Wurzeln für sich in Anspruch nimmt. Aber dies ist
nich t nur eine k östlich e Art, das Leben zu seh en. Auch die westlich e Volk sweish eit
enth ält zah lreich e äh nlich e Sprichwörter. Das Seiton-Z itat von oben floss auch aus
der Feder eines P riesters in Ohio, der Ordentlichk eit buch stäblich »als Gegenmittel
für j ede Art von Bösem« sah . Was ist mit Seiso? Sauberkeit kommt gleich nach Gött-

17
Vorwo rt

lichkeit. So sch ön ein Haus auch sein mag, ein unordentlich er Tisch raubt ih m sei­
nen ganzen Glanz. Was bedeutet Sh utsuk e in diesen kleinen Dingen? Wer an wenig
glaubt, glaubt an viel. Wie wär's damit, das Refactoring des Codes rech tzeitig durch ­
zufüh ren, um seine Position fü r folgende »große« Entsch eidungen zu stärk en, als
die Aufg abe aufzusch ieben? Ein Stich zur rechten Zeit ersP.art dir neun weitere. Wer
früh kommt, mahlt zuerst. Was du heute kannst besorgen, das verschiebe nicht auf mor­
gen. (Dies war die ursprünglich e Bedeutung des Ausdruck s »der letzte tragbare
Moment« bei Lean, bevor er in die Hände von Software-Beratern fiel.) Was ist mit
der Ordnung eines Arbeitsplatzes im Kleinen im Vergleich zum großen Ganzen?
Auch die größte Eiche wächst aus einer kleinen Eichel. Oder wie ist es damit, einfach e
vorbeugende Arbeiten in den Alltag einzubauen? Vorsicht ist besser als Nachsicht. Ein
Apfel am Tag hält den Doktorfern. Sauberer Code anerk ennt die verwurzelte Weish eit
unserer Kultur im Allgemeinen, oder wie sie einmal war, oder wie sie sein sollte,
oder wie sie sein könnte, indem er den Details die sch uldige Aufmerk samk eit
sch enkt.
Selbst in der Literatur über große Arch itektur greifen Autoren auf Sprichwörter
über die Bedeutung von Details zurück . Denk en Sie an die Türgriffe von Mies van
der Roh e. Das ist Seiri. Das bedeutet, sich um j eden Variablennamen zu k ümmern.
Sie sollten Variablennamen mit derselben Sorgfalt auswäh len wie den Namen eines
erstgeborenen Kindes.
Jeder Hausbesitzer weiß, dass eine solch e P flege und fortwäh rende Verbesserung
niemals zu Ende ist. Der Arch itekt Ch ristoph er Alexander- Vater der Patterns und
P attern- Sprach en - betrach tetj eden Design -Ak t selbst als einen kleinen, lok alen Ak t
der Reparatur. Und er betrach tet die Anwendung des Könnens auffeine Strukturen
als den einzigen legitimen Arbeitsbereich des Arch itekten; die größeren Formen
k önnen den Patterns und deren Anwendung durch die Bewoh ner überlassen wer­
den. Design geh t immer weiter. Es betrifft nich t nur den Anbau neuer Räumlich ­
k eiten, sondern auch profanere Aufg aben wie ein neuer Anstrich , das Ersetzen
eines abgelaufenen Teppich s oder die Modernisierung der Spüle in der Küch e . I n
den meisten Künsten werden äh nlich e Überzeugungen vertreten. Bei unserer
Such e nach anderen, die das Haus Gottes in den Details seh en, stießen wir bei­
spielsweise aufden französisch en Autor Gustav Flaubert aus dem 1 9 . Jah rh undert.
Der französisch e Dich ter Paul Valery sagt uns, ein Gedich t wäre niemals fertig und
müsse laufend überarbeitet werden; mit dieser Arbeit aufz uh ören wäre vergleich ­
bar damit, dieses Gedich t zu verwerfen. Eine solch e Besessenh eit von Details ist
allen Bemüh ungen gemeinsam, die auf Exzellenz, also aufh erausragende Leistun­
gen gerich tet sind. Also: Vielleich t gibt es h ier wenig Neues, aber wenn Sie dieses
Buch lesen, werden Sie h erausgefordert, die guten Disziplinen wieder aufzuneh ­
men, die Sie vor langer Z eit aus Apath ie, dem Wunsch nach Spontanität oder ein­
fach deswegen aufg egeben h aben, weil Sie »etwas anderes« mach en wollten.
Leider betrach ten wir solch e Überlegungen normalerweise nich t als Grundbau­
steine der Kunst der P rogrammierung. Wir entlassen unseren Code früh aus unse-
rer Obh ut, nicht, weil er fertig ist, sondern weil unser Wertesystem meh r auf die
äußere Ersch einung als auf die Substanz des gelieferten P roduk ts gerich tet ist.
Diese fehlende Aufmerk samk eit k ommt uns letztendlich teuer zu steh en: I rgend­
wann mach en sich die Defek te immer bemerkbar. Weder in ak ademisch en Kreisen
noch in der I ndustrie begibt sich die Forsch ung in die niedrigen Ebenen h inunter,
Code sauber zu h alten. Früh er, als ich noch bei der Bell Labs Software P roduction
Research Organization (also wirklich P roduktion!) besch äftigt war, mach ten wir die
beiläufige Entdeck ung, dass ein k onsistenter Stil beim Einrück en von Klammern
der statistisch signifik anteste I ndik ator für eine geringe Feh lerh äufigk eit war. Wir
wollen, dass die Arch itektur, die P rogrammiersprach e oder irgendein anderes h och
angesiedeltes Konzept die Ursach e für Q ualität sein soll. Als Entwiclder, deren vor­
geblich e P rofessionalität auf der Meisterung von Werk zeugen und abgeh obenen
Design-Meth oden basiert, fühlen wir uns von dem Meh rwert beleidigt, den diese
Masch inen aus der Fabrikh alle, pardon! , Codierer, erzielen, indem sie einfach einen
bestimmten Stil der Einrück ung von Klammern k onsistent anwenden. Um mein
eigenes Buch zu zitieren, das ich vor r7 Jah ren gesch rieben h abe: Ein solch er Stil
untersch eidet Exzellenz von bloßer Kompetenz. Die j apanisch e Weltsich t versteh t
den entsch eidenden Beitrag j edes gewöh nlich en Arbeiters und meh r noch , wie Ent­
wicklungssysteme von den einfach en gewöh nlich en Ak tionen dieser Arbeiter
abh ängen. Q ualität ist das Ergebnis eine Million selbstloser Akte der Sorgfalt- nich t
nur das einer großartigen Meth ode, die vom Himmel gefallen ist. Dass diese Ak te
einfach sind, bedeutet nicht, dass sie simplizistisch (einfältig) sind. Es bedeutet
auch nich t, dass sie leich t sind. Dennoch sind sie der Stoff, aus dem die Größe und
meh r noch die Sch önh eit menschlich er Anstrengungen gemach t ist. Wer diese
Ak te ignoriert, h at noch nich t sein volles menschlich es P otenzial realisiert.
Natürlich befü rworte ich immer noch , auch den größeren Rah men zu betrach ten
und besonders den Wert von arch itektonisch en Ansätzen zu berück sich tigen, die in
einem tiefen Wissen sowoh l des P roblembereich es als auch der Software-Usability
verwurzelt sind. Doch darum geh t es in diesem Buch nich t - zumindest nich t vor­
dergründig. Dieses Buch will eine subtilere Botsch aft verbreiten, deren Gewich tig­
k eit nich t untersch ätzt werden sollte. Sie passt zu dem gegenwärtigen Credo
wirklich codebasierter Entwickler wie Peter Sommerlad, Kevlin Henney und Gio­
vanni Asproni. Ih re Mantras lauten: »Der Code ist das Design« und »Einfach er
Code«. Wäh rend wir darauf ach ten müssen, dass die Sch nittstelle das P rogramm ist
und dass ih re Struk tur viel über die Struk tur unseres P rogrammes aussagt, ist es
von erfolgsentsch eidender Bedeutung, dass wir uns laufend mit der besch eidenen
Einstellung identifizieren, dass das Design tatsächlich nur im Code existiert. Und
wäh rend eine Überarbeitung in der Metaph er der fabrik mäßigen P roduk tion zu
h öh eren Kosten füh rt, füh rt sie beim Design zu einem Mehrwert. Wir sollten unse­
ren Code als den wundersch önen Ausdruck edler Design-Anstrengungen betrach ­
ten- Design als P rozess, nich t als statisch er Endpunkt. Nur i m Code lassen sich
letztlich die arch itektonisch en Metrik en der Kopplung und Koh äsion nachweisen.
Wenn Larry Constantine Kopplung und Koh äsion besch reibt, sprich t er von Code
Vorwo rt

- nich t von abgeh obenen abstrakten Konzepten, die man vielleich t in UML findet.
Rich ard Gabriel rät uns in seinem E ssay Abstraction Descant, Abstraktion sei böse.
Code ist anti-böse, und sauberer Code ist vielleich t göttlich .
Zurück zu meiner kleinen Packung Ga- Jol: I ch glaube, dass es wich tig ist, anzumer­
k en, dass uns die dänisch e Weish eit nich t nur rät, unsere Aufmerk samk eit auf die
kleinen Aufgaben zu lenk en, sondern auch , bei kleinen Aufgaben eh rlich zu sein.
Dies bedeutet, wir müssen dem Code gegenüber eh rlich sein, wir müssen unseren
Kollegen gegenüber über den Zustand unseres Codes eh rlich sein und vor allem,
wir müssen uns selbst gegenüber über den Zustand unseres Codes eh rlich sein.
Haben wir unser Bestes gegeben, um den »Campingplatz sauberer zu h interlassen,
als wir ih n gefunden h aben«? Haben wir das Refactoring des Codes erledigt, bevor
wir ih n eingech eckt h aben? Dies sind k eine nebensäch lich en Belange, sondern
Belange, die zum Kern agiler Werte geh ören. Zum Beispiel empfieh lt Serum, das
Refactoring zu einem Bestandteil des Konzepts »Done« (»Fertig«) zu mach en.
Weder Arch itektur noch sauberer Code verlangt nach Perfektion, sondern nur nach
Ehrlichk eit und dem Bemüh en, unser Bestes zu geben. I rren ist mensch lich , ver­
geben göttlich . Bei Serum mach en wir alles sichtbar. Wir zeigen unsere sch mutzige
Wäsch e. Wir sind eh rlich über den Zustand unseres Codes, weil Code niemals per­
fekt ist. Wir werden mensch lich er, werden würdiger, das Göttlich e zu empfangen,
und näh ern uns der Größe in den Details.
In unserem Beruf brauch en wir verzweifelt alle Hilfe, die wir bek ommen k önnen.
Wenn ein sauberer Fabrikboden die Anzahl der Unfälle reduziert und woh lgeord­
nete Werk zeugkästen die P roduktivität verbessern, dann k ann man dies nur befür­
worten. Was dieses Buch angeht: Es ist die beste pragmatisch e Anwendung von
Lean-P rinzipien auf Software, die ich j e in Druck form geseh en h abe. Aber weniger
h atte ich von dieser praktisch en kleinen Gruppe denk ender I ndividuen auch nich t
erwartet, die sich nich t nur seit Jah ren darum bemüh en, immer besser z u werden,
sondern auch ih r Wissen an die Branch e weiterzugeben. Dieses Buch ist ein Aus­
druck dieses Bemüh ens. Es h interlässt die Welt ein wenig besser, als ich sie vorfand,
bevor Uncle Bob mir das Manusk ript sch ickte.
Doch genug von meinen abgeh obenen Einsich ten. I ch mus s meinen Sch reibtisch
aufräumen.
James 0 . Coplien

M0rdrup, Dänemark

20
Einführun g

Th e o rvLy VA c id M e ASY �t.e ne rvr­


oF ccde.. O G\ A l.- 'r r '{ : WTT s I1"1 i "' t.n-e.

,I
-

ßAo\ c o ol. e.. .


Good c ock_ .
Abb. 1 : Abdruck m it fre u n d l icher G e n eh m i g u n g von Thom H olwerd a
( h t t p : // www . o s n ews . com / s t o r y / 192 66 / WTFs_m)

Welche Tür repräsentiert I hren Code? Welche Tür repräsentiert I hr Team oder I hr
Unternehmen ? Warum sind wir in diesem Raum? Geht es nur um einen normalen
Code-Review oder haben wir eine Reihe schrecklicher Probleme gefunden, die kaum
geringer als das Leben sind? Debuggen wir in Panik; brüten wir über Code, der unse­
rer Meinung nach funktionieren sollte? Laufen uns die Kunden in Scharen davon
und hängen uns die Manager im Nacken? Wie können wir dafür sorgen, dass wir
hinter der richtigen Tür landen, wenn es heiß hergeht? Die Antwort ist: Könnerschaft.
Können zu erwerben, hat zwei Aspekte: Wissen und Arbeit. Sie müssen sich Prin­
zipien, Patterns, Techniken und Heuristiken aneignen, die jeder Fachmann kennt,
und Sie müssen dieses Wissen in I hre Finger, I hre Augen und I hren Bauch einprä­
gen, indem Sie hart arbeiten und üben.
I ch kann I hnen die physikalischen Gesetzmäßigkeiten des Fahrradfahrens vermit­
teln. Tatsächlich ist die entsprechende klassische Mathematik relativ einfach.
Schwerkraft, Reibung, Drehmoment, Massenschwerpunkt usw. können auf weni­
ger als einer Seite mit Gleichungen demonstriert werden. Mit diesen Formel::
E i n fü h ru n g

könnte ich I hnen beweisen, dass Fahrradfahren praktisch ist, und I hnen das
gesamte Wissen vermitteln, das Sie brauchen, um es auszuüben. Doch was passiert
beim ersten Mal, wenn Sie ein Fahrrad besteigen? Sie fallen runter.
Beim Codieren ist es genauso. Wir könnten alle » So-geht's«-Prinzipien von saube­
rem Code niederschreiben und dann daraufvertrauen, dass Sie die Arbeit erledigen.
(Anders ausgedrückt Wir könnten Sie einfach aufi die Nase fallen lassen: wenn Sie
das Fahrrad besteigen.) Doch was für eine Art von Lehrer wären wir dann, und wel­
che Art von Schüler \\·ürde dann aus I hnen werden?
So also nicht. So soll dieses Buch nicht funktionieren.
Sauberen Code schreiben zu lernen, ist harte Arbeit. Es erfordert mehr, als nur die
Prinzipien und Patterns zu kennen. Sie müssen sich an dem Code abarbeiten. Sie
müssen es selbst üben und aus I hren Fehlern lernen. Sie müssen andere dabei beo­
bachten, wie sie es ausprobieren, welche Fehler sie machen. Sie müssen erkennen,
wo sie stolpern. und ihre Schritte nachvollziehen. Sie müssen sehen, welche Ent­
scheidungen ihnen besonders schwerfallen und welchen Preis sie bezahlen müs­
sen, wenn sie die falschen Entscheidungen treffen.
Sie sollten sich aufhane Arbeit einstellen, wenn Sie dieses Buch lesen. Dies ist kein
Buch, das I hnen »angenehme« Unterhaltung bietet und das Sie im Flugzeug lesen
und vor der Landung beenden können. Dieses Buch fordert Sie auf, hart zu arbeiten.
Um welche Art \·on _-\rbeit geht es dabei? Sie werden Code lesen - sehr viel Code.
Und Sie werden aufgefordert, darüber nachzudenken, wo dieser Code richtig und
wo er falsch ist. S1e werden aufgefordert, uns dabei zu folgen, wie wir Module zer­
pflücken und v.ieder zusammensetzen. Dies braucht Z eit und Mühe; aber wir sind
der Ansicht. dass es sich für Sie lohnt.
Wir haben dieses Buch in drei Teile aufgeteilt. Die ersten Kapitel beschreiben die
Prinzipien. Patterns und Techniken für das Schreiben von sauberem Code. Diese
Kapitel enthalten eine ganze Menge Code, und es wird nicht einfach sein, ihn zu
lesen. Sie bereiten S:e aufden zweiten Teil vor. Wenn Sie das Buch nach dem Lesen
des ersten Abschnitts aus der Hand legen, alles Gute für Sie!
Der zweite Teil des Buches enthält die schwerere Arbeit. Er besteht aus mehreren
Fallstudien, die zunehmend komplexer werden. Jede Fallstudie ist ein Beispiel für
die Bereinigung von Code - also von der Umwandlung von Code. der gewisse Pro­
bleme enthält. in Code . der weniger Probleme enthält. In diesen Teil geht es sehr
ins Detail. Sie müssen ständig zwischen dem beschreibenden Text und den Code­
Listings hin- und herblättern. Sie müssen den Code, mit dem wir arbeiten, analy­
sieren und verstehen und unsere Überlegungen für die durchgeführten Änderun­
gen nachvollziehen. Sie müssen dafür schon einige Z eit resen·ieren, denn Sie
werden dafür einige Tage benötigen.
I m dritten Teil des Buches erhalten Sie I hren Lohn. Er besteht aus einem einzigen
Kapitel mit einer Liste von Heuristiken und Smells, die während der Erstellung der

22
Vorwort

Fallstudien zusammengetragen wurden. Wäh rend wir den Code in den Fallstudien
analysierten und bereinigten, h aben wir alle Gründe für unsere Aktionen als Heu­
ristik oder Smell dokumentiert. Wir versuch ten, unsere eigenen Reaktionen auf den
Code zu versteh en, den wir gelesen und geändert h atten, und mühten uns wirklich
ab, h erauszufinden und festzuh alten, warum wir fühlten, was für fühlten, und
warum wir taten, was wir taten. Das Ergebnis ist eine Wissensbasis, die unsere Art
zu denken besch reibt, wenn wir sauberen Code schreiben und lesen.
Diese Wissensbasis h at für Sie nur einen besch ränkten Wert, wenn Sie die Fallstu­
dien im zweiten Teil dieses Buch es nicht sorgfältig lesen und nachvollzieh en. I n die­
sen Fallstudien h aben wir sorgfältig alle durch geführten Änderungen mit
Referenzen aufi die Wissensbasis verseh en. Diese Referenzen werden wie folgt in
eckigen Klammern angegeben: [H22]. Dies ermöglicht es Ih nen, den Kontext zu
seh en, in dem diese Heuristiken angewendet und geschrieben wurden! Es sind
nicht die Heuristiken selbst, die so wertvoll sind, sondern die Bezieh ungen zwi­
sch en diesen Heuristiken und den einzelnen Entsch eidungen, die wir beim Berei­
nigen des Codes in den Fallstudien getroffen h aben.
Um Ih nen mit diesen Bezieh ungen noch weiterzuh elfen, h aben wir am Anfang des
I ndexes Querverweise aufidie Seitenzah len eingefügt, unter denen Sie die jeweilige
Referenz finden können. So können Sie leicht alle Stellen lokalisieren, an denen
eine bestimmte Heuristik angewendet wurde.
Wenn Sie nur den ersten und dritten Teil lesen und die Fallstudien überspringen,
dann h aben Sie nur ein weiteres unterh altsames Buch über das Schreiben von Soft­
ware gelesen. Doch wenn Sie sich die Zeit neh men, die Fallstudien durch zuarbeiten
und jedem winzigen Sch ritt und jeder kleinen Entsch eidung zu folgen- wenn Sie
sich also an unsere Stelle versetzt und sich gezwungen h aben, in denselben Bah nen
zu denken wie wir, dann werden Sie ein viel tieferes Verständnis dieser Prinzipien,
Patterns, Tech niken und Heuristiken gewonnen h aben. Diese werden dann nich t
mehr nur gewisse »ganz nützlich e« Techniken unter anderen sein, sondern werden
Ih nen in Fleisch und Blut übergegangen sein. Sie werden ein Teil von Ih nen gewor­
den sein, äh nlich wie ein Fahrrad eine Erweiterung Ih res Willens wird, wenn Sie das
Fah ren erlernt h aben.

D a n ksagu n ge n

G rafiken

Danke an meine beiden Künstlerinnen Jenniffer Koh nke und Angela Brooks. Jen­
nifer ist für die beeindruckenden und kreativen Bilder am Anfang i edes Kapitels
und die P orträts von Kent Beck, Ward Cunningh am. Bjarne Stroustrup, Ron Jeffries,
Grady Booch , Dave Th omas, Mich ael Feath ers und mir ...-erantwortlich .

23
Für Ann Marie: die Liebe meines Lebens.
Kapitel 1

Sau berer Code

Sie lesen das Buch aus zwei Gründen. Erstens: Sie sind P rogrammierer. Zweitens:
Sie wollen ein besserer P rogrammierer werden. Gut. Wir brauch en bessere P ro­
grammierer.
Dieses Buch h andelt von guter P rogrammierung. Es ist voller Code. Wir werden uns
Code aus allen möglich en Rich tungen ansch auen. Wir werden ih n von oben und
unten und von außen und innen betrach ten. Wenn wir fertig sind, werden Sie seh r
viel über Code wissen. Darüber h inaus werden Sie guten Code von sch lech tem Code
untersch eiden k önnen. Sie werden wissen, wie Sie guten Code sch reiben k önnen.
Und Sie werden wissen, wie Sie sch lech ten Code in guten Code transformieren k ön­
nen.
Kapitel l
S a u b e re r Code

1 .1 Cod e, Cod e u n d n oc h m a l s Cod e

Vielleich t k önnte man einwenden, ein Buch über Code wäre doch etwas altmodisch
- Code wäre doch längst k ein T;h ema meh r; stattdessen sollte man sich mit Model­
len und Anforderungen befassen. Tatsächlich vertreten einige Leute die Auffas­
sung, die Ära des Codes ginge zu Ende. B ald werde aller Code nich t meh r
gesch rieben, sondern generiert werden. P rogrammierer würden einfach überflüs­
sig werden, weil Gesch äftsentwickler P rogramme einfach aus Spezifik ationen
generieren würden.
Unsinn! Wir werden niemals oh ne Code arbeiten k önnen, weil der Code die Details
der Anforderungen repräsentiert. Auu einer gewissen Ebene k önnen diese Details
nich t ignoriert oder abstrah iert werden; sie müssen spezifi ziert werden. Und die
Spezifik ation von Anforderungen in einer Detailgenauigk eit, dass sie von einer
Masch ine ausgefüh rt werden k önnen, ist Programmierung. Und eine solch e Spezi­
fik ation ist Code.
I ch rech ne damit, dass die Abstrak tionsebene unserer Sprach en noch h öh er geh en
wird. I ch erwarte auch , dass die Anzahl der domänenspezifi sch en Sprach en wei­
terh in wach sen wird. Diese Entwicklung bringt Vorteile mit sich , aber sie wird den
Code nich t eliminieren. Tatsächlich werden alle Spezifik ationen, die auf diesen
h öh eren Ebenen und in den domänenspezifi sch en Sprach en gesch rieben werden,
Code sein! Sie müssen immer noch stringent, genau und so formal und detailliert
sein, dass sie von einer Masch ine verstanden und ausgefüh rt werden k önnen.
Leute, die denk en, Code werde eines Tages verschwinden, äh neln Math ematik ern,
die h offen, eines Tages eine Math ematik zu entdeck en, die nich t formal sein muss.
Sie h offen, dass wir eines Tages eine Meth ode entdeck en werden, Masch inen zu
ersch affen, die tun, was wir wollen, und nich t, was wir sagen. Diese Masch inen
müssen in der Lage sein, uns so gut zu versteh en, dass sie unsere unsch arf formu­
lierten Bedürfnisse in perfekt ausgefüh rte P rogramme übersetzen k önnen, die
genau diese Bedürfnisse erfü llen.
Dies wird nie passieren. Nich t einmal Mensch en mit all ih rer I ntuition und Krea­
tivität sind bis j etzt in der Lage gewesen, aus den unsch arfen Gefüh len ih rer Kunden
erfolgreich e Systeme abzuleiten. Wenn wir überh aupt etwas aus der Disziplin der
Anforderungsspezifik ation gelernt h aben, ist es Folgendes: Woh lspezifi zierte
Anforderungen sind genauso formal wie Code und k önnen als ausfüh rbare Tests
dieses Codes verwendet werden!
Vergessen Sie nich t, dass Code letztlich die Sprach e ist, in der wir die Anforderun­
gen ausdrück en. Wir k önnen Sprach en k onstruieren, die näh er bei den Anforde­
rungen angesiedelt sind. Wir k önnen Werk zeuge sch affen, die uns h elfen, diese
Anforderungen zu parsen und zu formalen Struk turen zusammenzusetzen. Aber
wir werden niemals die erforderlich e P räzision eliminieren k önnen- und desh alb
wird es immer Code geben.

26
1 .2
Sch I echter Code

1 .2 Sch I echter Cod e

Abb. 1.1: Ke nt Beck

Neulich las ich das Vorwort zu dem Buch Implementation Patterns von Kent Beck
[Beck o7] . Darin sch reibt er: » . . . dieses Buch basiert auf einer rech t fragilen P rä­
misse: dass guter Code eine Rolle spiele . . . « . Eine fragile P rämisse? Dem k ann ich
nich t zustimmen! I ch glaube, dass diese P rämisse zu den robustesten, am besten
unterstützten und meistdiskutierten P rämissen unserer Zunft geh ört (und ich
glaube, das weiß Kent Beck auch ) . Wir wissen, dass guter Code eine Rolle spielt, weil
wir uns so lange mit seiner mangelnden Qualität auseinandersetzen mussten.
I ch k enne ein Unterneh men, das in den späten 8oer-Jah ren eine Killer-Applik ation
h erausbrachte. Sie war seh r beliebt, und zah lreich e professionelle Anwender k auf­
ten und nutzten sie. Aber dann wurden die Release-Zyk len immer länger. Bugs wur­
den von einem Release zum näch sten nich t meh r repariert. Die Startzeiten wurden
länger und die Abstürze h äufiger. I ch erinnere mich an den Tag, an dem ich das P ro­
dukt frustriert absch altete und niemals wieder benutzte. Kurz danach verschwand
das Unterneh men vom Mark t.
Zwei Jah rzeh nte später traf ich einen früh eren Mitarbeiter dieses Unterneh mens
und fragte ih n, was damals passiert wäre. Die Antwort bestätigte meine Befürch ­
tungen. Das Unterneh men h atte das P roduk t zu sch nell aufiden Mark t gebrach t und
im Code ein riesiges Ch aos angerich tet. Je meh r Funk tionen zu dem Code h inzu­
gefügt wurden, desto sch lech ter wurde er, bis das Unterneh men ih n einfach nich t
meh r verwalten k onnte. Es war der schlechte Code, der das Unternehmen i n den
Abgrund trieb.
Sind Sie j emals erh eblich von sch lech tem Code beeinträch tigt worden? Wenn Sie als
P rogrammierer auch nur ein bissch en Erfah rung h aben, dann h aben Sie eine sol­
ch e Beh inderung viele Male erlebt. Tatsäch lich h aben wir eine spezielle Bezeich -

27
Kapitel l
S a u b e re r Code

nung dafür: Wading (Waten) . Wir waten durch sch lech ten Code. Wir k ämpfen uns
durch einen Morast verschlungener Sch lingpflanzen und verborgener Fallgruben.
Wir müh en uns ab, den rich tigen Weg zu finden, und h offen aufirgendwelch e Hin­
weise, die uns zeigen, was passiert; aber alles, was wir seh en, ist ein sch ier endloses
Meer von sinnlosem Code.
Natürlich sind Sie von sch lech tem Code beh indert worden. Also- warum h aben Sie
ih n gesch rieben?
Haben Sie zu sch nell gearbeitet? Waren Sie unter Druck ? Wah rsch einlich . Vielleich t
h atten Sie das Gefüh l, k eine Z eit für gute Arbeit z u h aben, meinten, Ih r Ch efiwürde
ärgerlich werden, wenn Sie sich die Z eit neh men würden, Ih ren Code aufzuräu­
men. Vielleich t waren Sie es einfa ch leid, an diesem P rogramm zu arbeiten, und
wollten endlich damit fertig werden. Oder vielleich t h aben Sie Ih ren Stapel unerle­
digter Arbeit angesch aut, die Sie längst h ätten erledigen müssen, und sind zu dem
Sch luss gek ommen, Sie müssen dieses Modul zusammensch ustern, um mit dem
näch sten weitermach en zu k önnen. Wir alle k ennen diese Erfah rung.
Wir alle h aben uns das Ch aos angesch aut, das wir gerade angerich tet h atten, und
dann besch lossen, es an einem anderen Tag zu beseitigen. Wir alle h aben die
Erleich terung gefühlt, zu seh en, dass unser ch aotisch es P rogramm lief) und
besch lossen, dass ein laufendes Ch aos besser wäre als nichts. Wir alle h aben uns
vorgenommen, später zurück zuk ommen und das Ch aos zu beseitigen. Natürlich
k annten wir damals das Gesetz von LeBlanc nicht: Später gleich niemals.

1 .3 Die Lebe n s zyk l u s koste n ei n es C h aos

Wenn Sie sch on länger als zwei bis drei Jah re programmieren, h aben Sie wah r­
sch einlich die Erfah rung gemach t, dass Ih re Arbeit von dem ch aotisch en Code eines
anderen Entwicklers erh eblich verlangsamt worden ist. Die Verlangsamung k ann
beträch tlich sein. I m Laufe von einem oder zwei Jah ren k ann es passieren, dass
Teams, die am Anfang eines P roj ekts seh r sch nell vorangek ommen waren, sich
plötzlich nur noch im Sch neck entempo vorwärtsbewegen. Jede Änderung des
Codes füh rt zur Defekten an zwei oder drei anderen Stellen des Codes. Keine Ände­
rung ist trivial. Für j ede zusätzlich e Funktion oder Modifik ation des Systems müs­
sen alle Verzweigungen, Varianten und Knoten »verstanden« werden, damit weitere
Verzweigungen, Varianten und Knoten h inzugefügt werden k önnen. I m Laufe der
Z eit wird das Ch aos so groß und so verfilzt, dass Sie es nich t meh r bereinigen k ön­
nen. Sie sind am Ende Ihres Weges angelangt.
Wäh rend das Ch aos immer größer wird, nimmt die P roduktivität des Teams laufend
ab und geh t asymptotisch gegen null. Wäh rend die P roduktivität sinkt, tut das
Management das Einzige, was es k ann: Es weist dem P roj ekt meh r Personal zu in
der Hoffnung, die P roduktivität zu steigern. Aber das neue Personal versteh t das
Design des Systems nich t. Es k ennt nich t den Untersch ied zwisch en einer Ände-
1 .3
D i e Le ben szyk l u s kosten e i nes Chaos

rung, die zum Zweck des Designs passt, und einer Änderung, die dem zuwiderläuft.
Darüber h inaus steh en Sie und die anderen Teammitglieder unter sch recklich em
Druck , die P roduktivität zu verbessern. Desh alb produzieren alle immer meh r
Ch aos und senk en damit die P roduktivität immer weiter gegen null (sieh e Abbil­
dung r.2) .

1 00 -------
� 80 '
.� 60 \

::::s 40 '
"' ......
eD. 20 --
0

Zeit
Abb. 1 .2: P rod u ktivität u n d Zeit

Das große Redesign i n d e n Wol ke n

Sch ließlich rebelliert das Team. Das Management wird darüber informiert, das s
man mit dieser zweifelh aften Code-Basis nich t weiterarbeiten k önne. E s wird ein
Redesign gefordert. Das Management will aber nich t die Ressourcen für ein k om­
plett neues Redesign des P roj ekts aufwenden, k ann sich aber auch nicht der
Erk enntnis versch ließen, dass die P roduk tivität nich t ak zeptabel ist. Sch ließlich
beugt es sich den Forderungen der Entwickler und autorisiert das große Redesign
in den Wolk en.
Es wird ein neues Tliger-Team zusammengestellt. Jeder möchte zu diesem Team
geh ören, weil es ein frisch es neues P roj ekt ist. Man darfi neu anfa ngen und etwas
wirklich Sch önes erstellen. Aber nur die Besten und Hellsten werden für das Tliger­
Team ausgewäh lt. Alle anderen müssen sich um die Wa rtung des gegenwärtigen
Systems kümmern.
Jetzt gibt es ein Wettrennen zwisch en den beiden Teams. Das Tci.ger-Team muss ein
neues System erstellen, das alle Funktionen des alten erfüllt. Und nicht nur das : Es
muss auch mit den Änderungen Sch ritt h alten, die laufend an dem alten System
vorgenommen werden. Das Management wird das alte System nich t ersetzen, bevor
nich t das neue alle Funktionen des alten erfüllt.
Dieser Wettlaufk ann sich seh r lange h inzieh en. I ch h abe Z eitspannen von bis zu
zeh n Jah ren erlebt. Und wenn er beendet wird, sind die ursprünglich en Mitglieder
des Tliger-Teams längst nicht meh r da, und die gegenwärtigen Mitglieder verlangen
nach einem Redesign des neuen Systems , weil es ein solch es Ch aos sei.

29
Kapitel l
S a u b e re r Code

Wenn nur ein kleiner Teil dieser Gesch ich te Ih rer Erfah rung entsprich t, dann wis­
sen Sie bereits, dass die Z eit, die Sie für das Sauberh alten Ih res Codes verwenden,
nicht nur k osteneffizient, sondern eine Frage des beruflich en Überlebens ist.

E i nste l l u ng

Sind Sie j emals durch einen Morast gewatet, der so dich t war, dass es Woch en dau­
erte, um zu tun, was nur einige Stunden h ätte dauern sollen? Haben Sie erlebt, dass
eine Änderung, die nur eine Z eile h ätte erfordern sollen, in Hunderten versch iede­
ner Module durch gefüh rt werden musste? Diese Symptome k ommen leider allzu
oft vor.
Warum passiert das mit Code? Warum verrottet guter Code so sch nell zu schlech ­
tem Code? Dafür h aben wir viele Erklärungen. Wir beklagen uns , dass die Anfor­
derungen in einer Weise geändert wurden, die dem ursprünglich en Design
zuwiderläuft. Sie j ammern, dass der Z eitplan zu eng bemessen war, um die Aufga­
ben richtig zu erledigen. Geben dummen Managern und den toleranten Kunden
und nutzlosen Mark eting- Typen und einem unzureich enden Telefon- Support die
Sch uld. Aber der Fehler, lieber Dilbert, liegt nich t in unseren Sternen, sondern in
uns selbst. Wir sind unprofessionell.
Diese Pille zu sch luck en, mag etwas bitter sein. Wie k önnte dieses Ch aos unsere
Sch uld sein? Was ist mit den Anforderungen? Was ist mit dem Z eitplan? Gibt es
etwa k eine dummen Manager und nutzlose Mark eting-Typen? Tragen sie nich t
einen Teil der Sch uld?
N ein. Die Manager und Mark eting- Leute fragen uns nach den I nformationen, die
sie benötigen, um Versprech ungen und Zusagen zu mach en; und selbst wenn sie
uns nich t fragen, sollten wir k eine Hemmungen h aben, ih nen zu sagen, was wir
denk en. Die Benutzer wenden sich an uns , damit wir ih nen zeigen, wie das System
ih re Anforderungen erfüllt. Die P roj ektmanager benutzen unsere I nformationen,
um ih re Z eitpläne aufzustellen. Wir sind eng in die Planung des P roj ekts einge­
bunden und tragen einen großen Teil der Verantwortung für auftretende Fehler, ins­
besondere wenn diese Fehler mit schlechtem Code zu tun h aben!
»Doch h alt!«, sagen Sie. »Wenn ich nich t tue, was mein Manager sagt, werde ich
gefeuert.« Wahrsch einlich nicht. Die meisten Manager wollen die Wahrh eit wissen,
selbst wenn sie sich nich t immer entsprech end verh alten. Die meisten Manager
wollen guten Code h aben, selbst wenn sie von ih rem Z eitplan besessen sind. V:iel­
leich t verteidigen sie leidensch aftlich den Z eitplan und die Anforderungen; aber das
ist ih r Job. Dagegen ist es Ihr Job, den Code mit gleich er Leidensch aft zu verteidigen.
Betrachten wir eine Analogie: Was würden Sie als Arzt mach en, wenn ein Patient
Sie auffordern würde, dieses blödsinnige Händewasch en bei der Vorbereitung aufi
einen ch irurgisch en Eingriffi zu lassen, weil es zu viel Z eit kostet? (Als das Hände­
wasch en r847 den Ärzten erstmals von I gnaz Semmelweis empfohlen wurde,
wurde es mit der Begründung zurück gewiesen, die Ärzte wären zu besch äftigt und
1 .3
D i e Le ben szy k l u s kosten e i nes C hao s

h ätten k eine Z eit, sich die Hände zwisch en ih ren Patientenbesuch en zu wasch en.)
Natürlich h at der Patient Vorrang. Dennoch sollte der Arzt in diesem Fall die For­
derung k ompromisslos zurückweisen. Warum? Weil der Arzt meh r über die Risi­
k en einer Erk rankung und I nfektion weiß als der Patient. Es wäre unprofessionell
(und in diesem Fall sogar k riminell) , wenn der Arzt der Forderung des P atienten
nach geben würde.
Desh alb ist es auch unprofessional, dass sich P rogrammierer dem Willen von
Managern beugen, die die Risik en nich t versteh en, die mit dem Erzeugen von
Ch aos im Code verbunden sind.

Das gru n d legende Problem

P rogrammierer werden mit einem grundlegenden Wertek onflikt k onfrontiert.


Erfah rene Entwickler wissen, dass ih re Arbeit durch alten ch aotisch en Code erh eb­
lich beh indert wird. Dennoch fühlen alle Entwickler den Druck , ch aotisch en Code
zu sch reiben, um Termine einzuh alten. Kurz gesagt: Sie neh men sich nich t die Z eit,
es rich tig zu mach en!
Ech te P rofi s wissen, dass der zweite Teil dieses Konflikts falsch ist. Man erfüllt einen
Termin eben nicht, indem man ch aotisch en Code produziert. Tatsäch lich verlang­
samt ch aotisch er Code Ih re Arbeit sofort und füh rt dazu, dass Sie Ih ren Termin
nich t einh alten k önnen. Die einzige Meth ode, den Termin einzuh alten, besteht
darin, den Code jederzeit so sauber wie möglich zu h alten.

Sa u beren Code sch re i be n - e i n e K u n st?

Angenommen, Sie glaubten, ch aotisch er Code wäre eine beträch tlich e Beh inde­
rung Ih rer Arbeit. Wenn Sie jetzt ak zeptieren, dass die einzige Möglichk eit, sch nel­
ler zu arbeiten, darin besteh t, den eigenen Code sauber zu h alten, müssen Sie sich
zwangsläufi g fragen: »Wie sch reibe ich sauberen Code?« Es h at k einen Sinn, zu ver­
such en, sauberen Code zu sch reiben, wenn Sie nich t wissen, wie sauberer Code aus­
sieht!
Leider h aben wir h ier eine sch lech te Nach rich t: Sauberen Code zu sch reiben, h at
seh r viel mit dem Malen eines Bildes zu tun. Die meisten k önnen erk ennen, wann
ein Bild gut oder sch lech t gemalt ist. Aber dies erk ennen zu k önnen, bedeutet nich t,
dass wir auch malen k önnen. Wenn Sie also in der Lage sind, sauberen von sch lech ­
tem Code z u untersch eiden, bedeutet dies nich t automatisch , dass Sie sauberen
Code sch reiben k önnen!
Sauberen Code zu sch reiben, erfordert den disziplinierten Einsatz zah lreich er klei­
ner Tech nik en, die mit einem sorgfältig erworbenen Gefühl für » Sauberk eit« ange­
wendet werden. Dieses »Gefüh l für den Code« ist der Sch lüssel. Einige sind damit
geboren. Einige müssen es sich meh r oder weniger müh sam erarbeiten. Dieses
Gefühl für den Code h ilft uns nich t nur, guten von sch lechtem Code zu untersch ei-
Kapitel l
S a u b e re r Code

den; sondern zeigt uns die Strategie, wie wir unser Arsenal von erworbenen Tech­
nik en anwenden müssen, um sch lech ten Code in guten zu transformieren.
Ein P rogrammierer oh ne dieses »Gefühl für den Code« k ann sich ein ch aotisches
Modul ansch auen und das Ch aos erk ennen, h at aber absolut k eine Vorstellung
davon, was er dagegen tun k önnte. Ein P rogrammierer mit »Gefühl für den Code«
sch aut sich das ch aotisch e Modul an und erk ennt seine Optionen und Änderungs­
möglichk eiten. Sein »Gefüh l für den Code« h ilft ih m dabei, die beste Option aus­
zuwäh len und eine Reih e von Änderungssch ritten festzulegen, die ih n zum Z iel
bringen und zugleich nach jedem Teilsch ritt die volle Funktionsfäh igk eit des Codes
erh alten.
Kurz gesagt: Ein P rogrammierer, der sauberen Code sch reibt, ist ein Künstler, der
einen leeren Bildsch irm mit einer Reih e von Transformationen in ein elegant
codiertes System umwandelt.

Was i st s a u bere r Code?

Es gibt wah rsch einlich so viele Defi nitionen wie P rogrammierer. Desh alb fragte ich
einige seh r bek annte und seh r erfah rene P rogrammierer nach ih rer Meinung.

Bjarne Stroustru p

Abb. 1 .3: Bjarne Stro u stru p

Bjarne Stroustrup, Erfi nder von C++ und Autor von The C++ Programming Language
Mein Code sollte möglichst elegant und effizient sein. Die Logik sollte gradlinig
sein, damit sich Bugs nur schwer verstecken können, die Abhängigkeiten soll-
ten minimal sein, um die Wartung zu vereirifachen, das Fehler-Handling
sollte vollständig gemäß einer vordefinierten Strategie eifolgen, und das Leis­
tungsverhalten sollte dem Optimum so nah wie möglich kommen, damit der
Entwickler nicht versucht ist, den Code durch Ad-hoc-Optimierungen zu ver­
unstalten. Sauberer Code erledigt eine Aufgabe gut.

32
1 .3
D i e Le ben szy k l u s kosten e i nes C h aos

Bj arne verwendet das Wort »elegant«. Was für ein Wort! Im Wörterbuch findet man
auch folgende Synonyme: ansprechende Anmut und Kunstftrtigkeit in Aussehen oder
Verhalten; ansprechende Sinnlichkeit und Einfachheit. Wich tig ist dabei die Betonung
von »ansprech end«. Offensich tlich glaubt Bj arne, dass sauberer Code angenehm zu
lesen sein soll. Solch en Code zu lesen, sollte einen Ausdruck des Woh lgefallens auf
Ih r Gesich t zaubern, den Sie auch vom Ansch auen eines rassigen Automobils k en­
nen.
Bj arne erwäh nt auch die Effizienz des Codes. Vielleich t sollte uns dies bei dem
Erfinder von C++ weniger überrasch en; aber ich glaube, dass er damit meh r als sei­
nen Wunsch nach einer Geschwindigk eit meint. Verschwendete Zyklen sind une­
legant, sie vermitteln k ein angeneh mes Gefühl. Und j etzt beach ten Sie, wie Bj arne
die Folgen dieser Uneleganz besch reibt. Er verwendet den Ausdruck »versuch t
sein«. Darin ist eine tiefe Weish eit verborgen. Schlech ter Code verleitet dazu, das
Ch aos zu vergrößern! Wenn andere schlech ten Code ändern, neigen sie dazu, ih n
noch sch lech ter zu mach en.
Die P ragmatisch en P rogrammierer Dave Thomas und Andy Hunt drückten dasselbe
auf andere Weise aus. Sie verwendeten die Metaph er der zerbroch enen Fenster
( h t t p : //www . p rag p rog . com/t h e - p ragmati c - p rog ramme r/ext racts/softwa re­
e n t ropy) . Ein Gebäude mit zerbroch enen Fenstern sieh t so aus, als würde sich nie­
mand darum kümmern. Desh alb kümmern sich auch andere Entwickler nicht um
den Code. Sie lassen es gewissermaßen zu, dass weitere Fenster zerbroch en werden.
Sch ließlich h elfen sie aktiv dabei. Sie versch mieren die Vorderfront mit Graffiti und
lassen zu, dass sich Müll ansammelt. DerP rozess des Zerfalls beginnt mit einem zer­
broch enen Fenster.
Bj arne erwäh nt auch , dass das Feh ler-Handling vollständig sein sollte. Dies geh ört
zur Disziplin, aufmerk sam in den Details zu sein. Ein verkürztes Feh ler-Handling
ist nur eine Meth ode, wie P rogrammierer Details vernach lässigen. Speich erleck s
und Race-Bedingungen sind weitere Beispiele dafür. Eine ink onsistente Namens­
gebung zäh lt ebenfalls zu. Das Fazit ist: Sauberer Code bedeutet auch große Sorgfalt
im Detail.
Bj arne sch ließt mit der Zusich erung, dass sauberer Code eine Aufgabe gut erledigt.
Es ist k ein Zufall, dass so viele P rinzipien des Software-Designs auf diese einfach e
Mah nung zurück gefüh rt werden k önnen. Autor für Autor h at versuch t, diesen
einen Gedank en zu k ommunizieren. Sch lech ter Code tut zu viel; seine Ab sich t ist
nich t klar zu erk ennen und er versuch t, meh rere Zweck e auf einmal zu erfüllen.
Sauberer Code ist fokussiert. Jede Funktion, j ede Klasse, j edes Modul ist eindeutig
aufi einen einzigen Zweck ausgerich tet und lässt sich von den umgebenden Details
weder ablenk en noch verunreinigen.

33
Kapitel l
S a u b e re r Code

G rady Booch

Abb. 1 .4: G rady Booch

Grady Booch , Autor von Object-Oriented Analysis and Design with Applications
Sauberer Code ist einfach und direkt. Sauberer Code liest sich wie wohlge­
schriebene Prosa. Sauberer Code verdunkelt niemals die Absicht des Designers,
sondern ist voller griffiger (engl. crisp) Abstraktionen und geradliniger Kon­
trollstrukturen.
Einige Punkte von Grady deck en sich mit denen von Bj arne, aber er betrach tet das
Ganze aus der Perspektive der Lesbarkeit. Mir gefällt besonders seine Auffassung,
dass sich sauberer Code wie woh lgesch riebene P rosa lesen lassen soll. Denk en Sie
zurück an ein wirklich gutes Buch , das Sie gelesen h aben. Erinnern Sie sich , wie die
Wörter verschwanden und durch Bilder ersetzt wurden? Es war, als würden Sie
einen Film seh en, nich t wah r? Besser! Sie sah en die Z eich en, Sie h ärten die Geräu­
sch e, Sie erlebten die Leidensch aften und den Humor.
Sauberen Code zu lesen, wird natürlich niemals dasselbe sein, wie Der Herr der
Ringe zu lesen. Dennoch ist die literarisch e Metaph er brauch bar. Wie ein guter
Roman sollte sauberer Code die Spannung in den zu lösenden P roblemen sauber
h erausarbeiten. Diese Spannung sollte einem Höh epunkt zutreiben und dann dem
Leser dieses »Ah a ! Ja natürlich !« vermitteln, wenn die P robleme und Spannungen
bei der Enth üllung einer offensich tlich en Lösung aufgelöst werden.
Für mich ist der Ausdruck »griffige Ab straktion« (im Original: »crisp abstraction«,
wörtl. »k nack ige oder frisch e Ab straktion«, unübersetzbar) ein faszinierendes Oxy­
moron (ein Widerspruch in sich ) ! Sch ließlich bedeutet das Wort »griffig« eh er etwas
Handgreiflich es, Konk retes, praktisch Nutzbares. Trotz dieses sch einbaren Wider­
spruch s der Wörter vermitteln sie eine klare Botsch aft. Unser Code sollte nich t spe­
k ulativ, sondern nüch tern und sach bezogen sein. Es sollte nur das Erforderlich e
enth alten. Unsere Leser sollten unsere Bestimmth eit erk ennen k önnen.

34
1 .3
D i e Le ben szyk l u s kosten e i nes C hao s

Dave Thomas

Abb. 1 .5: Dave T h o m a s

»Big« Dave Th omas, Gründer der OTI , der P ate (Godfath er) der Eclipse-Strategie
Sauberer Code kann von anderen Entwicklern gelesen und verbessert werden.
Er veifügt über Unit- und Acceptance-Tests. Er enthält bedeutungsvolle
Namen. Er stellt zur Lösung einer Aufgabe nicht mehrere, sondern eine
Lösung zur Veifügung. Er enthält minimale Abhängigkeiten, die ausdrücklich
defi n iert sind, und stellte ein klares und minimales AP I zur Veifügung. Code
sollte »literate« sein, da je nach Sprache nicht alle eiforderlichen Informatio-
nen allein im Code klar ausgedrückt werden können. (A.d. Ü. : »Literate Pro­
gramming« ist eine Unterströmung, bei der die Einheit von Kommentaren
und Code betont und geifördert wird.)
Auch Big Dave strebt wie Grady Lesbark eit an, fordert aber eine wich tige Ergänzung.
Für Dave ist es wichtig, dass sauberer Code es anderen Entwicklern erleich tert, ih n
zu verbessern. Dies mag off ensichtlich sch einen, aber es k ann nicht genug betont
werden. Schließlich gibt es einen Untersch ied zwisch en Code, der einfach zu lesen
ist, und Code, der einfach zu ändern ist.
Dave mach t Sauberk eit von Tests abh ängig! Vor zeh n Jah ren h ätten viele bei dieser
Forderung die Stirn gerunzelt. Ab er die Disziplin der Test Driven Development
(TDD) h at einen wesentlich en Einfluss auf die Soft ware-Branch e geh abt und h at
sich zu einer der grundlegenden Disziplinen entwick elt. Dave h at recht. Code oh ne
Tests ist nich t sauber. Egal wie elegant er ist, egal wie lesbar und änderungsfteund­
lich er ist, oh ne Tests ist er unsauber.
Dave verwendet das Wort minimal zweimal. Off ensichtlich sch ätzt er Code, der
einen geringen Umfang h at. Tatsächlich h at sich im Laufe der letzten Jah re ein Kon­
sens in der Software-Literatur gebildet: Kleiner ist besser.

35
Kapitel l
S a u b e re r Code

Dave sagt auch , Code solle literate sein. Dies ist ein sanfter Hinweis auf das Literate
Programming von Donald Knuth [Knuth 92]. Die Q uintessenz lautet Code sollte so
aufb ereitet werden, dass er von Mensch en gelesen werden k ann.

M ichael Feathers

Abb. 1 .6: M i chael Feath e rs

Mich ael Feath ers, Autor von Warking Effectively with Legacy Code
Ich könnte alle Eigenschaften auflisten, die mir bei sauberem Code auffa llen;
aber es gibt eine übergre ifende Qualität, die alle anderen überragt: Sauberer
Code sieht immer so aus, als wäre er von jemandem geschrieben worden, dem
dies wirklich wichtig war. Es fällt nichts ins Auge, wie man den Code verbes­
sern könnte. Alle diese Dinge hat der Autor des Codes bereits selbst durchdacht;
und wenn Sie versuchen, sich Verbesserungen vorzustellen, landen Sie wieder
an der Stelle, an der Sie gerade sind: Sie sitzen einfach da und bewundern den
Code, den Ihnen jemand hinterlassen hat -jemand, der sein ganzes Können
in sorgfältige Arbeit gesteckt hat.
Ein Wort: Sorgfalt. Das ist das eigentlich e Th ema dieses Buch es . Vielleich t h ätte ein
passender Untertitel gelautet: Wie man seinem Code Sorgfalt angedeihen lässt.
Mich ael trifft den Nagel auf den Kopf. Sauberer Code ist Code, der sorgfältig erstellt
worden ist. Jemand h at sich die Z eit genommen, den Code sauber zu strukturieren
und zu sch reiben. Jemand h at den Details die erforderlich e Sorgfalt angedeih en las­
sen. Jemand war es nich t egal, wie sein Arbeitsergebnis aussah .
1 .3
D i e Le ben szy k l u s kosten e i nes C h aos

Ron J effi-ies

Abb. 1 .7: Ron J effries

Ron Jeffries, Autor von Extreme Programming Installed und Extreme Programming
Adventures in C#
Ron begann seine Karriere als P rogrammierer in Fortran bei dem Strategie Air
Command und h at Code in fast j eder Sprach e und auf fast j eder Masch ine gesch rie­
ben. Es zah lt sich aus, seine Worte sorgfältig zu überdenk en.
In den letzten Jahren beginne ich (und ende ich fast immer) mit den Regeln
von Beck für einfachen Code. In der Reihenfolge der Priorität eifüllt einfacher
Code die folgenden Bedingungen:
• Er besteht alle Tests.
• Er enthält keine Duplizierung.
• Er drückt alle Design-Ideen aus, die in dem System enthalten sind.
• Er minimiert die Anzahl der Entities, also der Klassen, Methoden, Funktionen
usw.
Unter diesen Bedingungen konzentriere ich mich hauptsächlich auf die Dup­
lizierung. Wenn dieselbe Sache immer wieder gemacht wird, ist dies ein Zei­
chen dafür, dass wir einen bestimmten Gedanken in unserem Code nicht gut
genug repräsentiert haben. Dann versuche ich herauszufinden, diesen Gedan­
ken zu fassen und klarer auszudrücken.
Ausdruckskraft umfasst für mich bedeutungsvolle Namen. Häufig ändere ich
die Namen der Dinge mehifach, bis ich die endgültige Version gefunden habe.
Mit modernen Entwicklungswerkzeugen wie etwa Eclipse ist die Umbenen­
nung recht einfach, weshalb ich mir darüber keine Gedanken mache. Doch die
Ausdruckskraft geht über Namen hinaus. Ich schaue mir auch an, ob ein
Objekt oder eine Methode mehr als eine Aufgabe eifüllt. Ist dies der Fall, zer-

37
Kapitel l
S a u b e re r Code

lege ich ein Objekt in zwei oder mehr kleinere Objekte oderführe ein Refacto­
ring einer Methode mit der Extract-Methode so durch, dass die neue Methode
klarer zum Ausdruck bringt, was sie tut, und einige Untermethoden sagen, wie
dies getan wird.
Duplizierungen zu eliminieren und die Ausdruckskraft zu steigern, bringen
mich meinem Ideal von sauberem Code schon sehr viel näher. Chaotischen
Code allein unter zwei dieser Gesichtspunkte zuvor zu verbessern, kann bereits
zu einem erheblich besseren Ergebnis führen. Es gibt jedoch noch einen ande­
ren Aspekt meiner Bemühungen, der etwas schwerer zu erklären ist.
Aufisrund meiner langen Erfahrung beim Programmieren scheint es mir, dass
alle Programme aus sehr ähnlichen Elementen aufgebaut sind. Ein Beispiel:
»Suche Dinge in einer Collection.« Egal was wir durchsuchen wollen, eine
Datenbank mit Mitarbeiter-Datensätzen, eine Hash-Map mit Schlüsseln und
Werten oder ein Array mit Elementen bestimmter Art, immer wollen wir ein
spezielles Element aus dieser Collection abrufen. Wenn ich eine solche Aufgabe
identifiziere, verpacke ich oft ihre spezielle Implementierung in eine abstrak­
tere Methode oder Klasse. Dadurch erziele ich eine Reihe interessanter Vor­
teile.
Ich kann die Funktionalitätjetzt mit etwas Einfachem, etwa einer Hash-Map,
implementieren. Doch da jetzt alle Referenzen auf diese Suche in meiner klei­
nen Abstraktion eingekapselt sind, kann ich die Implementierung jederzeit
ändern. Ich komme schneller voran und bewahre mir trotzdem meine Fähig­
keit, den Code später zu ändern.
Außerdem lenkt die Collection-Abstraktion oft meine Aufmerksamkeit auf
das, was »wirklich« passiert, und hält mich davon ab, Implementierungspfade
zu verfolgen, auf denen ich alle möglichen Collection-Verhaltensweisen reali­
siere, auch wenn ich wirklich nur eine ziemlich einfache Methode brauche,
um etwas Gesuchtes zu finden.
Reduzierung der Duplizierung, Steigerung der Ausdruckskraft undfrühe For­
mulierung einfacher Abstraktionen machen für mich sauberen Code aus.
Hier h at Ron in einigen k urzen Ab sätzen den I nh alt dieses Buch es zusammenge­
fasst: k eine Duplizierung, eine Aufg abe, Ausdruck sk raft, kleine Ab straktionen. Es
ist alles da.

Ward Cu n n i ngham

Ward Cunningh am, Erfinder des Wil<i, Erfinder von Fit, Miterfinder des eXtreme
P rogramming. Treibende Kraft h inter den Design Patterns. Smalltalk- und 00-Vor­
denk er. Der Pate aller Leute, denen ih r Code nich t egal ist.
1 .3
D i e Le ben szyk l u s kosten e i nes Chaos

Abb. 1 .8: Wa rd C u n n i n g h a m

Sie wissen, dass Sie an sauberem Code arbeiten, wenn jede Routine, die Sie
lesen, ziemlich genau so funktioniert, wie Sie es erwartet haben. Sie können
den Code auch »schön« nennen, wenn er die Sprache so aussehen lässt, als
wäre sie für das Problem geschaffen worden.
Solch e Aussagen sind für Ward ch arakteristisch . Sie lesen sie, nick en mit dem Kopf
und wenden sich dann dem näch sten Th ema zu. Die Aussage h ört sich so vernünf•
tig, so offensich tlich an, dass sie k aum als etwas Grundlegendes wah rgenommen
wird. Sie glauben, sie drück e rech t genau das aus, was Sie erwartet h aben. Doch
sch auen wir etwas genauer h in.
» . . . ziemlich genau so, wie Sie es erwartet h aben.« Wann h aben Sie zum letzten Mal
ein Modul geseh en, das ziemlich genau dem entsprach , was Sie erwartet h aben? I st
es nich t wah rsch einlich er, dass die Module, die Sie sich ansch auen, rätselh aft, k om­
pliziert und verworren ausseh en? I st es nich t die Regel, dass Sie in die falsch e Rich ­
tung gelockt werden? Sind Sie e s nich t gewoh nt, verzweifelt und frustriert die
Denk fäden aufz uspüren und durch das ganze System zu verfolgen, aus denen letzt­
lich das Modul gewebt ist? Wann h aben Sie zum letzten Mal Code gelesen und dabei
mit dem Kopf genickt, wie Sie eben die Aussage von Ward aufgenommen h aben?
Ward erwartet, dass Sie beim Lesen von sauberem Code überh aupt k eine Überra­
sch ungen erleben. Tatsächlich sollte das Ganze fast müh elos sein. Sie lesen den
Code, und er entsprich t ziemlich genau dem, was Sie erwartet h aben. Er ist offen­
sich tlich , einfach und überzeugend. Jedes Modul ist eine Stufe für das näch ste.
Jedes gibt vor, wie das näch ste gesch rieben sein wird. P rogramme, die so sauber
sind, sind so außerordentlich gut gesch rieben, dass Sie es gar nich t mal bemerk en.
Der Designer lässt das P roblem läch erlich einfach ausseh en- ein h erausragendes
Merk mal aller außerordentlich en Designs.
Und was ist mit Wards Vorstellung von Sch önh eit? Wir h aben alle sch on darüber
gesch impft, dass unsere Sprach en nich t für unsere P robleme k onzipiert worden
wären. Ab er Wards Aussage gibt uns den Sch warzen Peter zurück . Er sagt, sch öner
Code lasse die Sprache aussehen, als wäre sie für das Problem gemacht worden! Es liegt

39
Kapitel l
S a u b e re r Code

also in unserer Verantwortung, die Sprach e einfach ausseh en zu lassen! Dies sollte
Sprach eiferern jeder Couleur zu denk en geben! Es ist nich t die Sprach e, die ein P ro­
gramm einfach ausseh en lässt. Es ist der P rogrammierer, der die Sprach e einfach
ausseh en lässt!

1 .4 Den ksc h u le n

Abb. 1 .9: U n cle Bob (Robert C. M a rt i n )

Was ist mit mir (Uncle Bob) ? Was ist für mich sauberer Code? Dieses Buch wird
Ih nen bis ins kleinste Detail sagen, was meine Mitstreiter und ich über sauberen
Code denk en. Wir werden Ih nen sagen, was unserer Meinung nach einen sauberen
Variablennamen, eine saubere Funktion, eine saubere Klasse usw. ausmach t. Wir
werden diese Meinungen als Absoluta formulieren, und wir werden uns nich t für
unsere Sch ärfe entsch uldigen. Für uns sind sie, an diesem Punkt unserer Karriere,
absolute P ostulate. Sie sind unsere Denkschule für sauberen Code.
Kampfsportk ünstler sind sich über die beste Kampfsportart oder die beste Tech nik
innerh alb einer Kampfsportart überh aupt nich t einig. Oft gründen Meister einer
Kampfsportart ih re eigene Denk sch ule und sammeln Sch üler um sich , denen sie
ih ren speziellen Kampfstil vermitteln. So gibt es etwa ein Gracie jiu ]istu, das von
der Gracie-Familie in Brasilien begründet wurde und geleh rt wird. Es gibt ein Hak­
koryu]iu]istu, das von Okuyama Ryuh o in Tokyo begründet wurde und geleh rt wird.
Es gibt ein ]eet Kune Do, das von Bruce Lee in den Vereinigten Staaten begründet
wurde und von seinen N ach folgern geleh rt wird.
Sch üler dieser Ansätze unterzieh en sich einem intensiven Studium der Leh ren der
Gründer. Sie widmen ih re Z eit dem Erlernen des speziellen Kampfstils des jewei­
ligen Meisters. Oft blenden sie dabei die Leh ren aller anderen Meister aus. Später,
wenn die Sch üler in ih rem Kampfstil eine gewisse Reife erreich t h aben, k önnen sie
auch bei einem anderen Meister studieren, um ih r Wissen und ih re Kampftech ni­
k en auf eine breitere Basis zu stellen. Einige entwick eln und verfeinern ih re Fäh ig-
1 .5
W i r s i n d Autoren

k eiten sch ließlich so weit, dass sie neue Tech niken entdecken und eigene Sch ulen
gründen.
Keine dieser versch iedenen Sch ulen ist die absolut richtige. Doch innerh alb einer
speziellen Sch ule verhalten wir uns so, als wären die Leh ren und Tech niken rich tig.
Denn sch ließlich gibt es eine korrekte Meth ode, Hakkoryu Jiu Jitsu oder Jeet Kune
Do auszuüben. Aber diese Selbstgerech tigkeit innerh alb einer Sch ule entwertet
nich t die Leh ren einer anderen Sch ule.
Betrach ten Sie dieses Buch als eine Besch reibung der Object Mentor School of Clean
Code (Object-Mentor- Sch ule des sauberen Codes) . Die Tech niken und Leh ren in die­
sem Buch sind die Meth oden, wie wir unsere Kunst praktizieren. Wir geh en so weit
zu beh aupten, dass Sie, wenn Sie diese Leh ren befolgen, die Vorteile erlangen wer­
den, die wir erlangt h aben, und dass Sie lernen werden, sauberen und professio­
nellen Code zu sch reiben. Sie sollten aber nich t den Feh ler mach en zu glauben, dass
wir in irgendeinem absoluten Sinne »rech t« h ätten. Es gibt andere Sch ulen und
andere Meister, die mit demselben Nach druck P rofessionalität für sich in Anspruch
neh men wie wir. Und auch von ih nen zu lernen, würde Ih rem Können nur zugu­
tekommen.
Tatsäch lich werden viele Empfeh lungen in diesem Buch kontrovers diskutiert. Sie
werden wah rsch einlich nich t mit allem einverstanden sein. Vielleich t werden Sie
einige sogar h eftig ableh nen. Das ist in Ordnung. Wir können keinen Anspruch auf
die endgültige Autorität erh eben. Andererseits sind die Empfeh lungen in diesem
Buch Dinge, über die wir lange und gründlich nach gedach t h aben. Sie basieren aufi
j ah rzeh ntelanger Erfah rung und h aben sich in wiederh olten Versuch -und-I rrtum­
Zyklen h erauskristallisiert. Also: Egal, ob Sie mit uns übereinstimmen oder nich t,
es wäre eine Sch ande, wenn Sie unseren Standpunkt nich t kennen lernen und
respektieren würden.

1 .5 Wir s i n d Autore n

Das @au t h o r-Feld einer Javadoc sagt, wer wir sind. Wir sind Autoren. Ein Merkmal
von Autoren ist es, dass sie Leser h aben. Tatsäch lich sind Autoren dafür verantwort­
lich, erfolgreich mit ih ren Lesern zu kommunizieren. Wenn Sie Ih re näch ste Code­
zeile sch reiben, sollten Sie daran denken, dass Sie ein Autor sind, der für Leser
sch reibt, die Ih re Anstrengung beurteilen.
V:ielleich t fragen Sie, wie viel Code wirklich gelesen wird. Steckt der größte Aufwand
nich t darin, den Code zu sch reiben?
Haben Sie jemals ein Play-back einer Edit- Sitzung durch gefüh rt? I n den 8oern und
9 0ern h atten wir Editoren wie Emacs, die jeden Tastenansch lag speich ern konnten.
Sie konnten eine Stunde lang arbeiten und dann ein Play-back Ih rer gesamten Edit­
Sitzung wie im Z eitraffer ablaufen lassen. Als ich dies tat, waren die Ergebnisse fas­
zinierend.

41
Kapitel l
S a u b e re r Code

Der bei Weitem größte Anteil des Play-back s bestand darin, dass ich h erumscrollte
und zu anderen Modulen navigierte!
Bob kommt zu dem Modul.
Er scrollt zu der Funktion herunter, die geändert werden muss.
Er macht eine Pause und denkt über seine Optionen nach.
Oh, er scrollt wieder nach oben an den Anfang des Moduls, um die Initialisie­
rung einer Variablen zu überprüfen.
jetzt scrollt er zurück nach unten und beginnt zu tippen.
Ups, er löscht, was er getippt hat!
Er tippt erneut.
Er löscht erneut!
Er tippt die Hälfte von etwas anderem, aber löscht es dann wieder!
Er scrollt runter zu einer anderen Funktion, die die Funktion aufruft, die er
ändert, um zu sehen, wie sie aufgerufen wird.
Er scrollt zurück und tippt denselben Code ein, den er gerade gelöscht hat.
Er macht eine Pause.
Er löscht den Code wieder!
Er ö.ffn et ein anderes Fenster und schaut sich eine Unterklasse an. Wird die
Funktion überschrieben ?

Sie versteh en, was abgeht. Tatsäch lich beträgt das Verh ältnis der Z eit, die mit Lesen
verbrach t wird, zu der Z eit, die mit Sch reiben verbrach t wird, weit über 1 0 : 1 . Wir
lesen permanent alten Code als Teil unseres Bemüh ens, neuen Code zu sch reiben.
Weil dieses Verh ältnis so h och ist, sollte das Lesen von Code leich t sein, selbst wenn
dadurch das Sch reiben sch werer wird. Natürlich ist es unmöglich , Code zu sch rei­
ben, oh ne ih n zu lesen. Desh alb ist das Bemüh en, das Lesen von Code zu erleichtern,
zugleich ein Bemüh en, das Schreiben von Code leichter zu machen.
Dieser Logik k ann man sich nich t entzieh en. Man k ann k einen Code sch reiben,
wenn man den umgebenden Code nich t lesen k ann. Der Code, den Sie h eute zu
sch reiben versuchen, wird schwer oder leich t zu sch reiben sein, und zwar abh ängig
davon, wie sch wer oder leich t Sie den umgebenden Code lesen k önnen. Wenn Sie
also sch nell vorwärtsk ommen wollen, wenn Sie sch nell fertig werden wollen, wenn
Sie Ih ren Code leich t sch reiben wollen, mach en Sie es leicht, ih n zu lesen.
1 .6
D i e Pfa d fi n d e r- Re g e l

1 .6 Die Pfadfi n d e r- Regel

Es reich t nich t aus, guten Code zu sch reiben. Der Code muss auch im Zeitablauf
sauber gehalten werden. Wir h aben alle erlebt, wie Code im Laufe der Z eit verrottet
und immer schlech ter geworden ist. Desh alb müssen wir ak tiv tätig werden, um die­
sem schleich enden Verfall vorzubeugen.
Bei den P fadfindern gibt es eine einfach e Regel, die wir auch auf unseren Beruf
anwenden k önnen. (Sie wurde aus der Absch iedsbotsch aft, Versuche die Welt ein
wenig besser zu hinterlassen, als du sie gefonden hast, . . . , von Robert Steph enson Smyth
Baden-Poweil an die P fadfinder, abgeleitet.) Die Botsch aft lautet:
Hinterlasse den Campingplatz sauberer, als du ihn gefonden hast.
Wenn wir alle unseren Code ein wenig sauberer einch eck en, als wir ih n ausgech eck t
h aben, k ann der Code einfach nich t verrotten. Es muss k ein Großreinemach en sein.
Verbessern Sie h ier einen Va riablennamen, zerlegen Sie dort eine etwas zu große
Funktion, eliminieren Sie doppelte Codezeilen oder bauen Sie eine zusammenge­
setzte i f-Anweisung um.
Können Sie sich vorstellen, an einem P rojekt zu arbeiten, bei dem der Code im
Laufe der Z eit einfach besser wurde? Glauben Sie, dass ein anderes Verh alten profes­
sionell wäre? Oder ist die laufende Verbesserung vielleich t ein wesentliches Merk ­
mal von P rofessionalität?

1 .7 Vo rläufer u n d Pri n z i p i e n

I n vieler Hinsich t ist dieses Buch ein »Vorläufer« z u meinem Buch Agile Software
Development: Principles, Patterns, and Practices (PPP ) aus dem Jah re 2002. Das PPP ­
Buch beh andelt die P rinzipien des Objekt-orientierten Design (OOD) und viele
Tech nik en, die von professionellen Entwicklern eingesetzt werden. Falls Sie PPP
nich t gelesen h aben, werden Sie meinen, dass es eine Fortsetzung der Gesch ich te
ist, die in diesem Buch erzählt wird. Wenn Sie es bereits gelesen h aben, werden Sie
in dem vorliegenden Buch einen Widerh all vieler Aussagen aus dem PPP - Buch fin­
den, und zwar diesmal auf der Ebene des Codes.
In diesem Buch werden gelegentlich versch iedene Design-P rinzipien referenziert:
das Single-Responsibility-P rinzip ( S RP ) , das Open-Closed-P rinzip (OCP ) und das
Dependency-I nversion-P rinzip (DIP ) und andere. Diese P rinzipien werden in PPP
ausfüh rlich besch rieben.

1 .8 Z u sa m m e nfass u n g

Büch er über Kunst versprech en Ih nen nich t, aus Ih nen einen Künstler zu mach en.
Sie k önnen Ih nen nur einige Werk zeuge, Tech nik en und Gedank enprozesse ver-

43
Kapitel l
S a u b e re r Code

mitteln, die von anderen Künstlern verwendet worden sind. Desh alb versprich t
Ih nen dieses Buch nicht, aus Ih nen einen guten P rogrammierer z u mach en. Es
k ann Ih nen nicht versprech en, Ih nen ein Gefühl für den Code zu vermitteln. Es
k ann Ih nen nur die Gedank enprozesse von guten P rogrammierern aufzeigen und
die Trick s, Tech nik en und Werk zeuge mitteilen, die sie verwenden.
Äh nlich wie ein Kunstbuch enth ält dieses Buch zah lreich e Details. Es enth ält
umfangreich en Code. Sie seh en guten Code, und Sie seh en sch lech ten Code. Es
wird demonstriert, wie sch lech ter Code in guten Code transformiert werden k ann.
Sie finden Listen mit Heuristik en, P rinzipien und Tech nik en. Und es wird Ih nen
ein Beispiel nach dem anderen gezeigt. Danach sind Sie auf sich gestellt.
Kennen Sie die Gesch ich te von dem Konzert-Violinenspieler, der sich aufidem Weg
zur Vorfüh rung verlaufen h atte ? Er fragte einen alten Mann an der Straßeneck e
nach dem Weg zur Carnegie Hall. Der alte Mann sch aute den Violinenspieler an,
sah die Geige unter seinem Arm und sagte: » Übung, mein Sohn. Übung!«

44
Kapitel 2

Aussagekräftige N amen

von Tim Ottinger

2.1 E i nfü h ru n g

Namen kommen in Software überall vor. Wir benennen unsere Va riablen, unsere
Funktionen, unsere Argumente, Klassen und Packages. Wir benennen unsere
Q uelldateien und die Verzeich nisse, in denen sie enth alten sind. Wir benennen
unsere j a r-Dateien und wa r-Dateien und ea r-Dateien. Wir benennen und benen­
nen und benennen. Was wir so h äufig tun, sollten wir besser gut tun. In den fol­
genden Absch nitten finden Sie einige einfach e Regeln für die Bildung guter Namen.

2.2 Zweckbesch re i be n d e N a m e n wä h l e n

Es ist einfach zu sagen, Namen sollten den Zweck besch reiben. Wir möch ten Ih nen
vermitteln, dass wir dies ernst meinen. Gute Namen zu wäh len, brauch t Zeit. spart
aber letztlich meh r Z eit ein. Desh alb sollten Sie Ih re Namen sorgfältig auswähle::
Kapitel 2
A u s s age k räfti g e N a m e n

und ändern, wenn Sie bessere finden. Jeder Leser Ihres Codes ( Sie eingeschlossen)
profitiert davon.
Der Name einer Variablen, Funktion oder Klasse sollte alle großen Fragen beant­
worten. Er sollte Ihnen sagen, warum er existiert. was er tut und wie er benutzt wird.
Wenn ein Name einen Kommentar erfordert, dann drückt er seinen Zweck nicht
aus .

i n t d ; II a b g e l a u f e n d e Z e i t i n Tag e n

Der Name d enthüllt nichts . Er ruft weder das Bild einer Zeitspanne noch einen
Gedanken an Tage hervor. Wir sollten einen Namen wählen. der angibt, was gemes­
sen wird und in welcher Einheit es gemessen wird:

i nt e l a p s edTi me i n Days ;
i nt d a y s S i n c e C r e at i on ;
i nt day s S i n c eModi fi c a t i on ;
i nt fi l eAg e i n Day s ;

Namen zu wählen, die den Zweck ausdrücken, macht es viel leichter. den Code zu
verstehen und zu ändern. Welchen Zweck erfüllt der folgende Code ?

p u b l i c L i s t < i n t [ ] > g e tTh e m ( ) {


Li st<i nt [] > l i stl = new A r r a y l i s t < i n t [ ] > ( ) ;
fo r ( i n t [ ] x : t h e l i s t )
if (x [O] 4)
==

1 i s t l . add ( x ) ;
ret u r n l i stl ;
}

Warum ist es so schwierig zu erkennen, was dieser Code tut? Es enthält keine kom­
plexen Ausdrücke. Die Zeichenabstände und Einrückungen sind vernünftig. Es gibt
nur drei Variablen und zwei Konstanten. Es gibt keine exotischen Klassen oder poly­
morphe Methoden . nur eine Liste von Arrays (so scheint es jedenfalls) .
Das Problem ist nicht die Einfachheit des Codes, sondern seine Implizität (um einen
Begriff zu prägen) : ein Maß dafür, wie weit der Kontext explizit aus dem Code selbst
hervorgeht oder nicht. Der Code erfordert von uns implizit, dass wir die Antworten
auf die folgenden Fragen kennen:
I. Welche Dinge sind in t h e l i s t gespeichert?
2. Welche Bedeutung hat das Subskript n u l l eines Elements von t h e l i s t ?
3 · Welche Bedeutung hat der Wert 4 ?

4 · Wie wird die zurückgegebene Liste verwendet?

Die Antworten auf diese Fragen gehen aus dem Code-Beispiel nicht hervor, aber sie
hätten daraus hervorgehen können. Angenommen, Sie arbeiteten an einem Mine-
2. 3
Feh l i n fo r m a t i o n e n verm e i d e n

Sweeper- Spiel. Sie stellen fest, dass Sie das Spielfeld als eine Liste von Z ellen reprä­
sentieren können, die Sie als t h e l i s t bezeichnen. Wir wollen diese Liste in g am e ­
B o a r d umbenennen.
Jede Z elle des Spielfelds wird durch ein einfaches Array repräsentiert. Sie stellen
weiter fest, dass das Subskript n u l l einen Statuswert enthält und dass der Status­
wert 4 »flagged (markiert)« bedeutet. Einfach, indem Sie diese entsprechenden
Konzepte benennen, k önnen Sie den Code erheblich verbessern:

publ i c Li st<i n t [ ] > getFl aggedCel l s () {


L i s t < i n t [ ] > fl a g g e d C e l l s new A r r a y l i s t < i n t [ ] > ( ) ;
=

fo r ( i n t [ ] c e l l : gameBo a r d )
i f ( c e l l [ STATUS_VA L U E ] F LAGG ED)
==

fl a g g e d C e l l s . ad d ( c e l l ) ;
r e t u rn fl a g g e d C e l l s ;
}

Beachten Sie, dass sich die Einfachheit des Codes nicht geändert hat. Er enthält
immer noch genau dieselbe Z ahl von Operatoren und Konstanten, mit genau der­
selben Z ahl von Verschachtelungsebenen. Aber der Code ist jetzt sehr viel expliziter,
drückt also seinen Zweck sehr viel klarer aus.
Wir k önnen einen Schritt weitergehen und eine einfache Klasse für Z ellen schrei­
ben, anstatt ein Array von i n t s zu benutzen. Die Klasse k ann eine den Zweck aus­
drück ende Funktion (nennen wir sie i s Fl agged) enthalten, um die magischen
Z ahlen zu verbergen. Hier ist die neue Version der Funktion:

publ i c Li st<Cel l > getFl aggedCel l s () {


L i s t < C e l l > fl a g g e d C e l l s =n ew A r r a y l i s t <C e l l > ( ) ;
fo r (Ce l l c e l l : g a m e Bo a r d )
i f ( c e l l . i s F l a g g e d () )
fl a g g e d Ce l l s . ad d ( c e l l ) ;
r e t u r n fl a g g e d C e l l s ;
}

Nach diesen einfachen Namensänderungen ist es nicht mehr schwierig zu verste­


hen, was passiert. Dies ist ein Beispiel für die Kraft, die gut gewählten Namen inne­
liegt.

2.3 Fe h l i nformationen vermeiden

Programmierer sollten k eine irreführenden Hinweise hinterlassen, die die Bedeu­


tung des Codes verdunk eln. Wir sollten Wörter vermeiden, deren etablierte Bedeu­
tungen von unserer beabsichtigten Bedeutung abweichen. Beispielsweise wären h p,
ai x oder s c o als Va riablennamen ungeeignet, weil es sich um die Namen von Unix­
Plattformen oder -Va rianten handelt. Selbst wenn Sie eine Hypotenuse codieren
und h p für eine geeignete Abkürzung halten, könnte dieser Name irreführend sein.

47
I Kapitel 2
A u s s a g ekräfti g e N a me n

Bezeich nen Sie eine Gruppe von Konten nur dann als a c c o u n t l i s t, wenn es sich
wirklich um eine L i s t h andelt. Das Wort Liste bedeutet für einen P rogrammierer
etwas ganz Spezielles. Wenn der Container, der die Konten enth ält, nich t tatsächlich
eine L i s t ist, k önnte der Name zu falsch en Sch lussfolgerungen füh ren. Desh alb
wäre a c c o u n t G r o u p oder b u n c h OfAc c o u n t s oder einfach a c c o u n t s besser. Und
selbst wenn der Container eine Liste ist, wäre es wahrsch einlich besser, den Con­
tainer-Typ nicht im Namen zu codieren. Meh r darüber später.
Achten Sie aufN amen, die sich geringfügig untersch eiden. Wie lange dauert es, den
subtilen Untersch ied zwisch en XYZCo n t ro l l e r Fo r Effi c i e n t H a n d l i n güf­
S t r i ngs in einem Modul und XYZCo n t ro l l e r Fo r Effi c i e n t Sto rag eOfSt r i n g s
an etwas entfernterer Stelle z u entdecken? Die äußere Form der Wörter ist ersch re­
ck end äh nlich .
Äh nlich e Konzepte äh nlich zu sch reiben, vermittelt I nformationen. Eine inkonsis­
tente Sch reibweise ist Desinformation. Moderne Java-Umgehungen bieten uns den
Luxus der automatisch en Code-Ergänzung (code completion). Wir schreiben einige
Z eich en eines Namens und drück en eine Hotkey-Kombination aus (i f th at) und
werden mit einer Liste möglich er Ergänzungen für diesen Namen verwöh nt. Es ist
seh r h ilfreich , wenn Namen für sehr äh nlich e Aufgaben alph abetisch benachbart
steh en und wenn die Untersch iede klar erk ennbar sind, weil der Entwickler wah r­
sch einlich ein Objekt anh and seines Namens auswählt, oh ne Ih re umfangreich en
Kommentare oder sogar die Liste der Meth oden dieser Klasse zu studieren.
Wirk lich absch reck ende Beispiele für irreführende Namen sind der Kleinbuch stabe
l oder der Großbuch stabe 0 als Variablennamen, besonders wenn sie kombiniert
werden. Das P roblem liegt h ier natürlich darin, dass sie fast genau wie die Konstan­
ten eins bzw. null ausseh en.

i nt a 1 ;
=

if ( 0 ==1 )
a =01 ;
e1 se
1 = 01 ;

Wenn Sie meinen, dieses Beispiel wäre getürkt, k önnen wir nur sagen, dass wir
Code untersucht h aben, in dem derartige Dinge in Hülle und Fülle vork amen. I n
einem Fall sch lug der Autor des Codes vor, eine andere Sch riftart zu verwenden,
damit die Untersch iede besser sichtbar wären. Eine solch e Lösung müsste dann
natürlich an alle künftigen Entwickler weitergegeben werden, entweder als münd­
lich e Überlieferung oder in einem sch riftlich en Dokument. Durch eine einfach e
Umbenennung wird das P roblem endgültig und oh ne zusätzlich en AufWand erle­
digt.
2 -4
U n ters c h i e d e d e u t l i ch m achen

2.4 U ntersch iede de utl i c h m achen

P rogrammierer sch affen sich ih re eigenen P robleme, wenn sie Code schreiben, nur
um einem Compiler oder I nterpreter gerecht zu werden. Ein Beispiel: Weil man im
selben Geltungsbereich nicht denselben Namen zur Bezeich nung versch iedener
Aufgaben verwenden darf; könnte man versucht sein, einen Namen willkürlich zu
ändern. Manch mal wird für diesen Zweck einfach der Name falsch geschrieben, was
zu einer überrasch enden Situation füh rt, dass eine Korrektur des vorgeblich en
»Sch reibfehlers« zu Feh lern beim Kompilieren füh rt. Betrachten Sie beispielsweise
die wirklich absch eulich e P raxis, eine Variable namens kl ass zu erstellen, einfach
weil der Name c l a s s für etwas anderes verwendet wird.
Es reicht nicht aus, eine Z ah lenfolge oder leere Wörter anzuh ängen, auch wenn der
Compiler damit zufrieden sein sollte. Wenn die Namen untersch iedlich sein müs­
sen, dann sollten sie auch etwas Versch iedenes bezeichnen.
Namen mit Z ah lenserien (al , a2 , . . aN) sind das Gegenteil einer zweckvollen
Benennung. Solch e Namen sind nicht irreführend - sie sind informationsleer; sie
enth alten keinen Hinweis auf die Absicht des Autors . Ein Beispiel:

p u b l i c s t at i c voi d copyCh a r s ( c h a r al [ ] , c h a r a2 [ ] ) {
fo r ( i n t i = 0 ; i < al . l e n g t h ; i ++) {
a2 [ i ]
= al [ i ] ;
}
}

Diese Funktion liest sich viel besser, wenn sou rce und des t i na t i on als Argument­
Namen benutzt werden.
Leere Wörter sind eine andere Form der bedeutungsleeren Untersch eidung. Ange­
nommen, Sie h ätten eine P ro d u c t - Klasse. Wenn Sie eine andere Klasse namens
P ro d u c t i n fo oder P ro d u c tData h aben, h aben Sie zwar einen anderen Namen,
aber keine andere Bedeutung. I n fo und Data sind unbestimmte Leerwörter wie a,
an und t h e .
Beach ten Sie, dass nichts dagegen einzuwenden ist, wenn Sie P räfix- Konventionen
wie a und t h e verwenden, solange Sie damit eine sinnvolle Untersch eidung aus­
drücken. Beispielsweise könnten Sie a für alle lokalen Variablen und t h e für alle
Funktionsargumente verwenden. (Uncle Bob h at diese Tech nik früh er in C++ ein­
gesetzt, aber dann aufgegeben, weil sie durch moderne I D Es überflüssig wurde.)
Das P roblem tritt auf, wenn Sie besch ließen, eine Variable t heZo r k zu nennen, weil
Sie bereits eine andere Variable namens zo r k h aben.
Leerwörter sind redundant. Das Wort va r i abl e sollte niemals in einem Variablen­
namen ersch einen. Das Wort tab l e sollte niemals in einem Tabellennamen vor­
kommen. Wieso ist Name S t r i ng besser als Nam e ? Könnte ein Name j emals eine
Fließkommazahl sein? Wäre dies der Fall, würden Sie gegen eine früh ere Regel übe
Kapitel 2
A u s s a g ekräfti g e N a m e n

Fehlinformationen verstoßen. Stellen Sie sich vor, Sie stießen auf eine Klasse
namens C u s tome r und eine andere namens C u s tome rObj e c t . Wodurch unter­
sch eiden sich die Klassen? Welch e repräsentiert den besten Pfad zu der Zah lungs­
h istorie eines Kunden?
Wir kennen eine Anwendung, in der dies illustriert wird. Wir h aben den Namen
geändert, um die Sch uldigen zu sch ützen; doch h ier ist die genaue Form des Feh ­
lers:

g e tA c t i veAcco u n t ( ) ;
g e tA c t i veAcco u n t s ( ) ;
g e tAc t i veAccou n t i n fo () ;

Woh er sollen die Programmierer in diesem Projekt wissen, welch e Funktion sie auf­
rufen müssen?
Wenn keine speziellen Konventionen vereinbart sind, ist die Va riable moneyAmo u n t
von m o n e y nicht untersch eidbar; dasselbe gilt für c u s tome r l n fo und c u stome r,
a c c o u n t Data und a c c o u n t sowie t h eMe s s ag e und m e s s ag e . Namen sollten so
untersch ieden werden. dass der Leser weiß, was der Untersch ied bedeutet.

2.5 A u s s p rec h b a re N a me n verwe nden

Mensch en können gut mit Wörtern umgeh en. Ein großer Teil unseres Geh irns
dient dem HerYorbringen und Verarbeiten von Wörtern. Und Wörter sind per Defi­
nition aussprechbar. Es wäre eine Sch ande, diesen riesigen Teil unseres Geh irns,
der für den Cmgang mit gesproch ener Sprach e entwickelt worden ist, nicht zu
unserem Vorteil zu nutzen. Desh alb sollten Ih re Namen aussprech bar sein.
Wenn Sie einen �amen nicht aussprech en können, können Sie nich t darüber dis­
kutieren, oh ne sich \\·ie ein I diot anzuh ören. »Na ja, h ier bei dem be ce er drei ce
en te h aben wir pe es ze qu int, nich t wah r?« Dies spielt eine Rolle; denn Program­
mieren ist eine soziale Aktivität.
Ein mir bekanntes Unterneh men h at g e n ymd h m s (generation date, year, month , day,
h our, minute und second; Erstellungsdatum, Jahr, Monat, Tag, Stunde, Minute und
Sekunde) . Desh alb laufen sie rum und reden von »gen wh y emm dee aich emm ess«
(in Englisch ! ) . I ch h abe die nervige Angewoh nh eit, alles so auszusprech en, wie es
gesch rieben ist. Desh alb fing ich an mit: »gen-yah -mudda-h ims«. Später h aben
meh rere Designer und Analysten meine Sprechweise übernommen, sie h ört sich
trotzdem immer noch albern an. Aber dann war es für uns h alt ein I nsider-Witz.
Spaß oder nicht. wir tolerierten einen schlech ten Namen. Neue Entwickler brauch ­
ten eine Erklärung und fingen dann ebenfalls an, alberne erfundene Wörter anstelle
verständlich er umgangs- oder fach sprachlich er Wörter zu benutzen. Vergleich en
Sie

so
2.6
S u c h b a re N a m e n verwe n d e n

c l a s s DtaRc rd102 {
p r i vate D a t e g e n ym d h m s ;
p r i vate D a t e modymdh m s ;
p r i vate f i n a l S t r i n g p s zq i n t = " 10 2 " ;
!* . . . *!
};

mit

c l a s s C u s tome r {
p r i vate D a t e g e n e r a t i onTi m e s t amp ;
p r i vate D a t e modi f i c a t i o nTi m e s tamp ; ;
p r i vate f i n a l S t r i n g reco r d l d = " 10 2 " ;
/* . . . * /
};

Jetzt kann man sich intelligent darüber unterh alten: »Hey, Mikey, sch au dir diesen
Datensatz an! Der g e n e rati o nTi m e s t amp wird auf das morgige Datum gesetzt!
Wie kann das sein?«

2.6 S u c h bare N a men verwe nden

B e i Namen aus einzelnen Buch staben und numerisch en Konstanten gibt es ein spe­
zielles P roblem: Sie sind in einem Textabsch nitt nur schwer zu finden.
Während es leicht ist, per g rep nach MAX_C LASS E S_P E R_STU D E NT zu such en, berei­
tet die Z ahl 7 woh l mehr Schwierigkeiten. Die Such e weist Ergebnisse aus , die die
Ziffer 7 als Bestandteil eines Dateinamens, in anderen konstanten Definitionen
und in versch iedenen Ausdrücken enth alten, wo sie jeweils untersch iedlich e Zwe­
cke erfüllt. Noch schlimmer ist es bei einer konstanten langen Z ahl: Jemand könnte
aus Verseh en Ziffern vertausch en und damit einen Bug verursach en, wäh rend die
Variable dadurch gleich zeitig durch den Such filter des P rogrammierers fällt.
I n diesem Sinne ist auch der Name e als Variablenname ungeeignet. Weil dieser
Buch stabe zu den h äufigsten der normalen Sprach e geh ört, führt eine Such e nach
diesem Namen zu zahlreich en Nieten. I n dieser Hinsicht sind längere besser als
kürzere, und such bare Name sind besser als Konstanten im Code.
Persönlich zieh e ich es vor, Variablennamen aus einem einzigen Buch staben NUR
als lokale Variablen in kurzen Methoden zu verwenden. Die Länge eines Namens sollte
der Größe seines Geltungsbereiches entsprechen [N5]. Wenn eine Variable oder Kon­
stante an mehreren Stellen des Codes ersch eint oder benutzt wird, muss sie einen
such freundlich en Namen h aben. Vergleich en Sie wieder

fo r ( i n t j -0 ; j < 3 4 ; j ++) {
s += ( t [ j ] * 4) / 5 ;
}
Kapitel 2
A us s a g ekräfti ge N a m e n

mit

i n t r e a l Days P e r i d e a l Day = 4 ;
con s t i n t WORK_DAYS_ P E R_W E E K = 5 ;
i n t sum = 0 ;
fo r ( i n t j =O ; j < N U M B E R_Q F_TAS K S ; j ++) {
i n t r e a l Tas kDays = tas k E s t i mate [ j ] * r e a l Days Pe r i d e a l Day ;
i n t r e a l Tas kWe e k s = ( r e a l days I WORK_DAYS_P E R_� E E� J :
s u m += r e a l Tas kWee k s ;
}

Beachten Sie, dass s u m in dem obigen Beispiel kein besonders nützlich er Name ist,
aber wenigstens ist er such bar. Der Code mit den zwech·ollen Namen ist länger.
Doch bedenken Sie, wie viel leich ter es sein wird, WO RK_DAYS_P E R_W E E K zu such en,
als alle Stellen zu prüf en, an denen 5 verwendet wurde und die Liste aufdie Instan­
zen mit der beabsichtigten Bedeutung zu reduzieren.

2.7 Cod ieru n ge n vermeiden

Wir müssen uns sch on mit genug Codierungen h erumschlagen und brauch en uns
keine weiteren aufzuladen. Inf ormationen über den Typ oder den Geltungsbereich
per Codierung in Namen aufzuneh men, erschwert einfach nur zusätzlich die Last
der Entschlüsselung. Es gibt selten einen vernünftigen Grunci dafur. dass neue Mit­
arbeiter zusätzlich zu dem (normalerweise beträchtlich em vorh andenen Code, mit
dem sie arbeiten sollen, noch eine weitere Verschlüsselungs - ,. Sprach e« lernen müs­
sen. Es ist eine unnötige mentale Belastung, wenn wir versuch en. ein Problem zu
lösen. Codierte Namen lassen sich selten aussprech en und können leicht falsch
getippt werden.

U n garische N otati on

In der guten alten Zeit, als die Länge der Namen in den Sprach en allzu beschränkt
war, mussten wir, leider, zwangsläufig gegen diese Regel verstoßen. In Fortran
musste der erste Buch stabe eines Codes den Typ angeben. In früh en Versionen von
BAS IC durften Namen nur aus einem Buch staben plus einer Ziffer besteh en. Und
die Hungarian Notation (HN; ungarisch e Notation) h ob diese Kunst auf eine ganz
neue Ebene.
Zu Zeiten des C-AP I von Windows, als alles ein Integer-Handle oder ein La ng-Poin­
ter oder ein vo i d-Pointer oder eine von mehreren Implementierungen von »String«
(mit versch iedenen Anwendungszwecken und Attributen) war. galt die HN als h oh e
Kunst. Damals füh rten Compiler keine Typ-Prüfungen durch . desh alb brauch ten
Programmierer eine Krücke, die ih nen h alf; die Typen zu erkennen und auseinan­
derzuh alten.

52
2.7
Cod i e ru n g en verm e i d e n

In modernen Sprachen verfügen wir über viel reichhaltigere Typensysteme, und die
Compiler prüfen die Typen und erzwingen ihre Einhaltung. Darüber hinaus gibt es
einen Trend zu kleineren Klassen und kürzeren Funktionen, wodurch Entwickler
normalerweise die Stelle der Deklaration aller Variablen sehen können, mit denen
sie arbeiten.
Java-P rogrammierer brauchen keine Typ-Codierung. Objekte sind stark typisiert,
und die Entwicklungsumgehungen sind so weit fortgeschritten, dass sie Typenfeh­
ler entdecken, lange bevor Sie das P rogramm kompilieren können! Deshalb sind
heute die HN und anderen Formen der Typ-Codierung einfach nur hinderlich. Sie
erschweren das Ändern des Namens undfa der Typs einer Variablen, Funktion oder
Klasse. Sie erschweren das Lesen des Codes. Und sie schaffen das Risiko, dass das
Codierungssystem den Leser irreführt.

P h o n e N u mbe r p h o n e S t r i n g ;
II N am e wi rd n i c h t g e ä n d e r t , we n n s i c h d e r T y p ä n d e r t !

M em ber- Präfixe

Außerdem braucht man heute keine Member-Variablen mehr mit dem P räfix m_ zu
versehen. I hre Klassen und Funktionen sollten so klein sein, dass Sie es einfach
nicht benötigen. Und Sie sollten eine Entwicklungsumgebung benutzen, die Mem­
ber-Variablen durch eine geeignete Farbe hervorhebt, um sie von anderen zu unter­
scheiden. Also nicht

p u b l i c c l as s P a r t {
p r i vate S t r i n g m_d s c ; II d i e t e x t l i c h e B e s c h r e i b u n g
voi d s e t N am e ( S t r i n g n am e ) {
m_d s c =n am e ;
}
}

sondern

p u b l i c c l as s P a r t {
Stri ng descri pti on ;
voi d s e tD e s c r i p t i o n ( S t r i n g d e s c r i p t i o n ) {
th i s . descri pti on =descri pti on ;
}
}

Außerdem lernen Entwickler schnell, das P räfix (oder Suffix) zu ignorieren, und
achten nur auf den bedeutungsvollen Teil des Namens. Je häufiger wir den Code
lesen, desto weniger bemerken wir die P räfixe. Schließlich werden die P räfixe unbe­
merkter Müll und ein Z eichen für älteren Code.
Kapitel 2
A us s a g e k räfti g e N a m e n

I nterfaces u n d I m p lementieru ngen

Diese sind manch mal spezielle Fälle von Codierungen. Angenommen, Sie wollten
eine Abstract Factory für die Erstellung von geometrisch en Formen entwickeln.
Diese Factory soll ein I nterface h aben und durch eine konkrete Klasse implemen­
tiert werden. Wie sollte sie h eißen? I S h ape Fac t o r y und S h ape Facto ry? I ch zieh e
e s vor, I nterfaces nicht mit einem dekorierten Namen z u benennen. Das vorange­
h ende I, das in den h eutigen Legacy-Biblioth eken so h äufig anzutreffen ist, ist bes­
tenfalls eine Ablenkung und liefert schlimmstenfalls zu viele I nformationen. I ch
möchte nicht, dass meine Benutzer wissen, dass ich Ih nen ein I nterface übergebe.
I ch will nur, dass sie wissen, dass es sich um eine S h a pe Fac t o r y h andelt. Wenn ich
also entsch eiden muss, ob ich entweder das I nterface oder die I mplementierung
codiere, wähle ich die I mplementierung. Ein Name wie S h ape Facto rylmp oder
selbst der schrecklich e Name C S h a p e Facto ry sind einer Codierung des I nterface­
Namens vorzuzieh en.

2.8 M e nta l e M a p p i n gs ve rmeiden

Die Leser sollten Ih ren Namen nicht mental in einen anderen Namen übersetzen
müssen, den sie bereits kennen. Dieses P roblem tritt im Allgemeinen auf, wenn
man weder die Termini der P roblemdomäne noch die der Lösungsdomäne verwen­
det.
Bei Variablennamen aus einem einzigen Buch staben kann es P robleme geben.
Sich er ist ein Schleifenzähler namens i oder j oder k (obwoh l niemals l !) akzep­
tabel, wenn sein Geltungsbereich seh r klein ist und keine Konflikte mit anderen
Namen auftreten können. Derartige Namen für Schleifenzäh ler h aben eine lange
Tradition. Doch in den meisten anderen Kontexten ist ein Name aus einem einzigen
Buch staben schlech t gewählt; er ist nur ein Platzh alter, den der Leser mental in das
tatsächlich e Konzept übersetzen muss. Es gibt keinen sch limmeren Grund dafür,
den Namen c nur desh alb zu verwenden, weil a und b bereits vergeben waren.
I m Allgemeinen sind P rogrammierer ziemlich schlau. Manch e schlaue Leute
geben gerne mit ih rer Schlauh eit an und zeigen, wie gut sie mental jonglieren kön­
nen. Denn sch ließlich müssen sie, wenn sie sich zuverlässig daran erinnern kön­
nen, dass r die Kleinbuch stabenversion eines U RLs oh ne Host und Sch ema ist,
sich er sehr sch lau sein.
Der Untersch ied zwisch en einem schlauen P rogrammierer und einem professio­
nellen P rogrammierer besteh t darin, dass der professionelle P rogrammierer weiß,
dass die Klarheit absoluten Vorrang hat. P rofis nutzen ih re Fäh igkeiten zum Guten
und sch reiben Code, den andere versteh en.

54
2.9
Klassen namen

2.9 Klassen n a m e n

Klassen und Objekte sollten Namen h aben, die aus einem Substantiv oder einem
substantivisch en Ausdruck besteh en: C u s tome r, Wi ki Page, Ac c o u n t oder Add r e s s ­
Pa r s e r. Vermeiden Sie Wörter wie Manage r, P ro c e s s o r, Data oder I n fo im Namen
einer Klasse. Ein Klassenname sollte kein Verb sein.

2.1 0 M ethoden n a m en

Meth oden sollten Namen h aben, die aus einem Verb oder einem Ausdruck mit
einem Verb besteh en: pos t Paym e n t , de l e t e Page oder s av e . Accessoren, Mutato­
ren und P rädikate sollten nach ih rem Wert benannt werden und, dem JavaBean­
Standard folgend, ein P räfix wie g e t , s e t und i s h aben ( h t t p : I jj ava . s u n . comj
j ava s e/te c h n o l ogi e s /d e s ktop/j avabean s / i n d e x . j s p) .

s t r i n g n ame = e m p l o ye e . g e t Name ( ) ;
c u s t ome r . s e t N ame ( " mi k e " ) ;
i f ( p ayc h ec k . i s Po s t e d ( ) ) . . .

Wenn Konstrukta ren überladen werden, sollten Sie statisch e Factory-Meth oden mit
Namen verwenden, die die Argumente beschreiben. Ein Beispiel,

Comp l ex f u l c r u m Po i n t = Com p l e x . F romRe a l N u m be r ( 2 3 . 0) ;

ist im Allgemeinen besser als

Com p l e x fu l c r u m Po i n t = n ew Com p l e x ( 2 3 . 0) ;

Um die Anwendung der entsprech enden Konstrukta ren zu erzwingen, können Sie
sie als p r i vate deklarieren.

2.1 1 Vermeiden Sie h u morige N a m e n

Wenn Namen z u h umorig gewählt sind, erinnern sich nur Entwickler daran, die
denselben Sinn für Humor wie der Autor h aben, und auch nur so lange, wie sie sich
an den Witz erinnern. Werden sie später noch wissen, was die Funktion namens
Hol yHandG r e n ad e tun soll? Sicher, der Name ist h umorig, aber vielleicht wäre in
diesem Fall ein nüch ternes De l e t e l t em s vielleicht doch der bessere Name. Klarh eit
ist wichtiger als der Unterh altungswert
Humorigkeit im Code zeigt sich oft auch in Form von umgangssprachlich en Aus­
drücken oder Slang. Beispielsweise sollten Sie nicht den Namen whac k ( ) für
ki l l ( ) verwenden. Erzählen Sie keine kleinen kulturspezifisch en Witze wie eat ­
MyS h o rt s ( ) , wenn Sie a bo r t ( ) meinen.
Sagen Sie, was Sie meinen. Meinen Sie, was Sie sagen.
Kapitel 2
A u s s a g e kräfti g e N a m e n

2.1 2 Wä h l en Sie ei n Wort pro Ko n zept

Wäh len Sie ein Wort für ein abstraktes Konzept aus und bleiben Sie dabei. Beispiels­
weise ist es verwirrend, wenn Sie fe t c h , ret r i eve und g e t als Namen für gleich ­
wertige Meth oden versch iedener Klassen verwenden. Wie erinnern Sie sich ,
welch er Meth odenname zu welch er Klasse geh ört? Traurigerweise müssen Sie sich
oft merken, welch es Unterneh men, welch e Gruppe oder welch e Person die Library
oder Klasse schrieb, um sich daran zu erinnern, welch e Bezeich nung benutzt
wurde. Andernfalls verbringen Sie sch recklich viel Z eit damit, Header und vorh e­
rige Code-Beispiele zu durch such en.
Moderne Entwicklungsumgehungen wie Eclipse und IntelliJ stellen Ih nen kontext­
sensitive Hinweise zur Verfügung, etwa die Liste der Meth oden, die Sie bei einem
gegebenen Obj ekt aufrufen können. Allerdings zeigt die Liste normalerweise nich t
die Kommentare mit an, die Sie z u Ih ren Funktionsnamen und P arameterlisten
gesch rieben h aben. Wenn Sie Glück h aben, zeigt sie die Parameter-Namen aus den
Funktionsdeklarationen an. Die Funktionsnamen müssen für sich selbst sprech en,
und sie müssen konsistent sein, damit Sie die korrekte Meth ode oh ne zusätzlich e
Such arbeit auswählen können.
Äh nlich ist es auch verwirrend, wenn Sie in einer Code-Basis einen co n t ro l l e r
und einen manag e r und einen d ri ve r verwenden. Was ist der wesentlich e Unter­
sch ied zwisch en einem Devi c eManag e r und einem P rotocol Con t rol l e r ?
Warum sind nich t beide cont rol l e rs oder beide m a n a g e rs? Sind beide wirklich
Drivers ? Der Name verleitet Sie dazu, zwei Objekte zu erwarten, die einem seh r ver­
sch iedenen Typ angeh ören und ganz versch iedene Klassen h aben.
Ein konsistentes Lexikon ist ein großer Vorteil für die P rogrammierer. die Ih ren
Code verwenden müssen.

2.1 3 Ke i n e Worts piele

Verwenden Sie nicht dasselbe Wort für zwei Zwecke. Wenn Sie dieselbe Bezeich ­
nung für zwei versch iedene Konzepte verwenden, treiben Sie ein \Vortspiel.
Wenn Sie die Regel »ein Wort pro Konzept« beach ten, erh alten Sie möglich erweise
viele Klassen. die beispielsweise eine add-Meth ode enth alten. Solange die Parame­
terlisten und Rückgabewerte der versch iedenen add-Meth oden semantisch gleich ­
wertig sind, ist alles in Ordnung.
Doch möglich erweise besch ließen Sie, das Wort add aus Gründen der »Konsistenz«
für einen Zweck zu verwenden, bei dem keine Addition in demselben Sinne erfolgt.
Angenommen, Sie h ätten viele Klassen, in denen mit add ein neuer Wert erstellt
wird, indem zwei vorh andene Werte addiert oder verkettet werden. Doch jetzt woll­
ten Sie eine neue Klasse sch reiben, die über eine Meth ode verfügt, die ih ren einzi­
gen Parameter in eine Collection einfügt. Sollten Sie diese \feth ode add nennen ?

sG
2. 1 4
N a m e n d e r Lös u n g s d o m ä n e verwe n d e n

Vielleich t h alten Sie dies für k onsistent, weil Sie so viele andere add-Meth oden
h aben. Doch in diesem Fall ist die Bedeutung eine andere; desh alb sollten Sie statt­
dessen einen Namen wie i n s e rt oder append verwenden. Die neue Meth ode eben­
falls add zu nennen, wäre ein Wortspiel.
Als Autoren wollen wir Code sch reiben, der so leich t lesbar wie möglich ist. Der
Leser soll den Code sch nell überfliegen k önnen und nich t intensiv studieren müs­
sen. Sie sollten dem beliebten Tasch enbuch -Modell folgen und sich als Autor dafür
verantwortlich fühlen, sich klar auszudrück en. Dagegen ist es beim ak ademisch en
Modell die Aufgabe des Geleh rten, die Bedeutung aus dem Papier auszugraben.

2.1 4 N a m e n der Lös u n gsd o m ä n e verwe n d e n

Denk en Sie daran, dass die Entwickler, die Ih ren Code lesen, Programmierer sind.
Desh alb sollten Sie Fachbegriffe der I nformatik , Algorith mennamen, Pattern­
Namen, math ematisch e Begriffe usw. verwenden. Es ist nicht sinnvoll, jeden
Namen aus der Problemdomäne zu entleh nen, weil unsere Kollegen nicht jedes Mal
gezwungen sein sollten, beim Kunden rück zufragen, um die Bedeutung eines
Namens zu erfah ren, wenn sie das Konzept bereits unter einem anderen Namen
k ennen.
Der Name Ac c o u n tVi s i to r sagt einem Programmierer seh r viel, der mit dem Visi­
tor-Pattern vertraut ist. Dagegen würde ein Programmierer wah rsch einlich nich t
wissen, was eine J o bQu e u e ist. Programmierer müssen zah lreich e seh r tech nisch e
Aufg aben erledigen. Tech nisch e Namen für diese Aufgaben zu wäh len, ist norma­
lerweise die angemessenste Vorgeh ensweise.

2.1 5 N a m e n der Problemd o m ä n e verwe n d e n

Wenn e s keine Termini aus der I nformatik für Ih re Aufgaben gibt, sollten Sie die
Namen aus der Problemdomäne entleh nen. Dann k önnen Programmierer, die
Ih ren Code warten, wenigstens einen Bereich sexperten nach der Bedeutung fragen.
Das Konzept der Lösungs- und der Problemdomäne zu trennen, geh ört zu den Auf­
gaben eines guten Programmierers und Designers. Der Code, der meh r mit Kon­
zepten der Problemdomäne zu tun h at, sollte entsprech end meh r Namen aus der
Problemdomäne enth alten.

2.1 6 Bedeutu n gsvo l l e n Kontext h i n z u fü ge n

Es gibt einige Namen, die a n sich sch on bedeutungsvoll sind - die meisten sind e s
nich t. Stattdessen müssen Sie die Namen für Ih re Leser i n einen Kontext stellen.
indem Sie die Namen in wohlbenannte Klassen, Funktionen oder Namespaces ein-

57
Kapitel 2
A u s s a g e k räfti g e N a m e n

fügen. Wenn alles andere k einen Erfolg h at, ist möglich erweise ein Präfix als letzter
Ausweg erforderlich .
Stellen Sie sich vor, Sie h ätten Variablen namens fi r s t N ame, l a s t N ame, s t r e e t ,
h o u s e N u m b e r, c i ty, s t ate und z i p co d e . Zusammengenommen ist es ziemlich
klar, dass sie eine Adresse bilden. Aber was wäre, wenn Sie nur die s tate-Variable
allein in einer Meth ode seh en würden? Würden Sie automatisch sch ließen, dass es
sich um einen Teil einer Adresse h andelte?
Sie k önnen den Kontext mith ilfe von Präfixen h inzufügen: add r F i r s t N ame,
add r Las t N am e , add r State usw. Wenigstens versteh t dann der Leser, dass diese
Variablen zu einer größeren Struktur geh ören. Natürlich wäre es besser, eine Klasse
namens Add r e s s zu erstellen. Dann wüsste sogar der Compiler, dass die Variablen
zu einem größeren Konzept geh örten.
Betrach ten Sie die Meth ode in Listing 2.r. Brauch en die Variablen einen bedeu­
tungsvollereD Kontext? Der Funktionsname stellt nur einen Teil des Kontextes zur
Verfügung; der Algorith mus liefert den Rest. Wenn Sie die Funktion lesen, seh en
Sie, dass die drei Variablen, n u mbe r, ve r b und p l u ra l Mod i fi e r zu der Nach rich t
»guess statistics« (etwa: »Statistisch e Daten erraten«) geh ören. Leider muss der
Kontext ersch lossen werden. Wenn Sie sich die Meth ode zum ersten Mal
ansch auen, sind die Bedeutungen der Variablen nich t zu erk ennen.

listi ng 2.1 : Vari a blen m it u n klarem Kontext


p r i vate voi d p r i n tG u e s s S t a t i s t i c s ( c h a r c a n d i d a t e , i n t c o u n t ) {
S t r i n g n u m be r ;
Stri ng verb ;
S t ri n g p l u ral Mod i fi e r ;
i f (co u n t 0) {
==

n umbe r = " no" ;


ve r b = " a re" ;
p l u r a l Mod i fi e r "s" ; =

} e l s e i f ( co u n t == 1) {
numbe r "1" ;
=

ve r b = "i s" ;
p l u r a l Modi fi e r = " " ·
'

} el se {
n umbe r = I n t e g e r . to S t r i n g ( c o u n t ) ;
ve r b = " a re" ;
p l u ra l Modi fi e r = "s" ;
}
Stri ng guessMessage S t r i n g . fo rmat (
=

" Th e r e %s %s %s%s " , ve r b , n u m be r , c a n d i d a t e , p l u r a l Modi fi e r


);
p ri nt (gues sMessage) ;
}
2.1 6
B e d e u t u n g svo l l e n Kon text h i n z ufü g e n

Die Funktion ist ein wenig zu lang und die Variablen werden in der ganzen Funktion
benutzt. Um die Funktion in kleinere Teile zu zerlegen, müssen wir eine G u e s s ­
Stati s t i c s M e s s a g e Klasse erstellen und die drei Variablen als Felder dieser
-

Klasse deklarieren. Dadurch erstellen wir einen klaren Kontext für die drei Variab­
len. Sie gehören definitiv zu der G u e s s Stati s t i c s Me s s ag e . Wegen der Verbesse­
rung des Kontextes wird auch der Algorithmus viel sauberer, indem wir ihn in viele
kleinere Funktionen zerlegen (siehe Listing 2 . 2 ) .

listi ng 2.2: Variablen mit Kontext


p u b l i c c l a s s G u e s s S t at i s t i c s M e s s ag e {
p r i vate S t r i n g n u mbe r ;
p r i vate S t r i n g ve r b ;
p r i vate S t r i n g p l u r a l Modi fi e r ;

p u b l i c S t r i n g make ( c h a r c a n d i d a t e , i n t c o u n t ) {
c r eate P l u r a l D e p e n d e n t Me s s ag e P a r t s ( c o u n t ) ;
r e t u rn S t r i n g . fo rmat (
" Th e r e %s %s %s%s " ,
ve r b , n u mbe r , c a n d i dat e , p l u r a l Mod i fi e r ) ;
}

p r i vate voi d c re a t e P l u r al D e p e n d e n t Me s s ag e P a r t s ( i n t cou n t ) {


i f (cou n t == 0) {
the reAreNolette rs () ;
} e l s e i f (c o u n t == 1) {
t h e re i sO n e l e t t e r () ;
} e l se {
t h e reA r e M a n y l e tt e r s ( co u n t ) ;
}
}

p r i vate voi d t h e reA reMany l e t t e r s ( i n t cou n t ) {


n u mbe r = I n t e g e r . t o S t r i n g ( c o u n t ) ;
ve r b = " a r e " ;
p l u r a l M odi fi e r = " s " ;
}

p r i vate voi d t h e r e i sO n e l e t t e r () {
n u mbe r = "1" ;
ve r b = " i s " ;
p l u r a l Mod i fi e r = " " ·
'

p r i vate voi d t h e r e A r e N o l e t t e r s ( ) {
n u mbe r = " no " ;
verb = "are" ;
p l u r a l Mod i fi e r = " s " ;
}
}
Kapitel 2
A u s s a g ekräfti g e N a m e n

2.1 7 Kei nen ü be rfl ü ss i ge n Ko ntext h i nz ufü ge n

Angenommen, Sie erstellten eine Anwendung namens »Gas Station Deluxe«. Dann
wäre es keine gute Idee, jede Klasse mit dem P räfix GSD zu beginnen. Offen gesagt:
Sie würden dann gegen Ihre Tools arbeiten. Sie tippen G ein, drücken au:fi Cample­
tion Key und werden mit einer ellenlangen Liste aller Klassen des Systems belohnt.
I st das intelligent? Warum sollten Sie es der I D E schwermachen, Ihnen zu helfen?
Ein ähnlicher Fall: Sie habe eine Mai l i n gAdd r e s s - Kl asse in dem Buchhaltungsmo­
dul von GSD entwickelt und nennen Sie GSDAc c o u n tAdd r e s s . Später brauchen Sie
eine Postanschrift für Ihre Kundenkontakt-Anwendung. Verwenden Sie G S D ­
A c c o u n tAdd r es s ? Hört sich das wie der richtige Name an? Zehn von 17 Zeichen
sind redundant oder irrelevant.
Kürzere Namen sind im Allgemeinen besser als längere, solange sie klar sind.
Fügen Sie nicht mehr Kontext zu einem Namen hinzu, als erforderlich ist.
Die Namen a c c o u n tAd d r e s s und c u s tome rAd d r e s s sind geeignete Namen für
Instanzen der Klasse Add r e s s , könnten aber für Klassen ungeeignet sein. Add r e s s
ist ein geeigneter Name für eine Klasse. Wenn ich zwischen MAC-Adressen, P ort­
Adressen und Web-Adressen unterscheiden muss, würde ich Postal Ad d r e s s , MAC
und U R I in Erwägung ziehen. Die fertigen Namen sind präziser, und darum geht
es bei der Benennung.

2.1 8 Absch l ieße n d e Wo rte


Die Schwierigkeit, gute Namen zu wählen, geht auf zwei Faktoren zurück. Erstens
muss man über gute Beschreibungsfahigkeiten verfügen. Zweitens braucht man
einen gemeinsamen kulturellen Hintergrund. Dies ist ein P roblem des Lebrens
und Lernens und kein technisches, geschäftliches oder organisatorisches P roblem.
Deshalb haben viele Entwickler auf diesem Teilgebiet nicht sehr viel gelernt.
Viele haben auch Angst, Dinge umzubenennen, weil sie P roteste ihrer Kollegen
fürchten. Wir teilen diese Angst nicht und sind eigentlich dankbar, wenn Namen
verbessert werden. Meistens lernen wir die Namen von Klassen und Methoden
nicht auswendig. Wir verwenden unsere modernen Werkzeuge, um die Details zu
handhaben, damit wir uns darau:fi konzentrieren können, ob sich der Code wie
Absätze und Sätze oder wenigstens wie Tabellen und Datenstrukturen lesen lässt.
(Ein Satz ist nicht immer die beste Form, Daten anzuzeigen. ) Wahrscheinlich wer­
den Sie jeden überraschen, wenn Sie etwas umbenennen; aber das gilt für andere
Code-Verbesserungen auch. Lassen Sie sich davon nicht abhalten.
Befolgen Sie einige dieser Regeln und prüfen Sie, ob Sie damit die Lesbarkeit Ihres
Codes verbessern. Wenn Sie den Code eines anderen Entwicklers warten, sollten Sie
Refactoring-Werkzeuge heranziehen, die Ihnen helfen, diese P robleme zu lösen. Es
zahlt sich schon kurzfristig aus, und auch langfristig werden Sie immer weiteren
Nutzen ernten.

Go
Kapitel 3

Fu n ktionen

In den Anfangstagen der P rogrammierung setzen wir unsere Systeme aus Routi­
nen und Subroutinen zusammen. Dann, in der Ära von Fortran und P L/ I , setzen
wir unsere Systeme aus P rogrammen, Unterprogrammen und Funktionen zusam­
men. Heutzutage haben nur die Funktionen aus den Anfangstagen überlebt. Funk­
tionen sind die ersten Organisationseinheiten j edes P rogramms. Wie man gute
Funktionen schreibt, ist Thema dieses Kapitels.
Betrachten Sie den Code in Listing 3 - l · Es ist schwer, eine lange Funktion in Fit­
Nesse, einem Open- Source-Testwerkzeug ( h t t p : I /fi t n e s s e . o rg) , zu finden;
aber nachdem ich ein wenig gesucht hatte, stieß ich auf die hier gezeigte Funktion.
Sie ist nicht nur lang, sondern sie enthält duplizierten Code, zahlreiche komische
Strings und viele seltsame und nicht offensichtliche Datentypen und AP is. Versu­
chen Sie, ob Sie in den nächsten drei Minuten daraus schlau werden.
Kapitel 3
F u n kt i o n e n

Listi ng 3.1: Html U t i 1 . j ava (FitNesse 2007061 9)


p u b l i c s t at i c S t r i n g t e s t a b l e H t m l (
PageData pageDat a ,
bool e a n i n c l u d e S u i t e S e t u p
) t h rows E x c e p t i on {
Wi ki Page wi ki Page = p a g e Dat a . g e tWi k i Page ( ) ;
S t r i n g ß u ffe r b u ffe r = n ew S t r i n g ß u ffe r ( ) ;
i f ( pageData . h asAtt r i b u t e ( "Tes t " ) ) {
i f ( i n c l u d e S u i t e S e t u p) {
Wi ki Page s u i t e S e t u p =
PageC rawl e r i m p l . g e t i n h e r i t e d P ag e (
S u i t e Re s po n d e r . S U ITE_S ETU P_NAM E , wi ki Page
);
i f (sui teSetup ! = nul l ) {
Wi ki P a g e P a t h p a g e P a t h =
s u i t e S e t u p . g e t PageC r awl e r ( ) . g e t F u l l Pat h ( s u i t e S e t u p) ;
S t r i n g pag e P a t h N ame = Pat h P a r s e r . r e n d e r ( p a g e P a t h ) ;
b u ffe r . a p p e n d ( " ! i n c l u d e - s e t u p . " )
. a ppe n d ( pa g e P a t h N ame)
. append ( "\n " ) ;
}
}
Wi k i Page s e t u p =
PageC rawl e r i m p 1 . g e t i n n e r i t e d Page ( " S e t U p " , wi ki Page) ;
i f (setup ! = nul l ) {
Wi ki P a g e P a t h s e t u p Pa t h =
wi ki Page . g e t PageC ra� l e r ( ) . g e t F u l l Pat h ( s e t u p ) ;
S t r i n g s e t u p P a t h � a - e = Pat h Pa r s e r . r e n d e r ( s e t u p Pat h ) ;
b u ffe r . a p p e n d ( " ! i n c l u d e - s e t u p . " )
. a p p e n d ( s e t u p Pa t h N am e )
. append ( " \ n " ) ;
}
}
b u f fe r . a p p e n d ( pageData . g e tCon t e n t () ) ;
i f ( pageData . h asAtt r i b u t e ( " Te s t " ) ) {
Wi ki Page t e a rdo�n =

PageC rawl e r i m p l . g e t i n h e r i t e d P ag e ( "Tea rDown " , wi ki Page) ;


i f ( t e a rdown ! = n u l l ) {
Wi ki P a g e P a t h t e a rDown Path =
wi k i Page . g e t PageC rawl e r ( ) . g e t F u l l Pat h ( t e a rdown ) ;
S t r i n g tea rDo � n P a t h Name = Pat h Pa r s e r . r e n d e r ( t e a rDown Pat h ) ;
b u ffe r . a p p e n d ( " \ n " )
. a p p e n d C ! i n c l u d e - t e a rdown . " )
. a ppe n d ( t e a rDown P at h Name)
. a p p e n d ( " \n " ) ;
}
i f ( i n c l u d e S u i t e S e t u p) {
Wi ki Page s u i teTea rdown =
Pag e C r awl e r i m p l . g e t i n h e r i t e d Pag e (

62
2.18
Absc h l ieße n d e Worte

S u i t e R e s p o n d e r . SU ITE_TEARDOWN_NAM E ,
wi ki Page
);
i f ( s u i teTea rdown ! = n u l l ) {
Wi k i Pag e Pa t h p a g e P a t h =
s u i teTe a r down . g e t Pag e C r awl e r ( ) . g e t Fu l l P a t h ( s u i t eTe a r down ) ;
S t r i n g pag e Pa t h Name = Pat h P a r s e r . r e n d e r ( p a g e Pat h ) ;
b u ffe r . a p p e n d ( " ! i n c l u d e - t e a r down . " )
. ap p e n d ( p a g e P a t h N am e )
. ap p e n d ( " \ n " ) ;
}
}
}
p a g e Data . s e tCon t e n t ( b uffe r . to S t r i n g ( ) ) ;
r e t u rn pageData . g e t H t m l ( ) ;
}

Versteh en Sie die Funktion nach drei Minuten Studium ? Wah rsch einlich nicht. Es
passiert zu viel auf zu vielen verschiedenen Abstraktionsebenen. Es gibt seltsame
Strings und gelegentlich e Funktionsaufrufe, die mit doppelt versch achtelten i f­
Anweisungen vermengt sind, die durch Flags gesteuert werden.
Doch durch Extrah ieren einiger einfach er Meth oden, einige Umbenennungen und
ein wenig Umstrukturierung konnte ich den Zweck der Funktion in den neun Z ei­
len von Listing 3 . 2 zum Ausdruck bringen. Sch auen Sie, ob Sie das in den näch sten
drei Minuten versteh en können.

Listi ng 3.2: Html U t i l . j ava (nach Refactori ng)


p u b l i c s t a t i c S t r i n g r e n d e r Pag eWi t h S e t u psAndTea rdown s (
PageData p a g e D at a , bool ean i s S u i t e
) t h rows E x c e p t i on {
boo l e a n i sT e s t Page = pageData . h asAtt r i b u t e ( "Tes t " ) ;
i f ( i sTe s t Pa g e ) {
Wi k i Page t e s t Pa g e = pageData . g etWi k i Pag e ( ) ;
S t r i n g B u ffe r n ewPageCo n t e n t = new S t r i n g B u ffe r () ;
i n c l u d e S e t u p Pag e s ( t e s t Pa g e , n ewPag eCon t e n t , i s S u i t e ) ;
n ewPageCo n t e n t . a p p e n d ( p a g e D a t a . g etCon te n t () ) ;
i n c l u d eTe a r d own Pag e s ( t e s t Page , n ewPageCo n t e n t , i s S u i t e ) ;
p a g e D a t a . s e tCon t e n t ( n ewPageCo n t e n t . t o St r i n g ( ) ) ;
}

ret u r n pageData . g etHtml () ;


}

Wenn Sie FitNesse nicht näh er studiert h aben, versteh en Sie wah rsch einlich nicht
alle Details. Dennoch versteh en Sie wah rsch einlich , dass diese Funktion einige
Setup- und Teardown-Seiten in eine Test- Seite einfügt und dann diese Seite i1:
HTIM L darstellt. Wenn Sie JUnit, ein Open- Source-Testwerkzeug für ' _ _
Kapitel 3
F u n kt i o n e n

(www . j u n i t . o rg) , kennen, erkennen Sie wahrscheinlich, dass diese Funktion zu


einem webbasierten Test-Framework gehört. Das stimmt natürlich. Diese Informa­
tionen lassen sich aus Listing 3 . 2 recht leicht erschließen. dagegen sind sie in Listing
3 . 1 ziemlich gut verborgen.
Also: Wodurch wird eine Funktion wie Listing 3.2 leicht lesbar und verstehbar? Wie
können wir den Zweck einer Funktion klar kommunizieren ? Welche Eigenschaften
müssen unsere Funktionen haben, damit ein zufälliger Leser die Art von Programm
erschließen kann, zu der die Funktion gehört?

3.1 Klei n !

Die erste Regel für Funktionen lautet: Funktionen sollten klein sein. Die zweite
Regel für Funktionen lautet: Funktionen sollten noch kleiner sein . Diese Aussage kann
ich nicht weiter begründen. Ich kann keine professionellen Forschungsarbeiten
zitieren, die gezeigt haben, dass sehr kleine Funktionen besser sind. Ich kann Ihnen
allerdings sagen, dass ich in fast vier Jahrzehnten Funktionen aller Größen
geschrieben habe: sehr hässliche 3.ooo Zeilen lange Monster: tonnenweise Funk­
tionen im Bereich von 1 0 0 bis 300 Zeilen; zahlreiche Funktionen . die 20 bis 30 Zei­
len lang waren. Aus dieser Erfahrung habe ich durch Versuch und Irrtum gelernt,
dass Funktionen sehr klein sein sollten.
In den 8oer-Jahren pflegten wir zu sagen, dass eine Funktion nicht länger als eine
Bildschirmseite sein sollte. Allerdings waren damals unsere VT10o-Bildschirme 24
Zeilen hoch und 8o Spalten breit; und unsere Editoren brauchten vier Zeilen für
administrative Zwecke. Heute könnten Sie mit einem kleinen Font und einem schö­
nen großen Monitor 150 Zeichen in einer Zeile und etwa 100 Zeilen oder mehr auf
einem Bildschirm unterbringen. Zeilen sollten nicht länger als 150 Zeichen sein.
Funktionen sollten nicht länger als 100 Zeilen sein. Funktionen sollten kaum
jemals länger als 20 Zeilen sein.
Wie kurz sollte eine Funktion sein? 1 9 9 9 besuchte ich Kent Beck zu Hause in Ore­
gon. Wir setzten uns zusammen und programmierten ein wenig. Irgendwann
zeigte er mir ein hübsches kleines JavajSwing-Programm, das er Sparkle nannte. Es
produzierte auf dem Bildschirm einen visuellen Effekt, der dem Feenstaub ähnelte,
den der Zauberstab der Feenmutter aus dem Märchen-Zeichentrickfilm Aschen­
puttel von Walt Disney versprühte. Wenn man die Maus bewegte. lösten sich die
Staubkörnchen mit einem befriedigenden Funkeln vom Cursor und fielen dann
durch ein simuliertes Gravitationsfeld auf den »Boden« des Fensters. Als Kent mir
den Code zeigte, war ich überrascht, wie viele kleine Funktionen der Code enthielt.
Ich war an die Funktionen in Swing-Programmen gewöhnt. die vertikal sehr viel
Platz beanspruchten. Jede Funktion in Becks Programm war nur zwei oder drei oder
vier Zeilen lang. Jede hat einen klar erkennbaren Zweck. Jede erzählte eine
Geschichte. Und eine Funktion führte zwangsläufig zur nächsten. Das Programm
zeigt eindringlich, wie kurz Ihre Funktionen sein sollten! Ich habe Kent gefragt, ob
3 -2
E i n e Aufg abe e rfü l l e n

er noch eine Kopie dieses Programms hätte; leider konnte er keine finden. Ich habe
auch alle meine alten Computer durchsucht. Leider ebenfalls vergeblich. Ich habe
nur noch meine Erinnerung an dieses Programm.
Wie kurz sollten Ihre Funktionen sein? Normalerweise sollten sie kürzer sein als
Listing 3-2 ! Tatsächlich sollte Listing 3-2 zu Listing 3·3 verkürzt werden.

listi ng 3.3: Html U t i l . j ava (nach erneutem Refactori ng)


p u b l i c s t a t i c S t r i n g r e n d e r Pag eWi t h S e t u psAn dTea rdown s (
PageData pageData , bool e a n i s S u i t e ) t h rows E x c e p t i on {
i f ( i sTe s t P ag e ( pageData) )
i n c l u de S e t u pAndTe a r down P a g e s ( pageData , i s S u i t e ) ;
r et u r n pageDat a . g e t H tm1 () ;
}

B l öc ke u nd E i n rücku n gen

Dies bedeutet auch, dass die Blöcke innerhalb von i f-, e 1 se-, w h i 1 e- und ähnlichen
Anweisungen eine Zeile lang sein sollten. Wahrscheinlich sollte diese Zeile einen
Funktionsaufruf enthalten. Dadurch wird nicht nur der Umfang der einschließen­
den Funktion kleiner gehalten, sondern auch ihr dokumentarischer Wert erhöht,
weil die Funktionsaufrufe innerhalb der Blöcke aussagestarke beschreibende
Namen haben können.
Dies bedeutet auch, dass Funktionen nicht groß genug sind, um verschachtelte
Strukturen aufzunehmen. Deshalb sollte die Einrückungstiefe einer Funktion nicht
größer als eine oder zwei Ebenen sein. Dadurch wird es natürlich leichter, die Funk­
tionen zu lesen und zu verstehen.

3.2 E i n e Aufga be erfü l le n

Aus Listing 3 - 1 sollte ganz klar hervorgehen, dass die Funktion sehr viel mehr als
eine Aufgabe erfüllt. Sie erstellt Puffer, ruft Seiten ab, sucht nach geerbten Seiten,
setzt Pfade zusammen, hängt geheime Strings an, generiert HTM L u.a. Die Funk­
tion in Listing 3- 1 ist stark damit beschäftigt, zahlreiche verschiedene Aufgaben zu
erfüllen. Andererseits erfüllt die Funktion in Listing 3 ·3 eine einfache Aufgabe. Sie
fügt Setups und Teardowns in Testseiten ein.
Der folgende Rat wird in der ein oder anderen Form seit über 30 Jahren ausgespro­
chen:
Funktionen sollten eine Aufgabe erledigen. Sie sollten sie gut erledigen. Sie soll­
ten nur diese Aufgabe erledigen.
Dieser Rat ist in einer Hinsicht problematisch: Es ist schwer zu erkennen, was »eine
Aufgabe« ist. Erledigt Listing 3 · 3 eine Aufgabe ? Man könnte leicht begründen, dass
sie drei Aufgaben erfüllt:
Kapitel 3
F u n kt i o n e n

1. Sie bestimmt, ob die Seite eine Testseite ist.


2. Falls ja, schließt sie Setups und Teardowns ein.
3 · Sie stellt die Seite in HTML dar.
Was ist denn nun richtig? Erledigt die Funktion eine Aufgabe oder drei Aufgaben?
Beachten Sie, dass die drei Schritte der Funktion eine Abstraktionsebene unter dem
Zweck liegen, der durch den Namen der Funktion ausgedrückt wird. Wir können
die Funktion beschreiben, indem wir sie mit einem kurzen TO-Absatz (UM-ZU­
Absatz) beschreiben:
TO Rende rPageWi thSetupsAndTea rdowns, prüfen wir, ob die Seite eine
Testseite ist, und wenn dies der Fall ist, schließen wir die Setups und Teardowns
ein. In beiden Fällen stellen wir die Seite in HTML dar.
Die Technik ist der Programmiersprache LOGO entlehnt. In LOGO wurde das
Schlüsselwort TO in derselben Art und Weise verwendet wie d e f in Ruby oder
Python. Deshalb begann jede Funktion mit dem Schlüsselwort TO. Dies hatte eine
interessante Auswirkung auf das Design von Funktionen.
Wenn eine Funktion nur solche Schritte ausführt, die eine Abstraktionsebene unter
dem im Namen der Funktion ausgedrückten Zweck liegen, dann erledigt die Funk­
tion eine Aufgabe. Schließlich besteht der Grund, warum wir Funktionen schreiben,
darin, dass wir ein größeres Konzept (anders ausgedrückt: den Namen der Funktion)
in einen Satz von Schritten auf der nächsttieferen Abstraktionsebene zerlegen.
Es sollte klar sein. dass Listing 3 - 1 Schritte aufi vielen verschiedenen Abstraktions­
ebenen enthält. Deshalb erfüllt die Funktion zweifellos mehr als eine Aufgabe. Sogar
Listing 3 - 2 enthält zwei Abstraktionsebenen. Wir konnten dies durch unsere Fähig­
keit beweisen, die Funktion zu verkürzen. Aber es wäre sehr schwer. Listing 3 · 3 sinn­
voll zu verkleinern. Wir könnten die i f-Anweisung in eine Funktion namens
i n c 1 u d e S e t u p sAndTe a rdown s i fTe s t Page extrahieren, aber damit würde nur der
Code unter einem neuen Namen an eine andere Stelle verlagert. ohne die Abstrak­
tionsebene zu ändern.
Damit haben Sie eine andere Methode, zu erkennen, dass eine Funktion mehr als
»eine Aufgabe« erfüllt: wenn Sie aus ihr eine andere Funktion mit einem Namen
extrahieren können, der nicht nur eine Neuformulierung ihrer Implementierung
ist [G34] .

Absc h n itte i n nerh a l b von Fu n ktionen

Blättern Sie vor zu Listing 4·7 im folgenden Kapitel. Beachten Sie. dass die Funktion
g e n e r a t e P r i mes in Abschnitte unterteilt ist, wie etwa dedarations, initializations
und sieve. Dies ist offensichtlich ein Symptom dafür, dass sie mehr als eine Aufgabe
erfüllt. Funktionen, die eine Aufgabe erledigen, können nicht \"ernünftigerweise in
Abschnitte zerlegt werden.

66
3·3
E i n e Abstra kti o n s e b e n e p ro F u n kt i o n

3·3 E i n e Abstra ktionsebene p ro Fu n ktion

Wenn unsere Funktionen »(nur) eine Aufgabe« erledigen sollen, müssen sich die
Anweisungen innerhalb unserer Funktion alle auf derselben Abstraktionsebene
befinden. Es ist einfach zu sehen, wie Listing 3 - 1 gegen diese Regel verstößt. Die
Funktion enthält Konzepte, die auf einer sehr hohen Abstraktionsebene angesiedelt
sind, wie etwa g e t H tm l () ; andere befinden sich auf mittleren Abstraktionsebenen,
wie etwa: St r i ng pag e Pat h N ame Pat h Pa r s e r . r e n d e r ( pag e Pat h ) ; und wieder
=

andere sind auf einer bemerkenswert tiefen Ebene angesiedelt, wie etwa:
. append ( " \n " ) .
Abstraktionsebenen innerhalb einer Funktion zu vermischen, ist immer verwir­
rend. Möglicherweise können die Leser nicht erkennen, ob ein spezieller Ausdruck
ein wesentliches Konzept oder ein Detail ist. Noch schlimmer: Ähnlich wie ein zer­
brochenes Fenster den Verfall einleitet, führen Details, die mit wesentlichen Kon­
zepten vermischt sind, dazu, dass die Funktion im Laufe der Zeit immer mehr
Details anzieht.

Code Top-down lese n : d i e Stepdow n - Regel

Code sollte wie eine Erzählung von oben nach unten gelesen werden können
([KP78], S. 37) . Danach sollten hinter jeder Funktion andere Funktionen auf der
nächsttieferen Abstraktionsebene stehen, damit wir das Programm lesen können,
indem wir jeweils eine Abstraktionsebene tiefer gehen, wenn wir die Liste der Funk­
tionen von oben nach unten lesen. Ich bezeichne dies als die Stepdown-Regel (»eine
Treppe runtergehen«) .
Anders ausgedrückt: Wir sollten das Programm wie eine Folge von UM-Z U-Absät­
zen lesen können, die jeweils die gegenwärtige Abstraktionsebene beschreiben und
die eine Abstraktionsebene tiefer liegenden UM-Z U-Absätze referenzieren.
Um die Setups und Teardowns einzuschließen, schließen wir Setups ein, dann
schließen wir den Inhalt der Testseite ein, und dann schließen wir die Tear­
downs ein.
Um die Setups einzuschließen, schließen wir das Suite-Setup ein, wenn dies
eine Suite ist; dann schliqsen wir das normale Setup ein.
Um das Suite-Setup einzuschließen, durchsuchen wir die Parent-Hierarchie
nach der »Suite Set Up«-Seite und fügen eine »include«-Anweisung mit dem
Pfad dieser Seite hinzu.
Um die Parent-Hierarchie . . .
Es hat sich gezeigt, dass Programmierer nur sehr schwer lernen, diese Regel zu
befolgen und Funktionen zu schreiben, die auf einer einzigen Abstraktionsebene
bleiben. Doch diese Technik beherrschen zu lernen, ist ebenfalls sehr wichtig. Sie
ist der Schlüssel dazu, kurze Funktionen zu schreiben, die »eine Aufgabe« erfüllen
Kapitel 3
F u n kt i o n e n

Den Code so zu strukturieren, dass er wie ein Top-down-Satz von UM-Z U-Absätzen
gelesen werden kann, ist eine wirksame Technik, um das Abstraktionsniveau kon­
sistent zu halten.
Betrachten Sie Listing 3 ·7 am Ende dieses Kapitels. Es zeigt, wie ein Refactoring der
kompletten tes tabl eHtml -Funktion nach den hier beschriebenen Prinzipien
durchgeführt wurde. Beachten Sie, wie j ede Funktion die nächste einführt, und jede
Funktion auf einer konsistenten Abstraktionsebene bleibt.

3 ·4 Switch -Anwei s u n gen

Es ist schwer, kleine swi t c h -Anweisungen zu schreiben. (Das gilt natürlich auch
für i fIe l s e-Ketten.) Selbst eine swi t c h -Anweisung mit nur zwei Fällen ist größer,
als ein einziger Block oder eine Funktion für meinen Geschmack sein sollte. Es ist
ebenfalls schwer, eine swi t c h-Anweisung zu schreiben, die nur eine Aufgabe
erfüllt. Von Natur aus sind swi t c h-Anweisungen immer auf N Aufgaben ausgelegt.
Leider können wir swi t c h-Anweisungen nicht immer vermeiden, aber wir können
dafür sorgen, dass jede swi t c h-Anweisung tief in einer niedrig angesiedelten
Klasse vergraben ist und niemals wiederholt wird. Wir verwenden zu diesem Zweck
natürlich den Polymorphismus.
Betrachten Sie Listing 3 + Es zeigt nur eine der Operationen . die vom Typ des Mit­
arbeiters abhängen könnten.

Listing 3 .4: Payrol l . j ava


p u b l i c M o n e y c a l c u l ate Pay ( Em p l o ye e e)
t h rows I nval i d Em p l oyeeTyp e {
swi t c h ( e . t y p e ) {
c a s e COMM I S S IONED :
r et u rn c a l c u l at eCommi s s i o n e d Pay ( e ) ;
c a s e HOU R LY :
r e t u rn c a l c u l a t e Ho u r l y Pay ( e ) ;
c a s e SALARI ED :
r e t u rn c a l c u l a t e S a l a r i e d Pay ( e ) ;
defaul t :
t h row n ew I n v a l i d Em p l oyeeTy p e ( e . t y p e ) ;
}
}

Bei dieser Funktion gibt es mehrere Probleme:


• Erstens: Sie ist groß; und wenn neue Mitarbeiter-Typen hinzugefügt werden,
wird sie noch größer.
• Zweitens: Sie erfüllt mehr als eine Aufgabe.
• Drittens: Sie verstößt gegen das Single-Responsibility-Prinzip (SRP), weil es
mehr als einen Grund für eine Änderung gibt; siehe:

68
H
Switch-Anwe i s u n ge n

h t t p : //en . wi ki p e d i a . o rg/wi ki /Si n g l e_ r e s p on s i b i l i ty_p ri n c i p l e,


h t t p : //www . obj ectme n to r . com/ r e s o u r c e s j a r t i c l e s / s rp . p d f

• Viertens : Sie verstößt gegen das Open-Closed-Prinzip (OCP) , weil sie geändert
werden muss , wenn ein neuer Typ hinzugefügt wird; siehe:
h t t p : //en . wi ki p e d i a . o rg/wi ki /Open_c l o s ed_p r i n c i p l e,
h t t p : //www . obj e c tmento r . com/ r e s o u r c e s j a r t i c l e s /o c p . pd f

• Fünftens: Doch möglicherweise am schlimmsten ist es, dass es eine unbe­


grenzte Anzahl anderer Funktionen mit derselben Struktur gibt. So könnten
wir beispielsweise die Funktion

i s Payday ( E m p l oyee e , Date d a t e ) ,

oder

d e l i ve r Pay ( E m p l oyee e , Mo n e y pay) ,

oder zahlreiche andere haben, die alle dieselbe schädliche Struktur hätten.
Die Lösung für dieses Problem (siehe Listing 3-5) besteht darin, die swi t c h -Anwei­
sung in den Keller einer Abstract Factory [GOF] zu verbannen und niemals von
jemandem sehen zu lassen. Die Factory erstellt anhand der swi t c h-Anweisung die
entsprechenden Instanzen der abgeleiteten Klassen von Emp l oyee. Die verschiede­
nen Funktionen, wie etwa c a l c u l atePay, i s Payday und d e l i ve r Pay, werden
polymorph von dem E m p l o yee -lnterface an die richtige Stelle dirigiert.

Listi ng 3-5: Employee u nd Factory


p u b l i c abs t r a c t c l a s s Empl oyee {
p u b l i c a b s t r a c t bool e a n i s Payd a y ( ) ;
p u b l i c a b s t r a c t M o n e y cal c u l ate Pay ( ) ;
p u b l i c a b s t ra c t voi d d e l i ve r Pay (Mo n e y pay) ;
}

p u b l i c i n t e rface Empl o y e e F a c t o r y {
p u b l i c E m pl oyee m a k e E m p l oyee ( E m p l o y e e R e co rd r) t h rows I n va l i d E m p l oyeeType ;
}

p u b l i c c l a s s E m p l o y e e F a c t o r y i m p l i mp l e m e n t s E mp l oyee Fac t o r y {
p u b l i c Emp l oyee make Emp l oyee ( Em p l oyeeReco r d r) t h rows I nva l i d E m p l o y e eType {
swi t c h ( r . t y p e ) {
c a s e COMM I S SIONED :
r e t u r n new Commi s s i o n e d Em p l o ye e ( r)
c a s e HOU R LY :
r e t u r n new Hou r l yEmpl oyee ( r ) ;
c a s e SALARI ED :
r e t u r n new S a l a r i ed Em p l oye ( r ) ;
d e fau l t :
t h row n ew I n v a l i d E m p l oyeeType ( r . ty p e ) ;
Kapitel 3
F u n kt i o n e n

}
}
}

Meine allgemeine Regel für swi t c h -Anweisungen lautet: Sie können toleriert wer­
den, wenn sie nur einmal auftauchen, zur Erstellung polymorpher Objekte verwen­
det werden und hinter einer Vererbungsbeziehung verborgen werden, damit sie für
den Rest des Systems unsichtbar sind [G23] . Natürlich ist jeder Fall einzigartig; und
es gibt Gelegenheiten, bei denen ich gegen einen oder mehrere Teile dieser Regel
verstoße.

3·5 Besch rei bende N a m e n verwe n d e n

In Listing 3 ·7 habe ich den Namen unserer Beispielfunktion von te s t a b l e H t m l in


S e t u pTea rdown i n c l u d e r . r e n d e r geändert. Dieser Name ist erheblich besser,
weil er die Aktion der Funktion besser beschreibt. Ich habe auch alle privaten Metho­
den mit gleichermaßen beschreibenden Namen versehen, wie etwa i sTe s t a b l e
oder i n c l u d e S et u pAn dTe a r down Pag e s . Es ist schwer, den Wert guter Namen zu
überschätzen. Erinnern Sie sich an Wards Prinzip: »Sie erkennen, dass Sie mit sau­
berem Code arbeiten. wen njede Routine im Wesentlichen das tut, was Sie erwartet haben. «
Die halbe Schlacht im Kampf um die Realisierung dieses Prinzips besteht darin,
gute Namen für kleine Funktionen zu finden, die eine Aufgabe erledigen. Je kleiner
und fokussierter eine Funktion ist, desto leichter finden Sie einen beschreibenden
Namen.
Haben Sie keine Angst vor langen Namen. Ein langer beschreibender Name ist bes­
ser als ein kurzer geheimnisvoller Name. Ein langer beschreibender Name ist bes­
ser als ein langer beschreibender Kommentar. Verwenden Sie eine
Namenskonvention. die die Unterscheidung mehrerer Wörter in Funktionsnamen
leicht macht. Nutzen Sie dann diese mehreren Wörter, um der Funktion einen
Namen zu geben. der beschreibt, was sie tut.
Haben Sie keine Angst davor, sich Zeit für die Auswahl eines Namens zu nehmen.
Tatsächlich sollten Sie mehrere Namen ausprobieren und den Code probehalber
mit ihnen lesen. Bei modernen IDEs wie Eclipse oder IntelliJ ist es trivial, Namen
zu ändern. Arbeiten Sie mit einer dieser I D E s und probieren Sie verschiedene
Namen aus, bis Sie einen finden, der so beschreibend wie möglich ist.
Beschreibende Namen verhelfen Ihnen in Ihrer Vorstellung zu einem klareren Bild
des Designs des Moduls und helfen Ihnen, es zu verbessern. Es ist nicht ungewöhn­
lich, dass die Suche nach einem guten Namen zu einer vorteilhaften Umstruktu­
rierung des Codes führt.
Vergeben Sie Namen konsistent. Verwenden Sie dieselben Ausdrücke, Substantive
und Verben in den Funktionsnamen, die Sie für Ihre Module wählen. Betrachten
Sie beispielsweise die !\amen i n c l u d e S e t u pA n dTe a r down Pag e s , i n c l u d e S e t u p -

]0
3-6
F u n kt i o n s a r g u m ente

Pag e s , i n c l u d e S u i t e S e t u pPage und i n c l u d e S e t u p Page. Wegen der ähnlichen


Ausdrücke in diesen Namen kann diese Aufreihung eine Geschichte erzählen. Tat­
sächlich könnten Sie, wenn ich Ihnen nur die obige Folge von Namen zeigen würde,
sich fragen: »Was ist mit i n c l u d eTea rdown Pag e s , i n c l u d e S u i t eTe a rdown Page
und i ncl u d eTe a rdown Page passiert?« Wäre das nicht ein Beispiel für » ... im
Wesentlichen das, was Sie erwartet haben«?

3.6 Fu n ktio n s a rgu m e nte

Die ideale Anzahl von Argumenten für eine Funktion ist null (niladisch) . Als Nächs­
tes kommt eins (monadisch) , dicht gefolgt von zwei (dyadisch) . Drei Argumente (tri­
adisch) sollten, wenn möglich, vermieden werden. Mehr als drei (polyadisch)
erfordert eine sehr spezielle Begründung - und sollte dann trotzdem nicht benutzt
werden.
Argumente sind schwer. Sie erfordern eine beträchtliche konzeptionelle Kraft. Des­
halb habe ich in dem Beispiel fast keine Argumente verwendet. Betrachten Sie etwa
den S t r i n g ß u ffe r in dem Beispiel. Wir hätten ihn als Argument herumreichen
können, anstatt ihn zu einer Instanzvariablen zu machen, aber dann hätte der Leser
ihn jedes Mal interpretieren müssen, wenn er ihn gesehen hätte. Wenn Sie die
Geschichte lesen, die von dem Modul erzählt wird, ist i n c l u d e S et u p Page () leich­
ter zu verstehen als i n c l u d e S et u p Pa g e l n t o ( n ewPageCon t e n t ) . Das Argument
befindet sich auf einer anderen Abstraktionsebene als der Funktionsname und
zwingt Sie, ein Detail (anders ausgedrückt: S t r i n g ß u ffe r) zu kennen, das an die­
sem Punkt nicht besonders wichtig ist.
Vom Gesichtspunkt des Testens aus sind Argumente noch schwieriger. Stellen Sie
sich vor, wie schwierig es ist, alle Testfälle zu schreiben, um zu gewährleisten, dass
alle verschiedenen Kombinationen von Argumenten korrekt funktionieren. Gibt es
keine Argumente, ist diese Aufgabe trivial. Bei einem Argument ist es nicht zu
schwer. Bei zwei Argumenten wird die Lösung des Problems etwas schwieriger. Bei
mehr als zwei Argumenten kann das Testen aller Kombinationen von entsprechen­
den Werten eine Riesenaufgabe sein.
Output-Argumente sind schwerer zu verstehen als Input-Argumente. Wenn wir
eine Funktion lesen, sind wir an die Idee gewöhnt, dass Informationen als Argu­
mente in die Funktion hineingehen und als Rückgabewert aus der Funktion her­
auskommen. Normalerweise erwarten wir nicht, dass Informationen durch die
Argumente herauskommen. Deshalb müssen wir bei Output-Argumenten oft dop­
pelt hinschauen.
Ein Input-Argument ist nach null Argumenten die nächstbeste Variante. S e t u p ­
Te a r down l n c l u d e r . r e n d e r ( p a g e Data) ist ziemlich leicht z u verstehen. Daraus
geht klar hervor, dass wir die Daten in dem pageDat a-Obj ekt darstellen werden.
Kapitel 3
F u n kt i o n e n

Gebrä u c h l iche m o n ad ische Formen

Es gibt zwei sehr verbreitete Gründe, ein einziges Argument an eine Funktion zu
übergeben. Sie können eine Frage über das Argument stellen, wie etwa in boo l ean
fi l e E x i s t s ( " My F i l e " ) . Oder Sie möchten das Argument manipulieren, etwa
indem Sie es in etwas anderes umwandeln und dann zurückgeben. Beispielsweise
wandelt I n p u t St ream fi l eüpen ( " My Fi l e " ) einen Dateinamens-St r i n g in einen
I n p u t S t r e am -Rückgabewert um. Diese beiden Anwendungen werden von dem
Leser erwartet, wenn sie eine Funktion sehen. Sie sollten Namen wählen, die diesen
Unterschied deutlich machen, und die beiden Formen immer in einem konsisten­
ten Kontext verwenden (siehe den Abschnitt Anweisung und Abfrage trennen etwas
später) .
Eine etwas weniger gebräuchliche, aber immer noch sehr nützliche Form einer
Funktion mit einem einzigen Argument ist ein Event (Ereignis) . Diese Form hat ein
Input-Argument, aber kein Output-Argument. Das übergreifende Programm soll
die Funktionsaufrufe als Events interpretieren und mit dem Argument den Zustand
des Systems verändern, beispielsweise voi d pas swo r dAttem p t F a i l e d N ­
t i mes C i n t attempt s ) . Verwenden Sie diese Form mit Bedacht. Es sollte für den
Leser ganz deutlich sein, dass dies ein Event ist. Wählen Sie Namen und Kontexte
sorgfältig.
Verwenden Sie möglichst keine monadische Funktionen, die nicht eine dieser For­
men verwenden, beispielsweise voi d i n c l u d e S e t u p Pag e i n to ( S t ri n g ß u ffe r
pageTex t) . Ein Output-Argument anstelle eines Rückgabewerts für eine Transfor­
mation zu verwenden, ist verwirrend. Wenn eine Funktion ihr Input-Argument
transformiert, sollte die Transformation als Rückgabewert zurückgegeben werden.
Tatsächlich ist St ri n g ß u ffe r t ra n s fo rm ( S t ri n g ß u ffe r i n ) besser als voi d
t r a n s fo rm- ( S t r i n g ß u ffe r o u t ) , selbst wenn die Implementierung im ersten
Fall einfach das Input-Argument zurückgibt. Wenigstens folgt sie immer noch der
Form einer Transformation.

Fl ag-Argu m e nte

Flag-Argumente sind hässlich. Ein boolesches Argument an eine Funktion zu über­


geben, ist eine wirklich schreckliche Technik. Es verkompliziert sofort die Signatur
der Methode und gibt laut und deutlich zu verstehen, dass diese Funktion mehr als
eine Aufgabe erfüllt: eine, wenn das Flag wahr ist, und eine andere, wenn es falsch
ist!
In Listing 3·7 hatten wir keine Wahl, weil die Aufrufer bereits dieses Flag übergeben
hatten und ich den Umfang des Refactorings aufdie Funktion und tiefer begrenzen
wollte. Dennoch ist der Methodenaufruf r e n d e r ( t r u e ) für einen armen Leser ein­
fach nur verwirrend. Mit der Maus über den Aufruf zu fahren und r e n d e r ( boo l ean
i s S u i te) zu sehen, hilft nicht allzu viel. Wir hätten die Funktion in zwei zerlegen
sollen: r e n d e r Fo r S u i t e ( ) und r e n d e r Fo r S i n g l eTe s t ( ) .

72
3·6
F u n kt i o n s a r g u m e n te

Dyad ische Fu n ktionen

Eine Funktion mit zwei Argumenten ist schwerer zu verstehen als eine monadische
Funktion. Beispielsweise ist w r i t e F i e l d ( n am e ) leichter zu verstehen als w r i t e ­
F i e l d ( o u t p u t S t ream , n am e ) . (Ich habe gerade das Refactoring eines Moduls
durchgeführt, das eine dyadische Form verwendete. Ich konnte den o u t p u t S t r e am
zu einem Feld der Klasse machen und alle w r i t e F i e l d-Aufrufe in eine monadische
Form umwandeln. Das Ergebnis war viel sauberer.) Obwohl die Bedeutung der bei­
den Argumente klar ist, liest man leicht über das erste hinweg und verpasst seine
Bedeutung. Das zweite Argument erfordert eine kurze Pause, bis wir gelernt haben,
den ersten Parameter zu ignorieren. Und das führt natürlich zu Problemen, weil wir
niemals irgendeinen Teil des Codes ignorieren sollten. Die Teile, die wir ignorieren,
sind die Stellen, an denen sich die Bugs verbergen.
Manchmal sind natürlich zwei Argumente die passende Lösung. Beispielsweise ist
an Poi n t p n ew Poi n t (0 , 0) ; überhaupt nichts auszusetzen. Kartesische Koor­
=

dinaten erfordern von Natur aus zwei Argumente. Tatsächlich wären wir sehr über­
rascht, n ew Poi n t (O) zu sehen. Doch die beiden Argumente sind in diesem Fall
geordnete Komponenten eines einzigen Werts! Dagegen haben o u t p u t S t r e am und
n ame weder eine natürliche Kohäsion noch eine natürliche Reihenfolge.

Selbst offensichtlich dyadische Funktionen wie a s s e r t Eq u a l s ( e x p e c t e d ,


a c t u a l ) sind problematisch. Wie oft haben Sie das a c t u a l an die Stelle gesetzt, an
der e x p e c t e d stehen sollte? Die beiden Argumente haben keine natürliche Reihen­
folge. Die Reihenfolge e x p e c t e d , a c t u a l ist eine Konvention, die durch Übung
gelernt werden muss.
Dyaden sind kein Übel, und Sie werden sie sicher schreiben müssen. Doch Sie soll­
ten wissen, dass sie mit gewissen Kosten verbunden sind und Sie sollten jede Mög­
lichkeit nutzen, sie in Monaden umzuwandeln. Beispielsweise könnten Sie die
w r i t e F i e l d-Methode zu einem Element von o u t p u t S t ream machen, damit Sie
o u t p u t S t ream . w r i t e F i e l d ( n am e ) sagen können. Oder Sie könnten o u t p u t ­
St ream z u einer Member-Variablen der gegenwärtigen Klasse machen, damit Sie
ihn nicht übergeben müssen. Oder Sie könnten eine neue Klasse wie Fi e l dWri t e r
extrahieren, die den o u t p u t S t r e am in ihrem Konstruktor übernimmt und über
eine w r i te-Methode verfügt.

Triaden

Funktionen, die drei Argumente übernehmen, sind erheblich schwerer zu verste­


hen als Dyaden. Die Probleme, zum Beispiel der Reihenfolge, des mehrmaligen
Lesens und des Übersehens, werden mehr als verdoppelt. Ich rate Ihnen, sehr sorg­
faltig nachzudenken, bevor Sie eine Triade erstellen.
Betrachten Sie beispielsweise die gebräuchliche Überladung der Funktion von
a s s e r t E q u a l s , die drei Argumente übernimmt: a s s e r t E q u a l s ( m e s s a g e ,
e x p e c ted , a c t u a l ) . Wie oft haben Sie m e s s ag e gelesen und gedacht, es wäre

73
Kapitel 3
F u n kt i o n e n

e x p e c t e d ? Ich bin sehr oft über diese Triade gestolpert und musste mehrfach nach­
lesen. Tatsächlich schaue ichjedes Mal, wenn ich sie sehe, doppelt nach und ignoriere
dann die Nachricht.
Andererseits gibt es auch Triaden, die nicht ganz so tückisch sind: as s e r t ­
E q u a l s ( 1 . 0 , amo u n t , . 001) . Obwohl Sie auch hier doppelt hinschauen müssen,
lohnt es in diesem Fall. Es ist immer gut, daran erinnert zu werden, dass die Gleich­
heit von Fließkommawerten eine relative Sache ist.

Argu m e nt-Objekte

Wenn eine Funktion anscheinend mehr als zwei oder drei Argumente benötigt, ist
es wahrscheinlich, dass einige dieser Argumente in eine separate Klasse eingehüllt
werden sollten. Betrachten Sie beispielsweise den Unterschied zwischen den bei­
den folgenden Deklarationen:

Ci r c l e m a k e C i r c l e ( d o u b l e x , d o u b l e y , d o u b l e radi u s ) ;
C i r c l e makeCi r c l e ( Poi n t c e n t e r , d o u b l e r a d i u s ) ;

Die Anzahl der Argumente zu reduzieren, indem daraus Obj ekte erstellt werden,
mag wie Schummelei aussehen, ist es aber nicht. Wenn Gruppen von Variablen
zusammen übergeben werden, wie etwa x und y in dem obigen Beispiel, gehören
sie wahrscheinlich zu einem Konzept, das einen eigenen Namen haben sollte.

Argu ment- Li ste n

Manchmal wollen wir eine Yariable Anzahl von Argumenten an eine Funktion über­
geben. Betrachten Sie beispielsweise die Methode St r i n g . fo rmat:

S t r i n g . fo rmat ( "� s v. o r ke d % . 2 f h o u r s . " , name , h o u r s ) ;

Wenn die variablen Argumente alle identisch behandelt werden. wie in dem obigen
Beispiel, dann entsprechen sie einem einzigen Argument vom Typ L i s t . Folgt man
dieser Auffassung, ist St r i ng . fo rmat faktisch dyadisch. Tatsächlich ist die fol­
gende Deklaration von St r i n g . fo rmat deutlich dyadisch.

p u b l i c S t r i n g fo rmat ( S t r i n g format , O b j e c t . . . a rg s )

Deshalb gelten alle entsprechenden Regeln. Funktionen, die eine Yariable Anzahl
von Argumenten übernehmen, können Monaden, Dyaden oder sogar Triaden sein.
Aber es wäre ein Fehler, ihnen noch mehr Argumente zu übergeben.

voi d mo n a d ( I nt e g e r . . . a rg s ) ;
voi d dyad ( S t ri n g name , I n t e g e r . . . a r g s ) ;
voi d t r i ad ( S t r i n g name , i n t cou n t , I n t eg e r . . . a rg s ) :

74
3-7
N e b e n effekte ve r m e i d e n

Verben u n d Sch l ü sselwörter

Ein guter Name für eine Funktion kann sehr viel dazu beitragen, den Zweck der
Funktion und die Reihenfolge und den Zweck der Argumente zu erklären. Bei einer
Monade sollten Funktion und Argument ein aussagestarkes Verb/ Substantiv-Paar
bilden. Beispielsweise erklärt sich w r i te ( n ame) von selbst. Was immer » n ame« sein
mag, es wird »geschrieben«. Möglicherweise wäre ein Name wie w r i t e ­
F i e l d ( n ame) noch besser, weil e r uns zugleich sagt, dass der » n ame« ein »Feld« ist.
Diese letzte Variante ist ein Beispiel für die Schlüsselwort-Form eines Funktionsna­
mens . Bei dieser Form codieren wir die Namen der Argumente in den Funktions­
namen. Beispielsweise könnte a s s e r t E q u a l s besser als a s s e r t E x p e c t e d E q u a l s
A c t u a l ( e x p e c t e d , a c t u a l ) geschrieben werden. Dadurch wird das Problem, sich
die Reihenfolge der Argumente merken zu müssen, erheblich geringer.

3·7 N eben effekte vermeiden

Nebeneffekte sind Lügen. Ihre Funktion verspricht, eine Aufgabe zu erfüllen, aber
sie erledigt auch andere verborgene Aufgaben. Manchmal führt sie unerwartete
Änderungen an Variablen ihrer eigenen Klasse durch. Manchmal macht sie daraus
Parameter, die an die Funktion übergeben werden, oder System-Globals. In jedem
Fall sind sie unaufrichtige und schädigende Falschheiten, die oft zu seltsamen zeit­
lichen Kopplungen und anderen Abhängigkeiten führen.
Betrachten Sie beispielsweise die anscheinend unverfängliche Funktion in Listing
3 . 6 . Diese Funktion verwendet einen Standardalgorithmus, um einen u s e r N am e
mit einem p a s s wa r d abzugleichen. Bei einem Treffer gibt sie t r u e zurück, andern­
falls fa l s e . Aber sie hat auch einen Nebeneffekt Können Sie ihn entdecken?

Listi ng 3.6: Use rVal i dator . j ava


p u b l i c c l a s s U s e rVa l i d a t o r {
p r i vat e C ry p t o g r a p h e r c ryptog r a p h e r ;

p u b l i c boo l ean c h e c k Pa s s wo rd ( S t r i n g u s e rName , S t r i n g p a s s wo rd ) {


U s e r u s e r = U s e rGat eway . fi n d ByName ( u s e rName) ;
i f (user ! = Use r . NULL) {
S t ri n g c o d e d P h r a s e=u s e r . g e t P h r as e E n co d e d B y P a s swo rd ( ) ;
S t ri n g p h r a s e = c ryptog r a p h e r . d e c rypt ( c o d e d P h r a s e , pas swo rd ) ;
i f ( " Val i d Pas swo rd " . eq u a l s ( p h r a s e ) ) {
S e s s i on . i n i t i al i ze () ;
r e t u rn t r u e ;
}
}
r e t u r n fal s e ;
}
}

75
Kapitel 3
F u n kt i o n e n

Der Nebeneffekt ist natürlich der Aufrufvon S e s s i on . i ni t i a l i z e () . Die c h e c k ­


Passw o rd -Funktion sagt laut Name, dass sie Passwörter prüft. Der Name impliziert
nicht, dass sie die Sitzung initialisiert. Deshalb läuft ein Aufrufer, der glaubt, was
der Name der Funktion sagt, Gefahr, die vorhandenen Sitzungsdaten zu löschen,
wenn er die Validität des Benutzers prüfen will.
Dieser Nebeneffekt erzeugt eine zeitliche Kopplung. Das heißt, c h e c kPas swa rd
kann nur zu bestimmten Zeiten aufgerufen werden (anders ausgedrückt: wenn es
sicher ist, die Sitzung zu initialisieren) . Wenn die Funktion nicht zum normalen
Zeitpunkt aufgerufen wird, können Sitzungsdaten versehentlich verloren gehen.
Zeitliche Kopplungen sind verwirrend, besonders wenn sie als Nebeneffekt verbor­
gen sind. Wenn Sie eine zeitliche Kopplung brauchen, sollten Sie dies im Namen
der Funktion klar zum Ausdruck bringen. In diesem Fall könnten wir die Funktion
in c h e c kPas swo rdAn d i n i ti al i z e S e s s i on umbenennen, obwohl dies sicherlich
gegen »Tue eine Aufgabe« verstößt.

Outp ut-Argu m ente

Argumente werden auf natürlichste Weise als Inputs einer Funktion interpretiert.
Wenn Sie schon länger programmieren, haben Sie sicher schon Argumente näher
analysiert, die tatsächlich nicht als Input, sondern als Output verwendet wurden. Ein
Beispiel:

a p p e n d Foot e r ( s ) ;

Hängt diese Funktion s als Fußzeile an etwas an? Oder hängt sie eine Fußzeile an
s an? Ist s ein Input oder ein Output? Es dauert nicht lange, sich die Funktionssi­
gnatur anzuschauen:

p u b l i c vo i d a p p e n d Foot e r ( S t r i n g B u ffe r r e p o r t )

Damit wird die Frage geklärt, aber nur auf Kosten der Prüfung der Funktionsdekla­
ration. Alles, was Sie zwingt, die Funktionssignatur zu prüfen. ist gleichwertig mit
einem zweimaligen Nachschauen. Es ist eine kognitive Unterbrechung und sollte
vermieden werden.
Vor dem Aufkommen der objektorientierten Programmierung musste man manch­
mal Output-Argumente verwenden. Doch in 00-Sprachen werden Output-Argu­
mente weitgehend überflüssig, weil es eine Aufgabe von t h i s ist, als Output­
Argument zu agieren. Anders ausgedrückt: Es wäre besser. wenn appe n d Foote r
wie folgt aufgerufen werden würde:

r e po r t . a p p e n d Foote r ( ) ;

Im Allgemeinen sollten Sie keine Output-Argumente verwenden. Wenn Ihre Funk­


tion den Status einer Komponente ändern muss, sollte sie den Status des Eigentü­
merobjekts ändern.
3 ·8
Anwei s u n g u n d Abfra g e tre n n e n

3.8 Anwei s u n g u n d Abfrage tren n e n

Funktionen sollten entweder etwas tun oder etwas antworten, aber nicht beides. Ent­
weder sollte Ihre Funktion des Status eines Objekts ändern oder sie sollte Informa­
tionen über das Objekt zurückgeben. Beides zu tun, führt oft zu Verwirrung.
Betrachten Sie beispielsweise die folgende Funktion:

p u b l i c bool e a n s e t ( St r i n g at t r i b u t e , S t r·i n g val u e ) ;

Diese Funktion setzt den Wert eines benannten Attributs und gibt t r u e zurück,
wenn dies erfolgreich ist, und fa l s e , wenn kein solches Attribut existiert. Dies führt
zu seltsamen Anweisungen wie dieser:

i f ( s e t ( " u s e r n ame " , " u n c l e b o b " ) ) . . .

Betrachten Sie dies aus der Sicht des Lesers. Was bedeutet dies? Fragt die Anwei­
sung, ob das » u s e r n ame«-Attribut vorher aufi » U n e l ebob« gesetzt war? Oder fragt
sie, ob das » U s e r n ame«-Attribut erfolgreich auf » U n e l e bob« gesetzt worden ist? Es
ist schwer, die Bedeutung aus dem Aufruf abzuleiten, weil nicht klar ist, ob das Wort
»S et« ein Verb oder ein Adjektiv ist.
Für den Autor sollte s e t ein Verb sein, aber im Kontext der i f-Anweisungfohlt sich
das Wort wie ein Adjektiv an. Deshalb bedeutet sie: »Wenn das u s e r n ame-Attribut
vorher auf u n c l ebob gesetzt war . . . « und nicht »Setze das u s e r n ame-Attribut auf
u n c l ebob und falls dies funktioniert, dann . . . «. Wir könnten versuchen, dieses Pro­
blem zu beseitigen, indem wir die s et-Funktion in s etAn d C h e c kifExi s t s umbe­
nennen, aber das hilft dem Leser der i f-Anweisung auch nicht viel weiter. Die
richtige Lösung besteht darin, die Anweisung von der Abfrage zu trennen, damit die
Mehrdeutigkeit nicht auftreten kann.

i f ( a t t r i b u t e E x i s t s ( " u s e r n ame " ) ) {


s e tAtt r i b u t e ( " u s e r n ame " , " u n c l ebob " ) ;

3·9 Au s n a h m e n s i n d besser a l s Fe h l er-Cod es

Fehler-Codes von Befehlen zurückzugeben, ist eine subtile Verletzung der Anwei­
sung-Abfrage-Trennung. Diese Technik fördert den Gebrauch von Anweisungen als
Ausdrücke in den Prädikaten von i f-Anweisungen.

i f ( d e l e t e P ag e ( pa g e ) == E_OK)

77
Kapitel 3
F u n kt i o n e n

Diese Anweisung leidet nicht unter der Verb f Adjektiv-Verwechslung, führt aber zu
tief verschachtelten Strukturen. Wenn Sie einen Fehler-Code zurückgeben, schaf­
fen Sie das Problem, dass der Aufrufer den Fehler sofort behandeln muss.

i f ( d e 1 e t e Page ( p a g e ) E_OK) {
==

i f ( r e g i s t ry . d e 1 e t e Refe r e n c e ( p a g e . n ame) == E_OK) {


i f ( c o n f i g Ke y s . de 1 e t e K e y ( pa g e . n ame . makeKey ( ) ) == E_OK) {
1 og g e r . 1 og ( " pa g e d e 1 e t e d " ) ;
} e1 se {
1 og g e r . 1 og ( " co n f i g Key not d e 1 e t ed " ) ;
}
} e1 se {
1 og g e r . l og ( " d e 1 e t e Refe r e n c e f ro� r eg i s t r y fai 1 ed " ) ;
}
} e1 se {
1 og g e r . 1 og ( " d e1 e t e fai l e d " ) ;
r e t u r n E_E RROR ;
}

Wenn Sie dagegen Ausnahmen anstelle von Fehler-Codes \·erwenden, dann können
Sie den Code für die Fehler-Verarbeitung von dem Code des Normalverlaufs tren­
nen und sehr vereinfachen:

t ry {
d e l e t e Pa g e ( p a g e ) ;
r e g i s t r y . d e 1 e t e R e fe re n ce ( pa g e . n ame) :
confi g K e y s . d e 1 e t e K e y ( p a g e . n ame . m a k e K e : )-
}
c a t c h ( E x c e pt i o n e ) {
1 og g e r . 1 og (e . g e t Me s s ag e ( ) ) ;
}

TryJCatc h - B i öcke extra h i eren

T ry/c at c h - Blöcke sind von Natur aus hässlich . S : e .::::- .::.::.keln die Struktur des
-•

Codes und vermengen die Fehler-Verarbeitung :::: : :.,o::- :1 : :-malen Verarbeitung.


Deshalb ist es besser, die Körper der t ry- und catc � - :: :(üe ::1 separate Funktionen
zu extrahieren.

p u b l i c voi d d e 1 e t e ( Pa g e p a g e ) {
t ry {
d e 1 e t e PageAndA1 1 Refe r e n c e s ( p a g e ) ;
}
c a t c h ( Ex c e pt i on e) {
l og E r ro r ( e ) ;
}
}

p r i vate voi d d e 1 e t e PageAndAl 1 Re f e r e n ce s ( Pag e = � =�


3·9
Au s n a h m e n s i n d bes s er a l s F e h l er-Codes

t h rows E x c e pt i on {
d e l e t e P a g e ( pa g e ) ;
r e g i s t r y . d e l e t e Re f e r e n c e ( p age . n ame) ;
c o n f i g K e y s . d e l e t e Key ( p a g e . n ame . m a k e Key () ) ;
}

p r i vate voi d l og E r ro r ( E x c e pt i on e) {
l og g e r . l og ( e . g e t Me s s ag e ( ) ) ;
}

In dem obigen Code hat die de l ete-Funktion die Aufgabe der Fehler-Verarbeitung.
Man kann sie einfach verstehen und dann ignorieren. Die Funktion de l e t e ­
PageAndA l l Refe r e n c e s hat nur die Aufgabe, eine page komplett z u löschen. Die
Fehler-Verarbeitung kann ignoriert werden. Damit haben wir eine saubere Tren­
nung, die es erleichtert, den Code zu verstehen und zu ändern.

Feh l er-Vera rbeitu n g i st e i n e Aufgabe

Funktionen sollten eine Aufgabe erfüllen. Fehler-Verarbeitung ist eine Aufgabe.


Deshalb sollte eine Funktion, die Fehler verarbeitet, nichts anderes tun. Dies imp­
liziert (wie in dem obigen Beispiel) Folgendes : Wenn eine Funktion das Schlüssel­
wort t ry enthält, sollte es das allererste Wort in der Funktion sein und nach den
catc h/fi na l l y-Blöcken sollte nichts anderes stehen.

Der A b h ä n gi gkeits m agnet Error.j ava

Fehler-Codes zurückzugeben, bedeutet normalerweise auch, dass es eine Klasse


oder e n u m gibt, in der alle Fehler-Codes definiert sind.

p u b l i c e n u m E r ro r {
OK ,
I NVAL ID ,
NO_SUCH ,
LOCKED ,
OUT_OF_R ESOU RC E S ,
WAITING_FOR_EVENT ;
}

Klassen wie diese sind ein Abhängigkeitsmagnet; viele andere Klassen müssen sie
importieren und verwenden. Wird e n u m E r ro r geändert, ist eine Neukompilierung
und ein Redeployment all dieser anderen Klassen erforderlich. Dies übt einen nega­
tiven Druck auf die E r ro r-Klasse aus . Programmierer wollen keine neuen Fehler
hinzufügen, weil sie dann alles neu erstellen und ausliefern müssen. Deshalb wie­
derverwenden sie alte Fehler-Codes, anstatt neue hinzuzufügen.
Wenn Sie nicht mit Fehler-Codes, sondern mit Ausnahmen arbeiten, dann sind
neue Ausnahmen abgeleitete Klassen der Excepti on-Klasse (Ausnahme- Klasse) . Sie
Kapitel 3
F u n kt i o n e n

können sie hinzufügen, ohne dass eine Neukompilierung und ein Redeployment
erforderlich sind. Dies ist ein Beispiel für das Open-Closed-Prinzip (OCP) [PP Po2] .

3.1 0 Do n 't Repeat Yo u rself

Don't-Repeat-Yourself-Prinzip (DRY:; »Wiederhole dich nicht!«; [PRAG]) . Wenn Sie


sich Listing p weiter vorne noch einmal sorgfältig anschauen, stellen Sie fest, dass
ein Algorithmus vier Mal wiederholt wird, einmal für jeden Fall von S e t U p , S u i t e ­
S e t U p , Tea rDown und S u i teTea rDow n . Es ist nicht leicht, diese Duplizierung zu
erkennen, weil die vier Instanzen mit anderem Code vermischt sind und nicht ein­
heitlich dupliziert werden. Dennoch ist die Duplizierung ein Problem, weil sie den
Code aufbläht und eine vierfache Änderung erfordert, sollte der Algorithmus jemals
geändert werden müssen. Außerdem bietet er vier Mal die Gelegenheit für Auslas­
sungsfehler.
Diese Duplizierung wurde durch die i n c l ude-Methode in Listing 3·7 behoben.
Lesen Sie diesen Code noch einmal und achten Sie darauf, wie die Lesbarkeit des
gesamten Moduls durch die Verringerung dieser Duplizierung verbessert wird.
Möglicherweise ist die Duplizierung die Wurzel allen Übels bei der Software-Ent­
wicklung. Viele Prinzipien und Techniken wurden zu dem Zweck entwickelt, sie zu
kontrollieren oder zu eliminieren. Beispielsweise dienen alle Datenbank-Normal­
formen von Codd dazu, die Duplizierung von Daten zu eliminieren. Bei der objekt­
orientierten Programmierung bemüht man sich darum, andernfalls redundanten
Code in Basisklassen zu konzentrieren. Bei der Strukturierten Programmierung,
der Aspektorientierten Programmierung, der Komponentenorientierten Program­
mierung geht es immer auch um Strategien, Duplizierung zu eliminieren. Man
könnte sagen, dass alle Innovationen in Software-Entwicklung seit der Erfindung
der Subroutine ein fortlaufender Versuch sind, Duplizierung in unserem Source­
code zu eliminieren.

3.1 1 Stru ktu rierte Progra m m ieru n g

Einige Programmierer befolgen die Regeln der Strukturierten Programmierung


von Edsger Dijkstra [ S P72] . Dijkstra sagte, dass jede Funktion und i eder Block inner­
halb einer Funktion einen Eingang und einen Ausgang haben solle. Diesen Regeln
entsprechend sollte eine Funktion nur eine r e t u r n-Anweisung. keine b reak- oder
c o n t i n u e-Anweisungen in einer Schleife und niemals. wirklich niemals, goto­
Anweisungen enthalten.
Obwohl wir den Zielen und Techniken der Strukturierten Programmierung wohl­
wollend gegenüberstehen, bieten diese Regeln wenig Vorteile . wenn Funktionen
sehr klein sind. Nur bei größeren Funktionen können Sie durch solche Regeln
beträchtliche Vorteile erzielen.

8o
3. 1 2
W i e sch reibt m a n s o l c h e F u n kt i o n e n ?

Wenn Sie also Ihre Funktionen klein halten, dann richtet eine gelegentliche Anwen­
dung von mehreren r e t u rn-, b reak- oder c o n t i n u e-Anweisungen keinen Schaden
an und kann den Code manchmal ausdrucksstärker machen als die Ein-Eingang­
ein-Ausgang-Regel. Andererseits lässt sich goto nur bei großen Funktionen sinn­
voll einsetzen und sollte deshalb vermieden werden.

3.1 2 Wie sch re i bt m a n solche Fu n ktio n e n ?

Software zu schreiben, ist wie jede andere Art des Schreibens. Wenn Sie einen Auf­
satz oder einen Artikel schreiben, notieren Sie zunächst Ihre Gedanken, dann ord­
nen Sie sie, bis sie sich gut lesen lassen. Der erste Entwurfi mag unbeholfen und
unstrukturiert wirken, weshalb Sie ihn umformulieren und umstrukturieren wer­
den, bis er Ihren Vorstellungen entspricht.
Wenn ich Funktionen schreibe, sind sie zunächst lang und kompliziert. Sie haben
viele Einrückungen und verschachtelte Schleifen. Sie haben lange Argumentenlis­
ten. Die Namen sind willkürlich, und es gibt duplizierten Code. Aber ich verfüge
ebenfalls über eine Suite von Unit-Tests, die alle diese umständlichen Code-Zeilen
testen.
Dann massiere und verfeinere ich den Code, lagere Funktionen aus, ändere Namen
und eliminiere Duplizierungen. Ich verkleinere die Methoden und stelle sie um.
Manchmal breche ich ganze Klassen heraus, während ich gleichzeitig dafür sorge,
dass die Tests bestanden werden.
Schließlich folgen meine Funktionen den Regeln, die ich in diesem Kapitel
beschrieben habe. Ich schreibe sie nicht in dieser Form, wenn ich anfange. Ich
glaube nicht, dass dies irgendj emand könnte.

3.1 3 Z u sa m m e nfass u n g

Jedes System wird mit einer domänenspezifischen Sprache konzipiert, die von dem
Programmierer konzipiert wird, um das System zu beschreiben. Funktionen sind
die Verben dieser Sprache, und Klassen sind die Substantive. Dies ist kein Rück­
schritt zu der scheußlichen alten Auffassung, die Substantive und die Verben in
einem Anforderungsdokument lieferten die ersten Anhaltspunkte für die in einem
System benötigten Klassen und Funktionen. Stattdes sen kommt hier eine viel ältere
Wahrheit zum Ausdruck. Die Kunst der Programmierung ist und war immer die
Kunst des Sprachen-Designs.
Meister-Programmierer betrachten Systeme nicht als Programme, die geschrieben
werden müssen, sondern als Geschichten, die erzählt werden wollen. Sie verwen­
den die Fähigkeiten der von ihnen gewählten Programmiersprache, um eine viel
reichhaltigere und ausdrucksstärkere Sprache zu konstruieren, mit der diese
Geschichte erzählt werden kann. Ein Teil dieser domänenspezifischen Sprache wird

81
Kapitel 3
F u n kt i o n e n

von einer Hierarchie von Funktionen gebildet, die alle Aktionen beschreiben, die
innerhalb des Systems ausgeführt werden. In einer kunstvollen Rekursion werden
diese Aktionen so geschrieben, dass sie in derselben domänenspezifischen Spra­
che, die sie definieren, ihren eigenen kleinen Teil der Geschichte erzählen.
Dieses Kapitel behandelt die handwerklichen Fähigkeiten, um gute Funktionen zu
schreiben. Wenn Sie die Regeln aus diesem Kapitel befolgen, werden Ihre Funkti­
onen kurz sein, geeignete Namen haben und sauber strukturiert sein. Sie dürfen
aber niemals vergessen, das s Ihr eigentliches Ziel darin besteht, die Geschichte des
Systems zu erzählen, und dass Ihre Funktionen sauber zusammenpassen und in
einer klaren und präzisen Sprache geschrieben sein müssen, um Ihre Erzählung zu
unterstützen.

3.14 Setu pTeardown l ncl uder

Listi ng 3.7: SetupTea rdownlncl ude r . j ava


p a c k a g e fi t n e s s e . h t m l ;

i mpo r t fi t n e s s e . r e s po n d e r s . r u n . Su i t e R e s po n d e r ;
i mpo r t fi t n e s s e . wi ki . * ;

p u b l i c c l a s s S e t u pTe a r down l n c l u d e r {
p r i vate PageData p a g e D at a ;
p r i vate bool e a n i s S u i t e ;
p r i vate Wi ki Page t e s t Page ;
p r i vate S t r i n g B u ffe r n ewPageCon t e n t ;
p r i vate PageC r awl e r pageC r awl e r ;

p u b l i c s t at i c S t r i n g r e n d e r ( Pag e D a t a page Data) t h rows E x c e p t i on {


re t u r n r e n d e r ( p a g e D at a , fal s e ) ;
}

p u b l i c s t a t i c S t r i n g r e n d e r ( PageData p a g e D at a , bool e a n i s S u i t e )
t h rows E x c e pt i on {
r e t u r n new S e t u pTe a rdown l n c l u d e r ( pageData) . r e n d e r ( i s S u i t e ) ;
}

p r i vate S e t u pTe a rd own l n c l u de r ( PageData pageData) {


t h i s . pageData pageDat a ;
=

t e s t Page =pageData . g e tWi k i Pag e ( ) ;


pageC r awl e r t e s t Page . g e t PageC r awl e r ( ) ;
=

n ewPageCo n t e n t n ew S t r i n g B u ffe r ( ) ;
=

p r i vate S t r i n g r e n d e r ( bool e a n i s S u i t e ) t h rows Exc e p t i on {


t h i s . i s S u i te i s Sui te ;
=

i f ( i sTe s t Page ( ) )
3-14
Setu pTeardow n l ncl u d e r

i n c l u d e S e t u pAndTe a r down Pag e s ( ) ;


r e t u r n pageData . g e t H t m l ( ) ;
}

p r i vate boo l e a n i sTe s t Page ( ) t h rows E x c e p t i on {


r e t u rn pageData . h asAtt r i b u t e ( "Te s t " ) ;
}

p r i vate voi d i n c l u d e S e t u pAndTe a rdown Pag e s ( ) t h rows E x c e p t i o n {


i nc l udeSetupPages () ;
i n c l u d e PageCo n te n t ( ) ;
i n c l udeTe a rdown Pag e s ( ) ;
u pd a te PageCo n te n t ( ) ;
}

p r i vate voi d i n c l u d e S e t u p Pag e s ( ) t h rows E x c e p t i on {


i f (i s S u i t e )
i n cl udeSui teSetu pPage () ;
i n c l u d e S e t u p Pa g e ( ) ;
}

p r i vate voi d i n c l u d e S u i t e S e t u p Page ( ) t h rows E x c e pt i on {


i n c l u d e ( S u i t e R e s p o n d e r . S U ITE_S ETU P_NAM E , " - s e t u p " ) ;
}

p r i vate voi d i n c l u d e S e t u p Pag e ( ) t h rows E x c e pt i on {


i n cl ude ( " SetUp" , " - s e t u p " ) ;
}

p r i vate voi d i n c l u d e PageCon te n t ( ) t h rows E x c e pt i on {


n ewPageCon t e n t . a p p e n d ( pag e D at a . g etConte n t () ) ;
}

p r i vate voi d i n c l u d eTe a rdown Pag e s ( ) t h rows E x c e p t i o n {


i n c l udeTe a rdown Page ( ) ;
i f (i sSui te)
i n cl u d e S u i teTe a r d own Page ( ) ;
}

p r i vate voi d i n c l u d eTe a r down Pag e ( ) t h rows E x c e p t i on {


i n c l u d e ( " Te a rDown " , " - t e a rdown " ) ;
}

p r i vate voi d i n c l u d e S u i teTe a rdown Page ( ) t h rows E x c e pt i on {


i n c l u d e ( S u i t e R e s p o n d e r . S U I TE_TEARDOWN_NAM E , " - te a rdown " ) ;
}

p r i vate voi d u pd a t e PageCon t e n t ( ) t h rows Exce p t i on {


pageData . s e t Co n te n t ( n ewPageCo n t e n t . toSt r i n g () ) ;
Kapitel 3
F u n kt i o n e n

p r i vate voi d i n c l u d e ( S t r i n g p a g e N am e , S t r i n g a rg ) t h rows E x c e pt i o n {


Wi k i Page i n h e r i t e d Page = fi n d l n h e r i t e d Page ( p a g e N a m e ) ;
i f ( i n h e r i t e d Page ! = n u l l ) {
S t r i n g pag e P a t h Name = g e t P a t h N am e Fo r P a g e ( i n h e r i t e d P a g e ) ;
b u i l d l n c l u d e D i r e c t i ve ( p a g e P a t h N ame , a rg ) ;
}
}

p r i vate Wi k i Page f i n d l n h e r i t e d Page ( St r i n g pageName) t h rows E x c e p t i on {


r e t u r n PageC rawl e rl m p l . g e t l n h e r i t e d P ag e ( p a g e N ame , t e s t P a g e ) ;
}

p r i v a t e S t r i n g g e t P a t h N ame Fo r Pa g e (Wi k i Page pag e ) t h rows E x c e pt i on {


Wi k i P a g e P a t h pag e P a t h = pageC r awl e r . g e t F u l l P at h ( pa g e ) ;
r e t u r n Pat h Pa r s e r . r e n d e r ( p a g e Pat h ) ;
}

p r i vate voi d b u i l d l n c l u d e D i r e c t i ve ( S t r i n g p ag e P a t h N ame , S t r i n g a rg ) {


n ewPageCo n t e n t
. ap p e n d ( " \ n ! i n c l u d e " )
. ap p e n d ( a r g )
. ap p e n d ( " . " )
. ap p e n d (page P a t h N am e )
. ap p e n d ( " \n " ) ;
}
}
Kapitel 4

Kom mentare

»Kommentieren Sie schlechten Code nicht - schreiben Sie ihn um.«


- Brian W Kernighan und P. ]. Plaugher

Nichts kann so hilfreich sein wie ein wohlplatzierter Kommentar. Nichts kann ein
Modul mehr zumüllen als leichtfertige dogmatische Kommentare. Nichts kann so
viel Schaden anrichten wie ein alter überholter Kommentar, der Lügen und Fehlin·
formationen verbreitet.
Kommentare sind nicht wie Schindlers Liste. Sie sind nicht »das reine Gute«. Tat­
sächlich sind Kommentare bestenfalls ein erforderliches Übel. Wären unsere Pro­
grammiersprachen ausdrucksstark genug oder hätten wir genügend Talent, unsere
Absichten in den vorhandenen Sprachen immer klar genug auszudrücken, wir wür­
den wohl kaum Kommentare brauchen.
Der angemessene Einsatz von Kommentaren soll unsere Unfähigkeit ausgleichen,
uns in unserem Code klar auszudrücken. Beachten Sie das Wort Unfähigkeit. Genau
das meine ich. Kommentare sind immer ein Zeichen der Unfähigkeit. Wir brau­
chen sie, da es uns nicht immer gelingt herauszufinden, wie wir uns ohne sie aus­
drücken können. Doch ein Kommentar ist kein Grund zum Feiern.
Wenn Sie also einen Kommentar schreiben müssen, sollten Sie vorher überlegen,
ob Sie sich nicht doch noch in Ihrem Code besser ausdrücken könnten. Jedes Mal,
wenn Sie sich in Ihrem Code ausdrücken können, können Sie stolz auf sich sein.
Jedes Mal, wenn Sie einen Kommentar schreiben, sollten Sie sich mangelnder Aus­
drucksfähigkeit im Code bewusst sein.

Bs
Kapitel 4
Ko m m e ntare

Warum schätze ich Kommentare so gering? Weil sie lügen. Nicht immer und nicht
absichtlich, aber zu oft. Je älter ein Kommentar ist und je weiter er von dem Code
entfernt ist, den er beschreibt, desto wahrscheinlicher ist er einfach falsch. Der
Grund ist simpel. Realistisch betrachtet, können Programmierer Kommentare
nicht warten.
Code ändert und entwickelt sich. Teile des Codes werden von hier nach da verscho­
ben. Die Teile verzweigen, reproduzieren und vereinigen sich wieder und werden
zu Schimären. Leider folgen die Kommentare ihnen nicht immer - sie können ihnen
nicht immer folgen. Und allzu oft werden die Kommentare von dem Code getrennt,
den sie beschreiben, und verwaisen als immer ungenauer werdende »Waschzettel«.
Betrachten Sie beispielsweise das Schicksal des folgenden Kommentars und der
Zeile, die er beschreiben sollte:

Mec kR e q u e s t req u e s t ;
p r i vate fi n a l S t r i n g HTT P_DATE_REGEXP =

" [ SMTWF] [ a - z ] { 2 } \\ , \\ s [ 0 - 9 ] { 2 } \\ s [ J FMA S OND] [ a- z ] { 2 } \\s " +


" [ 0 - 9 ] { 4 } \\ s [ 0 - 9 ] { 2 } \\ : [ 0 - 9 ] { 2 } \\ : [ 0 - 9 ] { 2 }\\s G"!T " :
p r i vate R e s p o n s e r e s pon s e ;
p r i vate F i t N e s s eCon t e x t c o n t e x t ;
p r i vate F i l e Re s po n d e r r e s po n d e r ;
p r i vate L o c a l e s a v e l o c a l e ;
II Exampl e : "Tue , 02 Apr 2003 2 2 : 1 8 : 4 9 GMT '

Wahrscheinlich wurden später andere Instanznf.ablen zv.ischen der Konstanten


HTT P DAT E REGEXP und ihrem erklärenden Kommenta: emgefügt.
_ _

Natürlich könnte man fordern, dass Programmierer d1szipliniert genug sein soll­
ten, Kommentare immer zu aktualisieren, zu korrigieren usw. Ich stimme zu; sie
sollten. Aber mir wäre es lieber, sie würden diese Energ:e aufu-enden, um den Code
so klar und ausdrucksstark zu formulieren, dass er -u be:-�au.pt keine Kommentare
benötigt.
Ungenaue Kommentare sind viel schlimmer als gar i. e : :1 e r\.o!!lmentare. Sie führen
in die Irre. Sie wecken Erwartungen, die niemals erft.L: weden. Sie schreiben alte
Regeln fest, die nicht mehr befolgt werden müssen oce:- ;o-�a:- nicht mehr befolgt
werden sollten.
Die Wahrheit kann nur an einer Stelle gefunden wercie:::.� ::n Code. Nur der Code
kann wirklich sagen, was er tut. Er ist die einzige QueiJe ::.�:- ""'1rklich genaue Infor­
mationen. Deshalb sollten wir, auch wenn Kommen�:-e :-:-1anchmal erforderlich
sind, uns anstrengen, um sie zu minimieren.

4.1 Ko m me nta re s i n d kei n E rsatz fü r s c h l ec h te n Cod e

Einer der häufigeren Gründe, Kommentare zu schreibe: _ :s: s chlechter Code. Wir
schreiben ein Modul und wir wissen, dass es verwirre::c -_;:._� schlecht strukturiert

86
4- 2
Er k l ä re n S i e i m u n d d u rch d e n Cod e

ist. Wir wissen, dass es ein Chaos ist. Deshalb sagen wir uns: »Oh, das muss ich aber
kommentieren!« Nein! Sie müssen Ihren Code säubern!
Klarer und ausdrucksstarker Code mit wenigen Kommentaren ist viel besser als
unordentlicher und komplexer Code mit zahlreichen Kommentaren. Anstatt Ihre
Zeit mit dem Schreiben von Kommentaren zu verbringen, die Ihr Chaos erklären,
sollten Sie sie darauf verwenden, das Chaos zu beseitigen.

4.2 Erkläre n Sie i m u n d d u rc h d e n Code

Manchmal ist Code sicher kein gutes Medium für Erklärungen. Leider deuten viele
Programmierer dies so um, dass Code selten, falls überhaupt, ein gutes Medium für
Erklärungen ist. Dies ist grundfalsch. Was würden Sie lieber lesen? Dies:

II C h e c k to s e e i f t h e e m p l oyee i s e l i g i b l e fo r f u l l b e n efi t s
i f ( ( em pl oyee . fl a g s & HOURLY_F LAG) &&
( e m p l oyee . ag e > 6 5 ) )

Oder dies?

i f ( e m p l oyee . i s E l i g i b l e Fo r Fu l l B e n e f i t s ( ) )

I n Deutsch etwa:

II P r ü f e n , ob d e r Mi t a r b e i t e r al l e B e n e f i t s be kommen s o l l
i f ( ( e m p l oyee . fl ag s & HOURLY_F LAG) &&
( e m p l oyee . ag e > 65) )

Oder dies?

i f ( e m p l oyee . so l l Al l e B e n efi t s B e komm e n ( ) )

Meistens muss man nur wenige Sekunden nachdenken, um den Zweck durch den
Code auszudrücken. In vielen Fällen reicht es einfach, eine Funktion zu erstellen,
die dasselbe aussagt wie der Kommentar, den Sie schreiben wollen.

4·3 G ute Kom m e nta re

Einige Kommentare sind notwendig oder vorteilhaft. Es folgen einige, die meiner
Meinung die Bits wert sind, die sie verbrauchen. Denken Sie jedoch daran, dass der
einzig wirklich gute Kommentar der ist, den Sie nicht schreiben müssen.

J u risti sche Ko m m e ntare

Manchmal zwingen uns die Codierstandards unseres Unternehmens, bestimmte


Kommentare aus juristischen Gründen zu schreiben. Beispielsweise sind CopY-
Kapitel 4
Ko m m entare

right- und Autoren-Vermerke am Anfang aller Quelldateien oft notwendige und ver­
nünftige Angaben.
Hier ist beispielsweise der standardmäßige Kommentar-Header, den wir am
Anfang jeder Quelldatei in FitNesse einfügen. Glücklicherweise verbirgt unsere
I D E diesen Kommentar, so dass er nicht den Bildschirm verunstaltet.

II Copy r i g h t (C) 2003 , 2004 , 200 5 by Obj ect Mento r , I n c . Al l ri g h t s rese rved .
II Rel eased u n d e r t h e te rms of t h e GNU Gene ral Publ i c Li c e n s e ve r s i on 2 or l at e r .

Derartige Kommentare sollten keine langen Verträge oder Ähnliches enthalten.


Wenn möglich, sollten Sie auf eine Standardlizenz oder ein anderes externes Doku­
ment verweisen, anstatt alle Bedingungen in den Kommentar einzufügen.

I nform ierende Ko m mentare

Manchmal ist es nützlich, grundlegende Informationen in einem Kommentar mit­


zuteilen. So erklärt etwa der folgende Kommentar den Rückgabewert einer abstrak­
ten Methode:

II Gi bt ei ne Ins tanz des getesteten Res ponde r s zu rück


p ro t e c t e d a b s t r a c t R e s p an d e r r e s po n d e r l n s t a n c e ( ) ;

Ein derartiger Kommentar kann manchmal nützlich sein; aber es ist besser, die
Information möglichst mit dem Namen der Funktion zu Yermitteln. So könnte etwa
der Kommentar in diesem Fall redundant gemacht werden. indem man die Funk­
tion umbenennt: r e s po n d e r B e i ngTe s t e d .
Hier ist ein etwas besserer Fall:

II Ve r g l e i c h s fo rm a t k k : mm : s s E E E , MMM dd ,
P a t t e r n t i meMat e h e r P a t t e r n . compi l e (
=

" \ \d * : \\ d * : \\d * \\w* , \\w* \\d * , \\d * " ) ;

In diesem Fall teilt uns der Kommentar mit. das s de:- :-eguläre Ausdruck zum
Abgleich eines Ausdrucks mit Datum und Zeit Ye!"l\ e::1cie: werden soll, der mit der
Funktion S i m p l eDate Fo rmat . fo rmat und dem spez:!:z'.e:-ten Format- String for­
matiert worden ist. Dennoch könnte es besser und kla:-er gewesen sein, wenn dieser
Code in eine spezielle Klasse verschoben worden wäre. d:e :".;.:- die Umwandlung der
Formate von Datums- und Zeitangaben zuständig ge'i\ e s e:: wäre. Dann wäre der
Kommentar wahrscheinlich überflüssig gewesen.

E rkläru n g der Absicht

Manchmal liefert ein Kommentar nicht nur nützliche - ::::-: :>r:nationen über die Im­
plementierung, sondern auch über die Gründe hinte:- e::: e :- Entscheidung. Im fol­
genden Fall wird eine interessante Entscheidung c�:-2: einen Kommentar
dokumentiert. Um zwei Objekte zu vergleichen, wollte de: _-\utor sie so sortieren,

88
4·3
G ute K o m m e ntare

dass Objekte seiner Klasse in der Reihenfolge vor den Objekten aller anderen Klas­
sen stehen.

p u b l i c i n t com p a r eTo (Obj e c t o)


{
i f ( o i n s t a n c e o f Wi k i P a g e P at h )
{
Wi ki Pag e Path p =(Wi k i Pag e Pa t h ) o ;
S t r i n g com p r e s s e d N ame =S t r i n g U t i l . j o i n ( n ames , " " ) ;
S t r i n g com p r e s s e d A r g u m e n t N am e= S t r i n g U t i l . j o i n ( p . n ames , " " ) ;
r e t u r n com p r e s s e d N am e . com p a r eTo ( c om p r e s s ed A r g u m e n tName) ;
}
r e t u r n 1 ; II Wi r s i nd größe r , we i l wi r den r i c h t i gen Typ haben .
}

Hier ist ein noch besseres Beispiel. Vielleicht stimmen Sie nicht mit der Problem­
lösung des Programmierers überein, aber wenigstens wissen Sie, was er versuchte.

p u b l i c voi d t e s tCon c u r r e n tAddWi d g e t s ( ) t h rows E x c e p t i on {


Wi d g e t B u i l d e r wi d g e t ß u i l d e r =

n ew Wi d g e t B u i l d e r ( n ew Cl a s s [ ] { Bo l dWi d g e t . c l as s } ) ;
Stri n g text =" ' ' bo l d t e x t ' ' "' ;
'

P a r e n tWi d g e t p a r e n t =

n ew Bol dWi d g e t ( n ew Moc kWi d g e t Root ( ) , " ' ' ' bo 1 d t e x t ' ' ' " ) ;
Atom i cßool e a n fai l Fl ag = n ew Atom i cßool e a n ( ) ;
fai l Fl ag . s e t (fal s e ) ;

II Di es i s t u n s e r bester Versuch , e i ne Race-Cond i t i on


II (Gl e i chze i t i gkei t s bed i ngung) zu e rzeugen ,
II i ndem e i ne große Anzahl von Th reads e rstel l t wi rd .
fo r ( i n t i = 0 ; i < 2 5 000 ; i ++) {
Wi d g e t B u i l d e rTh r e ad wi d g e t B u i l d e rTh r e ad =

n ew Wi d g e t B u i l d e rTh r e ad (wi d g e t B u i l d e r , text , p a r e n t , fai l Fl ag ) ;


Th read t h read =n ew Th read (wi d g e t B u i l d e rTh r e a d ) ;
t h read . s t a r t ( ) ;
}
a s s e r t E q u a l s (fal s e , fai l Fl ag . g e t ( ) ) ;
}

K l a rste l l u n gen

Manchmal ist es einfach hilfreich, die Bedeutung eines obskuren Arguments oder
Rückgabewerts in etwas Lesbares zu übersetzen. Im Allgemeinen ist es besser, eine
Methode zu finden, die das Argument oder den Rückgabewert von sich aus klar macht;
aber wenn sie zu einer Standard-Library gehört oder in Code steht, den Sie nicht
ändern können, dann kann ein hilfreicher klärender Kommentar nützlicher sein.
Natürlich besteht ein beträchtliches Risiko, dass ein klärender Kommentar falsch
ist. So zeigt ein Blick auf das vorhergehende Beispiel, wie schwierig seine Korrekt-
Kapitel 4
Kom m e ntare

p u b l i c voi d t e s tCom p a r eTo ( ) t h rows E x c e p t i on


{
Wi ki Page Path a = P at h P a r s e r . p a r s e ( " PageA " ) ;
Wi ki PagePath ab = Pat h P a r s e r . p a r s e ( " PageA . Pag e B " ) ;
Wi ki Page Path b = Pat h P a r s e r . p a r s e ( " Pag e B " ) ;
Wi ki Page Path aa =Pat h P a r s e r . p a r s e ( " PageA . Pag e A " ) ;
Wi k i Page Path bb = P at h P a r s e r . p a r s e ( " P a g e B . Pag e B " ) ;
Wi ki Page Path ba Pat h P a r s e r . p a r s e ( " P a g e B . PageA " ) ;
=

a s s e r tT r u e ( a . comp a r eTo (a) == 0) ; II a == a


a s s e rtTr u e ( a . comp a r eTo ( b ) ! = 0 ) ; II a ! = b
a s s e r tT r u e (ab . comp a r eTo (ab) == 0) ; II ab == ab
a s s e r tT r u e ( a . comp a r eTo ( b ) == - 1) ; II a < b
a s s e r tT r u e (aa . compa r eTo (ab) == - 1) ; II aa < a b
a s s e r tT r u e ( b a . com p a r eTo ( b b ) == - 1) ; II b a < b b
a s s e rtTr u e ( b . comp a r eTo (a) = = 1) ; II b > a
a s s e r tT r u e (ab . comp a r eTo ( aa) 1) ;
== II ab > aa
a s s e r tT r u e ( b b . comp a r eTo (ba) 1) ;
== II bb > b a
}

heit zu verifizieren ist. Dies erklärt, warum die Klarstellung erforderlich und warum
sie riskant ist. Bevor Sie derartige Kommentare schreiben, sollten Sie deshalb sorg­
faltig prüfen, ob es keine bessere Alternative gibt, und dann noch sorgfaltiger darauf
achten, dass sie korrekt sind.

War n u ngen vor Ko nsequenzen

Manchmal ist es nützlicher, andere Programmierer vor bestimmten Konsequenzen


zu warnen. Beispielsweise erklärt der folgende Kommentar. \Yarum ein spezieller
Testfall abgeschaltet wurde:

II Nu r a u s führen , wenn Si e Zei t tot schl agen wol l e n .


p u b l i c voi d _t e s tWi t h R e a l l yB i g F i l e ( )
{
w r i t e l i n e sTo F i l e ( 10000000) ;
r e s pon s e . s e t Body ( t e s t F i l e) ;
r e s pon s e . r e ad yTo S e n d ( t h i s ) ;
S t r i n g r e s po n s e S t r i n g = o u t p u t . t o St r i n g ( ) ;
as s e r t S u b St r i n g ( " Con t e n t - L e n g t h : 1000000000 " , r e s pon s e S t r i n g ) ;
a s s e r tT r u e (by t e s S e n t > 1000000000 ) ;
}

Heutzutage schalten wir den Testfall natürlich mit einem �I g n o r e Attribut und -

einem entsprechenden aussagekräftigen String ab. @I g n o r e ( " D i e Au s fü h r u n g


d au e r t z u l an g e . " ) . Doch bevor e s J Unit 4 gab, war e s eine \·erbreitete Konven­
tion, ein Unterstreichungszeichen vor den Methodennamen zu setzen. Der Kom­
mentar ist zwar etwas flapsig, drückt aber den Punkt ziemheb gut aus.

90
4·3
G ute Ko m m e nt are

Hier ist ein anderes, treffenderes Beispiel:

p u b l i c s t at i c S i m p l eDate Fo rmat m a ke S t a n d a r d H t t pDate Format ( )


{
II S i mpl eDateFormat i s t n i cht t h read - s i c h e r ;
II deshal b müssen wi r j ede Ins tanz unabhäng i g e r s tel l en .
S i m p l eDate Fo rmat df n ew S i m p l e D a t e F o rmat ( " E E E , d d MMM yyyy H H : mm : s s z " ) ;
=

d f . s etTi meZo n e (Ti meZon e . g e tTi meZo n e ( " GMT " ) ) ;


r e t u rn d f ;
}

Sie könnten einwenden, dass es bessere Methoden zur Lösung dieses Problems
gibt. Ich würde Ihnen vielleicht sogar zustimmen. Aber der hier gezeigte Kommen·
tar ist vernünftig. Er warnt übereifrige Programmierer davor, aus Gründen der Effi·
zienz einen statischen Initialisierer zu verwenden.

TO DO- Ko m m entare

Manchmal ist es vernünftig, »To do«·Yermerke in Form von I /TODO-Kommentaren


in den Code einzufügen. Im folgenden Fall erklärt der TODO· Kommentar, warum die
Funktion eine veraltete Implementierung hat und wie die Funktion in Zukunft aus·
sehen sollte.

II TODO- MdM di es wi rd n i cht benöt i g t


I I Wi r e rwarte n , dass s i e m i t u n s e rem Checkou t - Model l übe r fl ü s s i g wi rd .
p ro t e c t e d Ve r s i o n i n fo makeVe r s i o n ( ) t h rows E x c e pt i o n
{
r e t u rn n u l l ;
}

TODOs sind Aufgaben, die nach Meinung des Programmierers erledigt werden soll·
ten, aber im Moment, aus welchem Grund auch immer, nicht gelöst werden kön·
nen. Es könnte sich um eine Erinnerung handeln, eine veraltete Funktion zu
ändern, oder eine Bitte an einen anderen Entwickler, sich des Problems anzuneh·
men. Es könnte eine Bitte sein, einen besseren Namen zu erfinden, oder eine Erin·
nerung, eine Änderung vorzunehmen, die von einem geplanten Ereignis abhängig
ist. Doch eines ist ein TODO keinesfalls : Es ist keine Entschuldigung dafür, schlechten
Code im System zu lassen.
Heutzutage stellen die meisten guten I D E s spezielle Funktionen zur Verfügung,
um alle TODO-Kommentare zu finden. Deshalb ist es unwahrscheinlich, dass sie ver·
loren gehen. Dennoch sollten Sie Ihren Code nicht mit TODOs verunstalten. Deshalb
sollten Sie Ihren Code regelmäßig durchsuchen und nicht mehr aktuelle TODOs
löschen.

Verstä rku n g

Mit einem Kommentar kann auch die Bedeutung eines Teil des Codes unterstrichen
werden, der andernfalls als unbedeutend angesehen werden könnte.
Kapitel 4
Ko m m e ntare

S t r i n g l i s t i t emCo n t e n t = mat c h . g rou p ( 3 ) . t r i m () ;


II De r t r i m - Be fehl i s t wi rkl i ch w i c h t i g . Er entfernt d i e füh renden
II Lee rze i chen , di e dazu füh ren könnten , dass das El ement al s
II e i ne wei tere L i ste i nterp ret i e rt wi rd .
n ew L i s t i t e mWi d g e t ( t h i s , l i s t i t emCo n t e n t , t h i s . l ev e l + 1) ;
r e t u r n b u i l d l i s t ( t e x t . s u b s t r i n g (mat c h . e n d ( ) ) ) ;

J avadocs i n öffentl i c h e n A P i s

Kaum etwas anderes ist so hilfreich und befriedigend wie ein wohlgeschriebenes
öffentliches API. Die Javadocs für die Standard-Java-Library sind ein Beispiel dafür.
Es wäre bestenfalls schwierig, Java-Programme ohne sie zu schreiben.
Wenn Sie ein öffentliches API schreiben, sollten Sie sicher gute Javadocs dafür
schreiben. Aber Sie sollten dabei an die anderen Empfehlungen in diesem Kapitel
denken. Javadocs können genauso irreführend, nicht-lokal und unehrlich sein wie
andere Kommentare.

4·4 Sc h l echte Ko m m e ntare

Die meisten Kommentare gehören zu dieser Kategorie. Normalerweise handelt es


sich um Krücken oder Entschuldigungen für schlechten Code oder Rechtfertigun­
gen für unzureichende Entscheidungen, die kaum über ein Selbstgespräch des Pro­
grammierers hinausgehen.

Gera u n e

Einen Kommentar nur deshalb einzufügen, weil Sie das Gefühl haben, Sie sollten
dies tun, oder weil der Prozess es erfordert, ist ein Hack. Wenn Sie sich dafür ent­
scheiden, einen Kommentar zu schreiben, sollten Sie die erforderliche Zeit aufwen­
den, um den Ihren Fähigkeiten entsprechend bestmöglichen Kommentar zu
schreiben.
Hier ist beispielsweise ein Fall aus FitNesse, bei dem ein Kommentar möglicher­
weise nützlicher gewesen wäre. Aber der Autor war in Eile oder einfach nur nach­
lässig. Sein »Geraune« hinterließ ein Geheimnis :

p u b l i c voi d l o ad P rope rt i e s ( )
{
t ry
{
S t r i n g p rope r t i e s Pa t h = p ro p e r t i e s lo c at i on + / + P RO P E RT I E S_FI L E ;
" "

F i l e i n p u t S t ream p ro p e r t i e s S t ream = n ew F i l e i n p u t S t r e am ( p ro p e rt i e s Pa t h ) ;
l o aded P rope r t i e s . l oad ( p rop e rt i e s S t r e am) ;
}
c a t c h ( I O E x c e pt i o n e )
{
II No p rope r t i e s fi l es means al l d e fau l t s are l oaded
}
}

92
4-4
Sch l echte Ko m m e ntare

(Deutsche Übersetzung des Kommentars: »Keine Properties-Dateien bedeutet, dass


alle Defaults oder Standardwerte geladen sind oder werden«; nicht eindeutig!) Was
bedeutet der Kommentar in dem c a t c h - Block? Man muss annehmen, dass es für den
Autor etwas bedeutete, doch was, geht kaum klar daraus hervor. Was können wir aus
dem Code schließen? Wenn eine I O E x c e p t i on ausgelöst wird, gab es offensichtlich
keine Properties-Datei; und in diesem Fall werden alle Standardwerte geladen. Aber
wer lädt die Standardwerte? Waren sie bereits vor dem Aufruf von l oad P ro p e r ­
t i e s . l o a d geladen? Oder fangt l oad P ro p e rti e s . l o a d c a t c h die Ausnahme ab,
lädt die Standardwerte und übergeht dann die Ausnahme, so dass wir uns nicht
darum kümmern müssen? Oder hat l oad P ro p e r t i e s . l oad alle Standardwerte
geladen, bevor sie versuchte, die Datei zu laden? Versuchte der Autor nur, sich über
die Tatsache hinwegzumogeln, dass er den cat c h - Elock leer ließ? Oder - und dies
ist die beängstigende Möglichkeit - wollte der Autor sich ermahnen, später an diese
Stelle zurückzukommen und den Code zu schreiben, der die Standardwerte lädt?
Unsere einzige Option besteht darin, den Code in den Teilen des Systems zu stu­
dieren, um herauszufinden, was passiert. Jeder Kommentar, der Sie zwingt, in ande­
ren Modulen nach seiner Bedeutung zu suchen, ist ein gescheiterter
Kommunikationsversuch und die Bits nicht wert, die er konsumiert.

Red u nda nte Kom m entare

Listing 4 - 1 zeigt eine einfache Funktion mit einem Header-Kommentar, der voll­
kommen redundant ist. Den Kommentar zu lesen, dauert wahrscheinlich länger, als
den Code selbst zu lesen.

Listing 4.1 : wai t Fo rC l o s e


I I U t i l i ty met hod t h a t ret u r n s when t h i s . cl osed i s t rue . Th rows a n
I I except i on i f t h e t i meout i s reached .
p u b l i c s y n c h r o n i zed voi d wai t Fo rC l o s e (fi n a l l a n g t i m e o u t M i l l i s )
t h rows E x c e pt i on
{
i f ( ! cl osed)
{
wai t (t i m e o u t M i l l i s ) ;
i f ( ! cl osed)
t h row n ew E x c e pt i on ( " M o c k R e s pon s e Se n d e r co u l d n o t b e c l o s ed " ) ;
}
}

(Übersetzung des Kommentars: »Utility-Methode, die zurückkehrt, wenn


this.closed wahr ist. Löst eine Ausnahme aus , wenn der Timeout erreicht ist.«) Wel­
chen Zweck erfüllt dieser Kommentar? Es ist sicher nicht informativer als der Code.
Weder rechtfertigt er den Code noch nennt er seinen Zweck oder Existenzgrund. Er
ist nicht leichter zu lesen als der Code. Tatsächlich ist er weniger präzise als der Code
und verführt den Leser, diesen Mangel an Präzision zu akzeptieren, anstatt sich zu

93
Kapitel 4
Ko m m entare

bemühen, den Code zu verstehen. Der Kommentar ähnelt einem Gebrauchtwagen­


händler, der Sie mit schönen Worten davon abhalten will, unter die Haube zu
schauen.
Betrachten Sie jetzt die zahlreichen nutzlosen und redundanten Javadocs in Listing
4-2aus Tomcat. Diese Kommentare bewirken nur, dass der Code unübersichtlicher
und unklarer wird. Sie erfüllen gar keinen dokumentarischen Zweck. Um die Sache
schlimmer zu machen: Ich zeige nur die ersten Kommentare. Dieses Modul enthält
zahlreiche weitere.

Listi n g 4.2: Con tai nerBase . j ava (Tomcat)


p u b l i c a b s t ract c l a s s Con t a i n e r B a s e
i m p l e m e n t s Con t a i n e r , L i fe c yc l e , P i p e l i n e ,
M B e a n Reg i s t r ati on , S e r i a l i za b l e {

I**
* T h e p ro c e s s o r d e l ay fo r t h i s compon e n t .
*I
p ro t e c t e d i n t b a c k g rou n d P ro c e s s o r D e l ay = - 1 ;

I* *
* T h e l i fecyc l e e v e n t s u p p o r t fo r t h i s compone n t .
*I
p ro t e c t e d L i fecyc l e S u p p o r t l i fe c y c l e =
n ew L i f e c y c l e S u p po rt ( t h i s ) ;

I**
* T h e c o n t a i n e r e v e n t l i s t e n e r s fo r t h i s C o n t a i n e r .
*I
p ro t e c t e d A r r ay L i s t l i s t e n e rs n ew A r r a y L i s t () ;

I**
* T h e Load e r i m p l e m e n t at i o n wi t h wh i c h t h i s Co n t a i n e r i s
* a s s o c i ated .
*I
p ro t e c t e d Load e r l oad e r = n u l l ;

I**
* T h e L og g e r i m p l eme n t at i o n wi t h w h i c h t h i s Co n t a i n e r i s
* a s s o c i ated .
*I
p ro t e c t e d Log l og g e r = n u l l ;

I* *
* A s s o c i at e d l og g e r n ame .
*I
p ro t e c t e d S t r i n g l ogName = n u l l ;

I**
* T h e M a n ag e r i mp l eme n t at i o n wi t h wh i c h t h i s C o n t a i n e r i s

94
4-4
Sch l echte Ko m m entare

* a s s o c i a ted .
*/
p ro t e c t e d M a n ag e r m a n a g e r = n u l l ;

/* *
* T h e c l u s t e r wi t h wh i ch t h i s C o n t a i n e r i s a s s o c i a t e d .
*/
p ro t e c t e d C l u s t e r c l u s t e r = n u l l ;

!* *
* T h e h u man - r e a d a b l e n am e o f t h i s C o n t a i n e r .
*/
p ro t e c t e d S t r i n g n am e = n u l l ;

/ '' *
* T h e p a r e n t C o n t a i n e r to wh i c h t h i s C o n t a i n e r i s a c h i l d .
*/
p ro t e c t e d C o n t a i n e r p a r e n t = n u l l ;

!* *
* T h e p a r e n t c l a s s l o ade r t o b e c o n f i g u r e d w h e n w e i n s tal l a
* Load e r .
*/
p ro t e c t e d C l a s s loade r p a r e n tC l a s s load e r = n u l l ;

!**
* T h e P i p e l i n e o b j e c t wi t h w h i c h t h i s C o n t a i n e r i s
* a s s o c i a ted .
*/
p ro t e c t e d P i p e l i n e p i p e l i n e = n ew S t a n d a r d P i p e l i n e ( t h i s ) ;

!* *
* T h e Real m wi t h w h i c h t h i s C o n t a i n e r i s a s s o c i a t e d .
*/
p ro t e c t e d Rea l m r e a l m = n u l l ;

/**
* T h e r e s o u r c e s D i r C o n t e x t o b j e c t wi t h wh i c h t h i s Contai n e r
* i s associ ated .
*/
p ro t e c t e d D i rCon t e x t r e s o u r c e s = n u l l ;

I rrefü h rende Ko m m entare

Selbst bei den besten Absichten fügt ein Programmierer manchmal Aussagen in
seine Kommentare ein, die nicht präzise genug sind, um korrekt zu sein. Betrachten
Sie noch einmal den redundanten, doch auch subtil irreführenden Kommentar aus
Listing 4 - r .

95
Kapitel 4
Ko m m e ntare

Haben Sie bemerkt, wo der Kommentar Sie in die Irre führt? Die Methode kehrt
nicht zurück, wenn t h i s . c l o s e d den Wert t r u e annimmt. Sie kehrt zurück, falls
t h i s . c 1 o s e d den Wert t r u e hat; andernfalls wartet sie auf einen blinden Timeout
und löst dann eine Ausnahme aus , falls t h i s . c l o s e d immer noch nicht den Wert
t r u e hat.
Diese subtile Fehlinformation, die in einem Kommentar eingebettet ist, der schwe·
rer zu lesen ist als der Body des Codes selbst, könnte einen anderen Programmierer
veranlassen, die Funktion unbekümmert aufzurufen und zu erwarten, dass sie
zurückkehrt, sobald t h i s . c l o s e d den Wert t r u e annimmt. Dieser arme Program­
mierer würde dann seine Zeit mit Debugging- Sitzungen vergeuden müssen, um
herauszufinden, warum sein Code so langsam ausgeführt wird.

Vorgesch rieben e Ko m m entare

Es ist einfach albern, per Regel vorzuschreiben, dass jede Funktion eine Javadoc
oder jede Variable einen Kommentar haben müsse. Derartige Kommentare machen
den Code nur unübersichtlicher, verbreiten Lügen und führen zu einer allgemeinen
Verwirrung und Unordnung.
Beispielsweise führt die Forderung, jede Funktion müsse Javadocs haben, zu
Abscheulichkeiten wie etwa in Listing 4 · 3. Dieser Wust Yon Kommentar liefert keine
zusätzlichen Informationen, macht den Code nur unklarer und birgt das Potenzial
für Lügen und Irreführungen.

Listi ng 4·3
/* *
*
* @pa ram t i t l e T h e t i t l e of t h e CD
* @pa r am a u t h o r T h e au t h o r of t h e CD
* @pa r am t r a c k s The n u m b e r of t r a c k s o n the CD
* @pa r am d u rati o n i nM i n u t e s The d u r a t i on of the CD i n - i n u t e s
*/
p u b l i c voi d addCD ( S t r i n g t i t l e , S t r i n g a u t h o r ,
i n t t ra c k s , i n t d u r a t i o n i nM i n u t e s ) {
CD cd =n ew CD () ;
cd . ti tl e = ti tl e ;
cd . author = autho r ;
cd . t racks = t ra c k s ;
c d . d u r a t i on =d u rati o n ;
c d l i s t . add ( c d) ;
}

Tagebuch - Ko m menta re

Manche Entwickler fügen jedes Mal, wenn sie ein �1odul editieren, an seinem
Anfang einen Kommentar ein. Diese Kommentare bilden im Laufe der Zeit eine Art
von Tagebuch oder Protokoll aller jemals vorgenommenen Anderungen. Ich habe

g6
4-4
Schlechte Kom mentare

Module gesehen, die Dutzende von Seiten dieser fortlaufenden Tagebucheinträge


enthielten.

�changes (from 11-0ct-2001)


* --------------------------
*11-0ct-2001 Re-organi sed the cl ass and moved it to new package
* com . j refi nery. date (DG) ;
*05-Nov-2001 Added a getDescri pti on() method , and el i mi nated Notabl eDate
* cl ass (DG) ;
*12-Nov-2001 IBD requi res setDescri ption() method , now that Notabl eDate
* cl ass i s gone (DG) ; Changed getPrevi ousDayOfWeek() ,
* getFol l owi ngDayOfWeek() and getNearestDayOfWeek() to correct
* bugs (DG) ;
*05-Dec-2001 Fi xed bug i n SpreadsheetDate cl ass (DG) ;
*29-May-2002 Moved the month constants i nto a separate i nterface
*
(MonthConstants) (DG) ;
*27-Aug-2002 Fi xed bug i n addMonths () method , thanks to N???l evka Petr (DG) ;
*03-0ct-2002 Fi xed errors reported by Checkstyl e (DG) ;
*13-Mar-2003 Impl emented Seri al i zabl e (DG) ;
*29-May-2003 Fi xed bug i n addMonths method (DG) ;
*04-Sep-2003 Impl emented Comparabl e . Updated the i sinRange j avadocs (DG) ;
*05-Jan-2005 Fi xed bug i n addYears() method (1096282) (DG) ;

Vor langer Zeit gab es einen guten Grund für die Erstellung und Pflege solcher Pro­
tokolleinträge am Anfang j edes Moduls. Es gab keine Sourcecode-Control-Systeme,
die dies für uns erledigten. Heute sind diese langen Journale jedoch eine weitere
Form von Stördaten, die das Modul unübersichtlicher machen. Sie sollten vollkom­
men entfernt werden.

Geschwätz

Manchmal stoßen Sie auf Kommentare, die reines Geschwätz sind. Sie wiederholen
das Offensichtliche und liefern keine neuen Informationen.

!**
*Defaul t constructo r .
*/
p rotected Ann ual DateRu l e () {
}

Tatsächlich? Oder wie wäre es damit:

/** De r Tag des Monats . * /


p r i vate i n t dayOfMonth ;

Und dann der Inbegriff der Redundanz (dt.: »Den Tag des Monats zurückgeben.«) :

/**
* Ret u r n s t h e day of the mon th .
*

97
Kapitel 4
Kommentare

* @retu rn the day of the mon th .


*I
publ i c i n t getDayOfMonth () {
retu rn dayOfMonth ;
}

Diese Kommentare sind so geschwätzig, dass wir lernen, sie zu ignorieren. Wenn
wir den Code lesen, springen unsere Augen einfach über sie hinweg. Irgendwann
fangen die Kommentare dann an zu lügen, wenn sich der umliegende Code ändert.
Der erste Kommentar in Listing 4 -4 scheint angemessen zu sein. (Der gegenwärtige
Trend bei der Entwicklung der I DEs, auch die Rechtschreibung der Kommentare zu
prüfen, ist Balsam für die Seelen von Entwicklern, die sehr viel Code lesen.) Er
erklärt, warum der catch-Block ignoriert wird. Aber der zweite Kommentar ist rei­
nes Geschwätz. Anscheinend war der Programmierer einfach so frustriert, t ry I
c at ch-Blöcke in dieser Funktion zu schreiben, dass er Dampf ablassen musste.

Listing 4.4: startSendi ng


p r i vate voi d sta rtSendi n g ( )
{
t ry
{
doSendi n g ( ) ;
}
catch ( SocketExcepti on e)
{
II no rmal . someon e stopped the request .
}
catch ( Ex cepti on e)
{
try
{
respo n s e . ad d ( E r ro rResponde r . makeExcepti o n S t r i n g ( e) ) ;
respon s e . cl o s eAl l () ;
}
catch ( Excepti on el)
{
IIGi ve me a b reak !
}
}
}

Statt seine Mühe in einen wertlosen Kommentar zu stecken, hätte der Program­
mierer erkennen müssen, dass seine Frustration durch eine Verbesserung der
Struktur seines Codes hätte behoben werden können. Er hätte sein Energie darauf
lenken sollen, den letzen t ry 1 catch- Block in eine separate Funktion zu extrahieren
(siehe Listing 4-5).

g8
4 -4
Schlechte Kommentare

Listing 4.5: startSendi ng (nach Refactoring)


p r i vate voi d s t a rtSendi n g ()
{
t ry
{
doSendi ng () ;
}
catch (SocketExce pti on e)
{
II no rmal . someone stopped the request .
}
catch ( Excepti on e )
{
addExcepti onAndCl oseRe s pon s e (e) ;
}
}

p r i vate voi d addExcepti onAndCl oseRespon s e (Exce pti on e)


{
try
{
re spon s e . add ( E r ro rResponde r . makeExcepti onSt ri n g ( e) ) ;
respon s e . cl os eA l l () ;
}
catch ( Except i on el)
{
}
}

Widerstehen Sie der Versuchung, geschwätzige Kommentare einzufügen, mit der


Entschlossenheit, Ihren Code zu säubern. Sie werden dadurch ein besserer und
glücklicherer Programmierer.

Beängstigendes Geschwätz

Javadocs können ebenfalls geschwätzig sein. Welchen Zweck erfüllen die folgenden
Javadocs (aus einer bekannten Open-Source-Library) ? Antwort: keinen! Sie sind nur
redundante geschwätzige Kommentare, die aus einem fehlgeleiteten Wunsch her­
aus geschrieben wurden, den Code zu dokumentieren.

I * * The n ame . * I
p r i vate S t r i ng name ;

I* * The v e r s i on . * I
p r i vate S t r i n g ve rs i on ;

I * * The l i cenceName . * I
p r i vate S t r i ng l i cenceName ;

99
Kapitel 4
Kommentare

I * * The versi on . *I
p r i vate S t r i n g i nfo ;

Lesen Sie diese Kommentare noch einmal etwas sorgfältiger. Erkennen Sie den Cut·
Paste-Fehler (Ausschneiden-Einfügen-Fehler) ? Wenn Autoren nicht aufpassen,
wenn sie Kommentare schreiben (oder einfügen) , warum sollten sie dann von den
Lesern erwarten dürfen, von den Kommentaren zu profitieren?

Verwenden Sie kei nen Kom mentar, wen n Sie ei n e Fu n ktion oder
eine Variable verwenden kön nen

Betrachten Sie den folgenden Code-Ausschnitt:

11 does t h e mod u l e from t h e gl obal l i st <mod> d e pend on the


II s u bsystem we a r e part of?
i f ( smodu l e . getDependSubsystems () . contai n s ( s u bSysMod . getSubSystem () ) )

Man könnte diesen Code auch ohne den Kommentar wie folgt schreiben:

A r rayl i s t modu l eDependees =


smodul e . getDependSubsystems () ;
S t r i ng ou rSubSystem =
s u bSysMod . ge tS u bSystem ( ) ;
i f (modu l eDependee s . contai ns (ou r SubSystem) )

Möglicherweise hat der Autor des ursprünglichen Codes den Kommentar zuerst
geschrieben (was unwahrscheinlich ist) und dann den Code ergänzt, um den Kom­
mentar zu erfüllen. Doch dann hätte der Autor ein Refactoring des Codes durch­
führen sollen, wie ich es getan habe, um den Kommentar zu entfernen.

Positionsbezeich ner

Manchmal markieren Programmierer spezielle Positionen in einer Quelldatei. Bei­


spielsweise fand ich kürzlich folgende Zeile in einem Programm:

II Acti o n s 11111111111!1!11111111111111111111

Gelegentlich ist es sinnvoll, bestimmte Funktionen unter einem derartigen Banner


zusammenzufassen. Aber im Allgemeinen sind solche Zeilen B allast, der eliminiert
werden sollte - besonders die überladen wirkende Folge von Schrägstrichen am
Ende.
Sehen Sie es einmal so: Banner sind nur dann auffällig, wenn sie sparsam verwen­
det werden. Sie sind nur nützlich, wenn sie einen beträchtlichen Vorteil bieten.
Wenn Sie zu viele Banner benutzen, wirken sie wie Hintergrundgeschwätz und
werden ignoriert.

100
4-4
Schlechte Kommentare

Kom mentare h i nter schließenden Klam mern

Manchmal setzen Programmierer besondere Kommentare hinter schließende


Klammern (siehe Listing 4.6). Auch wenn dies bei langen Funktionen mit tiefver­
schachtelten Strukturen sinnvoll sein mag, bedeutet dies bei den von uns bevorzug­
ten kleinen und eingekapselten Funktionen nur zusätzlichen Ballast. Wenn Sie also
versucht sind, Ihre schließenden Klammern zu kommentieren, sollten Sie stattdes­
sen versuchen, Ihre Funktionen zu verkürzen.

Listi ng 4.6: wc . java


publ i c c l as s wc {
publ i c s tati c voi d mai n (St ri ng [ ] a rg s ) {
=
B uffe redReade r i n new B uffe r edReade r ( new I n p utSt reamReade r (System . i n) ) ;
S t r i ng 1 i ne ;
i nt l i neCo u n t = 0 ;
i nt charCount = 0 ;
i nt wo rdCou n t = 0 ;
try {
whi l e ( ( l i ne = i n . readli n e ( ) ) ! = n u l l ) {
l i neCou nt++ ;
charCount += l i ne . l ength () ;
=
St ri ng wo rds [ ] l i ne . s p l i t ( " \\W" ) ;
wo rdCou n t += wo rds . l e ngth ;
} //wh i l e
= "
System . ou t . p r i n t l n ( "wo rdCou n t + wo rdCount) ;
System . ou t . p r i n t l n ( " l i n eCount = " + l i n eCount) ;
System . ou t . p r i n t l n ( "c h a rCount = " + charCount) ;
} //try
catch (IOExcept i o n e) {
System . e r r . p ri n t l n ( " E r r o r : " + e . getMe s s ag e () ) ;
} //catch
} //mai n
}

Zusch reibu ngen u nd N eben bemerku ngen

/* Added by Ri ck * I

Sourcecode-Control-Systeme können sich sehr gut merken, wer, wann, was hinzu­
gefügt hat. Es ist nicht notwendig, den Code mit kleinen Nebenbemerkungen zu
verschmutzen. Vielleicht meinen Sie, solche Kommentare wären nützlich, um
andere zu informieren, die über den Code reden. Aber in Wirklichkeit bleiben die
Kommentare einfach jahrelang stehen und werden immer ungenauer und unwich­
tiger.
Auch hier ist das Sourcecode-Control-System ein besserer Platz ftir diese Art von
Informationen.

101
Kapitel 4
Kommentare

Ausko m mentierter Code

Nur wenige Techniken sind so widerlich wie auskommentierter Code. Lassen Sie es
einfach!

I n putSt reamRespo n s e respo n s e = n ew I n putSt reamRe s po n s e ( ) ;


respo ns e . s etBody (fo rmatte r . getRe s u l tSt ream ( ) , fo rmatte r . getByteCoun t () ) ;
II InputSt ream resul tsSt ream = formatte r . getResul tSt ream () ;
II StreamReader reader = new S t reamReade r ( resul t sSt ream) ;
II response . setContent ( reade r . read ( formatter . getByteCount () ) ) ;

Andere, die diesen auskommentierten Code sehen, werden nicht den Mut aufbrin­
gen, ihn zu löschen. Sie werden glauben, dass es einen Grund gibt, warum der Code
dort steht, und dass er zu wichtig ist, um gelöscht zu werden. Deshalb sammelt sich
auskommentierter Code an wie der Satz auf dem Boden einer Flasche mit schlech­
tem Wein.
Betrachten Sie den folgenden Code von Apache Commons:

t h i s . bytePos = w r i teByte s ( pn gidBytes , 0) ;


llhdrPos = bytePos ;
w r i teHeader () ;
w r i teReso l ut i o n () ;
lldataPos = bytePos ;
i f (wri teimageDat a () ) {
w r i teEnd () ;
t h i s . pngBytes = resi zeByteAr ray ( t h i s . pngByte s , thi s . maxPos) ;
}
el s e {
thi s . pngBytes = n ul l ;
}
ret u rn th i s . pngBytes ;

Warum sind diese beiden Codezeilen auskommentiert? Sind sie wichtig? Wurden
sie als Erinnerungen an erforderliche Änderungen stehen gelassen? Oder sind sie
einfach Abfall, den jemand vor Jahren auskommentiert hat, ohne sich dann darum
zu kümmern, den Code zu bereinigen?
Vor langer Zeit, in den 6oer-Jahren, war das Auskommentieren von Code manch­
mal nützlich. Aber seit sehr langer Zeit verfügen wir schon über gute Sourcecode­
Control-Systeme. Diese Systeme merken sich den Code für uns. Wir müssen ihn
nicht mehr auskommentieren. Löschen Sie einfach den Code. Wir werden ihn nicht
verlieren. Versprochen.

HTM L-Kommentare

HTML in Sourcecode-Kommentaren ist abscheulich. Lesen Sie nur den folgenden


Beispielcode. HTML erschwert das Lesen des Kommentars an einer Stelle, wo er
leicht zu lesen sein sollte - im Editor oder in der IDE. Wenn Kommentare durch ein

102
4-4
Schlechte Kommentare

Werkzeug (wie etwa Javadoc) für eine Webseite extrahiert werden sollen, dann sollte
es Aufgabe dieses Werkzeugs sein, die Kommentare mit geeigneten H'tML-Tags
auszuzeichnen, nicht die des Programmierers.

!* *
· Tas k to r u n fi t tests .
• Th i s tas k runs fi t n es s e tes t s and publ i s h es the resul ts .
* <P/>
*<pre>
* U s age :
*&l t ; tas kdef n ame=&quot ; execute-fi tnes se-tests&quot ;
* c l as s n ame=&quot ; fi tnesse . an t . Exec uteFi tnes seTes tsTa s k&quot ;
* c l asspath ref=&quot ; c l as s path&quot ; /&gt ;
*OR
*&l t ; tas kdef cl asspath ref=&quot ; cl ass path&quot ;
* resou rce=&quot ; tasks . p roperti es&quot ; /&gt ;
*<p/>
*&l t ; execute-fi tnesse-tests
* s u i tepage=&q uot ; Fi tNes s e . Sui teAcceptanceTests&quot ;
* fi tness epo rt=&quot ; 8082&quot ;
* re s u l tsdi r=&quot ; $ { re s u l ts . d i r}&quot ;
* re s u l ts html page=&q uot ; fi t - re s u l t s . html &quot ;
* c l ass path ref=&quot ; cl ass path&quot ; /&gt ;
* </pre>
"I

N icht-lokale I nformationen

Wenn Sie einen Kommentar schreiben, müssen Sie dafür sorgen, dass er in der
Nähe des beschriebenen Codes steht. Das bedeutet zum Beispiel: keine systemweit
gültigen Informationen im Kontext eines lokalen Kommentars! Betrachten Sie bei­
spielsweise den folgenden Javadoc-Kommentar. Abgesehen von der Tatsache, dass
er schrecklich redundant ist, enthält er auch Informationen über den Standard-Port.
Dennoch hat die Funktion absolut keine Kontrolle über diesen Standardwert Der
Kommentar beschreibt nicht die Funktion, sondern einen anderen, weit entfernten
Teil des Systems. Natürlich gibt es keine Garantie, dass dieser Kommentar geändert
wird, wenn der Code mit dem Standardwert geändert wird.

!* *
� Po r t on wh i c h fi t n e s s e wou l d r u n . Defaul t s to <b>8082</b> .
*
*@pa ram fi tnessePort
*I
publ i c vo i d setFi tnessePo r t ( i n t fi tnessePo rt)
{
t h i s . fi tness ePo rt = fi tnessePort ;
}

1 03
Kapitel 4
Kommentare

Zu viele I nformationen

Fügen Sie keine interessanten historischen Diskussionen oder irrelevante Beschrei­


bungen von Details in Ihre Kommentare ein. Der folgende Kommentar stammt aus
einem Modul, mit dem eine Funktion getestet werden kann, die base64 codieren
und decodieren kann. Abgesehen von der RFC-Nummer sind alle anderen tief­
schürfenden Informationen für einen Leser des Kommentars irrelevant.

I*
RFC 2045 - M u l ti p u rpose Inte rnet Mai l Extens i on s (MIME)
Part One : Fo rmat of Inte rnet Mes s age Bodi es
s ecti o n 6 . 8 . Bas e64 Content-Tran s fe r - E n codi ng
The en codi ng p rocess represents 2 4 - bi t g roups o f i n put bi ts as output
s t ri ngs of 4 en coded cha ract e r s . Proceedi ng f rom l eft to ri ght , a
24-bi t i n put g roup i s fo rmed by concatenat i ng 3 8-bi t i nput g roups .
These 24 bi t s are then t reated as 4 con caten ated 6-bi t g roups , each
of whi ch i s t ransl ated i nto a s i ngl e di g i t i n the bas e64 al phabet .
When e n codi ng a bi t s t ream v i a the base64 encodi n g , the bi t st ream
must be p res umed to be o rdered wi th the most- s i gni fi cant- bi t fi rst .
That i s , the fi rst bi t i n the st ream wi l l be the hi g h - a rd e r bi t i n
the fi rst 8-bi t byte , and the ei ghth bi t wi l l be the l ow-o rder bi t i n
the fi rst 8-bi t byte , and s o o n .
*I

U n klarer Zusamme n ha ng

Der Zusammenhang zwischen einem Kommentar und dem von ihm beschriebe­
nen Code sollte offensichtlich sein. Wenn Sie sich die Mühe machen, einen Kom­
mentar zu schreiben, sollte der Leser wenigstens den Kommentar und den Code
anschauen und verstehen können, worum es in dem Kommentar geht.
Betrachten Sie beispielsweise den folgenden Kommentar aus Apache Commons:

/*
* start wi th an a r ray that i s bi g enough to hol d al l the pi xel s
* (pl u s fi l te r bytes) , and an ext r a 200 bytes for head e r i n fo
*/
thi s . png ßytes = n ew byte [ ( (t h i s . wi dt h + 1) * t h i s . h ei ght * 3 ) + 200] ;

Was ist ein Filter-Byte? Hat es mit der +1 zu tun? Oder mit dem * 3 ? Beidem? Ist ein
Pixel ein Byte? Warum 2 o o ? Der Zweck eines Kommentars besteht darin, Code zu
erklären, der sich nicht selbst erklärt. Es ist eine Schande, dass ein Kommentar
selbst erklärt werden muss.

Fu n ktions-Header

Kurze Funktionen müssen kaum beschrieben werden. Ein wohlgewählter Name für
eine kleine Funktion, die eine Aufgabe erledigt, ist normalerweise besser als ein
Kommentar-Header.

1 04
4-4
Schlechte Kom mentare

J avadocs i n n icht-öffentlichem Code

So nützlich Javadocs für öffentliche APis sind, so schädlich sind sie bei Code, der
nicht für den öffentlichen Gebrauch bestimmt ist. Javadoc-Seiten für die Klassen
und Funktionen innerhalb eines Systems zu generieren, ist im Allgemeinen nicht
nützlich; und die zusätzliche Formalität der Javadoc-Kommentare erzeugt kaum
mehr als noch mehr Ballast und Ablenkung.

Beis piel

Ich schrieb das Modul in Listing 4·7 für die erste XP Immersion. Es sollte ein Beispiel
für einen schlechten Codier- und Kommentarstil zeigen. Kent Beck führte dann ein
Refactoring des folgenden Codes vor mehreren Dutzend begeisterten Studenten in
eine viel angenehmere Form durch. Später passte ich das Beispiel für mein Buch
Agile Software Development, Principles, Patterns, and Practices sowie den ersten mei­
ner Craftsman-Artikel in der Zeitschrift Software Development an.
Eines fasziniert mich bei diesem Modul ganz besonders: Es gab eine Zeit, in der
viele von uns dieses Modul als »wohldokumentiert« bezeichnet hätten. Heute
betrachten wir es als ein kleines Chaos. Schauen Sie, wie viele verschiedene Kom­
mentar-Probleme Sie entdecken können.

Listing 4.7: GeneratePri mes . j ava


/**
*Thi s cl ass Gene rates p ri me n umbe r s up to a u s e r speci fi ed
*max i mum . The al gori thm u s ed i s the Si eve of E ratosthenes .
* <P>
A E ratosthenes of Cyren e , b . c . 2 7 6 BC , Cyren e , Li bya --
*d . c . 194 , Al exand ri a . The fi rst man to cal c u l ate the
* ci r cumfe rence of the Earth . Al s o known fo r wo r ki n g on
* cal endars wi th l eap years and r an the l i b r a ry at Al exand ri a .
* < P>
*The a l go r i thm i s q u i te si mpl e . Gi ven an a r ray of i ntegers
� starti ng at 2 . Cross out al l mul ti pl e s of 2 . Fi nd the next
* u n c rossed i ntege r , and c ro s s out al l of i t s m u l ti pl e s .
* Repeat unti l you have passed the square root o f the maxi mum
<'val ue .
*
*@author Al phanse
*@ve r s i on 13 Feb 2 002 atp
*/
i mport j ava . uti l . * ;

publ i c c l a s s Generate P r i mes


{
/**
*@pa ram maxVal ue i s the gene rati on l i mi t .
*/
publ i c stat i c i n t [] gene rate P ri mes (i n t maxVal ue)
{

1 05
Kapitel 4
Kommentare

i f (maxVal u e >= 2) II t h e o n l y val i d case


{
II decl a r at i o n s
i nt s = maxVal ue + 1 ; II s i ze of a r ray
bool e an [ ] f = n ew boo l ean [ s ] ;
i nt i ;

II i n i ti a l i ze a r ray to t r u e .
fo r ( i = 0 ; i < s ; i ++)
f [ i ] = t ru e ;

II get ri d of known n o n - p r i mes


f [O] =
f [ 1] =
fal s e ;

II s i eve
i nt j ;
fo r ( i = 2; i < Math . sq rt (s ) + 1 ; i ++)
{
i f (f [i ] ) II i f i i s u n c ro s s ed , c ro s s i ts m u l ti pl es .
{
for (j = 2 * i ; j < s ; j += i )
f[j] = fal s e ; II mul ti p l e i s not p r i me
}
}

II how many p r i me s are t h e re ?


i n t count = 0;
for ( i = 0 ; i < s ; i ++)
{
i f (f [i ] )
cou nt++ ; II bump cou n t .
}

i n t [] p ri mes = new i n t [count] ;


II move the p ri me s i n to the res u l t
fo r ( i = 0 , j = 0 ; i < s ; i ++)
{
i f (f [i ] ) II i f p r i me
p r i me s [ j ++]=i;
}

retu rn p r i me s ; II retu rn the p r i me s


}
e l s e I I maxVal u e < 2
retu rn new i nt [O] ; II retu r n n u l l a r ray i f bad i n p u t .
}
}

Listing 4.8 enthält eine Version desselben Moduls nach dem durchgeführten Re­
factoring. Beachten Sie, dass es erheblich weniger Kommentare enthält. Es gibt nur
zwei Kommentare in dem gesamten Modul. Beide Kommentare erklären etwas .

1 06
4-4
Schlechte Kommentare

Listing 4.8: Pri meGenerator . j ava (nach Refactoring)


I* *
*Thi s cl ass Generates p r i me numb e r s up to a use r speci fi ed
*maxi mum . The a l g o r i thm used i s the Si eve of E ratosthe n e s .
*Given an a r ray of i n teg e r s starti n g at 2 :
* Fi nd the fi rst unc rossed i nt ege r , and c ro s s out al l i ts
*mul t i pl e s . Repeat u n t i l t h e r e are no more mul t i pl e s
* i n the a r ray .
*I

publ i c c l a s s P r i meGen e rato r


{
p r i vate s tati c bool ean [ ] c ro s sedOu t ;
p r i vate s tati c i n t [] resul t ;

publ i c s tati c i nt [] gene rate P r i me s (i n t maxVal ue)


{
i f (maxVal u e < 2 )
return n ew i nt [O] ;
el se
{
u n c ro s s i ntege rsUpTo (maxVal ue) ;
c rossOutMul ti p l e s () ;
putU n c r o s sedintege r s i n toRes u l t () ;
retu rn resu l t ;
}
}

p r i vate s tati c voi d u n c ro s s i ntege rs UpTo (i n t maxVal ue)


{
c rossedOut = new bool ean [maxVal u e + 1] ;
fo r (i n t i = 2 ; i < c rossedOut . l ength ; i ++)
c rossedOut [ i ] = fal s e ;
}

p r i vate s tati c voi d c rossOutMu l ti pl es ()


{
i n t l i mi t = dete rmi neiterati o n l i m i t () ;
fo r (i n t i = 2 ; i <= l i mi t ; i ++)
i f (notCros sed (i ) )
c ro s sOutMul ti pl esOf(i ) ;
}

pri vate stati c i n t determi neiterat i onli m i t ()


{
II Eve ry m u l t i pl e i n the a r ray has a p r i me facto r that
II i s l es s than o r equal to the root of the a r ray s i ze ,
II so we don ' t have to c ro s s out mul t i pl e s of numbe r s
II l a rg e r t h a n that root .
doubl e i te rati o n l i mi t = Math . sq r t ( c ro s sedOut . l ength) ;
return (i nt) i te rati onli mi t ;
}

1 0]
Kapitel 4
Kommentare

p r i vate stati c voi d c ro s sOutMu l t i pl esOf ( i nt i )


{
for ( i nt m u l t i p l e = 2 * i ;
mul ti p l e < c rossedOut . l ength ;
m u l ti p l e += i )
=
c rossedOut [ mu l t i pl e] t rue ;
}

p r i vate s tati c boo l ean notCros s ed (i nt i )


{
r et u r n c rossedOut [ i ] == fal s e ;
}

p r i vate s tati c voi d putUncrossedinteg e r s i ntoRes ul t ()


{
res u l t = new i nt [numbe rOfU n c r o s sedintege rs () ] ;
fo r ( i nt j = 0 , i = 2 ; i < c ro ssedOut . l e ngth ; i ++)
i f (notCros s ed (i ) )
=
res u l t [ j ++] i;
}

p r i vate s tati c i nt numbe rOfUnc rossed intege r s ()


{
i nt count = 0 ;
fo r ( i nt i = 2 ; i < c rossedOu t . l ength ; i ++)
i f (notCros s ed (i ) )
count++ ;

ret u rn count ;
}
}

Natürlich könnte man auch einwenden, dass der erste Kommentar redundant sei,
weil er im Wesentlichen dasselbe sagt wie die gene rate P r i mes-Funktion selbst.
Dennoch meine ich, dass der Kommentar dem Leser das Lesen des Algorithmus
erleichtert; deshalb neige ich dazu, den Kommentar stehen zu lassen.
Der zweite Kommentar ist fast mit Sicherheit erforderlich. Er erklärt, warum die
Quadratwurzel als Schleifengrenze verwendet wird. Ich konnte weder einen einfa­
chen Variablennamen noch eine andere Codestruktur finden, der bzw. die diesen
Punkt klar macht. Andererseits wirkt der Gebrauch der Quadratwurzel etwas extra­
vagant. Spare ich wirklich so viel Zeit, wenn ich die Iteration durch die Quadrat­
wurzel begrenze? Könnte die Berechnung der Quadratwurzel nicht mehr Zeit
erfordern, als ich einspare?
Dies ist bedenkenswert Die Quadratwurzel als Iterationsgrenze zu benutzen,
befriedigt den alten C- und Assembler-Hacker in mir, aber ich bin nicht überzeugt,
ob es die Zeit und den Aufwand lohnt, dies auch anderen so zu erklären, dass sie
es verstehen.

108
Kapitel s

Formatieru ng

Wenn Entwickler unter die Haube schauen, wollen wir, dass sie von der Ordentlich­
keit, der Konsistenz und der Aufmerksamkeit im Detail beeindruckt sind, die sie
wahrnehmen. Sie sollen die gute Struktur bestaunen. Ihre Augen sollen sich weiten,
wenn sie die Module durchblättern. Sie sollen erkennen, dass hier Profis am Werk
waren. Wenn sie stattdessen eine verknäuelte Masse von Code sehen, der aussieht,
als wäre er von einer Schar betrunkener Seeleute geschrieben worden, würden sie
wahrscheinlich daraus schließen, dass dieselbe Missachtung der Details auch in
allen anderen Aspekten des Projekts vorherrscht.
Sie sollten dafür sorgen, dass Ihr Code sauber formatiert ist. Sie sollten einen Satz
einfacher Regeln für die Formatierung Ihres Codes auswählen und dann konsistent
anwenden. Wenn Sie in einem Team arbeiten, dann sollte sich das Team auf einen
einzigen Satz von Formatierungsregeln festlegen, die von alle Mitgliedern konse­
quent angewendet werden. Es hilft, wenn Sie über ein automatisiertes Tool verfü­
gen, das diese Formatierungsregeln für Sie anwenden kann.

5.1 Der Zweck der Formatieru ng


Zunächst einmal möchte ich klarstellen: Code-Formatierung ist wichtig. Sie ist zu
wichtig, um sie zu ignorieren, und sie ist zu wichtig, um sie dogmatisch zu behan-

1 09
Kapitel s
Formatierung

deln. Code-Formatierung hat mit Kommunikation zu tun, und Kommunikation ist


die vordringlichste Aufgabe eines professionellen Entwicklers.
V:ielleicht dachten Sie, dass »den Code zum Laufen bringen« die vordringlichste
Aufgabe eines professionellen Entwicklers wäre. Ich hoffe jedoch, dass dieses Buch
Ihnen inzwischen diese Vorstellung geraubt hat. Die Funktionalität, die Sie heute
erstellen, hat gute Chancen, das nächste Release Ihrer Software zu ändern, aber die
Lesbarkeit Ihres Codes wird eine nachhaltige Auswirkung auf alle Änderungen
haben, die j emals gemacht werden. Der Codierstil und die Lesbarkeit geben Maß­
stäbe vor, die die Wartbarkeit und Erweiterbarkeit beeinflussen, lange nachdem der
ursprüngliche Code bis jenseits aller Erkennbarkeit geändert worden ist. Ihr Stil
und Ihre Disziplin überleben Ihren Code.
Was also sind die Formatierungsprobleme, die wir lösen müssen, um bestmöglich
zu kommunizieren?

5.2 Vertikale Formatierung


Beginnen wir mit der vertikalen Größe. Wie groß sollte eine Quelldatei sein? I n Java
ist die Dateigröße eng mit der Klassengröße verbunden. Wir kommen zur Klassen­
größe, wenn wir Klassen behandeln. Im Moment soll die Dateigröße unser Thema
sein.
Wie groß sind die meisten Java-Quelldateien ? Es zeigt sich, dass es riesige Unter­
schiede in der Größe und einige bemerkenswerte Unterschiede im Stil gibt. Abbil­
dung 5.1 zeigt einige dieser Unterschiede.

10000.0

1000.0

..
:!1 10 0 . 0

l
i
10.0

junit fitnesse testNG tam jdepend ant tomcat

Abb. 5.1 : Vertei l u n g der Dateigrößen in einer loga rithmischen Skala (Kastenhöhe = Sigma)

110
5-2
Vertikale Formatierung

Es werden sieben verschiedene Projekte dargestellt. J Unit, FitNesse, TestNG, Time


and Money, J Depend, Ant und Tomcat. Die Linien durch die Kästen zeigen die mini­
malen und maximalen Dateigrößen in jedem Projekt. Der Kasten zeigt etwa ein
Drittel (eine Standardabweichung) der Dateien. Die Mitte eines Kastens entspricht
dem Mittelwert. Der Kasten zeigt Sigmaj2 über und unter dem Mittelwert. (Ja, ich
weiß, dass die Dateigrößen nicht normalverteilt sind und dass deshalb die Stan­
dardabweichung mathematisch nicht genau ist. Aber hier geht es nicht um Präzi­
sion. Wir versuchen nur, ein Gefühl für die Größen zu bekommen.)
Die durchschnittliche Dateigröße in dem FitNesse-Projekt beträgt also etwa 65 Zei­
len, und über ein Drittel der Dateien ist zwischen 40 und roo+ Zeilen lang. Die
größte Datei in FitNesse ist über 4 0 0 Zeilen, die kleinste 6 Zeilen lang. Beachten
Sie, dass dies eine logarithmische Skala ist; deshalb bedeuten die kleinen Unter­
schiede in der vertikalen Position sehr große Unterschiede in der absoluten Größe.
J Unit, FitNesse und Time and Money bestehen aus relativ kleinen Dateien. Keine
ist über 500 Zeilen lang und die meisten Dateien sind kürzer als 200 Zeilen. Dage­
gen enthalten Tomcat und Ant einige Dateien, die mehrere Tausend Zeilen lang
sind; etwa die Hälfte ihrer Dateien sind über 200 Zeilen lang.
Was bedeutet das für uns? Es scheint möglich zu sein, umfangreiche Systeme (Fit­
Nesse umfasst annähernd so.ooo Zeilen) aus Dateien zu erstellen, die typischer­
weise 200 Zeilen lang sind, wobei die Obergrenze bei 500 Zeilen liegt. Obwohl dies
keine unumstößliche Regel sein sollte, sollten diese Werte als sehr erstrebenswert
gelten. Kleine Dateien sind normalerweise leichter zu verstehen als große.

Die Zeitu ngs- M etapher

Stellen Sie sich einen gut geschriebenen Zeitungsartikel vor. Sie lesen ihn vertikal.
Am Anfang erwarten Sie eine Ü berschrift, die Ihnen sagt, wovon die Geschichte
handelt und es Ihnen ermöglicht zu entscheiden, ob Sie den Artikel lesen wollen.
Der erste Absatz gibt Ihnen eine Zusammenfassung der gesamten Geschichte. Er
verbirgt alle Details und stellt die wesentlichen allgemeinen Konzepte vor. Wenn Sie
nach unten weiterlesen, erfahren Sie zunehmend feinere Details, bis Sie alle Daten,
Namen, Zitate, Behauptungen und andere Kleinigkeiten kennen.
Eine Quelldatei sollte wie ein Zeitungsartikel lesbar sein. Der Name sollte einfach,
aber aussagekräftig sein. Der Name allein sollte uns sagen können, ob wir im rich­
tigen Modul sind oder nicht. Die oberen Teile der Quelldatei sollten die allgemei­
neren Konzepte und Algorithmen enthalten. Die Detailtiefe sollte nach unten hin
zunehmen, bis wir am Ende der Quelldatei die am niedrigsten angesiedelten Funk­
tionen und Details finden.
Eine Zeitung besteht aus vielen Artikeln; die meisten sind sehr klein. Einige sind
etwas länger. Sehr wenige enthalten so viel Text, wie auf eine Seite passt. Dadurch
wird die Zeitung benutzbar. Enthielte die Zeitung nur eine lange Geschichte mit
einer ungeordneten Anhäufung von Fakten, Daten und Namen, würden wir sie ein­
fach nicht lesen.

111
Kapitel 5
Formatierung

Verti kale Offenheit zwischen Konzepten

Fast der gesamte Code wird von links nach rechts und von oben nach unten gelesen.
Jede Zeile repräsentiert einen Ausdruck oder eine Klausel, und jede Gruppe von Zei­
len repräsentiert einen vollständigen Gedanken. Diese Gedanken sollten voneinan­
der durch Leerzeilen getrennt werden.
Betrachten Sie beispielsweise Listing p . Es enthält Leerzeilen, die die Package­
Deklaration, die lmport-Anweisung(en) und die Funktionen trennen. Diese extrem
einfache Regel hat eine nachhaltige Auswirkung auf das visuelle Layout des Codes.
Jede Leerzeile ist ein visueller Hinweis, der ein neues und separates Konzept iden­
tifiziert. Wenn Sie das Listing überfliegen, werden Ihre Augen jeweils zur ersten
Zeile nach einer Leerzeile gezogen.

Listing 5.1: Bol dWi dget . j ava


package fi tnesse . wi ki text . wi dget s ;

i mport j ava . uti l . regex . * ;

publ i c c l as s Bol dWi dget extends ParentWi dget {


"
publ i c s tati c fi nal S t r i ng REGEXP = ' ' . +? ' ' "' ;'

p r i vate s tati c fi nal Patte rn patt e r n = Patte rn . compi l e ( '" " C . +? ) " " ' ,
Patte rn . MULTI LINE + Patte rn . DOTALL
);

publ i c Bol dWi dge t ( ParentWi dget paren t , S t r i ng text) th rows Excepti on {
s u pe r (parent) ;
Matehe r match =
patte r n . matc h e r (text) ;
match . fi n d ( ) ;
addChi l dWi dgets (match . g roup (l) ) ;
}

publ i c S t r i ng rende r ( ) th rows Excepti o n {


S t r i ngBu ffe r html = new S t r i n g B u ffe r ( " < b> " ) ;
html . append (chi l dHtml ( ) ) . append (" </b> " ) ;
r e t u r n html . to St ri n g () ;
}
}

Wenn wir diese Leerzeilen auslassen (siehe Listing 5.2), wird die Lesbarkeit des
Codes deutlich verschlechtert.

Listing 5.2: Bo1 dWi dget . j ava


package fi t n e s s e . wi ki text . wi dget s ;
i mport j ava . uti l . regex . * ;
publ i c c l as s Bol dWi dget extends ParentWi dget {
publ i c stat i c fi nal S t r i ng REGEXP = '" ' ' . +? ' ' "' ;
p r i vate s tati c fi nal Patt e r n patte rn = Patte r n . compi l e ( " " ' C . +?) " " ' ,
Patte r n . MULTI LINE + Patte rn . DOTALL) ;

112
s.z
Vertikale Formatierung

publ i c Bol dWi dget (ParentWi dget pa rent , S t ri ng text) t h rows Except i on {
s u pe r (parent) ;
Mat e h e r match = patte rn . matc h e r (text) ;
match . fi nd () ;
addChi l dWi dgets (matc h . g ro u p ( l) ) ; }
publ i c St ri ng rende r () th rows Excepti o n {
S t r i ngßuffe r html = n ew S t r i ngßuffe r ("<b> " ) ;
html . append (c h i l dHtml () ) . append ( " </b> " ) ;
ret u rn html . toStri n g ( ) ;
}
}

Dieser Effekt wird noch deutlicher, wenn Sie den Fokus Ihrer Augen verändern. Im
ersten Beispiel fallen die verschiedenen Zeilengruppen sofort auf, während das
zweite Beispiel wie ein großer Brei wirkt. Der Unterschied zwischen diese beiden
Listings besteht nur in einer gewissen vertikalen Offenheit.

Vertikale Dichte

Wenn die Offenheit Konzepte trennt, dann bedeutet die vertikale Dichte eine enge
Zusammengehörigkeit. Deshalb sollten Codezeilen, die eng zusammengehören,
vertikal dicht beieinanderstehen. Beachten Sie, wie die nutzlosen Kommentare in
Listing 5·3 die enge Beziehung der beiden Instanzvariablen stören.

Listing 5.3:
publ i c c l ass Repo rte rConfi g {

/**
* The cl ass name o f the repo rter l i ste n e r
*/
p r i vate S t ri ng m_cl assName ;

!**
* T h e prope rti e s of t h e repo rter l i s t e n e r
*I
p ri vate Li s t<Property> m_p rope rti e s = n e w A r rayli s t< P roperty> () ;

publ i c voi d addPrope rty ( P roperty p rope r ty) {


m_p rope rt i es . ad d ( p rope rty) ;
}

Listing 5 -4 ist viel leichter zu lesen. Es ist gerade ein »Auge voll«, wenigstens für
mich. Ich kann es anschauen und sehe, dass es sich um eine Klasse mit zwei Vari­
ablen und einer Methode handelt, ohne meinen Kopfoder meinen Augen nennens­
wert zu bewegen. Bei dem vorherigen Listing musste ich meine Augen und meinen
Kopf viel stärker bewegen, um den Code genauso gut zu verstehen.

113
Kapitel 5
Formatierung

Listing 5.4:
publ i c c l as s Repo rte rConfi g {
p r i vate St ri ng m_c l assName ;
p r i vate Li st<Prope rty> m_p rope rti e s = n ew A r rayli s t< P rope rty> () ;

publ i c voi d add P rope rty ( P rope rty p rope rty) {


m_p rope rti es . add ( p rope rty) ;
}

Vertikaler Abstand

Haben Sie jemals wie eine Katze ihren Schwanz eine Klasse gejagt und sind von
einer Funktion zur nächsten gesprungen, wie wild durch die Quelldatei gescrollt,
versucht herauszufinden, wie die Funktionen zusammengehören und arbeiten, nur
um dann in einem Rattennest der Verwirrung zu landen? Haben Sie sich jemals
eine Vererbungskette emporgehangelt, um die Definition einer Variablen oder
Funktion zu finden? Dies ist frustrierend, weil Sie versuchen zu verstehen, was das
System tut, Sie aber Ihre Zeit und mentale Energie bei dem Versuch vergeuden, her­
auszufinden und sich zu merken, wo die Teile stehen.
Konzepte, die eng verwandt sind, sollten vertikal eng beisammenstehen [GIO].
Natürlich funktioniert diese Regel nicht bei Konzepten, die in separate Dateien
gehören. Doch andererseits sollten eng verwandte Konzepte nicht aufverschiedene
Dateien verteilt werden, es sei denn, Sie haben einen übergeordneten Grund dafür.
Tatsächlich ist dies einer der Gründe dafür, dass p rotected Variablen vermieden
werden sollten.
Bei Konzepten, die so eng verwandt sind, dass sie in dieselbe Quelldatei gehören,
sollte ihre vertikale Trennung ein Maß dafür sein, wie wichtig das eine Konzept ist,
um das jeweils andere zu verstehen. Wir wollen unsere Leser nicht zwingen, in
unseren Quelldateien und Klassen hin und her zu springen.
Variablendeklarationen. Variablen sollten so eng bei ihrem Verwendungsort wie
möglich deklariert werden. Weil unsere Funktionen sehr kurz sind, sollten lokale
Variablen am Anfang einer Funktion stehen, wie etwa in dieser längeren Funktion
aus JUnit4-3-1.

p r i vate stati c voi d read P refe rences () {


InputStream i s= nul l ;
t ry {
i s= n ew Fi l e l n putSt ream (get P refe rencesFi l e () ) ;
setPrefe rences (new Propert i e s (get P refe rences () ) ) ;
getPreferences () . l oad ( i s) ;
} catch (IOExcepti on e) {
try {
i f Ci s ! = n u l l )
i s . c l os e () ;
} catch ( IOExcepti on el) {
}
}
}
). 2
Vertikale Formatierun g

Kontrollvariablen für Schleifen sollten normalerweise innerhalb der Schleifenan­


weisung deklariert werden, wie etwa bei dieser hübschen kleinen Funktion aus der­
selben Quelle.

publ i c i n t countTestCas e s ( ) {
i nt count= 0 ;
for (Test each : tests)
count += each . cou ntTe s tCases () ;
retu rn count ;
}

In seltenen Fällen kann eine Variable in einer längeren Funktion auch am Anfang
eines Blocks oder unmittelbar vor einer Schleife deklariert werden. Eine solche Vari­
able finden Sie in diesem Code-Ausschnitt aus der Mitte einer sehr langen Funktion
in TestNG.

fo r (Xml Test t e s t : m_s u i te . getTes t s ( ) ) {


TestRunner tr = m_runnerFactory . newTestRunner (thi s , test) ;
t r . addLi s t e n e r (m_textRepo rter) ;
m_testRu n n e r s . add ( t r) ;

i nvo ke r = t r . ge t ! nvoke r () ;
fo r (ITe s tNGMethod m : t r . getßefo reS u i teMethods ( ) ) {
beforeSui teMethods . pu t (m . getMethod ( ) , m) ;
}

fo r (ITes tNGMethod m : t r . getAfte rSui teMethod s ( ) ) {


afte rSui teMethods . pu t (m . getMethod () , m) ;
}
}

Instanzvariablen sollten dagegen am Anfang der Klasse deklariert werden. Dies


sollte nicht den vertikalen Abstand dieser Variablen vergrößern, weil sie in einer gut
konzipierten Klasse von vielen, oder sogar von allen der Methoden der Klasse
benutzt werden.
Es hat zahlreiche Diskussionen über den geeigneten Platz von Instanzvariablen
gegeben. In C++ haben wir üblicherweise die so genannte Scissors-Regel ( Scheren­
Regel) angewendet, nach der alle Instanzvariablen am Ende platziert werden. In Java
werden sie jedoch per Konvention alle am Anfang der Klasse aufgeführt. Ich sehe
keinen Grund, einer anderen Konvention zu folgen. Das Wichtige ist, dass die
Instanzvariablen an einer wohlbekannten Stelle deklariert werden. Jeder sollte wis­
sen, wo er die Deklarationen finden kann.
Betrachten Sie beispielsweise den seltsamen Fall der TestS u i te-Klasse in JUnit
4·3-I· Ich habe diese Klasse stark ausgedünnt, um den Punkt deutlich zu machen.

115
Kapitel 5
Formatierung

Etwa in der Mitte des Listings werden zwei Instanzvariablen deklariert. Es wäre
schwer, sie an einer geeigneteren Stelle zu verstecken. Wer den folgenden Code
liest, muss schon per Zufall auf die Deklarationen stoßen (wie es mir ging) .

publ i c c l ass Test S u i te i mp l ements Test {


s t ati c publ i c Test c reateTest (Cl ass<? extends TestCas e> theCl ass ,
St ri ng name) {

publ i c stati c Const ructor<? extends TestCase>


getTes tConst ructo r (Cl as s<? extends TestCase> t h eCl ass)
th rows NoSu chMethodExcepti o n {

publ i c s tati c Test wa r n i ng (fi nal St ri ng message) {

p r i vate stat i c St ri ng except i o nToSt ri ng (Th rowabl e t) {

p r i vate Stri ng fName ;

p r i vate Vector<Test> fTests= new Vector<Test> (lO) ;

publ i c TestSui t e ( ) {
}

publ i c TestSu i te (fi nal Cl ass<? extends Tes tCase> theCl a s s ) {

publ i c TestS u i t e (Cl ass<? extends TestCase> theCl ass , S t r i ng name) {

Abhängige Funktionen. Wenn eine Funktion eine andere aufruft, sollten sie vertikal
eng beisammenstehen, und die aufrufende Funktion sollte über der aufgerufenen
Funktion stehen, falls möglich. Dadurch erhält das Programm einen natürlichen
Fluss. Wenn die Konventionen zuverlässig eingehalten werden, kann der Leser dar­
auf vertrauen, dass die Funktionsdefinitionen kurz nach ihrer Verwendung folgen
werden. Betrachten Sie beispielsweise den Code-Ausschnitt von FitNesse in Listing
5·5· Die obere Funktion ruft die Funktion unter ihr auf, diese ihrerseits die unter ihr

11 6
5· 2
Vertikale Formatierung

stehende Funktion usw. So kann der Leser leicht die aufgerufenen Funktionen fin­
den; und die Lesbarkeit des ganzen Moduls wird erheblich verbessert.

Listi ng 5.5: Wi ki PageResponder . j ava


publ i c c l ass Wi ki PageResponde r i mpl ements Secu reResponder {
p rotected Wi ki Page page ;
p rotected PageData pageData ;
p rotected St ri ng pageTi tl e ;
p rotected Request reques t ;
p rotected PageCrawl e r c rawl e r ;

publ i c Response makeRespons e ( Fi tNesseContext context , Request request)


th rows Exception {
Stri ng pageName = getPageNameOrDefaul t ( request , " FrontPage") ;
l oadPage (pageName , context) ;
i f (page == nul l )
return notFoundResponse (context , request) ;
e l se
retu rn makePageResponse (context) ;
}

pri vate St ri ng getPageNameOrDefaul t (Request request , St ri ng defaul tPageName)


{
Stri ng pageName = request . getResource () ;
i f (Stri ngUti l . i sBl ank(pageName) )
pageName = defaul tPageName ;

return pageName ;
}

p rotected voi d l oadPage (Stri ng resou rce , Fi tNesseContext context)


th rows Excepti on {
Wi ki PagePath path = PathPars e r . parse ( resou rce) ;
crawl e r = context . root . getPageCrawl e r () ;
c rawl e r . setDeadEndStrategy(new Vi rtual Enabl edPageCrawl e r () ) ;
page = c rawl e r . getPage(context . root , path) ;
i f (page ! = n ul l )
pageData = page . getData() ;
}

pri vate Response notFoundRes ponse (Fi tNesseContext context , Request request)
th rows Excepti on {
retu rn new NotFoundResponde r () . makeResponse (context , request) ;
}

pri vate Si mpl eResponse makePageResponse ( Fi tNesseContext context)


th rows Excepti on {
pageTi tl e = PathParse r . rende r ( c rawl e r . getFul l Path (page) ) ;
Stri ng html = makeHtml (context) ;

117
Kapitel 5
Formatierung

Si mpl eResponse response = new Si mpl eRespon s e () ;


response . setMaxAge(O) ;
response . setConten t (html ) ;
retu rn response ;
}

Eine Nebenbemerkung: Dieser Code-Ausschnitt zeigt ein hübsches Beispiel dafür,


wie Konstanten auf der passenden Stufe verwaltet werden [G35]. Die Konstante
" F rontPage " hätte in der get Pag eN ameOrDefaul t-Funktion vergraben werden
können, aber dadurch wäre eine wohlbekannte und erwartete Konstante unange­
messen in einer niedrig angesiedelten Funktion vergraben worden. Es war besser,
diese Konstante von der Stelle aus, an der sie bekannt sein sollte, nach unten an die
Stelle weiterzureichen, an der sie tatsächlich benutzt wird.
Konzeptionelle Affinität. Bestimmte Code-Stücke wollen in der Nähe anderer Code­
Stück stehen. Sie haben eine bestimmte konzeptionelle Affinität (Verwandtschaft) .
Je stärker diese Affinität ist, desto geringer sollte der vertikale Abstand zwischen
ihnen sein.
Wie wir gesehen haben, könnte diese Affinität auf einer direkten Abhängigkeit
basieren (Beispiele: Eine Funktion ruft eine andere auf; eine Funktion verwendet
eine Variable) . Aber es gibt andere mögliche Ursachen für die Affinität. Sie könnte
entstehen, weil mehrere Funktionen eine ähnliche Operation ausführen. Betrach­
ten Sie den folgenden Code-Ausschnitt aus dem Code von JUnit 4.3.1:

publ i c c l ass A s s e rt {
s t ati c publ i c voi d asse rtTrue ( St r i ng message , bool ean condi ti on) {
i f ( ! condi ti on)
fai l (message) ;
}

stati c publ i c voi d asse rtTrue ( bool ean condi ti on) {


a s s e rtTr u e ( n u l l , condi ti on) ;
}

s tati c publ i c voi d asse rt Fal s e ( S t ri ng m e s s age , bool e an condi ti on) {


asse rtTrue (me s s age , ! condi ti o n ) ;
}

stati c p u bl i c voi d asse rt Fal s e (bool ean condi ti on) {


asse rtFal se ( n u l l , condi ti on) ;
}

Diese Funktionen haben eine starke konzeptionelle Affinität, weil sie ein gemein­
sames Namensschema haben und Varianten derselben grundlegenden Aufgabe

1 18
5·3
Horizontale Formatierung

ausführen. Die Tatsache, dass sie sich gegenseitig aufrufen, ist sekundär. Selbst
wenn sie es nicht täten, sollten sie eng beieinander stehen.

Verti kale Anord n u ng

Im Allgemeinen sollen Funktionsaufruf-Abhängigkeiten nach unten zeigen. Das


heißt, eine Funktion, die aufgerufen wird, sollte sich unter einer Funktion befinden,
von der sie aufgerufen wird. Dadurch entsteht ein hübscher Fluss durch das Source­
code-Modul, der von der hohen Ebene am Anfang nach unten zu den niedrigeren
Stufen fließt. (Dies ist das genaue Gegenteil von Sprachen wie Pascal, C und C++,
die erzwingen, dass Funktionen definiert oder zumindest deklariert werden müs­
sen, bevor sie aufgerufen werden.)
Wie bei Zeitungsartikeln erwarten wir, dass die wichtigsten Konzepte zuerst kom­
men und dass sie mit der geringstmöglichen Verunreinigung durch Details ausge­
drückt werden. Wir erwarten, dass die Details der niedrigen Ebenen zum Schluss
kommen. So können wir die Quelldateien überfliegen und aus den ersten Funkti­
onen das Wesentliche ablesen, ohne in die Details eintauchen zu müssen. Listing
5 · 5 ist nach diesem Prinzip geordnet. Vielleicht noch bessere Beispiele finden Sie
in den Listings 15.5 und 3·7·

5·3 Horizontale Formatieru ng


Wie breit sollte eine Zeile sein? Um diese Frage zu beantworten, wollen wir einen
Blick auf die Breite der Zeilen in typischen Programmen werfen. Auch hier unter­
suchen wir die sieben verschiedenen Projekte. Abbildung 5.2 zeigt die Verteilung
der Zeilenbreiten für alle sieben Projekte. Die Regelmäßigkeit ist beeindruckend,
besonders im Bereich von 45 Zeichen. Tatsächlich machen alle Größen von 20 bis
6o jeweils über ein Prozent der Gesamtzahl der Zeilen aus. Das sind 40 Prozent!
Etwa 30 weitere Prozent sind weniger als 10 Zeichen breit. Vergessen Sie nicht, dass
dies eine logarithmische Skala ist; deshalb ist das lineare Aussehen des Abfalls über
8o Zeichen wirklich sehr signifikant. Programmierer ziehen ganz klar kurze Zeilen
vor.
Dies weist daraufhin, dass wir möglichst kurze Zeilen bevorzugen sollten. Die alte
Hollerith-Grenze von 8o ist ein wenig willkürlich, und ich habe nichts dagegen, die
Zeilenbreite auf 100 oder sogar 120 auszudehnen. Aber darüber hinaus ist wahr­
scheinlich nur sorglos.
Früher befolgte ich die Regel, dass man niemals nach rechts scrollen müssen sollte.
Aber die heutigen Monitore sind dafür zu breit. Jüngere Programmierer können die
Schriftart so weit verkleinern, dass sie 200 Zeichen in eine Zeile bekommen. Tun
Sie dies nicht. Ich habe meine Grenze auf 120 gesetzt.

119
Kapitel s
Formatierung

100.0000% ,.-------r---,

0.0010%

0.0001% 1-------+-�---+

_______________ _ _ _______
0 . 0000% L_ .... .J_ ..J._ __,

0 10 20 30 40 50 60 70 BO 90 100 1 10 120 130 140 150


-

Abb. 5.2: Vertei l u n g der Breite von J ava-Zeilen

Horizontale Offen heit u nd Dichte

Wir verwenden horizontalen Whitespace, um Aufgaben zusammenzufassen, die


enge Beziehungen haben, und Aufgaben zu trennen, die schwächere Beziehungen
haben. Betrachten Sie die folgende Funktion:

p r i vate voi d meas u reli n e ( S t r i n g l i ne ) {


l i n eCou nt++ ;
i n t l i neSi ze = l i ne . l engt h () ;
total Chars += l i neSi ze ;
l i neWi dthHi stog r am . addli n e ( l i neSi ze , l i neCount) ;
reco rdWi destl i n e ( l i neSi ze) ;
}

Ich habe die Zuweisungsoperatoren mit Whitespace umgeben, um sie zu betonen.


Zuweisungsanweisungen bestehen aus zwei separaten Hauptelementen: die linke
Seite und die rechte Seite. Die Leerzeichen machen diese Trennung deutlich.
Andererseits habe ich kein Leerzeichen zwischen die Funktionsnamen und die öff­
nende Klammer eingefügt, weil die Funktionen und ihre Argumente eng zusam­
mengehören. Eine optische Trennung würde sie als nicht zusammengehörig
erscheinen lassen. Ich trenne Argumente innerhalb der Klammern eines Funkti­
onsaufrufs, um das Komma zu betonen und zu zeigen, dass die Argumente separat
sind.
Mit Whitespace kann man auch den Vorrang von Operatoren hervorheben:

1 20
5· 3
Horizontale Formatierung

p u bl i c cl ass Quad r at i c {
p u bl i c s t at i c doub l e rootl (doubl e a , dou b l e b , doubl e c) {
dou bl e dete rmi nant = dete rmi nant ( a , b , c) ;
retu rn ( - b + Math . sq rt (determi nant) ) I (2 *a) ;
}

publ i c s tati c doubl e root 2 ( i nt a , i nt b , i nt c) {


doubl e determi nant = determi n an t (a , b , c) ;
return ( - b - Math . sq rt (dete rmi nant) ) I (2 *a) ;
}

p r i vate stati c doubl e determi n a n t (doubl e a , doubl e b , dou b l e c) {


retu rn b � b - 4 * a* c ;
}
}

Beachten Sie, wie gut sich die Gleichungen lesen lassen. Zwischen den Faktoren
steht kein Whitespace, weil sie einen hohen Vorrang haben. Die Terme sind durch
Whitespace getrennt, weil Addition und Subtraktion einen geringeren Vorrang
haben.
Leider sind die meisten Formatierungs-Tools blind für den Vorrang von Operatoren
und wenden durchgehend denselben Abstand an. Deshalb gehen subtil gesetzte
Abstände wie in dem obigen Beispiel verloren, wenn Sie den Code mit einem sol­
chen Tool neu formatieren.

Horizontale Ausrichtu ng

Als ich noch in Assembler programmierte, benutzte ich die horizontale Ausrich­
tung, um bestimmte Strukturen hervorzuheben. Als ich anfing, in C, C++ und
schließlich in Java zu programmieren, versuchte ich weiterhin, alle Variablenna­
men in einem Satz von Deklarationen oder alle rva l ues in einem Satz von Zuwei­
sungsanweisungen auszurichten. Mein Code könnte etwa wie folgt ausgesehen
haben:

p u b l i c cl ass Fi tNesseExped i t e r i mp l ements Res pon seSender


{
p r i vate Socket socket ;
p r i vate InputS t ream i nput ;
p r i vate OutputSt ream OUtput ;
p r i vate Req u e s t request ;
p r i vate Res pon s e respon s e ;
p r i vate Fi tNesseContext context ;
p rotected l ong request Pa r s i ngTi me l i mi t ;
p r i vate l on g r equest P rog r es s ;
p r i vate l ong req uestParsi ngDeadl i ne ;
p r i vate boo l ean h as E r ro r ;

1 21
Kapitel 5
Formatieru ng

p u b l i c Fi tNesseExpedi t e r ( Socket s,
Fi tNesseContext context) t h rows Excepti on
{
th i s . context = context ;
socke t = s;
i n put = s . ge t l n putSt ream ( ) ;
output = s . getOutputStream () ;
req u e s tParsi ngTi meli mi t = 10000 ;
}

Ich habe jedoch festgestellt, dass diese Art von Ausrichtung nicht nützlich ist. Die
Ausrichtung scheint die falschen Aufgaben zu betonen und lenkt mein Auge vom
eigentlichen Zweck ab. Beispielsweise ist man bei der obigen Deklarationsliste ver­
sucht, die Liste der Variablennamen von oben nach unten zu lesen, ohne auf ihre
Typen zu achten. Ähnlich ist man in der Liste der Zuweisungsanweisungen ver­
sucht, die Liste der rva 1 u e s zu lesen, ohne auf den Zuweisungsoperator zu achten.
Die Sache wird noch dadurch verschlimmert, dass automatische Formatierungs­
Tools diese Art von Ausrichtung normalerweise eliminieren.
Deshalb verwende ich diese Art von Ausrichtung nicht mehr. Heute ziehe ich nicht
ausgerichtete Deklarationen und Zuweisungen vor (siehe unten) , weil sie ein wich­
tiges Manko aufzeigen. Wenn ich lange Listen habe, die ausgerichtet werden müs­
sen, ist die Länge der Listen das Problem, nicht das Fehlen der Ausrichtung. Die Länge
der unten gezeigten Liste von Deklarationen in Fi tNe s s e Expedi t e r weist darauf
hin, dass diese Klasse aufgeteilt werden sollte.

publ i c cl ass Fi tNesseExpedi ter i mpl ements ResponseSende r


{
pri vate Socket socke t ;
pri vate InputSt ream i nput ;
pri vate OutputSt ream output ;
p ri vate Request reques t ;
p r i vate Response response ;
pri vate Fi tNesseContext context ;
p rotected l ang requestPa rsi ngTi meli mi t ;
pri vate l ang requestProg ress ;
pri vate l ang requestPa rsi ngDeadl i ne ;
pri vate bool ean has E r ro r ;

publ i c Fi tNesseExpedi ter (Socket s , Fi tNesseContext context) th rows Exception


{
thi s . context = context ;
socket = s ;
i nput = s . getlnputSt ream() ;
output = s . getOutputStream() ;
requestParsi ngTi meli mi t = 10000 ;
}

1 22
5·3
Horizontale Formatierung

Ei nrückung

Eine Quelldatei ist eine Hierarchie, ähnlich einer Gliederung. Es gibt Informatio­
nen, die zu der Datei insgesamt, zu den einzelnen Klassen innerhalb der Datei, zu
den Methoden innerhalb der Klassen, zu den Blöcken innerhalb der Methoden und
rekursiv zu den Blöcken innerhalb von Blöcken gehören. Jede Ebene dieser Hier­
archie ist ein Geltungsbereich, in dem Namen deklariert werden können und in
dem Deklarationen und ausführbare Anweisungen interpretiert werden.
Um diese Hierarchie der Geltungsbereiche sichtbar zu machen, rücken wir die Zei­
len des Sourcecodes entsprechend ihrer Position in der Hierarchie ein. Anweisun­
gen auf der Datei-Ebene, wie etwa die meisten Klassendeklarationen, werden
überhaupt nicht eingerückt. Methoden innerhalb einer Klasse werden eine Stufe
nach rechts innerhalb ihrer Klasse eingerückt. Implementierungen dieser Metho­
den werden eine Stufe weiter rechts innerhalb der Methoden-Definition eingerückt.
Block-Implementierungen werden eine Stufe weiter nach rechts innerhalb des ein­
schließenden Blocks eingerückt usw.
Programmierer verlassen sich auf dieses Einrückungsschema. Sie orientieren sich
am linken Rand der Zeilen, um zu sehen, zu welchem Geltungsbereich sie gehören.
Auf diese Weise können sie schnell Geltungsbereiche, wie etwa Implementierun­
gen von i f- oder w h i l e-Anweisungen, überspringen, die in ihrer gegenwärtigen
Situation nicht relevant sind. Sie scannen die linke Seite nach neuen Methoden­
Deklarationen, neuen Variablen und sogar neuen Klassen. Ohne Einrückung wären
Programme für Menschen praktisch unlesbar.
Betrachten Sie die folgenden Programme, die syntaktisch und semantisch identisch
sind:

publ i c cl ass Fi tNesseServer i mpl ements SocketServer { pri vate FitNesseContext context ;
=
publ i c Fi tNesseServer(Fi tNesseContext context) { thi s . context context ; } publ i c voi d
se rve(Socket s) { serve (s , 10000) ; } publi c voi d se rve(Socket s , l ang requestTi meout)
{ t r y { Fi tNes s e Expedi t e r s e n d e r = n ew Fi tNess eExpedi t e r ( s , context) ;
sende r . setReq uestParsi ngTi meli mi t ( requestTi meout) ; s e nde r . s t a r t ( ) ; }
catch ( Excepti on e) { e . p ri ntStackTrac e () ; } } }

publ i c c l a s s Fi tNes s e Se rve r i mpl ements SocketSe rve r {


p r i vate Fi tNesseCo ntext context ;
publ i c Fi tNesseServe r ( Fi tNesseContext context) {
thi s . context = context ;
}

publ i c voi d se rve (Socket s ) {


s e rve (s , 10000) ;
}

1 23
Kapitel 5
Formatierung

publ i c voi d se rve (Socket s , l ong reque stTi meout) {


t ry {
Fi tNesseExpedi te r sender = new Fi tNesse Expedi t e r (s , con text) ;
sende r . setRequest Pa r s i ngTi meli mi t ( reque stTi meout) ;
sende r . start () ;
}
catch ( Except i on e) {
e . p r i ntStackTrac e ( ) ;
}
}
}

Ihr Auge kann schnell die Struktur der eingerückten Datei erkennen. Sie finden fast
sofort die Variablen, Konstruktoren, Accessoren und Methoden. Es dauert nur ein
paar Sekunden, um zu erkennen, dass Sie es mit einem einfachen Front-End für ein
Socket mit einem Timeout zu tun haben. Die nicht eingerückte Version ist jedoch
ohne intensives Studium praktisch undurchdringlich.
Einrückungsregeln brechen. Manchmal ist man versucht, die Einrückungsregel für
kurze i f-Anweisungen, kurze wh i l e-Schleifen oder kurze Funktionen zu brechen.
Wenn ich dieser Versuchung nachgegeben habe, bin ich fast immer zurückgegan­
gen und habe die Einrückung wieder eingefügt. Deshalb vermeide ich es, Geltungs­
bereiche etwa wie folgt auf eine Zeile zu reduzieren:

p u b l i c cl ass CommentWi dget extends TextWidget


{
publ i c stati c fi nal S t r i n g REGEXP = " A# [A\r\n ] * (? : (? : \r\n) l \n l \ r) ? " ;

publ i c CommentWi dget (ParentWi dget parent , Stri ng text) { supe r (parent , text) ; }
pu b l i c St r i ng rende r ( ) t h r ows Except i on { retu rn " " ; }
}

Ich ziehe es vor, die Geltungsbereiche wie folgt zu erweitern und einzurücken:

publ i c cl ass CommentWi dget extends TextWi dget {


publ i c s tati c fi nal St ri ng R EGEXP =
" A# [A\r\n ] * (? : (? : \r\n) l \n l \ r) ? " ;

publ i c Commen tWi dge t ( Pa r entWi dget paren t , S t r i ng text) {


s u p e r (parent , text) ;
}

publ i c S t r i ng rend e r () th rows Except i on {


return " " ·'
}
}

Du m my-Bereiche

Manchmal ist der Body einer wh i 1 e- oder for-Anweisung ein Dummy (siehe
unten). I ch mag diese Art von Strukturen nicht und versuche, sie zu vermeiden.

1 24
54
Team-Regeln

Wenn ich sie nicht vermeiden kann, sorge ich dafür, dass der Dummy-B ody korrekt
eingerückt und durch Klammern eingeschlossen wird. Ich weiß nicht, wie oft ich
mich von einem Semikolon am Ende einer wh i l e-Schleife in derselben Zeile habe
täuschen lassen. Solange Sie dieses Semikolon nicht sichtbar machen, indem Sie es
in einer separaten Zeile einrücken, ist es einfach schwer zu sehen_

whi l e (di s . read (bu f , 0 , readBu ffe rSi ze) ! = - 1)

5 ·4 Team-Regeln
Jeder Programmierer hat seine eigenen Formatierungsregeln; doch wenn er im
Team arbeitet, hat das Team Vorrang.
Ein Team von Entwicklern sollte sich auf einen einzigen gemeinsamen Formatie­
rungsstil festlegen. Jedes Mitglied dieses Teams sollte diesen Stil benutzen. Die Soft­
ware soll in einem konsistenten Stil geschrieben werden. Sie soll nicht so aussehen,
als wäre sie von einer Horde Individuen geschrieben worden, die sich nicht einigen
konnten.
Als ich 2 002 mit der Arbeit an dem FitNesse-Projekt begann, setzte ich mich mit
dem Team zusammen, um einen Codierstil auszuarbeiten. Dies dauerte etwa zehn
Minuten. Wir legten fest, wo wir unsere Klammern setzen wollten, wie groß die Ein­
rückung sein sollte, wie die Klassen, Variablen und Methoden usw. benannt sein
sollten. Dann legten wir diese Regeln in dem Code-Formatierer unserer I D E fest
und haben uns seitdem daran gehalten. Es waren nicht die Regeln, die ich bevor­
zuge; es waren die Regeln, auf die sich das Team geeinigt hatte. Als Mitglied dieses
Teams befolgte ich sie, wenn ich Code für das FitNesse-Projekt schrieb.
Vergessen Sie nicht: Ein gutes Software-System besteht aus einem Satz von gut les­
baren Dokumenten. Sie müssen einen konsistenten und geschmeidigen Stil haben.
Der Leser muss daraufvertrauen können, dass die Formatierungskonstrukte, die er
in einer Quelldatei gesehen hat, in anderen Dateien dasselbe bedeuten. Wir wollen
keinesfalls die Komplexität des Sourcecodes mit einem Durcheinander verschiede­
ner einzelner Stile vergrößern.

5·5 U ncle Bobs Formatieru ngsregeln


Die Regeln, die ich persönlich verwende, sind sehr einfach. Sie werden durch den
Code in Listing 5 .6 illustriert. Betrachten Sie das Folgende als Beispiel daftir, wie der
Code selbst am besten Codierstandards dokumentiert.

Listing s.6: CodeAnal yzer . j ava


publ i c c l a s s CodeAnal y z e r i mp l ements J avaFi l eAnal ysi s {
p r i vate i nt l i n eCo u n t ;

1 25
Kapitel 5
Formatierung

p ri vate i nt max Li neWi dth ;


p r i vate i nt wi destLi n eN umbe r ;
p ri vate Li neWi dt h H i s tog ram l i n eWi dthHi stog ram ;
p r i vate i nt total Chars ;

publ i c CodeAna l yze r ( ) {


l i neWi dthHi s tog ram = new Li n eWidthH i s tog ram ( ) ;
}

publ i c s tati c Li st<Fi l e> fi nd J avaFi l es ( Fi l e parentDi recto ry) {


L i st<Fi l e> fi l es = new A r rayL i s t< Fi l e> () ;
fi nd J avaFi l es ( pa r e n tDi recto r y , fi l es ) ;
retu rn fi l es ;
}

pri vate stati c voi d fi ndJ avaFi l es ( Fi l e parentDi recto ry , Li s t<Fi l e> fi l es) {
for ( Fi l e fi l e : paren tDi rectory . l i stFi l es () ) {
i f (fi l e . getName () . end sWi t h ( " . j av a" ) )
fi l es . ad d (fi l e) ;
e l s e i f (fi l e . i sDi rectory ( ) )
fi nd J avaFi l e s (fi l e , fi l es ) ;
}
}

publ i c voi d anal yzeFi l e ( Fi l e j avaFi l e) t h rows Excepti on {


B u ffe redReade r b r = new B u ffe redReader (new Fi l eRead e r ( j avaFi l e) ) ;
S t ri ng l i n e ;
whi l e ( (l i ne = b r . readLi n e () ) ! = n u l l )
meas u reLi n e ( l i ne) ;
}

p ri vate voi d meas u reLi n e (S t r i ng l i n e ) {


l i neCount++ ;
i nt l i neSi ze = l i ne . l ength () ;
total C h a rs += l i neSi ze ;
l i n eWi dthHi stog r am . add L i n e ( l i neSi ze , l i neCount) ;
reco rdWi destL i n e (l i neSi z e ) ;
}

p r i vate voi d reco rdWi destL i ne (i n t l i neSi ze) {


i f ( l i neSi ze > max L i n eWi dth) {
max L i n eWi dth = l i neSi ze ;
wi destLi neNumb e r = l i neCount ;
}
}

publ i c i nt getLi n eCou n t ( ) {


retu r n l i neCou n t ;
}

126
5·5
U ncle Bobs Formatierungsregeln

publ i c i nt getMax li n eWi dth () {


retu rn maxli neWi dth ;
}

publ i c i nt getWi destli neNumbe r () {


retu rn wi destli neNumbe r ;
}

publ i c L i n eWi dthHi stog ram getli n eWi dthHi stog ram () {
retu rn l i n eWi dthHi stog ram ;
}

publ i c dou b l e getMean li neWi dth () {


ret u rn (dou bl e) total Chars/l i n eCo u n t ;
}

publ i c i nt getMedi an li neWi dth () {


I n t ege r [ ] so rtedWi d t h s = getSortedWi d th s () ;
i nt cumu l ati veLi n eCou n t = 0;
for (i n t wi dth : so rtedWi dths) {
cumu l ati veli neCo u n t += l i n eCou ntFo rWi dth (wi dth) ;
i f (cumu l ati veli neCount > l i n eCount/2)
retu rn wi dth ;
}
t h row n ew E r ro r ( "Cannot get h e r e " ) ;
}

pri vate i nt l i n eCou n t Fo rWi dth (i n t wi d t h ) {


retu rn l i neWi dthHi stog ram . getli n e s fo rWi dth (wi dt h ) . si z e ( ) ;
}

p ri vate I n tege r [ ] getSo rtedWi dth s () {


Set<Intege r> wi d t h s = l i n eWi dthHi stog ram . getWi dth s () ;
I n tege r [] so rtedWi d t h s = (wi d t h s . toA r ray ( new I ntege r [O] ) ) ;
A r rays . so rt (so rtedWi dths) ;
retu rn so rtedWi dth s ;
}
}

1 27
Kapitel 6

Objekte u nd Datenstru ktu ren

Es gibt einen Grund, warum wir unsere Variablen privat halten. Wir wollen nicht,
dass jemand von ihnen abhängig ist. Wir wollen uns die Freiheit bewahren, ihren
Typ oder ihre Implementierung nach B elieben zu ändern. Warum fügen dann so
viele Programmierer automatisch Getter und Setter zu ihren Objekten hinzu und
enthüllen damit ihre privaten Variablen so, als wären sie öffentlich?

6.1 Datenabstraktion
Betrachten Sie den Unterschied zwischen Listing 6.r und Listing 6.2. Beide reprä­
sentieren die Daten eines Punktes in einem Kartesischen Koordinatensystem. Und
dennoch enthüllt das eine Listing die Implementierung, während das andere sie
vollkommen verbirgt.

Listing 6.1 : Konkreter Punkt


publ i c c l a s s Poi n t {
publ i c doubl e x ;
publ i c doubl e y ;
}

1 29
Kapite1 6
Objekte u nd Datenstrukturen

Listing 6.2: Abstrakter Punkt


p u bl i c i n te rface Poi n t {
dou bl e getX () ;
dou b l e getY() ;
voi d setCartes i an (doubl e x , doubl e y) ;
dou b l e getR() ;
dou b l e getTheta () ;
voi d setPo l a r (doubl e r , doubl e theta) ;
}

Das Schöne an Listing 6.2 ist, dass Sie daraus nicht erkennen können, ob die Im­
plementierung in Rechteck- oder in Polar-Koordinaten erfolgt. Es könnte auch kei­
nes von beiden sein! Und dennoch repräsentiert das Interface zweifellos eine Daten­
struktur.
Aber es repräsentiert mehr als nur eine Datenstruktur. Die Methoden erzwingen
gewisse Zugriffsregeln. Sie können die einzelnen Koordinaten unabhängig lesen,
aber Sie müssen die Koordinaten zusammen als atomare Operation setzen.
Dagegen ist Listing 6.1 sehr klar in Rechteck-Koordinaten implementiert. Hier sind
wir gezwungen, diese Koordinaten unabhängig zu manipulieren. Dadurch wird die
Implementierung enthüllt. Tatsächlich würde die Implementierung selbst dann
enthüllt, wenn die Variablen privat wären und wir nur Getter und Setter für die ein­
zelnen Variablen verwenden würden.
Die Implementierung zu verbergen, bedeutet nicht, die Variablen einfach hinter
einer Schicht von Funktionen zu verstecken. Die Implementierung zu verbergen,
hat nichts mit Abstraktionen zu tun! Eine Klasse setzt und liest ihre Variablen nicht
einfach durch Getter und Setter, sondern sie enthüllt abstrakte Interfaces, mit denen
der Benutzer die Essenz der Daten manipulieren kann, ohne deren Implementie­
rung kennen zu müssen.
Betrachten Sie Listing 6.3 und Listing 6+ Das erste verwendet konkrete Termini,
um den Füllstand des Tanks eines Fahrzeugs zu kommunizieren, während das
zweite zu diesem Zweck mit der Abstraktion des Prozentsatzes arbeitet. In dem kon­
kreten Fall können Sie ziemlich sicher sein, dass es sich einfach um Accessoren der
Variablen handelt. In dem abstrakten Fall haben Sie überhaupt keinen Anhalts­
punkt für die Form der Daten.

Listing 6.3: Konkretes Fahrzeug


publ i c i n te rface Vehi c l e {
doubl e getFuelTankCapaci tyinGal l on s () ;
dou b l e getGal l onsOfGasol i ne () ;
}

Listing 6.4: Abstraktes Fahrzeug


p u bl i c i nte rface Veh i c l e {
dou b l e getPe rcentFuel Remai ni n g () ;
}
6.2
Daten/Objekt-Anti-Symmetrie

In beiden genannten Beispielen ist jeweils die zweite Variante vorzuziehen. Wir
wollen die Details unserer Daten nicht enthüllen, sondern wir wollen unsere Daten
in abstrakten Termini ausdrücken. Dies wird nicht einfach durch die Verwendung
von Interfaces undfader Gettern und Settern erreicht. Sie müssen gründlich nach­
denken, was der beste Weg ist, die Daten in einem Objekt zu repräsentieren. Unbe­
kümmert Getter und Setter hinzuzufügen, ist die schlimmste Option.

6.2 Daten JObjekt-Anti-Sym metrie


Diese beiden Beispiele zeigen den Unterschied zwischen Objekten und Datenstruk­
turen. Objekte verbergen ihre Daten hinter Abstraktionen und enthüllen Funktio­
nen, die mit diesen Daten arbeiten. Datenstrukturen enthüllen ihre Daten und
haben keine Funktionen. Lesen Sie dies bitte noch einmal. Beachten Sie die kom­
plementäre Natur der beiden Definitionen. Sie sind praktisch Gegenteile. Dieser
Unterschied mag trivial erscheinen, aber er hat weitreichende Konsequenzen.
Betrachten Sie das prozedurale Shape-Beispiel (Geometrische-Form-Beispiel) in
Listing 6 .5 . Die Geomet ry-Klasse arbeitet mit drei Shape-Klassen (Form-Klassen).
Die Shape-Klassen sind einfache Datenstrukturen ohne Verhalten. Alle Verhaltens­
weisen befinden sich in der Geomet ry-Klasse.

Listing 6.5: Prozedurales Shape-Beispiel


publ i c cl a s s Square {
publ i c Poi nt top left ;
publ i c doubl e s i de ;
}

publ i c cl a s s Reetangl e {
publ i c Poi nt topleft ;
publ i c doubl e hei ght ;
publ i c doubl e wi dth ;
}

publ i c cl a s s Ci rcl e {
publ i c Poi nt cente r ;
publ i c doubl e radi u s ;
}

publ i c cl a s s Geomet ry {
publ i c fi nal doubl e PI = 3 . 14159 2 6 5 3 589793 ;

publ i c doubl e a r e a (Obj e ct s hape) t h r ows NoSuchShape Excepti o n


{
i f ( s hape i n stanceof Square) {
Square s = (Square) s hape ;
retu r n s . si d e * s . si de ;
}

131
Kapitel 6
Objekte und Datenstrukturen

e l s e i f (s hape i n staneeof Reetang l e ) {


Reetangl e r = ( Reetangl e ) s hape ;
ret u rn r . hei ght * r . wi dth ;
}
e l se i f (s hape i n staneeof Ci r e l e) {
Ci rel e e = (Ci rel e ) s h ape ;
ret u rn PI * e . radi u s * e . radi u s ;
}
th row n ew NoS u eh S hapeExeept i o n () ;
}
}

Objektorientierte Programmierer würden wahrscheinlich die Nase rümpfen und


sich beschweren, dass dies prozedural wäre - und sie hätten recht. Aber vielleicht
ist das Naserümpfen nicht ganz berechtigt. Was würde passieren, wenn Geome t ry
um eine p e r i me t e r ( ) -Funktion ( Umfangsberechnung) erweitert würde? Die
Shape-Klassen wären davon nicht betroffen! Alle anderen Klassen, die von den
Shapes abhängen, wären ebenfalls nicht betroffen! Wenn ich andererseits ein neues
Shape hinzufügen wollte, müsste ich alle Funktionen in Geometry ändern, um das
neue Shape zu verarbeiten. Bitte lesen Sie auch diese Aussage noch einmal. Die bei­
den Bedingungen sind diametral entgegengesetzt.
Betrachten Sie jetzt die objektorientierte Lösung in Listing 6.6. Hier ist die a r ea ( ) ­
Methode polymorph. Es ist keine Geomet ry-Klasse erforderlich. Wenn ich jetzt ein
neues Shape hinzufügen wollte, wäre keine der vorhandenen Funktionen betroffen,
aber wenn ich eine neue Funktion hinzufügen wollte, müssten alle Shapes geändert
werden! (Es gibt Methoden, dieses Problem zu umgehen, die erfahrenen objekt­
orientierten Programmierern wohlvertraut sind, zum Beispiel Visitor oder Dual­
Dispatch. Aber diese Techniken sind selbst mit Kosten verbunden und kehren im
Allgemeinen zu der Struktur des prozeduralen Programms zurück.)

Listing 6.6: Polymorphe Shapes


publ i e el ass S q u a re i mpl emen t s Shape {
p r i vate Poi nt topleft ;
p r i vate dou bl e s i de ;

publ i e dou bl e a re a ( ) {
ret u rn s i de * s i de ;
}
}

publ i e el ass Reetan g l e i mpl emen t s S hape {


p r i vate Poi n t topleft ;
p r i vate doubl e hei ght ;
p r i vate doubl e wi dth ;

publ i e doubl e area() {


retu rn hei ght * wi dt h ;
6. 3
Das Law of Demeter

}
}

publ i c cl a s s Ci rcl e i mp l ements Shape {


pri vate Poi nt cente r ;
p r i vate doub l e radi u s ;
publ i c fi n a l dou b l e PI = 3 . 141 5 9 2 6 5 3 589793 ;

publ i c dou b l e a r e a ( ) {
retu rn PI * radi u s * radi u s ;
}
}

Auch hier sehen wir die komplementäre Natur dieser beiden Definitionen; sie sind
praktisch Gegenteile! Dies enthüllt die fundamentale Dichotomie zwischen Objek­
ten und Datenstrukturen:
Prozeduraler Code (Code, der Datenstrukturen verwendet) macht es leicht,
neue Funktionen hinzuzufügen, ohne die vorhandenen Datenstrukturen zu
ändern. Dagegen macht es 00-Code leicht, neue Klassen hinzuzufügen, ohne
vorhandene Funktionen zu ändern.
Die komplementäre Aussage ist ebenfalls wahr:
Prozeduraler Code macht es schwer, neue Datenstrukturen hinzuzufügen,
weil alle Funktionen geändert werden müssen. 00-Code macht es schwer,
neue Funktionen hinzuzufügen, weil alle Klassen geändert werden müssen.
Also sind die Dinge, die für 00 schwer sind, für Prozeduren leicht; und die Dinge,
die für Prozeduren schwer sind, sind leicht für 00!
In jedem komplexen System treten Situationen auf, in denen wir neue Datentypen
und keine neuen Funktionen hinzufügen wollen. In diesen Fällen sind Objekte und
00 am besten geeignet. Es gibt jedoch auch Situationen, in denen wir neue Funk­
tionen und keine Datentypen hinzufügen wollen. In diesem Fall sind prozeduraler
Code und Datenstrukturen besser geeignet.
Gestandene Programmierer wissen, dass die Vorstellung, alles wäre ein Objekt, ein
Mythos ist. Manchmal wollen Sie wirklich mit einfachen Datenstrukturen und mit
Prozeduren arbeiten, die diese Strukturen manipulieren.

6.3 Das Law of Demeter


Es gibt eine bekannte Heuristik, die als Law of Demeter (LoD; Gesetz von Demeter;
http : //en . wi ki pedi a . o rg/wi ki /Law_of_Demete r; http : //de . wi ki pedi a . o rg
/wi ki /Gesetz_von_Demete r) bezeichnet wird, das besagt, dass ein Modul nichts
über das Innere der Objekte wissen sollte, die es manipuliert. Wie im letzten
Abschnitt gezeigt wurde, verbergen Objekte ihre Daten und enthüllen Operationen.

133
Kapitel 6
Objekte und Datenstrukturen

Das bedeutet, dass ein Objekt seine interne Struktur nicht durch Aceesseren ent­
hüllen sollte; denn dadurch wird seine interne Struktur enthüllt, nicht verborgen.
Genauer formuliert, sagt das Law of Demeter, dass eine Methode feiner Klasse C
nur Methoden der folgenden Komponenten aufrufen sollte:
• C,
• ein Objekt, das von ferstellt wird,
• ein Objekt, das als Argument anfübergeben wird,
• ein Objekt, das in einer Instanzvariablen von C enthalten ist.
Die Methode sollte keine Methoden von Objekten aufrufen, die von einer der erlaub­
ten Funktionen zurückgegeben werden. Anders ausgedrückt: Sprich nur zu Freun­
den, nicht zu Fremden.
Der folgende Code (irgendwo aus dem Apache-Framework) scheint (unter ande­
rem) gegen das Law of Demeter zu verstoßen, weil er die getSc ratchDi r () -Funk­
tion des Rückgabewertes von getOpti ons () und dann getAbso 1 utePath () des
Rückgabewerts von getSc ratchDi r () aufruft.

fi nal Stri ng outputDi r = ctxt . getOpti on s () . getSc ratchDi r () . getAbsol utePath () ;

Zugkatastrophe

Diese Art von Code wird oft als Train Wreck (Zugkatastrophe) bezeichnet, weil er wie
eine Reihe zusammengekoppelter Eisenbahnwagen aussieht. Derartige Aufrufket­
ten gelten im Allgemeinen als nachlässiger Stil und sollten vermieden werden
[G36]. Es ist normalerweise üblich, sie wie folgt zu zerlegen:

Opti o n s opts = ctxt . getOpti on s () ;


Fi l e sc ratchDi r =opts . getScratchDi r () ;
fi nal St ri ng o utputDi r =s c ratchDi r . getAbsol utePath () ;

Verstoßen diese beiden Code-Ausschnitte gegen das Law of Demeter? Sicher weiß
das enthaltende Modul, dass das c tx t-Objekt Optionen enthält, die ein Scratch­
Directory (Arbeitsverzeichnis) enthalten, das über einen absoluten Pfad verfügt.
Das ist sehr viel Wissen für eine Funktion. Die aufrufende Funktion weiß, wie sie
zu vielen verschiedenen Objekten navigieren kann.
Ob dies ein Verstoß gegen das Law of Demeter ist, hängt davon ab, ob ctxt, Op­
t i ons and Sc ratchDi r Objekte oder Datenstrukturen sind. Sind sie Objekte,
dann sollte ihre interne Struktur nicht enthüllt, sondern verborgen werden; dann
wäre die Kenntnis ihrer inneren Struktur ein klarer Verstoß gegen das Law ofDeme­
ter. Sind ctxt, Opti ons und Sc ratchDi r dagegen einfach nur Datenstrukturen
ohne Verhalten, dann enthüllen sie natürlicherweise ihre interne Struktur; in die­
sem Fall wäre das Law of Demeter nicht anwendbar.

1 34
6. 3
Das Law of Demeter

Die Verwendung von Accessor-Funktionen verwirrt. Wäre der Code wie folgt
geschrieben worden, wäre die Frage nach einem Verstoß gegen das Law ot Demeter
wahrscheinlich nicht aufgekommen:

fi n a l S t ri ng outputDi r = ctxt . opti o n s . sc r atchDi r . absol utePath ;

Das Problem wäre sehr viel weniger verwirrend, hätten Datenstrukturen einfach
öffentliche Variablen und keine Funktionen und Objekte private Variablen und
öffentliche Funktionen. Doch es gibt Frameworks und Standards (zum Beispiel
» Beans«) , die verlangen, dass selbst einfache Datenstrukturen Accessoren und
Mutatoren haben sollen.

Hybride

Diese Verwirrung führt manchmal zu unglücklichen hybriden Strukturen, die halb


Objekt und halb Datenstruktur sind. Sie haben Funktionen, die wichtige Aufgaben
erfüllen, und sie haben auch entweder öffentliche Variablen oder öffentliche Acces­
soren und Mutatoren, die die privaten Variablen praktisch enthüllen und andere
externe Funktionen dazu verleiten, diese Variablen so zu verwenden, wie ein pro­
zedurales Programm eine Datenstruktur nutzen würde. Dies wird manchmal als
Feature Envy ([Refactoring]; »Funktionsneid«) bezeichnet.
Solche hybriden Strukturen erschweren sowohl das Hinzufügen neuer Funktionen
als auch das Hinzufügen neuer Datenstrukturen. Sie repräsentieren das
Schlimmste aus beiden Welten. Sie sollten vermeiden, solche Strukturen zu erstel­
len. Sie sind ein Anzeichen für unsauberes Design, dessen Autoren sich nicht sicher
sind - oder schlimmer: nicht wissen -, ob sie Schutz vor Funktionen oder Typen
brauchen.

Stru ktur verbergen

Was wäre, wenn ctxt, opt i on s und s c ratchDi r Objekte mit echten Verhaltens­
weisen wären? Weil Objekte ihre interne Struktur (hoffentlich) verbergen, sollten
wir dann nicht in der Lage sein, durch die Objekte zu navigieren. Wie würden wir
dann den absoluten Pfad des Scratch-Verzeichnisses bekommen?

ctxt . getAbso l ute PathOfSc ratchDi recto ryOpti o n () ;

oder

ctx . getScratchDi recto ryOpti o n () . getAbso l utePat h ()

Die erste Option könnte zu einer Explosion von Methoden in dem ctxt-Objekt füh­
ren. Die zweite Option nimmt an, dass getSc rat chDi rectoryüpti on () eine
Datenstruktur und kein Objekt zurückgibt. Keine der beiden Optionen schaut opti­
mal aus.

1 35
Kapitel 6
Objekte und Datenstrukturen

Wenn ctxt ein Objekt ist, sollten wir es anweisen, etwas zu tun; wir sollten es nicht
nach internen Daten befragen. Warum brauchen wir denn den absoluten Pfad des
Scratch-Verzeichnisses? Was wollen wir damit tun? Betrachten Sie den folgenden
Code aus demselben Modul (viele Zeilen weiter unten) :

S t r i ng outFi l e = outputDi r + "/" + cl assName . repl ace ( ' . ' , ' / ' ) + " . cl as s " ;
Fi l eOutpu tSt ream fou t = new Fi l eOutputSt ream (outFi l e) ;
B u ffe redOutputSt ream bos = new Buffe redOutputSt ream ( fout) ;

Die Mischung der verschiedenen Detailebenen [G34J [G6] ist etwas besorgniserre­
gend. Dots, Slashes, Dateierweiterungen und Fi l e-Objekte sollten nicht so sorglos
miteinander und mit dem einschließenden Code vermengt werden. Lassen wir dies
j edoch beiseite, sehen wir, dass der absolute Pfad des Scratch-Verzeichnisses abge­
rufen wurde, um eine Scratch-Datei (Arbeitsdatei) eines bestimmten Namens zu
erstellen.
Und wenn wir nun das ctxt-Objekt anwiesen, dies zu tun?

B u ffe redOutputStream bos = ctxt . c reateSc ratch Fi l eSt ream (cl a s s Fi l eName) ;

Das sieht wie eine vernünftige Aufgabe für ein Objekt aus! Damit kann ctxt seine
interne Struktur verbergen; und die gegenwärtige Funktion muss nicht gegen das
Law of Demeter verstoßen, indem sie zu Objekten navigiert, von denen sie gar
nichts wissen sollte.

6.4 Datentransfer-Objekte
Die idealtypische Form einer Datenstruktur ist eine Klasse mit öffentlichen Variab­
len und keinen Funktionen. Sie wird manchmal als Datentransfer-Objekt (DTO; engl.
Data Transfer Object) bezeichnet. DTOs sind sehr nützliche Strukturen, besonders
für die Kommunikation mit Datenbanken oder beim Parsen von Nachrichten von
Sockets usw. Sie bilden oft die erste Stufe einer Reihe von Ü bersetzungsstufen, die
rohe Daten aus einer Datenbank in Objekte im Anwendungscode übersetzen.
Etwas vertrauter ist die »Bean«-Form (siehe Listing 6.7) . Beans haben private Vari­
ablen, die von Gettern und Settern manipuliert werden. Die Quasi-Einkapselung bei
Beans scheint einigen 00-Puristen ein besseres Gefühl zu vermitteln, aber norma­
lerweise bietet sie keine anderen Vorteile.

Listing 6.7: addres s . j ava


publ i c c l ass Add ress {
p r i vate S t r i ng s t reet ;
p r i vate S t r i ng s t reetExt ra ;
p r i vate S t r i ng c i ty ;
p r i vate S t r i ng state ;
p r i vate S t r i ng zi p ;
6 -4
Datentransfer-Objekte

p u b l i c Add r e s s ( St ri ng s t reet , St ri ng s t reet Ext ra ,


St ri ng c i ty , St r i ng state , S t ri ng z i p) {
t h i s . st reet = s t reet ;
t h i s . st reetExt ra = st reetExt r a ;
t h i s . ci ty = c i ty ;
t h i s . state = state ;
t h i s . zi p = zi p ;
}

pu b l i c St ri ng getSt reet () {
retu rn s t reet ;
}

publ i c St ri ng getSt reetExt r a () {


retu rn s t reet Ext r a ;
}

pu b l i c St ri ng getCi t y () {
retu rn ci t y ;
}

publ i c St ri ng getState () {
ret u r n state ;
}

publ i c St r i ng getZi p ( ) {
retu rn zi p ;
}
}

Active Record

Active Records sind eine spezielle Form von DTOs. Sie sind Datenstrukturen mit
öffentlichen (oder mit Bean-Methoden zugänglichen) Variablen. Sie verfügen aber
üblicherweise auch über Navigationsmethoden wie save oder fi nd. Typischerweise
sind diese Active Records direkte Ü bersetzungen von Datenbanktabellen oder ande­
ren Datenquellen.
Leider stellen wir oft fest, dass Entwickler versuchen, diese Datenstrukturen wie
Objekte zu behandeln, indem sie Methoden mit Geschäftsregeln in sie einfügen.
Dies ist gefährlich, weil dadurch ein Hybrid aus einer Datenstruktur und einem
Objekt entsteht.
Die Lösung besteht natürlich darin, das Active Record als Datenstruktur zu behan­
deln und die Geschäftsregeln in separate Objekte zu packen, die ihre internen Daten
verbergen (die wahrscheinlich nur Instanzen des Active Records sind) .

1 37
Kapitel 6
Objekte und Datenstrukturen

6. 5 Zusammenfassung
Objekte enthüllen Verhaltensweisen und verbergen Daten. Dadurch können leicht
neue Arten von Objekten hinzugefügt werden, ohne vorhandene Verhaltensweisen
zu ändern. Dadurch wird es aber auch schwerer, neue Verhaltensweisen zu vorhan­
denen Objekten hinzuzufügen. Datenstrukturen enthüllen Daten und haben kein
wesentliches Verhalten. Dadurch können leicht neue Verhaltensweisen zu vorhan­
denen Datenstrukturen hinzugefügt werden. Dadurch wird es aber auch schwerer,
neue Datenstrukturen zu vorhandenen Funktionen hinzuzufügen.
In jedem System wünschen wir manchmal die Flexibilität, neue Datentypen hin­
zufügen, und bevorzugen deshalb in diesem Teil des Systems Objekte. Manchmal
wünschen wir jedoch auch die Flexibilität, neue Verhaltensweisen hinzuzufügen,
und bevorzugen deshalb in diesem Teil des Systems Datentypen und Prozeduren.
Gute Software-Entwickler betrachten diese Probleme unvoreingenommen und
wählen den Ansatz, der für die anstehende Aufgabe am besten geeignet ist.
Kapitel 7

Feh ler- H and l i ng

von Michael Feathers

Vielleicht scheint es etwas merkwürdig zu sein, in einem Buch über sauberen Code
ein Kapitel über das Fehler-Handling zu bringen. Fehler-Handling gehört zu den
ungeliebten, aber notwendigen Aufgaben, die wir beim Programmieren erledigen
müssen. Der Input kann das falsche Format haben, Geräte können ausfallen. Kurz
gesagt: Dinge können schiefgehen, und wenn sie dies tun, müssen wir als Program­
mierer dafür sorgen, dass unser Code tut, was er tun muss.
Die Verbindung mit sauberem Code sollte jedoch klar sein. Viele Code-Basen wer­
den vollkommen vom Fehler-Handling dominiert. Wenn ich sage >>dominiert«, will
ich nicht sagen, dass sie sich nur mit dem Fehler-Handling befassen, sondern dass
es fast unmöglich ist, zu erkennen, was der Code tut, weil seine Intention durch die
überall verstreuten Anweisungen für das Fehler-Handling verdeckt wird. Fehler­
Handling ist wichtig, aber wenn es die Logik verschleiert, ist es falsch.
In diesem Kapitel gebe ich einen ü berblick über mehrere Techniken und Überle­
gungen, mit deren Hilfe Sie Code schreiben können, der sowohl sauber als auch
robust ist - Code, der Fehler elegant und stilvoll handhabt.

7.1 Ausnahmen statt Rückgabe-Codes


Ganz früher kannten viele Programmiersprachen keine Ausnahmen. In diesen
Sprachen waren die Techniken für die Behandlung und Meldung von Fehlern

1 39
Kapitel 7
Fehler-Handling

beschränkt Man setzte entweder ein Fehler-Flag oder gab einen Fehler-Code
zurück, den der Aufrufer prüfen konnte. Der Code in Listing 7-I illustriert diese
Ansätze.

Listing 7.1 : Devi ceCont rol l e r . j ava


publ i c c l a s s Devi ceCo n t ro l l e r {

publ i c voi d sendShutDown () {


Devi ceHand l e hand l e = getHan d l e (DEVl) ;
I I Check the state of t h e devi c e
i f (handl e ! = Devi ceHandl e . INVALID) {
II Save the devi c e status to t h e record fi e l d
retri eveDevi ceReco rd (handl e) ;
II If not s u spended , s h u t down
i f ( record . getStatu s () ! = DEVICE_SUSPENDED) {
paus eDevi c e ( handl e) ;
c l ea rDevi c eWo r kQueue ( hand l e) ;
c l o s eDevi c e (handl e) ;
} e 1 se {
l ogger . l og ( " Devi c e suspended . U n abl e to s h u t down " ) ;
}
} e 1 se {
l ogger . l og ( " I nval i d hand l e fo r : " + DEVl . toStri ng () ) ;
}
}

Diese Ansätze haben ein Problem: Sie überhäufen den Aufrufer mit logikfremdem
Code. Der Aufrufer muss den Fehlerstatus unmittelbar nach dem Aufruf prüfen.
Leider kann man dies leicht vergessen. Aus diesem Grund ist es besser, eine Aus­
nahme auszulösen, wenn ein Fehler auftritt. Der aufrufende Code ist sauberer. Die
Logik wird nicht durch das Fehler-Handling verschleiert.
Listing 7.2 zeigt den Code, wenn in Methoden, die Fehler entdecken können, Aus­
nahmen ausgelöst werden.

Listing 7.2: Devi ceControl l er . j ava (mit Ausnahmen)


publ i c c l a s s Devi ceCon t ro l l e r {

p u bl i c voi d sendShutDown () {
try {
t r yToShutDown () ;
} catch (Devi ceShutDown E r ro r e) {
l ogger . l og (e) ;
}
}
] .2
Try-Catch- Finally-Anweisungen zuerst schreiben

p r i vate voi d t r yToShutDown () th rows Devi ceShutDown E r ro r {


Devi ceHand l e handl e = getHand l e (DEVl) ;
Devi ceRecord record = retri eveDevi ceRecord (handl e) ;

pauseDevi ce (handl e) ;
c l e a rDevi ceWo r kQu e u e ( hand l e) ;
c l os eDevi ce (hand l e) ;
}

p r i vate Devi ceHandl e getHand l e (Devi ceiD i d) {

t h row new Devi ceShutDown E r ro r ( " Inval i d handl e fo r : " + i d . toStri n g ( ) ) ;

Beachten Sie, wie viel sauberer dieser Code ist. Dies ist nicht nur eine Frage der
Ästhetik. Der Code ist besser, weil zwei miteinander vermengte Concerns (Belange) ,
nämlich der Algorithmus für das Geräte-Shutdown und das Fehler-Handling, jetzt
getrennt sind. Sie können jeden Cancern unabhängig vom anderen studieren und
verstehen.

7.2 Try-Catch-Finally-Anweisu ngen zuerst schreiben


Einer der interessantesten Aspekte von Ausnahmen ist die Tatsache, dass sie einen
Geltungsbereich innerhalb Ihres Programms definieren. Wenn Sie Code in dem t ry­
Abschnitt einer t ry-catc h-fi na 1 1 y-Anweisung ausführen, bringen Sie zum Aus­
druck, dass das Programm an jedem Punkt abgebrochen und dann mit dem catch­
Abschnitt fortgesetzt werden kann.
In gewisser Weise ähneln t ry-Blöcke Transaktionen. Ihr catch muss Ihr Pro­
gramm in einem konsistenten Zustand zurücklassen, etwas, was in dem t ry pas­
siert. Aus diesem Grund ist es sinnvoll, mit einer t ry-catch-fi na 1 1 y-Anweisung
zu beginnen, wenn Sie Code schreiben, der Ausnahmen auslösen könnte. Dies hilft
Ihnen zu definieren, was der Benutzer dieses Codes erwarten sollte, egal was in dem
Code schiefgeht, der in dem t ry ausgeführt wird.
Ein Beispiel: Wir müssen Code schreiben, der auf eine Datei zugreift und einige
serialisierte Objekte liest.
Wir beginnen mit einem Unit-Test, der zeigt, dass eine Ausnahme ausgelöst wird,
wenn die Datei nicht existiert:

@Te s t (expected =
Sto rageExcepti o n . c l as s )
publ i c voi d retri eve Secti onShoul dTh rowOninval i d Fi l eName () {
Kapitel 7
Fehler-Handl ing

s e ct i o n St o re . re t r i eveSecti on ( " i nval i d - fi l e " ) ;


}

Der Test veranlasst uns, den folgenden Stub zu erstellen:

publ i c Li s t<Reco rdedG ri p> ret r i eve Secti o n (St ri ng secti onName) {
II Dummy- Rü c kgabe , bi s wi r ei n e echte Impl ement i e rung h aben
retu rn new Ar rayli s t<Reco rdedG r i p> () ;
}

Unser Test scheitert, weil er keine Ausnahme auslöst_ Als Nächstes ändern wir
unsere Implementierung so, dass sie versucht, auf eine ungültige Datei zuzugrei­
fen. Diese Operation löst eine Ausnahme aus:

p u b l i c Li s t<Reco rdedG ri p> retri eveSecti o n (St ri ng secti onName) {


try {
Fi l einputSt ream s t ream =
new Fi l ei n p u t S t ream (secti onName)
} catch ( Excepti on e) {
t h row new Sto rageExcept i on ( " retri eval e r ro r " , e) ;
}
retu rn n ew Ar rayli st<Reco rdedG r i p> () ;
}

Jetzt besteht unser Test, weil wir die Ausnahme abgefangen haben. An diesem
Punkt können wir das Refactoring ansetzen. Wir können den Typ der Ausnahme
eingrenzen, damit er genau dem Typ von Ausnahme entspricht, die tatsächlich von
dem Fi 1 e i n putSt ream-Konstruktor ausgelöst wird: F i 1 eNotFo u n d Excepti on:

publ i c Li s t<RecordedG ri p> ret r i eveSect i o n (St ri ng secti onName) {


try {
Fi l ei n putSt ream s t re am =
n ew Fi l e i n p u tS t ream (secti onName) ;
s t ream . cl os e () ;
} catch ( Fi l eNot Fou nd Except i o n e) {
t h row n ew Sto rageExcept i on ( " retri eval e r ro r " , e) ;
}
retu rn n ew Ar rayli s t<RecordedG r i p>() ;
}

Da wir jetzt den Geltungsbereich mit einer t ry-catch-Struktur definiert haben,


können wir per TD D den Rest der erforderlichen Logik aufbauen. Diese Logik wird
zwischen der Erstellung von F i l einputSt ream und dem c l ose eingefügt und
kann so tun, als könne nichts schiefgehen.
Versuchen Sie, Tests zu schreiben, die Ausnahmen erzwingen. Fügen Sie dann Ver­
haltensweisen zu Ihrem Handler hinzu, um Ihre Testbedingungen zu erfüllen.
Diese Vorgehensweise bewirkt, dass Sie zuerst den Transaktionsgeltungsbereich
des t ry-Blocks erstellen, und hilft Ihnen, den Transaktionscharakter dieses Gel­
tungsbereiches zu erhalten.
7 ·3
U nchecked Exceptions

7·3 Unchecked Exceptions


Die Debatte ist vorbei. Jahrlang haben Java-Programmierer über die Vor- und Nach­
teile von Checked Exceptions diskutiert. Als Checked Exceptions in der ersten Ver­
sion von Java eingeführt wurden, schien dies eine großartige Idee zu sein. Die
Signatur jeder Methode würde alle Ausnahmen auflisten, die sie an ihren Aufrufer
übergeben könnte. Darüber hinaus gehörten diese Ausnahmen zum Typ der
Methode. Ihr Code ließ sich buchstäblich nicht kompilieren, wenn die Signatur
nicht mit dem übereinstimmte, was Ihr Code tun konnte.
Damals dachten wir, dass Checked Exceptions eine großartige Idee wären; und ja,
sie können einige Vorteile bieten. Doch heute wissen wir, dass sie nicht erforderlich
sind, um robuste Software zu produzieren. C# hat keine Checked Exceptions, und
trotz beherzter Versuche hat C++ auch keine. Dasselbe gilt für Python oder Ruby.
Dennoch kann man in allen diesen Sprachen robuste Software schreiben. Weil dies
der Fall ist, müssen wir - ernsthaft - überlegen, ob Checked Exceptions ihren Preis
wert sind.
Welcher Preis? Der Preis von Checked Exceptions ist ein Verstoßen gegen das Open­
Closed-Prinzip [Martin]. Wenn Sie eine Checked Exception von einer Methode in
Ihrem Code auslösen und der ca tch drei Ebenen höher erfolgt, müssen Sie diese Aus­
nahme in der Signatur aller Methoden deklarieren, die zwischen Ihnen und dem ca tch
liegen. Dies bedeutet, dass eine Änderung auf einer niedrigen Ebene der Software
Signaturänderungen auf vielen höheren Ebenen erforderlich machen kann. Die
geänderten Module müssen neu erstellt und verteilt werden, selbst wenn an ihren
Funktionen nichts geändert wurde.
Betrachten Sie die Aufruf-Hierarchie eines großen Systems. Funktionen an der
Spitze rufen untergeordnete Funktionen auf, die ihrerseits ihnen untergeordnete
Funktionen aufrufen usw. Nehmen wir jetzt an, dass eine der Funktionen auf der
untersten Ebene so modifiziert werde, dass sie eine Ausnahme auslösen muss.
Wenn es sich um eine Checked Exception handelt, muss die Funktionssignatur um
eine th rows-Klausel ergänzt werden. Doch dies bedeutet, dass jede Funktion, die
unsere modifizierte Funktion aufruft, ebenfalls modifiziert werden muss, um die
neue Ausnahme entweder abzufangen oder die entsprechende th rows-Klausel an
ihre Signatur anzuhängen. Bis hin zur Spitze der Hierarchie. Netto ist eine Kaskade
von Änderungen erforderlich, die von den unteren Ebenen der Software bis zu den
höchsten reicht! Die Einkapselung ist defekt, weil alle Funktionen auf dem Pfad
einer t h row-Klausel die Details dieser Low-Level-Ausnahme kennen müssen.
Bedenkt man, dass der Zweck von Ausnahmen darin besteht, Fehler separat behan­
deln zu können, ist es eine Schande, dass Checked Exceptions die Einkapselung in
dieser Weise durchbrechen.
Checked Exceptions können manchmal nützlich sein, wenn Sie eine kritische
Library schreiben: Sie müssen sie abfangen. Aber bei der allgemeinen Anwen­
dungsentwicklung überwiegen die Kosten der Abhängigkeit die Vorteile.

1 43
Kapitel ]
Fehler- H andling

7 ·4 Ausnahmen mit Kontext auslösen


Jede ausgelöste Ausnahme sollte genügend Kontext liefern, um die Quelle und den
Ort eines Fehlers zu bestimmen. In Java können Sie für jede Ausnahme ein Stack­
Trace abrufen; doch ein Stack-Trace kann Ihnen nicht den Zweck der gescheiterten
Operation mitteilen.
Erstellen Sie informative Fehlermeldungen und übergeben Sie diese zusammen
mit Ihren Ausnahmen. Erwähnen Sie die gescheiterte Operation sowie den Typ des
Scheiterns. Wenn Sie sich bei Ihrer Anwendung einloggen, übergeben Sie genü­
gend Informationen, damit Sie den Fehler in Ihrem catch protokollieren können.

7·5 Defin ieren Sie Exception-Kiassen mit Bl ick auf die


Anforderu ngen des Aufrufers
Es gibt viele Möglichkeiten, Fehler zu klassifizieren. Wir können Sie nach ihren
Quellen unterscheiden: Kamen sie aus der einen Komponente oder einer anderen?
Oder nach ihrem Typ: Sind es Gerätefehler, Netzwerkfehler oder Programmierfeh­
ler? Doch wenn wir Exception-Klassen in einer Anwendung definieren, sollten wir
uns vor allem darum kümmern, wie sie abgefangen werden.
Betrachten wir ein Beispiel für eine suboptimale Ausnahmeklassifikation. Hier ist
eine t ry-cat ch-fi na l l y-Anweisung für den Aufrufeiner Drittanbieter-Library. Sie
deckt alle Ausnahmen ab, die von den Aufrufen ausgelöst werden können:

ACMEPort port = n ew ACMEPo r t (12) ;

t ry {
port . open () ;
} catch (Devi c eRespo n s e Excepti o n e) {
repo rtPort E r ro r (e) ;
l ogge r . l og ( " Devi ce response excepti on " , e) ;
} catch (ATM1212Unl ockedExcept i on e) {
repo rtPort E r ro r (e) ;
l ogge r . l og ( " U n l ock excepti o n " , e) ;
} catch (GMXE r r o r e) {
repo rtPort E r ro r (e) ;
l ogge r . l og ( " Devi ce response excepti on " ) ;
} fi nal l y {

In dieser Anweisung wird viel Code dupliziert, und wir sollten nicht überrascht sein.
Die meisten Ausnahmen werden, unabhängig von der tatsächlichen Ursache, rela­
tiv standardmäßig behandelt. Wir müssen einen Fehler registrieren und dafür sor­
gen, dass wir weiterarbeiten können.

1 44
75
Definieren Sie Exception-Klassen mit Blick au� die Anforderungen des Aufrufers

Weil wir wissen, dass die zu leistende Arbeit unabhängig von der Ausnahme im
Wesentlichen dieselbe ist, können wir in diesem Fall unseren Code erheblich ver­
einfachen, indem wir das aufgerufene API einhüllen und dafür sorgen, dass es
einen gemeinsamen Ausnahmetyp zurückgibt:

Local Port port =


new Loca1 Po rt ( 12 ) ;
t ry {
port . op e n ( ) ;
} catch (Po rtDevi c e Fai l u re e) {
repo rt E r ro r (e) ;
l ogge r . l og (e . getMe s s age ( ) , e) ;
} fi nal l y {

Unsere Local P o r t -Klasse ist einfach nur ein Wrapper (eine Umhüllung), der die
von der ACMEPo rt-Klasse ausgelösten Ausnahmen abfangt und übersetzt:

publ i c cl ass Local Port {


p r i vate ACMEPo rt i n n e rPort ;

publ i c Local Po rt ( i n t portNumbe r) {


i n n e rPort = new ACMEPo rt (po rtNumbe r) ;
}

publ i c voi d open () {


t ry {
i n n e rPort . o pe n () ;
} catch (Devi ceRe s po n s e Except i o n e) {
th row new Po rtDevi c e Fai l u re (e) ;
} catch (ATM1212Unl ocked Excep t i o n e) {
t h r ow new Po rtDevi ce Fai l u re (e) ;
} catch (GMX E r ro r e) {
th row new Po rtDevi ce Fai l u re (e) ;
}
}

Wrapper wie der hier für ACMEPort definierte können sehr nützlich sein. Tatsäch­
lich zählt das Einhüllen von Drittanbieter-APis zu den Best Practices. Wenn Sie ein
Drittanbieter-API einhüllen, minimieren Sie Ihre Abhängigkeiten von dem API: Sie
können ohne größere Schwierigkeiten in der Zukunft auf eine andere Library
umsteigen. Das Einhüllen erleichtert es auch, Drittanbieter-Aufrufe zu simulieren,
wenn Sie eigenen Code testen.
Ein letzter Vorteil des Einhüllens liegt darin, dass Sie nicht an die API-Design-Ent­
scheidungen eines speziellen Anbieters gebunden sind. Sie können ein API defi-

1 45
Kapitel 7
Fehler-Handling

nieren, das für Sie nützlich ist In dem vorangegangenen Beispiel haben wir einen
einzigen Ausnahmetyp für po rt-Device-Fehler definiert und festgestellt, dass wir
viel saubereren Code schreiben konnten.
Oft reicht eine einzige Exception-Klasse für einen speziellen Bereich des Codes aus.
Die Informationen, die mit der Ausnahme gesendet werden, können die Fehler
unterscheiden. Verwenden Sie andere Klassen nur, wenn Sie gewisse Ausnahmen
abfangen und andere passieren lassen wollen.

7.6 Den normalen Ablauf definieren


Wenn Sie die Ratschläge aus den vorhergehenden Abschnitten befolgen, erreichen
Sie eine gute Trennung zwischen Ihrer Geschäftslogik und Ihrem Fehler-Handling.
Der Hauptteil Ihres Codes sieht einem schnörkellosen Algorithmus ähnlicher.
Doch damit verdrängen Sie die Fehlererkennung an den Rand Ihres Programms.
Sie hüllen externe APis ein, damit Sie eigene Ausnahmen auslösen können, und Sie
definieren einen Handler außerhalb Ihres normalen Codes, damit Sie abgebro­
chene Berechnungen handhaben können. Meistens ist dies ein passender Ansatz,
aber manchmal wollen Sie das Programm nicht abbrechen.
Ein BeispieL Der folgende Code addiert die Ausgaben in einer Abrechnungsanwen­
dung recht umständlich:

t ry {
Mea1 Expenses expe n s e s = expenseReportDAO . getMea1 s (emp1 oyee . getiD() ) ;
m_tota1 += expense s . getTota1 () ;
} catch (Mea1 Expe n s e sNotFound e) {
m_tota1 += getMea1 P e rDi em() ;
}

Hat der Mitarbeiter Ausgaben für Mahlzeiten eingereicht, gehen diese in die Aus­
gabensumme ein, andernfalls wird ein Tagessatz (per diem amount) für diesen Tag
angerechnet Die Ausnahme verschleiert die Logik Wäre es nicht besser, wenn wir
den Sonderfall nicht behandeln müssten? Denn dann würde unser Code viel ein­
facher aussehen, nämlich so:

Mea1 Expenses expe n s e s = expenseRepo r tDAO . getMea1 s (emp 1 oyee . getiD() ) ;


m_tota1 += expenses . getTota1 ( ) ;

Glücklicherweise können wir den Code entsprechend vereinfachen. Wir können


Expens eRepo rtDAO so ändern, dass immer ein Mea 1 E xpense - Obj ekt zurückgege­
ben wird. Wurden keine Ausgaben für Mahlzeiten abgerechnet, wird ein Mea 1 E x ­
pens e-Objekt zurückgegeben, das den Tagessatz als Summe zurückgibt:

pub1 i c c1 ass Pe rDi emMea1 Expe n ses i mp 1 ements Mea1 Expe n s e s {


pub1 i c i nt getTota1 () {
7 ·7
Keine N ull zu rückgeben

II den standa rdmäßi gen Tagessat z zu rückgeben


}
}

Dies wird als das Special Case Pattern [Fowler] (Sonderfall-Pattern) bezeichnet. Sie
erstellen eine Klasse oder konfigurieren ein Objekt so, dass es einen Sonderfall für
Sie handhabt. Dann muss sich der Client-Code nicht mit dem Ausnahmeverhalten
befassen. Die Verhaltensweise ist in dem Sonderfall-Obj ekt eingekapselt.

7·7 Keine N ull zurückgeben


Jede Beschreibung des Fehler-Handlings muss auch Dinge erwähnen, die Fehler
geradezu einladen. Das erste auf meiner Liste ist die Rückgabe von n u l l . Ich weiß
nicht, wie viele Anwendungen ich gesehen habe, in denen fast jede zweite Zeile eine
Prüfung auf n u l l enthielt. Ein Beispiel:

publ i c voi d regi s t e r!tem (Item i tem) {


i f (i tem ! = n u l l ) {
ItemRegi s t ry regi s t ry = peri stentSto r e . getitemReg i s t ry () ;
i f ( regi s t ry ! = n u l l ) {
Item exi sti ng = regi s t ry . getitem ( i tem . ge t i D () ) ;
i f (exi s t i ng . getBi l l i ng Pe r i od () . has Retai l Owne r ( ) ) {
exi sti ng . regi s te r (i tem) ;
}
}
}
}

Wenn Sie mit einer Code-Basis arbeiten, die derartigen Code enthält, sieht dies für
Sie möglicherweise nicht so schlecht aus, wie es tatsächlich ist! Wenn wir n u l l
zurückgeben, schaffen wir im Wesentlichen Arbeit für uns selbst und wälzen Pro­
bleme auf unsere Aufrufer ab. Man muss nur die n u l l -Prüfung vergessen, und
schon läuft eine Anwendung aus dem Ruder.
Haben Sie bemerkt, dass in der zweiten Zeile der ersten verschachtelten i f-Anwei­
sung eine n u l l -Prüfung fehlt? Was würde zur Laufzeit passieren, wenn pe r s i s ­
tentSto re den Wert n u l l hätte? Wir würden eine N u l l P o i nte rExcepti on zur
Laufzeit auslösen. Entweder wird diese N u l l Poi nte r E xcepti on aufoberer Ebene
abgefangen oder aber auch nicht. Beides wäre schlecht. Was genau sollten Sie bei
einer N u l l Poi nte rExcepti on tun, die in den Tiefen Ihrer Anwendung ausgelöst
wird?
Es ist leicht zu sagen, dass der obige Code das Problem einer fehlenden nu l l - Prü­
fung hat. Doch tatsächlich liegt sein Problem darin, dass er zu viele Prüfungen dieser
Art enthält. Wenn Sie versucht sind, n u l l von einer Methode zurückzugeben, soll­
ten Sie alternativ erwägen, eine Ausnahme auszulösen oder stattdessen ein Special
Case-Obj ekt zurückzugeben. Wenn Sie eine Methode aus einem Drittanbieter-AP I

1 47
Kapitel 7
Fehler-Handling

aufrufen, die n u l l zurückgibt, sollten Sie überlegen, wie Sie diese Methode in eine
Methode einhüllen können, die entweder eine Ausnahme auslöst oder ein Special
Case-Objekt zurückgibt_
In vielen Fällen bieten Special Case-Objekte eine leichte Abhilfe. Angenommen, Sie
hätten Code wie diesen:

Li st<Empl oyee> empl oyees = getEmpl oyees () ;


i f (empl oyees ! = n u l l ) {
fo r ( Empl oyee e : empl oyees) {
total Pay += e . getPay ( ) ;
}
}

Im Moment kann getEmpl oyees den Wert n u l l zurückgeben, aber ist dies erfor­
derlich ? Wenn wir getEmp l oyees so ändern, dass die Funktion eine leere Liste
zurückgibt, können wir den Code aufräumen:

Li s t<Empl oyee> empl oyees = g e t Empl oyees () ;


fo r ( Empl oyee e : empl oyee s ) {
total Pay += e . getPay ( ) ;
}

Glücklicherweise verfügt Java über Co l l ecti ons . emptyl i s t () , die eine vordefi­
nierte i mmutab l e (unveränderbare) Liste zurückgibt, die wir für diesen Zweck ver­
wenden können:

publ i c Li st<Empl oyee> getEmp l oyees () {


i f ( . . kei n e Mi tarbe i t e r vo rhanden si nd . . )
ret u rn Col l ecti o n s . emptyli s t () ;
}

Wenn Sie so codieren, werden Sie die Gefahr von N u l l Poi nte rExce pti ons mini­
mieren, und Ihr Code wird sauberer sein.

7.8 Keine N ull übergeben


Den Wert n u l l von Methoden zurückzugeben, ist schlecht, aber nul l an Methoden
zu übergeben, schlechter. Wenn Sie nicht gerade mit einem API arbeiten, das von
Ihnen die Ü bergabe von n u l l erwartet, sollten Sie, falls möglich, nie den Wert n u l l
in Ihrem Code übergeben.
Ein Beispiel soll zeigen, warum. Hier ist eine einfache Methode, die eine Metrik für
zwei Punkte berechnet:

publ i c cl ass Met ri csCal c u l at o r


{
publ i c doub l e x P ro j ecti o n ( Poi nt pl , Poi nt p2) {
] .8
Kei ne N ul l übergeben

ret u rn (p2 . x - pl . x) * 1 . 5 ;
}

Was passiert, wenn jemand n u l l als Argument übergibt?

cal c u l ato r . x P roj ecti on ( n u l l , n ew Poi nt (12 , 13) ) ;

Natürlich bekommen wir eine N u l l Poi nte r E xcepti o n .


Wie können wir sie beheben? Wir könnten einen neuen Ausnahmetyp erstellen und
auslösen:

publ i c cl ass Met ri csCal c u l ator


{
publ i c doubl e x P roj ecti on ( Poi nt pl , Poi nt p2) {
i f (pl == n u l l I I p2 == n u l l ) {
th row I nval i dArg umentExcept i on (
" Inval i d a rgument fo r Met r i c s Ca l c u l ato r . x P roj ecti o n " ) ;
}
retu rn (p2 . x - pl . x) * 1 . 5 ;
}
}

I st dies besser? Vielleicht ein wenig besser als eine n u l l -Pointer-Ausnahme. Ver­
gessen Sie aber nicht, dass wir einen Handler für Inva l i dA rgume n t Ex c e pti on
definieren müssen. Was sollte der Handler tun? Gibt es eine brauchbare Vorgehens­
weise?
Es gibt eine weitere Alternative. Wir könnten einen Satz von Zusicherungen ver­
wenden:

publ i c cl ass Met ri csCal c u l ato r


{
publ i c doubl e x P roj ecti on ( Poi nt pl , Poi n t p2) {
a s s e r t pl ! = n u l l : "pl s h o u l d not be n u l l " ;
a s s e r t p2 ! = n u l l : " p 2 s h o u l d not be n u l l " ;
retu rn (p2 . x - pl . x) * 1 . 5 ;
}
}

Dies ist zwar eine gute Dokumentation, löst aber das Problem nicht. Wenn jemand
n u l l übergibt, erhalten wir immer noch einen Laufzeitfehler.
In den meisten Programmiersprachen gibt es keine gute Methode, mit einem Wert
n u l l umzugehen, der aus Versehen von einem Aufrufer übergeben wird. Deswe­
gen besteht die rationale Lösung darin, die Ü bergabe von nu l l standardmäßig zu
verbieten. Denn dann können Sie mit dem Wissen codieren, dass der Wert n u l l in

1 49
Kapitel 7
Fehler-H andling

einer Argumentenliste ein Problem anzeigt, und machen viel weniger Fehler aus
Unachtsamkeit

7·9 Zusammenfassu ng
Sauberer Code ist lesbar, aber er muss auch robust sein. Dies sind keine konkur­
rierenden Ziele. Wir können robusten sauberen Code schreiben, wenn wir das Feh­
ler-Handling als separaten Concern betrachten, der unabhängig von unserer
Hauptlogik behandelt werden muss. In dem Maße, wie uns dies gelingt, können wir
isoliert darüber nachdenken und die Wartbarkeit unseres Codes verbessern.

1 50
Kapitel 8

G renzen

von ]ames Grenning

Wir kontrollieren selten die gesamte Software in unseren Systemen. Manchmal


kaufen wir Drittanbieter-Packages oder verwenden Open-Source-Software. Manch­
mal hängen wir davon ab, dass andere Teams in unserem Unternehmen Kompo­
nenten oder Subsysteme für uns erstellen. I rgendwie müssen wir diesen fremden
Code sauber mit unserem eigenen integrieren. In diesem Kapitel geht es um Ver­
fahren und Techniken, um Grenzen unserer Software sauber zu halten.

8.1 M it Drittan bieter-Code arbeiten


Es gibt eine natürliche Spannung zwischen dem Lieferanten eines Interfaces und
dem Benutzer eines Interfaces. Lieferanten von Drittanbieter-Packages und -Frame­
works streben nach einer breiten Anwendbarkeit, damit sie in vielen Umgehungen
arbeiten und eine größere Zielgruppe ansprechen können. Dagegen wünschen sich
die Benutzer ein Interface, das aufihre speziellen Anforderungen zugeschnitten ist.
Kapitel S
G renzen

Diese Spannung kann an den Grenzen unserer Systeme zu Problemen führen.


Betrachten wir die Methoden j ava . u t i l . Map als Beispiel:

cl e a r () voi d - Map
contai n s Ke y (Obj ect key) bool ean - Map
contai n sVal u e (Obj ect val ue) boo l ean - Map
e n t rySet () Set - Map
e q u al s (Obj ect o) boo l ean - Map
get (Ob j e ct key) Obj ect - Map
getCl ass () Cl ass<? extends Obj ect> - Obj ect
h as hCod e () i nt - Map
i s Empty () bool ean - Map
keySe t ( ) Set - Map
n ot i fy ( ) voi d - Obj e ct
noti fyAl l () voi d - Obj e ct
p u t (Obj ect key , Obj e ct val ue) Object - Map
pu tAl l (Map t ) voi d - Map
remove (Ob j e ct key) Obj ect - Map
s i z e ( ) i nt - Map
toSt ri n g () St ri n g - Obj ec t
val ues () Col l ecti on - Map
wai t ( ) voi d - Obj ect
wai t ( l ong ti meout) voi d - Obj ect
wai t ( l ong t i meout , i nt nanos) voi d - Obj ec t

Diese Liste zeigt, dass Maps über ein umfangreiches Interface mit zahlreichen Funk­
tionen verfügen. Diese Leistungsstärke und Flexibilität sind sicher nützlich, können
aber auch eine Belastung sein. Angenommen, unsere Anwendung erstellte eine
Map und reichte sie herum. Wir wollen nicht, dass einer der Empfänger unserer Map
Einträge in der Map löscht. Aber direkt am Anfang der Liste steht die c l ear ( ) ­
Methode. Jeder Benutzer der Map kann ihren Inhalt löschen. Vielleicht sieht unser
Design auch vor, dass nur spezielle Typen von Objekten in der Map gespeichert wer­
den sollen; aber Maps schränken die Typen der Obj ekte, die in ihnen gespeichert
werden, nicht zuverlässig ein. Jeder entschlossene Benutzer kann in jeder Map Ele­
mente beliebigen Typs speichern.
Wenn unsere Anwendung eine Map von Sensoren benötigt, werden die Sensoren
möglicherweise wie folgt definiert:

Map s e n s o r s = n ew HashMap () ;

In einem anderen Teil des Codes wird dann möglicherweise wie folgt auf einen Sen­
sor zugegriffen:

S e n s o r s = (Senso r) s e n s o r s . get ( s e n s o rld ) ;

Dieses Konstrukt finden Sie an vielen Stellen des Codes. Der Client dieses Codes ist
dafür verantwortlich, ein Obj ect aus der Map abzurufen und per Cast in den rich-
8.1
M it Drittanbieter-Code arbeiten

tigen Typ umzuwandeln. Dies funktioniert, ist aber kein sauberer Code. Außerdem
erzählt der folgende Code seine Geschichte nicht so gut, wie er könnte. Die Lesbar­
keit dieses Codes kann durch Generies erheblich verbessert werden; etwa so:

Map<Sen s o r> s en s o r s = n ew HashMap<Se n s o r> () ;

S e n s o r s = s e n s o r s . get(senso r!d ) ;

Doch damit ist das Problem nicht gelöst, dass Map<Sensor> mehr Funktionalität
anbietet, als wir brauchen oder wollen.
Wenn wir eine Instanz von Map<Senso r> freigiebig im System herumreichen, müs­
sen zahlreiche Stellen korrigiert werden, sollte sich das Interface von Map jemals
ändern. Vielleicht halten Sie eine solche Änderung für unwahrscheinlich; aber
genau das ist passiert, als die Generies in Java 5 hinzugefügt wurden. Tatsächlich
haben wir Systeme gesehen, die Generies nur deshalb nicht nutzen können, weil
der schiere Änderungsaufwand für einen freizügigen Einsatz von Maps einfach zu
groß wäre.
Ein saubererer Einsatz von Map könnte wie folgt aussehen. Dem Benutzer von Sen­
s o rs ist der Einsatz von Generies egal. Diese Entscheidung ist (wie es sich gehört)
zu einem Implementierungsdetail geworden.

publ i c c l ass Sensors {


p r i vate Map s e n s o r s = n ew HashMap ( ) ;
publ i c Sensor getByi d ( S t r i ng i d) {
retu rn (Sensor) s en s o r s . g et ( i d) ;
}

II

Das Interface an der Grenze (Map) ist verborgen. Es kann mit sehr geringen Aus­
wirkungen auf den Rest der Anwendung verändert werden. Der Einsatz von Gene­
ries ist kein großes Thema mehr, weil das Casting und die Typverwaltung innerhalb
der Sensors-Klasse gehandhabt werden.
Außerdem ist dieses Interface auf die Anforderungen der Anwendung zugeschnit­
ten. Es schränkt die zugänglichen Funktionen erheblich ein. Der resultierende Code
ist leichter zu verstehen und schwerer zu missbrauchen. Die Sensors-Klasse kann
das Design und die Geschäftsregeln durchsetzen.
Wir schlagen nicht vor, jede Anwendung von Map aufdiese Weise einzukapseln, son­
dern raten Ihnen nur, Maps (oder andere Interfaces an der Grenze) nicht in Ihrem
System herumzureichen. Wenn Sie ein Boundary-Interface wie Map verwenden,
sollte Sie es auf die Klasse oder die enge Familie von Klassen beschränken, in der
es benutzt wird. Sie sollten es beim Einsatz von öffentlichen APis möglichst nicht
zurückgeben und auch nicht als Argument übergeben.

1 53
Kapitel S
Grenzen

8.2 Grenzen erforschen u nd ken nen lernen


Drittanbieter-Code hilft uns, mehr Funktionalität in weniger Zeit zu realisieren. Wo
beginnen wir, wenn wir ein Drittanbieter-Package nutzen wollen? Es ist nicht
unsere Aufgabe, den Drittartbieter-Code zu testen. Aber es kann in unserem besten
Interesse liegen, Tests für den Drittanbieter-Code zu schreiben, den wir benutzen.
Angenommen, Sie wüssten nicht, wie Sie unsere Drittanbieter-Library nutzen kön­
nen. Sie könnten einige Tage die Dokumentation lesen, um herauszufinden, wie Sie
es einsetzen wollen. Dann können Sie Ihren Code schreiben, der auf den Drittan­
bieter-Code zugreift, um zu prüfen, ob er tut, was Sie glauben. Sie wären nicht über­
rascht, durch lange Debugging-Sitzungen aufgehalten zu werden, um
herauszufinden, ob auftretende Bugs durch Ihren oder den fremden Code verur­
sacht werden.
Drittartbieter-Code nutzen zu lernen, ist schwer. Drittanbieter-Code zu integrieren,
ist ebenfalls schwer. Beides gleichzeitig zu tun, ist doppelt so schwer. Was wäre,
wenn wir einen anderen Ansatz wählen würden ? Anstatt herumzuprobieren und
den neuen Code in unserem Produktionscode zu testen, könnten wir einige Tests
schreiben, um den Drittanbieter-Code besser kennen zu lernen. Jim Newkirk
bezeichnet solche Tests als Lern-Tests (engl. Leaming Tests; [BeckTDD], S. 136-137) ·
Bei Lern-Tests rufen wir das Drittanbieter-API so auf, wie wir es wahrscheinlich in
unserer Anwendung benutzen werden. Wir führen im Wesentlichen kontrollierte
Experimente durch, um zu prüfen, wie gut wir dieses API verstehen. Die Tests kon­
zentrieren sich auf das, was wir uns von dem API versprechen.

8.3 log4) ken nen lernen


Angenommen, wir wollten das l og4 j -Package von Apache anstelle unseres eigenen
benutzerspezifischen Loggers verwenden. Wir haben das Package heruntergeladen
und öffnen die einführende Dokumentationsseite. Ohne zu viel zu lesen, schreiben
wir unseren ersten Testfall und erwarten, dass er »hello« auf der Konsole ausgibt.

@Te s t
publ i c vo i d testlogCreate () {
Logger l ogge r = Logge r . getlogge r ( " Mylogge r " ) ;
l ogger . i nfo ( " h e l l o " ) ;
}

Wenn wir ihn ausführen, erzeugt der Logger einen Fehler, der uns mitteilt, dass wir
etwas benötigen, das als Appende r bezeichnet wird. Wenn wir etwas mehr lesen,
stellen wir fest, dass es einen Con s o l eAppende r gibt. Deshalb erstellen wir einen
Co nso l eAppende r und ptüfen, ob wir die Geheimnisse des Loggings aufider Kon­
sole gelüftet haben.

1 54
8. J
log4j kennen lernen

@Tes t
publ i c voi d testLogAddAppend e r () {
Logge r l ogge r = Logge r . getLogge r ( " MyLogg e r " ) ;
Consol eAppend e r append e r = new Con sol eAppend e r () ;
l ogge r . addAppende r (append e r ) ;
l ogge r . i nfo ( " h e l l o " ) ;
}

Diesmal stellen wir fest, dass der Appen d e r keinen Output-Stream hat. Seltsam ­
es scheint logisch zu sein, dass er keinen hat. Mit ein wenig Hilfe von Google pro­
bieren wir das Folgende:

@Tes t
publ i c voi d t e s tLogAddAppende r ( ) {
Logge r l ogge r = Logge r . getLogge r ( " MyLogge r " ) ;
l ogge r . removeAl l Appende r s ( ) ;
l ogge r . addAppe n d e r (new Consol eAppend e r (
new Patte rn Layou t ( "%p %t %m%n " ) ,
Consol eAppende r . SYSTEM_OUT) ) ;
l ogge r . i n fo ( " he l l o " ) ;
}

Das funktioniert; eine Protokollmeldung, die ein »hello« enthält, wird aufder Kon­
sole ausgegeben! Es scheint seltsam zu sein, dass wir den Conso l eAppe n d e r anwei­
sen müssen, aufdie Konsole zu schreiben.
Interessanterweise wird »hello« immer noch ausgegeben, wenn wir das Argument
Con sol eAppende r . Sys temOut entfernen. Doch wenn wir das Patte r n layout
herausnehmen, erscheint wieder die Beschwerde über das Fehlen eines Output­
Streams. Dieses Verhalten ist sehr seltsam.
Wenn wir uns die Dokumentation etwas sorgfaltiger anschauen, stellen wir fest,
dass der Default-Konstruktor Conso l eAppen de r »unconfigured« (nicht konfigu­
riert) ist, was nicht allzu offensichtlich oder nützlich zu sein scheint. Dies fühlt sich
wie ein Bug oder zumindest eine Inkonsistenz in 1 og4 j an.
Ein wenig mehr Googelei, Lesen und Testen bringt uns schließlich zu Listing 8.1.
Wir haben viel darüber gelernt, wie l og4 j funktioniert, und wir haben dieses Wis­
sen in einem Satz einfacher Unit-Tests codiert.

Listing 8.1: LogTest . j ava


publ i c c l as s LogTe s t {
p r i vate Logge r l ogge r ;

@ßefore
publ i c voi d i n i ti al i ze () {
l ogger = Logge r . getLogge r ( " l ogge r " ) ;
l ogge r . removeAl l Appende r s () ;
l ogge r . getRootLog g e r ( ) removeAl l Appende r s ( ) ;

1 55
Kapitel S
Grenzen

@Te s t
pub1 i c voi d basi clogge r () {
Basi cConfi gu rato r . confi g u r e () ;
1 ogg e r . i n fo ( " basi c logg e r " ) ;
}

@Te s t
pub1 i c voi d addAppende rWi thSt ream() {
1 ogge r . addAppende r (new Conso1 eAppende r (
new Patte r nlayo u t ("%p %t %m%n " ) ,
Conso1 eAppende r . SYSTEM_OUT) ) ;
1 ogge r . i n fo ( " addAppende rWi t h S t ream " ) ;
}

@Tes t
pub1 i c voi d addAppende rWi thoutSt ream () {
1 ogge r . addAppende r ( new Conso1 eAppende r (
n ew Patte r nlayou t (" %p %t %m%n " ) ) ) ;
1 ogg e r . i n fo ( " addAppende rWi t h o u tSt ream " ) ;
}
}

Jetzt wissen wir, wie wir einen einfachen Konsolen-Logger initialisieren müssen,
und können dieses Wissen in unserer eigenen Logger-Klasse einkapseln, damit der
Rest unserer Anwendung von dem 1 og4 j -Boundary-Interface isoliert ist.

8.4 Lern-Tests sind besser als kostenlos


Lern-Tests kosten nichts. Wir mussten das API trotzdem lernen; und diese Tests zu
schreiben, war eine leichte und isolierte Methode, dieses Wissen zu erwerben. Die
Lern-Tests waren präzise Experimente, die uns halfen, unser Verständnis zu vertiefen.
Lern-Tests sind nicht nur kostenlos, sondern haben ein positives Return-on-Invest­
ment. Wenn ein neues Release des Drittanbieter-Packages veröffentlicht wird, füh­
ren wir die Lern-Tests aus, um zu prüfen, ob es Verhaltensunterschiede gibt.
Lern-Tests verifizieren, ob die Drittanbieter-Packages so funktionieren, wie wir es
erwarten. Nach der Integration gibt es keine Garantien dafür, dass der Drittanbieter­
Code mit unseren Anforderungen kompatibel bleibt. Die ursprünglichen Autoren
stehen unter Druck, ihren Code an neue, eigene Anforderungen anzupassen. Sie
werden Bugs beheben und neue Fähigkeiten hinzufügen. Jedes Release birgt neue
Risiken. Wenn die Änderungen des Drittanbieter-Packages inkompatibel mit unse­
ren Tests sind, werden wir dies sofort feststellen.
Ob Sie das Wissen aus den Lern-Tests brauchen oder nicht, eine saubere Grenze
sollte von einem Satz von nach außen gerichteten Tests unterstützt werden, die das
Interface auf dieselbe Weise prüfen, wie wir den Produktionscode testen.
B. s
Code verwenden, der noch nicht existiert

Ohne diese Boundary-Tests, die die Migration erleichtern, könnten wir versucht sein,
die alte Version länger zu benutzen, als wir sollten.

8.5 Code verwenden, der noch n icht existiert


Es gibt eine andere Art von Grenzen: Sie trennt das Bekannte von dem Unbekann­
ten. Es gibt oft Stellen im Code, wo unser Wissen vor einem Abgrund zu stehen
scheint. Manchmal ist das, was auf der anderen Seite der Grenze ist, unergründlich
(zumindest im Moment). Manchmal schauen wir absichtlich nicht weiter als bis zu
der Grenze.
Vor einigen Jahren war ich Mitglied eines Teams, das Software für ein Radio-Kom­
munikationssystem entwickelte. Es gab ein Subsystem, den ))Transmitter«, über das
wir wenig wussten; und die Entwickler, die für das Subsystem verantwortlich waren,
hatten ihr Interface noch nicht definiert. Da wir uns nicht blockieren lassen wollten,
begannen wir unsere Arbeit an einem Teil des Systems, der von dem unbekannten
Teil des Codes weit entfernt war.
Wir hatten eine ziemlich klare Vorstellung davon, wo unsere Welt endete und die
neue Welt begann. Bei unserer Arbeit stießen wir manchmal an diese Grenze.
Obwohl Nebel und Wolken der Unwissenheit unsere Sicht auf die andere Seite der
Grenze behinderten, machte uns unsere Arbeit klar, wie das Boundary-Interface
unsere Meinung nach aussehen sollte. Wir wollten dem Transmitter etwa Folgendes
sagen können:
Stelle den Transmitter auf die angegebene Frequenz ein und gib eine analoge
Repräsentation der Daten aus, die aus diesem Stream kommen.
Wir hatten keine Vorstellung davon, wie das bewerkstelligt werden könnte, weil das
API noch nicht konzipiert worden war. Deshalb schoben wir die Ausarbeitung der
Details auf später auf.
Damit wir nicht weiter blockiert waren, definierten wir unser eigenes Interface. Wir
wählten einen griffigen Namen wie T ransmi t t e r. Wir gaben ihm eine Methode
namens t ransmi t, die eine Frequenz und einen Daten-Stream als Argumente
übernahm. Dies war das Interface, das wir uns wünschten.
Ein solches Wunsch-Interface hat einen Vorteil: Es unterliegt der eigenen Kontrolle.
Dies trägt dazu bei, die Lesbarkeit des Client-Codes und seinen Fokus auf seinen
Zweck zu erhalten.
Abbildung 8.2 zeigt, dass wir die Comm u n i cati o n sCont rol l e r- Klassen von dem
Transmitter-API (das nicht unserer Kontrolle unterlag und noch nicht definiert war)
isolierten. Indem wir unser eigenes anwendungsspezifisches Interface verwende­
ten, konnten wir unseren Commu n i cat i on sCont rol l e r-Code sauber und aus­
drucksstark halten. Nachdem das Transmitter-AP I definiert war, schrieben wir den
T ransmi tte rAdapt e r, um die Lücke zu überbrücken. Der Adapter [GOF] kapseit

1 57
Kapitel S
G renzen

die Interaktion mit dem API ein und ist die einzige Stelle, die bei einer Weiterent­
wicklung des APis geändert werden muss.

� «interface»
Transmitter
Com m u n ication
-
Controller
+ transmit(frequency, stream)


I I
«future»
Fake Transmitter
- Transmitter API
Trasmitter Adapter

Abb. 8.1 : Den Tra nsm itter vorhersagen

Dieses Design stellt uns in dem Code auch einen sehr bequemen Seam ( Saum;
mehr über Seams siehe [WELC]) für das Testen zur Verfügung. Mit einem geeig­
neten FakeT ransmi t t e r können wir die Communi cati onsCon t rol l e r- Klassen
testen. Wenn dann das Transmi tte rAPI zur Verfügung steht, können wir auch
Boundary-Tests erstellen, um zu prüfen, ob wir das API korrekt verwenden.

8.6 Saubere G renzen


An Grenzen passieren interessante Dinge. Änderung zählt zu diesen Dingen. Gute
Software-Designs ermöglichen Änderungen ohne riesige Investitionen und Ü ber­
arbeitungen. Wenn wir Code verwenden, der nicht unserer Kontrolle unterliegt,
müssen wir besonders darauf achten, unsere Investition zu schützen und dafür zu
sorgen, dass künftige Änderungen nicht zu teuer sind.
Code an den Grenzen braucht eine klare Trennung und Tests, die Erwartungen defi­
nieren. Wir sollten vermeiden, dass unser Code zu viel über die Details der Drittan­
bieter-Software weiß. Es ist besser, von etwas abhängig zu sein, das Sie kontrollieren
können, als von etwas, das Sie nicht kontrollieren können, sonst werden Sie von ihm
kontrolliert.
Wir halten Drittanbieter-Boundaries in Schach, indem wir sie nur an sehr wenigen
Stellen im Code referenzieren. Wir könnten sie, ähnlich wie bei Map weiter vorne,
einhüllen oder wir könnten unser perfektes Interface mit einem Adapter in das zur
Verfügung gestellte Interface umwandeln. Auf jeden Fall spricht unser Code besser
zu uns. Er fördert intern eine konsistente Anwendung über die Grenze hinweg; und
er enthält weniger Wartungs punkte, wenn sich der Drittanbieter-Code ändert.
Kapitel g

U n it-Tests

Unser Beruf hat in den letzten zehn Jahren eine wesentliche Entwicklung durch­
laufen. 1997 hatte noch niemand von der Test Driven Development (TDD) gehört.
Für die allermeisten Programmierer waren Unit-Tests kurze Abschnitte von Weg­
werf-Code, den wir schrieben, um zu prüfen, ob unsere Programme »funktionier­
ten«. Wir gaben uns große Mühe, unsere Klassen und Methoden zu schreiben, und
dann flickten wir Ad-hoc-Code zusammen, um sie zu testen. Ü blicherweise schrie­
ben wir dabei eine Art von Treiber-Programm, mit dem wir manuell mit dem Pro­
gramm interagieren konnten, das wir geschrieben hatten.
Ich erinnere mich an ein C++-Programm für ein eingebettetes Echtzeit-System, das
ich Mitte der 9 oer-Jahre entwickelt hatte. Das Programm war ein einfacher Timer
mit der folgende Signatur:

voi d T i me r : : Schedul eCommand (Command* th eCommand , i n t mi l l i seconds)

Die Idee war einfach: Die execute-Methode von Command sollte nach der spezifi­
zierten Anzahl von Millisekunden in einem neuen Thread ausgeführt werden. Das
Problem war, wie ich es testen konnte.

1 59
Kapitel 9
U nit-Tests

Ich schusterte ein einfaches Treiber-Programm zusammen, das die Tastatur über­
wachte. Jedes Mal, wenn ein Zeichen eingetippt wurde, wurde ein Schedule für
einen Befehl erstellt, der dasselbe Zeichen fünf Sekunden später noch einmal tip­
pen sollte. Dann tippte ich eine rhythmische Melodie auf der Tastatur ein und war­
tete darauf, dass dieses Lied fünf Sekunden später noch einmal abgespielt werden
würde.
>>Hänschen ... klein ... ging . . . allein . . . in . . . die . . . weite ... Welt . . . hinein«.
Tatsächlich sang ich das Lied beim Eintippen mit, während ich die ».«-Tasten
anschlug; und dann sang ich es noch einmal, als die Punkte auf dem Bildschirm
erschienen.
Das war mein Test! Als ich sah, dass er funktionierte, und ich ihn meinen Kollegen
demonstriert hatte, warf ich den Testcode weg.
Wie gesagt, unser Beruf hat eine lange Entwicklung hinter sich. Heute würde ich
einen Test schreiben, mit dem ich prüfen könnte, ob jeder Haken und jede Öse dieses
Codes so funktioniert, wie ich es erwarte. Ich würde meinen Code von dem Betriebs­
system isolieren, anstatt einfach die eingebaute Timing-Funktion aufzurufen. Ich
würde diese Timing-Funktion simulieren, um absolute Kontrolle über die Zeit zu
haben. Ich würde Befehle planen, die boolesche Flags setzen würden, und dann
würde ich die Zeit vorstellen, diese Flags beobachten und dafür sorgen, dass sie von
fa l s e auf t rue gesetzt würden, sobald sich die Zeit auf den richtigen Wert setzte.
Nachdem ich dafür gesorgt hätte, dass alle Tests dieser Suite bestanden werden,
würde ich dafür sorgen, dass diese Tests bequem von späteren Wartungsprogram­
mierern dieses Codes genutzt werden könnten. Ich würde dafür sorgen, dass die
Tests und der Code zusammen in dasselbe Source-Package eingecheckt werden
würden.
Ja, wir haben einen langen Weg hinter uns; aber wir sind noch längst nicht am Ziel.
Die Agile- und die TDD-Bewegung haben viele Programmierer ermutigt, automa­
tisierte Unit-Tests zu schreiben. Heute werden diese Techniken von immer mehr
Entwicklern übernommen. Aber in der raschen Anstrengung, das Testen fest in
ihren täglichen Arbeitsablauf zu integrieren, haben viele Programmierer einige
subtilere und wichtige Punkte übersehen, die zum Schreiben guter Tests unver­
zichtbar sind.

g.1 Die drei Gesetze der TOD


Heute weiß jeder, dass die T D D von uns verlangt, erst die Unit-Tests z u schreiben,
bevor wir Produktionscode schreiben. Aber diese Regel ist nur die Spitze des Eis­
bergs. Betrachten Sie die folgenden drei Gesetze [Martino7]:
Erstes Gesetz - Sie dürfen Produktionscode erst schreiben, wenn Sie einen schei­
ternden Unit-Test geschrieben haben.

1 60
9· 2
Tests sauber halten

Zweites Gesetz - Der U nit-Test darfnicht mehr Code enthalten, als für das Scheitern
und ein korrektes Kompilieren des Tests erforderlich ist.
Drittes Gesetz - Sie dürfen nur so viel Produktionscode schreiben, wie für das Beste­
hen des gegenwärtig scheiternden Tests ausreicht.
Diese drei Gesetze zwingen Sie in einen Zyklus, der vielleicht 30 Sekunden dauert.
Die Tests und der Produktionscode werden zusammen geschrieben, wobei die Tests
dem Produktionscode nur wenige Sekunden vorauseilen.
Wenn wir auf diese Weise arbeiten, schreiben wir Dutzende von Tests pro Tag, Hun­
derte von Tests pro Monat und Tausende von Tests pro Jahr. Wenn wir auf diese Weise
arbeiten, decken diese Tests praktisch unseren gesamten Produktionscode ab. Der
schiere Umfang dieser Tests kann mit dem Umfang des Produktionscodes selbst kon­
kurrieren und Sie vor ein entmutigendes Verwaltungsproblem stellen.

g.2 Tests sauber halten


Vor einigen Jahren wurde ich gebeten, ein Team anzuleiten, das sich ausdrücklich
dafür entschieden hatte, seinen Testcode nicht mit denselben Qualitätsstandards
wie für den Produktionscode zu warten. Jeder hatte die Lizenz, Regeln in den Unit­
Tests zu brechen. ))Quick and dirty« war angesagt. Die Variablen mussten keine
guten Namen haben, die Testfunktionen mussten nicht kurz und beschreibend
sein. Der Testcode musste nicht wohlkonzipiert und gut durchdacht partitioniert
sein. Solange der Testcode funktionierte und solange er den Produktionscode
abdeckte, war er gut genug.
Einige Leser mögen mit dieser Entscheidung sympathisieren. Vielleicht haben sie,
natürlich in einer weit zurückliegenden Vergangenheit, ähnliche Tests geschrieben
wie ich für diese Timer-Klasse. Es ist ein riesiger Schritt vom Schreiben derartiger
Wegwerf-Tests zum Schreiben einer Suite automatisierter Unit-Tests. Deshalb
könnten Sie, wie das Team, das ich anleiten sollte, für sich beschließen, dass
schmutzige Tests immer noch besser wären als gar keine Tests.
Doch dieses Team erkannte nicht, dass schmutzige Tests zu haben gleichbedeutend,
wenn nicht sogar schlimmer ist, als keine Tests zu haben. Das Problem liegt darin,
dass die Tests geändert werden müssen, wenn der Produktionscode weiterentwi­
ckelt wird. Je schmutziger die Tests sind, desto schwieriger sind die Änderungen.
Je verschlungener der Testcode ist, desto wahrscheinlicher müssen Sie mehr Zeit
damit verbringen, neue Tests in die Suite einzufügen, als Sie für das Schreiben des
neuen Produktionscodes benötigen. Wenn Sie den Produktionscode modifizieren,
fangen alte Tests an zu scheitern; und das Chaos in dem Testcode macht es schwer,
die Tests so zu ändern, dass sie wieder bestanden werden. Deshalb werden diese
Tests als eine immer größer werdende Belastung empfunden.
Von Release zu Release nahmen die Kosten für die Wartung der Test-Suite meines
Teams zu. Im Laufe der Zeit entwickelte sie sich zu dem hauptsächlichen Stein des
Kapitel g
U nit-Tests

Anstoßes der Entwickler. Wenn die Manager fragten, warum die Schätzungen zu
groß geworden waren, schoben die Entwickler die Schuld auf die Tests. Schließlich
waren sie gezwungen, die Test-Suite komplett aufzugeben.
Doch ohne eine Test-Suite hatten sie die Fähigkeit verloren, zu prüfen, ob Änderun­
gen an ihrer Code-Basis so funktionierten, wie sie es erwarteten. Ohne eine Test­
Suite konnten sie nicht garantieren, dass Änderungen in einem Teil ihres Systems
nicht zu Defekten in anderen Teilen ihres Systems führten. Deshalb begann ihre
Defektrate zu steigen. Als die Anzahl der nicht beabsichtigten Defekte zunahm,
wuchs die Angst vor weiteren Änderungen. Sie hörten auf, ihren Produktionscode
zu säubern, weil sie fürchteten, die Änderungen würden mehr Schaden als Nutzen
bringen. Ihr Produktionscode begann zu verrotten. Zum Schluss standen sie da
ohne Tests, mit verschlungenem Produktionscode voller Bugs, frustrierten Kunden
und dem Gefühl, dass ihre Testanstrengungen nicht den erhofften Erfolg gebracht
hätten.
In einer Hinsicht hatten sie recht Ihre Testanstrengungen hatten nicht den erhoff­
ten Erfolg gebracht Aber es war ihre Entscheidung, das Chaos in den Tests zuzu­
lassen, die den Keim für dieses Scheitern pflanzte. Hätten sie ihre Tests sauber
gehalten, dann hätten ihre Testanstrengungen sie nicht im Stich gelassen. Ich kann
dies mit einiger Bestimmtheit sagen, weil ich in vielen Teams mitgearbeitet habe
und viele Teams angeleitet habe, die mit sauberen Unit-Tests erfolgreich Proj ekte
abgewickelt haben.
Die Moral der Geschichte ist einfach: Testcode ist genauso wichtig wie Produktionscode.
Tests sind keine Bürger zweiter Klasse. Testcode erfordert Nachdenken, Design und
Pflege. Er muss genauso sauber wie der Produktionscode gehalten werden.

Tests ermögl ichen die -heiten und -keiten

Wenn Sie Ihre Tests nicht sauber halten, werden sie Ihnen entgleiten. Und ohne die
Tests verlieren Sie das Einzige, das ihnen die Flexibilität Ihres Produktionscodes
garantiert. Ja, Sie haben richtig gelesen: Es sind die Unit-Tests, die dafür sorgen, dass
unser Code flexibel, wartbar und wiederverwendbar bleibt. Der Grund dafür ist ein­
fach: Mit Tests haben Sie keine Angst, den Code zu ändern! Ohne Tests ist jede
Änderung ein möglicher Bug. Egal, wie flexibel Ihre Architektur sein mag oder wie
sauber Sie Ihr Design partitioniert haben, ohne Tests werden Sie zögern, Code zu
ändern, weil Sie Angst haben, damit unentdeckte Bugs einzuführen.
Aber mit Tests ist diese Angst praktisch verschwunden. Je vollständiger Ihre Testab­
deckung ist, desto geringer ist Ihre Angst Sie können Änderungen praktisch straf­
los durchführen, auch wenn der Code eine weniger umwerfende Architektur und
ein verworrenes und undurchschaubares Design hat. Tatsächlich können Sie diese
Architektur und dieses Design furchtlos verbessern!
Deshalb ist die Arbeit mit einer automatisierten Suite von Unit-Tests, die den Pro­
duktionscode abdecken, der Schlüssel dazu, Ihr Design und Ihre Architektur so sau-
9·3
Saubere Tests

her wie möglich zu halten. Tests ermöglichen all die -keiten, weil Tests Anderung
ermöglichen.
Wenn also Ihre Tests schmutzig sein sollten, dann ist Ihre Fähigkeit, Ihren Code zu
ändern, ernsthaft eingeschränkt, und Sie beginnen die Fähigkeit zu verlieren, die
Struktur dieses Codes zu verbessern. Je schmutziger Ihre Tests sind, desto schmut­
ziger wird Ihr Code. Schließlich entgleiten Ihnen die Tests, und Ihr Code verrottet.

9·3 Saubere Tests


Was zeichnet einen sauberen Test aus? Drei Dinge: Lesbarkeit, Lesbarkeit und Les­
barkeit. Lesbarkeit ist bei Unit-Tests vielleicht noch wichtiger als bei Produktions­
code. Was macht Tests lesbar? Dasselbe, was allen Code lesbar macht: Klarheit,
Einfachheit und Ausdrucksdichte. In einem Test wollen Sie so viel wie möglich mit
so wenig Ausdrücken wie möglich sagen.
Betrachten Sie den Code aus FitNesse in Listing 9.r. Diese drei Tests sind schwierig
zu verstehen und können bestimmt verbessert werden. Zunächst wird in den wie­
derholten Aufrufen von add Page und asse rtSubStri ng schrecklich viel Code dup­
liziert [G5]. Doch wichtiger ist: Der folgende Code ist einfach mit Details überladen,
die die Ausdruckskraft des Tests schwächen.

Listing 9.1: Ser i al i zedPageResponderTest . j ava


publ i c voi d testGet PageH i e ratchyAsXml () t h rows Except i on
{
c rawl e r . addPag e ( root , PathParse r . parse ( " PageOne " ) ) ;
c r awl e r . addPag e ( root , PathParse r . pa rs e ( " PageOn e . Ch i l dOne" ) ) ;
c r awl e r . addPag e ( root , PathParse r . pa rs e ( " PageTwo " ) ) ;

request . setReso u r ce ( " root " ) ;


request . add ! n pu t ( " type " , " pages " ) ;
Res pande r respan d e r = n ew S e r i al i zedPageRespon de r ( ) ;
Si mpl eRespon s e response =

(Si mpl e Re s pon se) r es ponde r . makeRespo n s e (


n ew F i t N e s s eContext ( root) , request) ;
St r i ng xml = respo n s e . getConten t () ;

a s s e rtEqua 1 s ( " text/xml " , respo n s e . getContentType ( ) ) ;


a s s e rtSubSt ri n g ( " <name>PageOne</name> " , xml ) ;
a s s e rtSubSt r i n g ( " <name>PageTwo</n ame> " , xml ) ;
asse rtSubSt ri n g ( " <name>Ch i l dOne</name> " , xml ) ;
}

publ i c voi d tes tGetPageH i e rat chyAsXml Doe s n tContai n Symbo l i c li n k s ()


th rows Except i on
{
Wi ki Page pageOne =
c rawl e r . addPage ( root , Pat h Pa r s e r . pa r se ( " PageOn e " ) ) ;
Kapitel 3
U nit-Tests

c r awl e r . addPage (root , Path Parse r . pa rs e ( " PageOne . Ch i l dOne ") ) ;


c rawl e r . addPage ( root , Pat h P a r s e r . parse ( " PageTwo" ) ) ;

PageData data = pageOn e . getData () ;


Wi ki PageP rope rti es p ropert i e s = data . getPrope rt i e s () ;
Wi ki PageProperty symli nks = properti e s . set (Symbol i c Page . PROPERTY_NAME) ;
symli n ks . s e t ( " SymPage " , " PageTwo " ) ;
pageOn e . commi t (data) ;

req u es t . setResou rce ( " root " ) ;


req u es t . addl n p u t ( "type " , " page s " ) ;
Respande r respande r = new Seri al i zed PageRespond e r () ;
S i mp l eResponse response =
(Si mpl eResponse) res pond e r . makeResponse (
n ew Fi tNes seContext ( root) , request) ;
Stri n g xml = respon s e . getContent () ;

asse rtEqual s ( " text/xml " , respo n s e . getContentType () ) ;


asse rtSubStri n g ( " <name> PageOne</name> " , xml ) ;
asse rtSubStri n g ( " <name> PageTwo</name> " , xml ) ;
a s s e rtSubStri ng ( "<name>Ch i l dOne</name> " , xml ) ;
asse rtNot S u bSt r i ng ( " SymPag e " , xml ) ;
}

publ i c voi d testGetDataAs Html () th rows Except i on


{
c rawl e r . addPag e ( root , Path Pars e r . p a r s e ( "TestPageOne " ) , " te s t page " ) ;

req u es t . setReso u rce ( "TestPageOn e " ) ;


req u es t . addlnpu t ( " type " , " data " ) ;
Respander re spande r = n ew Seri a l i zedPageRes ponde r () ;
S i mp l eResponse response =
(Si mpl eResponse) respo nde r . makeRespon s e (
n ew Fi tNesseContext ( root) , req uest) ;
S t r i n g xml = respo n s e . getContent () ;

asse rtEq u a l s ( " t ext/xml " , respo n s e . getContentType () ) ;


assertSubSt ri ng ( " t e s t page " , xml ) ;
assertSubSt ri ng ( " <Te s t " , xml ) ;
}

Betrachten Sie beispielsweise die PathPa r s e r-Aufrufe. Sie transformieren Strings


in Pag e Path-Instanzen, die von den Crawlern verwendet werden. Diese Transfor­
mation ist für den gegenwärtigen Test vollständig irrelevant und verschleiert nur
den Zweck. Die Details, die die Erstellung des r e s po n d e r-Objekts umgeben und die
Sammlung und das Casting der r e s pons e-Objekte sind ebenfalls nur Rauschen.
Dann gibt es die ungeschickte Methode, wie der Anfrage-URL aus einer resou r c e
und einem Argument zusammengesetzt wird. ( Ich habe geholfen, diesen Code zu
schreiben; deswegen nehme ich bei meiner Kritik kein Blatt vor den Mund.)
9·3
Saubere Tests

Schließlich wurde dieser Code nicht konzipiert, um gelesen zu werden. Der arme
Leser wird mit einer Unmenge von Details überhäuft, die er verstehen muss, bevor
er die Bedeutung der Tests wirklich begreifen kann.
Betrachten Sie j etzt die verbesserten Tests in Listing 9.2. Diese Tests leisten genau
dasselbe, wurden aber durch Refactoring in eine viel saubere und aussagekräftigere
Form gebracht.

Listing g.2: Ser i a1 i zedPageResponderTest . j ava (nach Refactoring)


pub1 i c voi d testGet PageHi e rarc hyAsXml () t h r ows Except i on {
makePages ( " PageOn e " , " Page0n e . Ch i 1 d0 ne " , " PageTwo " ) ;

s u bmi tRequest ( " root" , " type : pag es " ) ;

asse rtRespons elsXM L () ;


asse rtRespons eContai n s (
" <name> PageOne</name> " , "<name> PageTwo</name> " , "<name>Chi 1 dOne</name>"
);
}

pub1 i c voi d testSymbo1 i cli n ks AreNotlnXm1 Pag eHi e ra r c h y ( ) th rows Excepti o n {


Wi ki Page page = makePag e ( " PageO n e " ) ;
make Page s ( " Page0n e . Ch i 1 dOne " , " PageTwo " ) ;

add l i n kTo (page , " PageTwo " , " SymPag e " ) ;


s u bmi tReq u e s t ( " root " , " type : pag es " ) ;
assertRespons�IsXM L () ;
asse rtRespons eCo n tai n s (
" <name> PageOn e</name> " , "<name>PageTwo</name> " , "<name>Chi 1 dOne</name>"
);
asse rtResponseDoesNotContai n ( " SymPage " ) ;
}

pub1 i c voi d testGetDataAsXml () th rows Excepti o n {


make PageWi t hCont e n t ( " Test PageOn e " , " te s t page " ) ;

s u bmi tReq u e s t ( "Test PageOn e " , " type : data") ;

asse rtResponselsXML () ;
asse rtResponseContai n s ( " t e s t page " , " <Test " ) ;
}

Das Build-Operate-Check-Pattem (http : I /fi tnesse . o rg/Fi tNesse . Acceptance­


Te s t Patte rns) ist in der Struktur dieser Tests klar erkennbar. Jeder Test wird
erkennbar in drei Teile zerlegt. Der erste Teil erstellt die Testdaten, der zweite Teil
manipuliert diese Testdaten, und der dritte Teil prüft, ob die Operation die erwar­
teten Ergebnisse erzeugt hat.
Kapitel 9
U nit-Tests

Beachten Sie, dass der überwiegende Teil der ärgerlichen Details eliminiert worden
ist. Die Tests kommen direkt zur Sache und verwenden nur die Datentypen und
Funktionen, die sie wirklich benötigen. Jeder Leser dieser Tests sollte sehr schnell
herausfinden können, was sie tun, ohne durch Details in die Irre geführt oder über­
wältigt zu werden.

Domänenspezifische Testsprache

Die Tests in Listing 9.2 demonstrieren die Technik, eine domänenspezifische Spra­
che für Ihre Tests zu erstellen. Anstatt die APis zu verwenden, mit denen Program­
mierer das System manipulieren, erstellen wir einen Satz von Funktionen und
Utilities, die diese APis nutzen und uns das Schreiben und Lesen der Tests erleich­
tern. Diese Funktionen und Utilities bilden ein spezielles API , das von den Tests
benutzt wird. Sie bilden eine Testsprache, mit denen Programmierer sich selbst hel­
fen, ihre Tests zu schreiben, und auch denen helfen, die diese Tests später lesen
müssen.
Dieses Testing-API wird nicht vorher konzipiert, sondern entwickelt sich im Zuge
des laufenden Refactoring von Testcode, der selbst durch verschleiernde Details zu
unsauber geworden ist. So wie ich bei Listing 9.1 zu Listing 9.2, bringen diszipli­
nierte Entwickler ihren Testcode durch Refactoring in immer präzisere und aus­
drucksstärkere Formen.

Ein Doppelstandard

In einer Hinsicht machte das Team, das ich am Anfang dieses Kapitels erwähne,
etwas richtig. Der Code in dem Testing-API folgt tatsächlich einem anderen Satz von
technischen Standards als der Produktionscode. Er muss immer noch einfach, prä­
zise und ausdrucksstark sein, aber er muss nicht so effizient wie der Produktions­
code sein. Schließlich wird er in einer Test-Umgebung und nicht in einer
Produktionsumgebung ausgeführt; und diese beiden U mgebungen haben sehr ver­
schiedene Anforderungen.
Betrachten Sie den Test in Listing 9 -3- Ich habe diesen Test als Teil eines Systems
zur Umgebungskontrolle geschrieben, für das ich einen Prototyp entwickelte. Ohne
in die Details zu gehen, kann ich Ihnen sagen, dass dieser Test prüft, ob der Nied­
rigtemperatur-Alarm, der Heizer und der Ventilator alle eingeschaltet sind, wenn
die Temperatur ))way too cold« ())viel zu kalt«) ist.

Listing 9.3: Envi ronmentControl l erTest . j ava


@Te s t
publ i c voi d turnOn loTempAl a rmAtTh reasho l d () t h rows Excepti on {
hw . setTemp (WAY_TOO_COLD) ;
control l e r . ti c () ;
a s s e rtT rue (hw. heaterStat e () ) ;
asse rtT rue (hw . bl owe rState () ) ;

1 66
9·3
Saubere Tests

a s s e r t Fal se ( hw . cool e rState ( ) ) ;


a s s e r t Fal s e ( hw . hi TempAl a r m () ) ;
asse rtTru e ( hw . l oTempAl a r m () ) ;
}

Es gibt hier natürlich zahlreiche Details. Was hat es beispielsweise mit der ti c­
Funktion auf sich? Tatsächlich sollten Sie sich darüber keine Gedanken machen,
wenn Sie diesen Test lesen. Wichtig ist, dass Sie überlegen, ob der Endzustand des
Systems konsistent mit der Aussage ist, dass die Temperatur »way too cold« sei.
Beachten Sie, wie Ihre Augen beim Lesen des Tests zwischen dem Namen des zu prü­
fenden Zustands und dem Wert dieses Zustands hin und her springen müssen. Sie
sehen h eate rState, und dann gleiten Ihre Augen nach links zu a s s e rtT rue. Sie
sehen coo l e rState, und Ihre Augen müssen nach links, um asse r t Fa l se aufzu­
nehmen. Dies ist mühsam und unzuverlässig. Der Test wird dadurch schwer lesbar.
Ich habe die Lesbarkeit dieses Tests erheblich verbessert, indem ich ihn umge­
schrieben habe (siehe Listing 9-4) .

Listing 9.4: Envi ronmentCont rol l e rTest . j ava (nach Refactoring)


@Te s t
publ i c voi d t u rnOn loTempAl a rmAtTh reshol d () t h rows Excepti on {
wayTooCo l d () ;
asse rtEq ual s (" HB c h L " , h w . getState ( ) ) ;
}

Natürlich habe ich das Detail der t i c-Funktion verborgen, indem ich eine wayToo­
Co l d-Funktion erstellt habe. Doch das Wichtige ist der seltsame String in as s e rt ­
Eq ua l s . Großbuchstabe bedeutet »an«, Kleinbuchstabe bedeutet »aUS«, und die
Buchstaben haben immer die folgende Reihenfolge: { h eater , bl owe r , cool e r ,
h i -temp - al arm , l o -temp-al a rm } .
Doch obwohl dies eng a n einen Verstoß gegen die Regel über das mentale Mapping
(siehe den Abschnitt Mentale Mappings venneiden in Kapitel 2) herankommt, scheint
es in diesem Fall angemessen zu sein. Denn sobald Sie die Bedeutung kennen, glei­
ten Ihre Augen schnell über den String, und Sie können die Ergebnisse schnell
interpretieren. Den Test zu lesen, kann fast ein Vergnügen sein. Werfen Sie nur
einen Blick auf Listing 9·5 und sehen Sie, wie leicht diese Tests zu verstehen sind.

Listing 9·5= Envi ronmentControl l erTe s t . j ava (bessere Übersicht)


@Te s t
publ i c voi d t u r nOnCool e rAnd Bl owe r l fTooHot () th rows Excepti o n {
tooHot () ;
asse rtEqual s ( " hBChl " , hw . getState ( ) ) ;
}

@Tes t
publ i c voi d tu rnOnHeate rAndBl owe r l fTooCol d () th rows Excepti on {
Kapitel 9
U nit-Tests

tooCol d () ;
a s s e r t Equal s ( "H B c h l " , hw . getState ( ) ) ;
}

@Te s t
publ i c voi d t u rnOnHi TempAl a rmAtTh reshol d () t h rows Excepti on {
wayTooHot ( ) ;
asse rtEqua l s ( " h BCHl " , hw . getState ( ) ) ;
}

@Test
publ i c voi d t u rnOnLoTempAl a rmAtTh reshol d () t h rows Excepti o n {
wayTooCo l d () ;
asse rtEqual s ( " H B c h L " , hw . g etState ( ) ) ;
}

Die getState-Funktion wird in Listing 9.6 gezeigt Beachten Sie, dass dieser Code
nicht sehr effizient ist Um ihn effizient zu machen, hätte ich wahrscheinlich einen
S t r i n g Bu ffe r benutzen sollen.

Listing g.6: MockContro1 Hardware . j ava


publ i c S t r i ng getState () {
S t r i ng state = " " ;
s tate += heate r ? " H " "h" ;
s tate += bl owe r ? " B " "b" ;
s tate += coo l e r ? " C " "c" ;
state += hi TempAl a rm ? " H " "h" ;
s tate += l oTempAl arm ? " L " "l " ;
retu rn s tate ;
}

St r i ngBuffe rs sind etwas hässlich . Selbst bei Produktionscode vermeide ich sie,
wenn die Kosten gering sind. Man könnte auch einwenden, dass die Kosten des
Codes in Listing 9.6 sehr klein wären. Doch diese Anwendung ist sicher ein einge­
bettetes Echtzeit- System, und wahrscheinlich sind die Computer- und Speicher­
Ressourcen sehr eingeschränkt Doch in der Test- Umgebung bestehen wahrschein­
lich gar keine Einschränkungen.
Das liegt in der Natur des Doppelstandards. Es gibt Dinge, die Sie in einer Produk­
tionsumgebung niemals tun dürfen, die aber in einer Test-Umgebung absolut in
Ordnung sind. Normalerweise geht es dabei um Probleme der Speichernutzung
oder der CPU-Effizienz, aber niemals um Probleme der Sauberkeit

9·4 Ein assert pro Test


Es gibt eine Denkschule (siehe h ttp : //v.ww . a rti ma . com/webl ogs/vi ewpo st
. j s p?th read=3 5 5 7 8, ein Blag-Beitrag von Dave Astels), die fordert, jede Testfunk-

1 68
9-4
E i n assert pro Test

tion in einem JUnit-Test solle eine und nur eine a s s e rt-Anweisung enthalten.
Diese Regel mag drakonisch erscheinen, aber der Vorteil zeigt sich in Listing 9·5·
Diese Tests kommen zu einer einzigen Schlussfolgerung, die schnell und leicht zu
verstehen ist.
Aber was ist mit Listing 9.2? Es scheint unvernünftig zu sein, die Zusicherung, dass
der Output XM L ist, leicht mit der Zusicherung kombinieren zu können, dass er
bestimmte Substrings enthält. Doch wir können den Test in zwei separate Tests zer­
legen, die jeweils eine eigene spezielle Zusicherung enthalten (siehe Listing 9-7)·

Listing 9.7: Serial i zedPageResponderTes t . j ava (nur ein a ss ert)


publ i c voi d testGetPageHi e r a rchyAsXml () t h rows Excepti on {
g i ve n Pages ( " PageOne " , " PageOn e . Chi l dOn e " , " PageTwo " ) ;

whenReque s t i s i s sued ( " root " , " type : page s " ) ;

thenResponseShoul dßeXML () ;
}

publ i c voi d testGet PageH i e ra rchyHas Ri ghtTag s () t h r ows Excepti o n {


gi ven Page s ( " PageOn e " , " PageOn e . Chi l dOn e " , " PageTwo " ) ;

when Requ e s t i s i s sued ( " root " , " type : page s " ) ;

thenRespon seShoul dContai n (


" <n ame> PageOne</name> " , " <name> PageTwo</name> " , " <name>Ch i l dOne</name> "
);
}

Beachten Sie, dass ich die Namen der Funktionen geändert habe, um die übliche
g i ven-wh e n - t h e n - Konvention zu befolgen [RSpec]. Dadurch werden die Tests
noch leichter lesbar. Leider führt die Zerlegung des Tests zu einer erheblichen Code­
Duplizierung.
Wir können die Duplizierung mit dem Template Method-Pattern [GOF] eliminieren
und die g i ven /when-Teile in die Basisklasse und die then-Teile in verschiedene
abgeleitete Klassen einfügen. Oder wir könnten eine vollkommen separate Test­
klasse erstellen und die g i ve n - und wh e n-Teile in die @ßefo re-Funktion und die
then-Teile in jede @Test-Funktion einfügen. Aber dieser Aufwand scheint mir für
ein solches kleines Problem zu groß zu sein. Letztlich ziehe ich die mehrfachen
a s s e rt-Anweisungen in Listing 9.2 vor.
Ich betrachte die Regel, nur eine einzige a s s e rt-Anweisung zu verwenden, als eine
brauchbare Richtlinie. (»Halte dich an den Code!«) Normalerweise versuche ich,
eine domänenspezifische Testsprache zu erstellen, die diese Regel unterstützt
(siehe Listing 9·5)· Aber ich habe keine Angst davor, mehr als eine a s s e rt-Anwei­
sung in einen Test einzufügen. Ich glaube, das Beste, was wir sagen können, ist,
dass die Anzahl der a s s e rt-Anweisungen in einem Test minimiert werden sollte.

1 69
Kapitel �
U nit-Tests

Ein Konzept pro Test

Vielleicht wäre es besser, die Regel aufzustellen, dass in jeder Testfunktion nur ein
einziges Konzept getestet werden sollte. Wir wollen keine langen Testfunktionen
haben, die einen beliebigen Aspekt nach dem anderen testen. Listing 9.8 ist ein Bei­
spiel für einen solchen Test. Dieser Test sollte in drei unabhängige Tests zerlegt wer­
den, weil er drei unabhängige Dinge testet. Alle Tests in einer Funktion
zusammenzufassen, zwingt den Leser herauszufinden, warum ein bestimmter
Abschnitt vorhanden ist und was dieser Abschnitt testet.

Listing g.8: Verschiedene Tests für die addMonths ()-Methode


publ i c voi d testAddMonths () {
S e r i al Date dl = S e r i al Date . c reateinstan ce ( 3 1 , 5 , 2004) ;

S e r i al Date d2 = S e r i al Date . addMonths (l , d l) ;


asse rtEqual s (3 0 , d2 . getDayOfMont h () ) ;
a s s e rtEqual s (6 , d2 . getMont h ( ) ) ;
asse rtEqual s (2004 , d2 . getYYYY ( ) ) ;

S e r i al Date d 3= S e r i al Date . addMonths (2 , dl) ;


asse rtEqual s (3 1 , d 3 . getDayOfMont h () ) ;
asse rtEqual s ( 7 , d 3 . getMonth () ) ;
asse rtEqual s (2004 , d 3 . getYYYY () ) ;

S e r i al Date d4 = S e ri al Date . addMonth s ( l , S e r i al Date . addMonths ( l , d l) ) ;


asse rtEqual s ( 30 , d4 . g etDayOfMont h () ) ;
asse rtEqual s (7 , d4 . getMonth () ) ;
a s s e r t Eq ual s (2004 , d4 . ge tYYYY () ) ;
}

Die drei Testfunktionen sollten wahrscheinlich wie folgt formuliert werden:

• Gegeben sei der letzte Tag eines Monats mit 3 1 Tagen (wie Mai) :
1 . Wenn Sie einen Monat addieren und der letzte Tag dieses Monats der 3oste
ist (wie Juni) , dann sollte das Datum der 3oste dieses Monats, nicht der 31ste
sein.

2. Wenn Sie zwei Monate zu diesem Datum addieren, so dass der letzte Monat
31 Tage hat, dann sollte das Datum der 31ste sein.

• Gegeben sei der letzte Tag eines Monats mit 30 Tagen (wie Juni) :
1. Wenn Sie einen Monat addieren, s o dass der letzte Tag dieses Monats der
31ste ist, dann sollte das Datum der 3oste, nicht der 31ste sein.

So formuliert, können Sie eine allgemeine Regel erkennen, die in den verschiede­
nen Tests verborgen ist. Wenn Sie den Monat inkrementieren, kann das Datum
nicht größer werden als der letzte Tag dieses Monats. Dies bedeutet, dass die Inkre-

1 70
9·5
F.I. R.S.T.

mentierung des Monats am 28. Februar den 28. März ergeben sollte. Dieser Test
fehlt und wäre eine nützliche Ergänzung.
Deshalb wird das Problem nicht durch die Mehrzahl der a s s e rt-Anweisungen in
den Abschnitten von Listing 9.8 verursacht, sondern durch die Tatsache, dass mehr
als ein Konzept getestet wird. Deshalb wäre es wahrscheinlich am besten, die
Anzahl der a s s e rt-Anweisungen pro Konzept zu minimieren und nur ein Konzept
pro Testfunktion zu testen.

9·5 F. I. R.S.T.
Saubere Tests folgen fünf anderen Regeln, die die Abkürzung »F.I.R. S .T.« bilden
(aus den Schulungsunterlagen von Object Mentor) :
Fast (schnell) - Tests sollten schnell sein. Sie sollten schnell laufen. Wenn Tests lang­
sam laufen, wollen Sie sie seltener ausführen. Wenn Sie sie nicht regelmäßig aus­
führen, finden Sie Probleme nicht früh genug, um sie leicht beheben zu können.
Sie fühlen sich beim Aufräumen des Codes nicht frei genug. Schließlich fangt der
Code an zu verfallen.
Independent (unabhängig) - Tests sollten nicht voneinander abhängen. Ein Test
sollte keine Bedingungen für den nächsten Test setzen. Sie sollten j eden Test unab­
hängig ausführen können. Sie sollten die Tests in beliebiger Reihenfolge ausführen
können. Wenn Tests voneinander abhängen, dann löst der erste, der scheitert, eine
Kaskade weiterer nicht bestandener Tests stromabwärts aus, was die Diagnose
erschwert und stromabwärts liegende Defekte verbirgt.

Repeatahle (wiederholbar) - Tests sollten in jeder Umgebung wiederholbar sein. Sie


sollten die Tests in der Produktionsumgebung, in der QA-Umgebung und auf
Ihrem Laptop im Zug ohne Netzwerk ausführen können. Wenn Ihre Tests nicht in
jeder Umgebung wiederholbar sind, haben Sie immer eine Entschuldigung dafür,
warum sie scheitern. Dann kann es auch passieren, dass Sie die Tests nicht ausfüh­
ren können, wenn die Umgebung nicht zur Verfügung steht.
Self-Validating (selbst-validierend) - Die Tests sollten einen booleschen Output
haben. Entweder sie werden bestanden oder sie scheitern. Sie sollten keine Proto­
kolldatei studieren müssen, um zu erfahren, ob die Tests bestanden werden. Sie
sollten nicht manuell zwei verschiedene Textdateien vergleichen müssen, um zu
erfahren, ob die Tests bestanden werden. Wenn die Tests sich nicht selbst validieren,
kann das Scheitern subjektiv werden, und die Ausführung der Tests kann eine lange
manuelle Auswertung erfordern.
Timely (zeitgerecht) - Die Tests müssen rechtzeitig geschrieben werden. Unit-Tests
sollten kurz vor dem Produktionscode geschrieben werden, der dafür sorgt, dass sie
bestanden werden. Wenn Sie Tests nach dem Produktionscode schreiben, finden
Sie den Produktionscode möglicherweise schwer zu testen. Vielleicht kommen Sie

1]1
Kapitel 9
U nit-Tests

zu der Auffassung, ein Teil des Produktionscodes sei zu schwer zu testen. Sie dürfen
keinen Produktionscode entwickeln, der nicht testbar ist.

g.6 Zusammenfassu ng
Wir haben kaum die Oberfläche dieses Themas angekratzt Tatsächlich glaube ich,
dass man ein ganzes Buch über Saubere Tests schreiben könnte. Tests sind für die
Gesundheit eines Projekts genauso wichtig wie der Produktionscode. Vielleicht
sind sie sogar noch wichtiger, weil Tests die Flexibilität, Wartbarkeit und Wieder­
verwendbarkeit des Produktionscodes verbessern und sichern. Deshalb sollten Sie
Ihre Tests immer sauber halten. Bemühen Sie sich, sie ausdrucksstark zu formu­
lieren und auf den Punkt zu bringen. Erfinden Sie Test-APis, die als domänenspe­
zifische Sprache agieren und Sie beim Schreiben der Tests unterstützen.
Wenn Sie die Tests verkommen lassen, wird auch Ihr Code verkommen. Halten Sie
Ihre Tests sauber!

1 72
Kapitel 1 o

Klassen

mit ]elf Langr

Bis jetzt haben wir uns in diesem Buch darauf konzentriert, gute Codezeilen und
Codeblöcke zu schreiben. Wir haben uns mit der sauberen Zusammensetzung von
Funktionen und ihren Beziehungen befasst. Doch ausdrucksstarke Anweisungen,
die sauber zu Funktionen zusammengesetzt werden, garantieren noch keinen sau­
beren Code. Wir müssen uns auch um die höheren E benen der Organisation des
Codes kümmern: saubere Klassen.

1 0.1 Klassenaufbau
Folgt man den »Offiziellen« Java-Konventionen, sollte eine Klasse mit einer Liste
von Variablen beginnen. An erster Stelle sollten, falls vorhanden, publ i c stati c
Konstanten stehen. Dann die p r i vate s tati c Variablen, gefolgt von den p ri vate
Instanzvariablen. Gute Gründe für pub l i c Variablen sind selten.
Die publ i c Funktionen sollten nach der Liste der Variablen stehen. Wir ziehen es
vor, p ri vate Utilities, die von einer pub l i c Funktion aufgerufen werden, unmit­
telbar hinter die pub l i c Funktion selbst zu setzen. Damit erfüllen wir die Step­
down-Regel. Es hilft uns und anderen, das Programm wie einen Zeitungsartikel
lesen zu können.

1 73
Kapitel 1o
Klassen

E i n kapsel ung

Unsere Variablen und Utility-Funktionen sollten möglichst p r i vate sein, aber wir
sehen dies nicht dogmatisch. Manchmal müssen wir eine Variable oder Utility­
Funktion als p rotected deklarieren, damit ein Test auf sie zugreifen kann. Für uns
haben Tests Priorität. Wenn ein Test im selben Package eine Funktion aufrufen oder
auf eine Variable zugreifen muss, deklarieren wir sie als p rotected oder mit einem
Package-Geltungsbereich. Doch zunächst suchen wir nach Möglichkeiten, die Pri­
vatheit zu erhalten. Die Einkapselung zu lockern ist immer ein letzter Ausweg.

1 0.2 Klassen sollten klein sei n !


Die erste Regel für Klassen fordert, dass sie klein sein sollten. Die zweite Regel für
Klassen fordert, dass sie nicht noch kleiner sein sollten. Nein, wir werden hier nicht
den genauen Wortlaut aus dem Kapitel Funktionen wiederholen. Aber wie bei Funk­
tionen hat die Forderung, kleine Klassen zu schreiben, Priorität. Und wie bei Funk­
tionen schließt daran immer sofort die Frage an: »Wie klein?«
Bei Funktionen haben wir die Größe gemessen, indem wir die Anzahl der Codezei­
len gezählt haben. Bei Klassen verwenden wir ein anderes Maß . Wir zählen die Ver­
antwortlichkeiten oder Responsibilities [RDD] (Zuständigkeiten, Belange).
Listing ro.r zeigt eine Klasse, S u pe rDashboa rd, die über 70 p u b 1 i c Methoden ver­
öffentlicht. Die meisten Entwickler würden der Meinung zustimmen, dass die
Klasse etwas zu groß geraten ist. Einige Entwickler bezeichnen eine Klasse wie
S u pe rDas h board als »Gott-Klasse«.

Listing 10.1: Zu viele Verantwortlichkeilen


publ i c cl a s s Supe rDashboard extends J F rame i mpl ements MetaDataU se r {
publ i c S t r i ng getCu stomi ze rlanguagePat h ()
publ i c voi d setSystemConfi gPat h (St r i ng s ystemConfi g Path)
publ i c S t r i ng getSystemConfi gDocume n t ( )
publ i c voi d setSystemConfi gDocume n t ( St ri ng systemConfi gDocument)
publ i c bool ean getGu r u State ()
publ i c bool ean getNovi ceStat e ( )
publ i c bool ean getOpe nSou rceState ()
publ i c voi d showObj ect (MetaObj ect obj ect)
publ i c voi d s howP rog re s s (Stri ng s)
publ i c bool ean i sMetadataDi rty ()
publ i c voi d setl sMetadataDi rty (boo l ean i sMetadataDi rty)
publ i c Component get las t FocusedCompo n e n t ( )
publ i c voi d setLast Focused (Component l astFocused)
publ i c voi d setMo u seSel ectState (boo l ean i sMouseS e l e cted)
publ i c boo l ean i sMouseSe l ected ()
publ i c LanguageMan ager getlang u ageManage r ()
publ i c Proj ect get P ro j e c t ( )
publ i c Proj ect getFi r s t P roj e c t ( )

1 74
1Q2
Klassen sollten klein sei n !

publ i c P roj ect getlastProj ect ()


publ i c St r i n g getNewProj ectName ()
publ i c voi d s etComponentSi zes (Di me n s i on d i m)
publ i c St r i n g getCu rrentDi r ()
publ i c voi d s e tC u r re n tDi r (St ri ng n ewDi r)
publ i c voi d updateStatus ( i n t dotPo s , i nt markPos)
publ i c Cl ass [ ] getDataBaseCl a s s e s ( )
publ i c MetadataFeede r getMetadataFeed e r ()
publ i c voi d add Proj ect ( P roj e ct p roj ect)
publ i c boo l ean s etCu rrentProj ect ( P roj ect p roj ect)
publ i c boo l ean remove P roj ect ( Project p r o j e ct)
publ i c MetaPro j e ctHead e r get Prog ramMe tadata ()
publ i c voi d resetDa s h board ()
publ i c Proj ect l oad P roj ect ( S t ri ng fi l eName , S t ri ng p roj ectName)
publ i c voi d s etCan SaveMetadata (boo l ean can Save)
publ i c MetaObj ect getSel ectedOb j e c t ( )
publ i c voi d desel ectObj ect s ( )
publ i c voi d s e t P roj ect (Project p roj ect)
publ i c voi d edi torActi o n ( St ri ng act i o n Name , Act i o n Event event)
publ i c vo i d s etMode ( i nt mode)
publ i c Fi l eManage r getFi l eManag e r ( )
publ i c voi d setFi l eManage r (Fi l eManage r fi l eManager)
publ i c Confi gManage r getConfi gManage r ()
publ i c voi d setConfi gManag e r (Confi gManager confi gManager)
publ i c Cl a s s loade r g etCl a s s Loade r ( )
publ i c voi d s e tCl a s s Loade r (C l a s s loade r c l a s s load e r )
publ i c P rope rti e s getPro p s ( )
publ i c St ri ng getUse rHome ()
publ i c St ri ng getBaseDi r ()
publ i c i nt getMaj o rVe rsi onNumbe r ()
publ i c i nt getMi n o rVe rs i onNumb e r ( )
publ i c i nt getBui l dNumbe r ()
publ i c MetaObj ect pasti n g (
MetaOb j e ct target , MetaObj ect pasted , MetaProj ect p roj ect)
publ i c voi d p rocessMenu items (MetaObj e c t metaObj ect)
publ i c voi d p rocessMe nuSeparators (MetaObject metaObject)
publ i c voi d processTabPage s (MetaObj ect metaObj ect)
publ i c voi d p roce s s Pl acement (MetaObj ect obj e ct)
publ i c voi d processCreatelayout (MetaObj ect obj ect)
publ i c voi d updateDi s p l aylay e r (MetaObj ect obj ect , i nt l ayeri ndex)
publ i c voi d p rope rtyEdi tedRepai n t (MetaObj ect obj ect)
publ i c voi d p rocessDel eteObj ect (MetaObj e ct obj ect)
publ i c boo l ean getAttachedToDes i g n e r ( )
publ i c voi d p roce s s ProjectChangedState (boo l ean h a s P roj ectChanged)
publ i c voi d p roces sObj ectNameChanged (MetaObj ect obj e ct )
publ i c voi d r u n P roj e ct ()
publ i c voi d setA�owDragg i n g (boo l ean a l l owDragg i ng)
publ i c boo l ean al l owDraggi n g ( )
publ i c boo l ean i sCustomi z i n g ( )

1 75
Kapitel 10
Klassen

publ i c voi d setTi t l e (S t ri n g t i t l e)


publ i c IdeMenuBar getldeMe n u Ba r ()
publ i c voi d showHel p e r (MetaObject metaObject , S t r i ng p rope rtyName)
II . . . vi e l e wei t e r e non-publ i c Methoden . . .
}

Aber was wäre, wenn SuperDas hboard nur die Methoden in Listing 10.2 enthielte?

Li stin g 10.2: Klein genug?


publ i c c l ass Supe rDas hboard extends J F rame i mpl ements MetaDataUs e r {
publ i c Component getla s t FocusedCompon e n t ( )
publ i c voi d setlastFocu sed (Component l ast Focused)
publ i c i nt getMa j o rVe rsi onNumbe r ()
publ i c i nt getMi no rVe rsi onNumbe r ()
publ i c i nt getB ui l d Numbe r ()
}

Fü nf M ethoden sind n icht zu viel, oder?

In diesem Fall doch, weil Supe rDas hboard trotz der geringen Anzahl von Metho­
den zu viele Verantwortlichkeiten hat.
Der Name einer Klasse sollte ihre Verantwortlichkeiten beschreiben. Tatsächlich ist
die Wahl des Namens wahrscheinlich die erste Methode, um die Größe einer Klasse
abzugrenzen. Wenn wir keinen prägnanten Namen für eine Klasse finden, ist sie
wahrscheinlich zu groß. Je mehrdeutiger der Klassenname ist, desto eher hat sie zu
viele Verantwortlichkeiten. Beispielsweise geben uns Klassennamen, die Sammel­
ausdrücke wie Processor, Manager oder Super enthalten, oft einen Hinweis auf eine
unglückliche Bündelung von Verantwortlichkeiten.
Wir sollten ebenfalls eine kurze Beschreibung der Klasse in etwa 25 Wörtern schrei­
ben können, ohne die Wörter »wenn«, »und«, »oder« oder »aber« zu verwenden.
Wie würden wir Supe rDas h board beschreiben? »Die Supe rDashboard-Klasse
ermöglicht den Zugriff auf die Komponente, die zuletzt den Fokus hatte, und sie
ermöglicht es uns auch, die Versions- und Build-Nummern zu verfolgen.« Das erste
»und« ist ein Hinweis darauf, dass S u p e rDas hboard zu viele Verantwortlichkeiten
hat.

Das S i ngle-Responsibil ity-Pri nzip

Das Single-Responsibility-Prinzip ( S RP; Eine-Verantwortlichkeit-Prinzip; ausführ­


liche Informationen darüber finden Sie in [PPP]) fordert, dass eine Klasse oder ein
Modul einen und nur einen Grund zur Anderung haben sollte. Dieses Prinzip liefert
uns sowohl eine Definition der Verantwortlichkeit als auch eine Richtlinie für die
Klassengröße. Klassen sollten eine Verantwortlichkeit haben - einen Grund zur
Änderung.
1 0.2
Klassen sollten klein sem!

Die auf den ersten Blick kleine Supe rDashboa rd-Klasse in Listing 10.2 enthält zwei
Gründe zur Änderung. Erstens überwacht sie die Versionsinformationen, die
anscheidend bei jeder Auslieferung der Software aktualisiert werden müssen. Zwei­
tens verwaltet sie Java-Swing-Komponenten. (Sie ist von der Klasse J F rame abge­
leitet, der Swing-Repräsentation eines GUI-Fensters auf der obersten Ebene.)
Zweifellos sollte die Versionsnummer aktualisiert werden, wenn der Swing-Code
geändert wird; aber das Umgekehrte ist nicht unbedingt wahr: Wir können die Ver­
sionsinformationen auch wegen Änderungen an anderem Code im System aktua­
lisieren.
Der Versuch, Verantwortlichkeiten (Gründe zur Änderung) zu identifizieren, hilft
uns oft, bessere Abstraktionen in unserem Code zu erkennen und zu erstellen. Wir
können leicht alle drei Supe r Dash boa r d-Methoden, die mit Versionsinformationen
zu tun haben, in eine separate Klasse namens Ve r s i on extrahieren (siehe Listing
10. 3 ) . Die Ve r s i on-Klasse ist ein Konstrukt, das potenziell in vielen anderen Anwen­
dungen wiederverwendet werden kann!

Listing 10.3: Klasse mit einer einzigen Verantwortlichkeit


publ i c cl ass Ve r s i on {
publ i c i nt getMaj o rVe r s i on N umbe r ()
publ i c i nt getM i n o rVe r s i on Numbe r ()
publ i c i nt getBu i l dNumbe r ()
}

Das SRP zählt zu den wichtigeren Konzepten des 00-Designs. Außerdem ist es
sowohl vom Verständnis als auch von der Befolgung her eines der einfacheren Kon­
zepte. Doch seltsamerweise ist das SRP oft das am meisten missbrauchte Klassen­
design-Prinzip. Wir stoßen regelmäßig auf Klassen, die viel zu viele Aufgaben
erfüllen. Warum?
Software zum Laufen zu bringen und saubere Software zu schreiben, sind zwei
ganz verschiedene Aktivitäten. Da unsere Bewusstseinskapazität grundsätzlich
beschränkt ist, konzentrieren wir uns meist eher darauf, den Code zum Laufen brin­
gen, und weniger auf seinen Aufbau und seine Sauberkeit. Das ist absolut in Ord­
nung. Die Concems (Belange) zu trennen, ist bei unserer Programmiertätigkeit
genauso wichtig wie in unseren Programmen.
Das Problem liegt darin, dass zu viele denken, sie wären fertig, wenn das Programm
funktioniert. Sie versäumen es dann, sich um die anderen Concems der Organisa­
tion und Sauberkeit zu kümmern, und wenden sich dem nächsten Problem zu, statt
zurückzugehen und ihre übervollen Klassen in Einheiten zu zerlegen, die nur noch
eine einzige Verantwortlichkeit haben.
Zugleich fürchten viele Entwickler, dass eine große Zahl kleiner, auf einen Zweck
beschränkter Klassen das Verstehen des Großen und Ganzen erschwere. Sie sind
besorgt, dass sie von Klasse zu Klasse navigieren müssen, um herauszufinden, wie
eine größere Komponente des Ganzen funktioniert.

177
Kapitel 1o
Klassen

Doch ein System mit vielen kleinen Klassen hat nicht mehr bewegliche Teile als ein
System mit einigen wenigen großen Klassen. Bei einem System mit einigen weni­
gen großen Klassen muss man genauso viel lernen. Deshalb lautet die Frage: Haben
Sie lieber einen Werkzeugkasten mit vielen kleinen Fächern, in dem alle Werkzeuge
sauber geordnet und beschriftet aufbewahrt werden? Oder ziehen Sie es vor, alles
zusammen in einige wenige große Fächer zu werfen?
Jedes größere System enthält eine umfangreiche Logik und Komplexität. Das
Hauptziel bei der Verwaltung einer solchen Komplexität besteht darin, sie so zu
organisieren, dass ein Entwickler weiß, wo er nachschauen muss, um bestimmte
Dinge zu finden, und jeweils nur die direkt betroffene Komplexität verstehen muss.
Dagegen behindert uns ein System mit größeren Mehrzweckklassen immer, weil
es uns zwingt, durch zahlreiche Dinge zu waten, die wir im Moment gar nicht wis­
sen müssen.
Um diesen Punkt noch einmal zu betonen: Unsere Systeme sollten aus vielen klei­
nen Klassen, nicht aus wenigen großen aufgebaut sein. Jede kleine Klasse kapseit
eine einzige Verantwortlichkeit ein, hat einen einzigen Grund zur Änderung und
arbeitet mit wenigen anderen zusammen, um das gewünschte Systemverhalten zu
realisieren.

Kohäsion

Klassen sollten eine kleine Anzahl von Instanzvariablen haben. Jede Methode einer
Klasse sollte eine oder mehrere dieser Variablen manipulieren. Im Allgemeinen
hängen eine Methode und eine Klasse um so kohäsiver zusammen, je mehr Vari­
ablen die Methode manipuliert. Eine Klasse, in der jede Variable von jeder Methode
verwendet wird, ist maximal kohäsiv.
Im Allgemeinen ist es weder ratsam noch möglich, solche maximal kohäsiven Klas­
sen zu erstellen; andererseits soll die Kohäsion hoch sein. Eine hohe Kohäsion ist
ein Kennzeichen dafür, dass die Methoden und Variablen der Klasse als logische
Gesamtheit voneinander abhängen.
Betrachten Sie die Implementierung von Stack in Listing ro-4- Dies ist eine hoch
kohäsive Klasse. Von den drei Methoden verwendet nur s i z e () nicht beide Vari­
ablen.

Listin g 10.4: Stac k . j ava, eine kohäsive Klasse


publ i c c l ass Stack {
p r i vate i nt topOfStack = 0 ;
Li st<In tege r> e l emen t s = new Li n kedli st<In tege r> ( ) ;

publ i c i n t s i z e () {
r e t u r n topOfStac k ;
}
1 0.2
Klassen sollten klein sei n !

publ i c voi d push (i n t e l ement) {


topOfStac k++ ;
e l ement s . add (el ement) ;
}

publ i c i nt pop() t h r ows PoppedWhen Empty {


i f (topOfStack == 0)
t h row new PoppedWhenEmpt y ( ) ;
i nt e l ement = e l ements . ge t ( - -topOfStac k) ;
e l ements . remove (topOfStack) ;
return e l eme n t ;
}
}

Die Strategie, kleine Funktionen zu schreiben und kurze Parameterlisten z u ver­


wenden, kann manchmal zu einer Proliferation (Vervielfältigung) von Instanzvari­
ablen führen, die nur von einer Teilmenge der Methoden verwendet werden. Dies
ist fast immer ein Zeichen dafür, dass wenigstens eine andere Klasse versucht, aus
der größeren Klasse auszubrechen. Sie sollten dann versuchen, die Variablen und
Methoden so auf zwei oder mehr Klassen zu verteilen, dass die neuen Klassen eine
höhere Kohäsion aufweisen.

Kohäs ion zu erhalten, fü hrt zu vielen klei nen Klassen

Allein die Zerlegung großer Funktionen in kleinere Funktionen führt zu einer Pro­
liferation von Klassen. Betrachten Sie eine große Funktion, in der viele Variablen
deklariert werden. Angenommen, Sie wollten einen kleinen Teil dieser Funktion in
eine separate Funktion auslagern. Doch der Code, den Sie extrahieren wollen, ver­
wendet vier Variablen, die in der Funktion deklariert sind. Müssen Sie diese vier
Variablen alle als Argumente an die neue Funktion übergeben?
Überhaupt nicht! Wenn wir diese vier Variablen zu Instanzvariablen der Klasse
befördern, könnten wir den Code extrahieren, ohne irgendeine Variable zu überge­
ben. Es wäre leicht, die Funktion in kleinere Teile zu zerlegen.
Leider bedeutet dies auch, dass die Kohäsion unserer Klassen schwächer wird, weil
sich immer mehr Instanzvariablen ansammeln, die nur existieren, damit sie von
einigen Funktionen gemeinsam genutzt werden können. Doch ist nicht gerade die
Tatsache, dass einige Funktionen bestimmte Variablen gemeinsam nutzen wollen,
ein Zeichen dafür, dass sie in eine separate Klasse gehören? Natürlich! Wenn Klas­
sen ihre Kohäsion verlieren, sollten Sie sie aufteilen!
Deshalb gibt uns die Zerlegung einer großen Funktion in viele kleinere Funktionen
oft die Gelegenheit, auch mehrere kleinere Klassen herauszulösen. Damit verbes­
sern wir die Struktur und Transparenz unseres Programms.
Um zu demonstrieren, was ich meine, möchte ich ein bewährtes Beispiel aus dem
wundervollen Buch Literate Progra mming von Donald Knuth [Knuth92] verwenden.

1 79
Kapitel 10
Klassen

Listing ro.5 zeigt eine Übersetzung von Knuths P r i n t P ri mes-Programm in Java.


Fairerweise muss man sagen, dass dies nicht das Programm ist, das Knuth geschrie­
ben hat, sondern die Version, die von seinem WEB-Tool generiert wurde. Ich ver­
wende dieses Programm, weil es hervorragend als Ausgangspunkt dient, um zu
zeigen, wie eine große Funktion in viele kleinere Funktionen und Klassen zerlegt
werden kann.

Listing 1 0.5: Pri ntPri me s . j ava


package l i te rate P ri mes ;

publ i c c l ass Pri n t P r i me s {


publ i c stati c voi d mai n (S t r i ng [ ] args) {
fi nal i n t M = 1000 ;
fi nal i n t RR = 50 ;
fi nal i n t CC = 4 ;
fi nal i n t WW = 10 ;
fi nal i n t ORDMAX = 3 0 ;
i n t P [] = new i n t [M + 1] ;
i n t PAGENUMBE R ;
i n t PAGEOFFSET ;
i n t ROWOFFSET ;
i nt C ;
int J ;
i nt K ;
boo l ean J PRIME ;
i nt ORD ;
i nt SQUARE ;
i nt N ;
i n t MULT [ ] = new i n t [ORDMAX + 1] ;

J = 1;
K = 1;
P [ 1] = 2 ;
ORD = 2 ;
SQUARE = 9 ;

whi l e (K < M) {
do {
J = J + 2;
i f ( J == SQUARE) {
ORD = ORD + 1 ;
SQUARE = P [ORD] * P [ORD] ;
MULT [ORD - 1] = J ;
}
N = 2;
J PRIME = t rue ;
whi l e (N < ORD && J PRIME) {
whi l e (MULT [N] < J )
MULT [N] = MULT [N] + P [N ] + P [N ] ;

1 80
1 0.2
Klassen sollten klein sein !

i f (MULT [N] == J )
J PRIME = fal s e ;
N = N + 1;
}
} whi l e ( ! J PRIME) ;
K = K + 1;
P [K] = J ;
}
{
PAG ENUMBER = 1 ;
PAGEOFFSET = 1 ;
whi l e ( PAGEOFFSET <= M) {
Syst em . ou t . p ri n t l n ( "The Fi rst " + M +
" P r i me Numbers - - - Page " + PAGENUMBER) ;
System . o u t . p r i n t l n (" " ) ;
for (ROWOFFSET = PAGEOFFSET ; ROWOFFSET < PAGEOFFSET + RR ; ROWOFFSET ++) {
fo r (C = 0 ; C < CC ; C++)
i f (ROWOFFSET + C * RR <= M)
System . ou t . fo rmat ( "%10d " , P [ROWOFFSET + C * RR] ) ;
System . ou t . p r i n tl n ( " " ) ;
}
System . o u t . p ri n t l n ( " \f") ;
PAGENUMBER = PAGENUMBER + 1 ;
PAGEOFFSET = PAGEOFFSET + R R * CC ;
}
}
}
}

Dieses Programm, geschrieben als eine einzige Funktion, ist chaotisch. Es verfügt
über eine tief verschachtelte Struktur, zahlreiche seltsame Variablen und eine stark
gekoppelte Struktur. Zumindest sollte die eine große Funktion in einige kleinere
Funktionen zerlegt werden.
Die Listings 10 .6 bis 10.8 zeigen das Ergebnis einer Zerlegung des Codes aus Listing
10.5 in kleinere Klassen und Funktionen sowie der Wahl aussagefahigerer Namen
für diese Klassen, Funktionen und Variablen.

Listing 10.6: PrimePri nter . j ava (nach Refactoring)


package l i te rate P r i me s ;

publ i c c l as s Pri me P r i n t e r {
publ i c stati c voi d mai n (S t r i n g [ ) args) {
fi nal i nt NUMBER_OF_PRIMES = 1000 ;
i nt [) p r i me s = Pri meGene rato r . gene rate (NUMBER_OF_PRIMES) ;

fi nal i nt ROWS_PER_PAGE = SO ;
fi nal i nt COLUMNS_PER_PAGE = 4 ;
RowCo l umn Page P r i nte r tabl e P r i n t e r =
Kapitel 10
Klassen

new RowCol umnPag e P r i n t e r ( ROWS_PER_PAGE ,


COLUMNS_PER_PAGE ,
"The Fi r s t " + NUMBER_OF_PRIMES +

" P ri me Numbe r s " ) ;


tabl e P r i n t e r . p ri n t ( p r i me s ) ;
}
}

Listing 10.7: RowCol umnPagePri nter . j ava


package l i t e rate P r i mes ;

i mport j ava . i o . P ri ntSt ream ;

publ i c cl a s s RowCo l umn PageP ri n t e r {


p r i vate i nt rowsPe rPage ;
p r i vate i n t col umn sPe r Page ;
p r i vate i n t numbe rsPe r Page ;
p r i vate S t r i ng pageHead e r ;
p r i vate P r i ntSt ream p r i n tS t ream ;

publ i c RowCol umnPag e P r i n te r (i n t rows Pe r Page ,


i nt col umn s Pe r Page ,
St ri ng pageHeader) {
thi s . rows Pe r Page = rows Pe r Page ;
thi s . col umnsPe r Page = col umn s Pe r P ag e ;
t h i s . pageHeade r = pageHead e r ;
n umbe r s P e r Page = rows Pe r Page * col umn s Pe r Page ;
p r i ntSt ream = System . out ;
}

publ i c voi d pri n t (i n t data [ ] ) {


i nt pageNumb e r = 1 ;
fo r ( i n t fi r s t lndexOn Page = 0 ;
fi r s t l ndexOn Page < data . l ength ;
fi r s t lndexOn Page += numbe r s Pe r Page) {
i nt l as tl ndexOn Page =
Math . mi n (fi rstlndexOn Page + n umbe r s P e r Page - 1 ,
data . l ength - 1) ;
p r i n t PageHead e r (pageHead e r , pageNumbe r) ;
p r i n tPag e (fi rstindexOn Pag e , l as tl n d exOnPag e , data) ;
p r i ntSt ream . p r i ntl n ( "\f" ) ;
pageNumbe r++ ;
}
}

p r i vate voi d p r i n t Pag e ( i n t fi r s tl n dexOn Page ,


i n t l as t lndexOnPag e ,
i n t [] data) {
i n t fi r s t lndexOflas tRowOn Page =
fi r s t lndexOn Page + rows Pe r Page - 1 ;
1 0.2
Klassen sollten klein sein!

for ( i n t fi r stindexin Row = fi r s t in dexOn Page ;


fi rs tindexinRow <= fi rstindexOflastRowOn Page ;
fi rstindexin Row++) {
p r i ntRow (fi rstindexinRow , l astindexOn Page , data) ;
p r i ntStream . p r i n tl n ( " " ) ;
}
}

p r i vate voi d p r i n t Row ( i n t fi rstindexinRow ,


i nt l astindexOn Page ,
i nt [] data) {
for ( i nt col umn = 0 ; col umn < col umn s Pe rPage ; col umn++) {
i n t i ndex = fi rstindexinRow + col umn * rows Pe r Page ;
i f ( i ndex <= l astindexOn Page)
p r i ntSt ream . format ( "%10d " , data [ i n dex] ) ;
}
}

p r i vate voi d p r i nt PageHead e r (S t r i ng pageHeade r ,


i n t pageNumbe r) {
p r i ntSt ream . p r i ntl n (pageHeade r + " - - - Page " + pageNumbe r) ;
p r i ntSt ream . p ri ntl n ( " " ) ;
}

publ i c voi d setOutp u t ( P r i ntSt ream p r i n t S t r eam) {


th i s . pr i ntSt ream = p r i ntSt ream ;
}
}

Listing 10.8: Pri meGenerator . j ava


package l i te rate P r i me s ;

i mport j ava . ut i l . Ar rayli st ;

publ i c c l ass P r i meGene rato r {


p ri vate stat i c i n t [ ] p r i me s ;
p r i vate stat i c A r rayli st<Integer> mul t i p l e sOfPri meFacto rs ;

p rotected s t at i c i nt [] gen e rate (i n t n) {


p r i mes = n ew i nt [ n ] ;
mul t i pl e sOfPri me Facto rs = new A r rayli s t<Integer> () ;
s et2AsFi r s t P r i me () ;
checkOddNumbe r s Fo rS u b seque n t P r i me s () ;
retu rn p r i mes ;
}

p r i vate stat i c voi d s et2As F i r s t P r i me () {


p r i mes [O] = 2 ;
mul t i pl esOfPri meFactors . add (2) ;
}
Kapitel 1 0
Klassen

p r i vate s t at i c voi d checkOddNumbe r s Fo rSubsequen t P r i mes () {


i nt p ri melndex = 1 ;
fo r ( i nt candi date = 3 ;
p r i melndex < p r i me s . l e ngth ;
cand i date += 2) {
i f ( i s P r i me (cand i date) )
p r i mes [ p r i meindex++] = cand i date ;
}
}

p r i vate s t at i c boo l ean i s P r i me (i n t candi date) {


i f (i s LeastRe l evantMu l ti pl eOfNextlarge r P r i meFacto r ( candi d ate) ) {
mu l t i pl esOfPri me Facto r s . add (candi d ate) ;
retu r n fal s e ;
}
r e t u r n i s NotMu l t i pl eOfAn y P revi o u s P r i me Facto r ( candi date) ;
}

p ri vate s tati c boo l ean


i sleastRel evantMu l ti pl eOfNextLa rge r P r i meFacto r ( i n t can d i date) {
i nt nextlarge r P r i me Facto r = p r i me s [m u l t i p l esOfP r i me Facto r s . s i ze () ] ;
·•
i nt l eastRel evantMul ti pl e = nextlarge rPri meFactor nextlarge r P ri meFacto r ;
r et u r n candi date = = l eastRel evantMu l t i pl e ;
}

p ri vate stat i c bool ean


i sNotMu l t i pl eOfAny P r evi ou s P ri me Facto r ( i n t cand i date) {
for ( i nt n = 1 ; n < m u l t i p l e sOfPri me Facto r s . s i z e ( ) ; n++) {
i f (i sM u l ti p l eOfN th P r i me Facto r (candi date , n ) )
return fal s e ;
}
retu rn t rue ;
}

p r i vate s tati c boo l ean


i sMu l t i pl eOfNt h P r i me Facto r ( i n t candi date , i nt n ) {
return candidate = smal l estOddNthMul t i pl eNotlessThanCandi date(candi date , n) ;
}

p r i vate s tati c i nt
smal l es tOddNthMu l t i pl eNotle s sThanCandi date ( i n t candi date , i nt n) {
i nt m u l t i p l e = mul ti p l esOfPri meFacto r s . get (n) ;
wh i l e (mu l t i p l e < candi date)
m u l ti p l e += 2 * p r i me s [ n ] ;
mul ti p l esOfPri meFacto r s . s et ( n , m u l t i p l e ) ;
retu r n m u l t i p l e ;
}
}
10.3
Änderungen e i n planen

Vielleicht fällt Ihnen zunächst auf, dass das Programm länger geworden ist. War es
vorher etwas über eine Seite lang, braucht es j etzt fast drei Seiten. Dafür gibt es meh­
rere Gründe. Erstens verwendet das refaktorisierte Programm längere, aussagekräf­
tigere Variablennamen. Zweitens verwendet das refaktorisierte Programm
Funktionen- und Klassendeklarationen als Möglichkeit, den Code zu kommentie­
ren. Drittens verwenden wir Whitespace und Formatierungstechniken, um die Les­
barkeit des Programms zu erhalten.
Beachten Sie, wie das Programm in drei Hauptverantwortlichkeiten zerlegt worden
ist. Das Hauptprogramm steht ganz allein in einer separaten P r i me P ri nte r-Klasse.
Es ist für die Verwaltung der Ausführungsumgebung zuständig. Es ändert sich,
wenn sich die Methode seines Aufrufs ändert. Würde dieses Programm beispiels­
weise in einen SOAP-Service umgewandelt, wäre diese Klasse davon betroffen.
Die Klasse RowCol umnPageP r i nte r weiß alles über die Formatierung einer Liste
von Zahlen in Form von Seiten mit einer bestimmten Anzahl von Zeilen und Spal­
ten. Müsste die Formatierung des Outputs geändert werden, wäre diese Klasse
betroffen.
Die Klasse P r i meGene rato r weiß, wie man eine Liste mit Primzahlen generiert.
Beachten Sie, dass die Klasse nicht als Objekt instanziert werden soll. Sie ist nur ein
nützlicher Geltungsbereich, in dem ihre Variablen deklariert und verborgen werden
können. Diese Klasse ändert sich, wenn der Algorithmus zur Berechnung der Prim­
zahlen geändert wird.
Das Programm wurde nicht neu geschrieben! Wir begannen nicht von vorne und
schrieben das Programm noch einmal. Wenn Sie sich die beiden Programme
anschauen, werden Sie feststellen, dass sie ihre Aufgaben mit denselben Algorith­
men und Techniken erfüllen.
Die Änderungen wurden gemacht, indem wir eine Test-Suite schrieben, die die
genaue Verhaltensweise des ersten Programms verifizierte. Dann wurden zahlrei­
che Änderungen nacheinander durchgeführt. Nach jeder Änderung wurde das Pro­
gramm ausgeführt, um zu prüfen, dass sich sein Verhalten nicht geändert hatte. In
kleinen aufeinanderfolgenden Schritten wurde das erste Programm bereinigt und
in das zweite transformiert.

1 0.3 Änderungen einplanen


Die meisten Systeme werden laufend geändert. Jede Änderung setzt uns dem
Risiko aus, dass der Rest des Systems nicht mehr wie beabsichtigt funktioniert. In
einem sauberen System sind die Klassen so strukturiert, dass dieses Risiko bei
Änderungen reduziert wird.
Mit der Sq l -Klasse in Listing r o . 9 werden aus entsprechenden Metadaten korrekt
formulierte S Q L- Strings generiert. Das Programm befindet sich noch in der Ent-
Kapitel 1o
Klassen

wicklung. Deshalb unterstützt es bestimmte S Q L-Funktionen wie etwa U pd ate­


Anweisungen noch nicht. Wenn wir diese Funktionalität in die Sq 1 -Klasse einbauen
wollen, müssen wir diese Klasse »öffnen«. Das öffnen einer Klasse ist immer mit
einem Risiko verbunden. Jede Änderung der Klasse kann potenziell anderen Code
in ihr beschädigen. Sie muss komplett neu getestet werden.

Listing 10.9: Eine Klasse, die zwecks Änderung geöffnet werden muss
publ i c c l ass Sql {
publ i c Sql (St ri ng tabl e , Col umn [ ] col umns)
publ i c St ri ng c reate ()
publ i c St ri ng i n s e r t (Obj e c t [ ] fi e l d s )
publ i c St ri ng sel ectAl l ()
publ i c St ri ng fi ndByKey (St r i ng keyCo l umn , S t r i ng keyVal ue)
publ i c St ri ng sel ect (Col umn col umn , St ri ng patte rn)
publ i c St ri ng sel ect(Cri teri a c r i te r i a)
publ i c St ri ng p repared ! n s e rt ()
p r i vate S t r i ng col umnLi s t (Col umn [ ] col umns)
p ri vate S t r i n g val u e s Li s t (Obj ect [] fi el ds , fi n al Col umn [] col umn s)
p ri vate S t ri ng sel ectWi thCri t e r i a ( S t r i ng c r i te r i a)
p ri vate S t r i ng pl acehol d e r L i s t (Col umn [ ] col umn s )
}

Die Sql -Klasse muss geändert werden, wenn wir einen neuen Anweisungstyp hin­
zufügen. Sie muss ebenfalls geändert werden, wenn wir die Details eines einzigen
Anweisungstyps ändern - etwa wenn wir die se 1 ect-Funktion ändern müssen,
damit sie Subselects unterstützt. Diese beiden Änderungsgründe bedeuten, dass
die Sql -Klasse gegen das S RP verstößt.
Wir können diese SRP-Verletzung rein organisatorisch erkennen. Die Methoden­
aufzählung von Sql zeigt, dass es private Methoden, wie etwa se l e ctWi thCri ­
te r i a, gibt, die sich anscheinend nur auf s e 1 ect-Anweisungen beziehen.
Das Verhalten von p r i vate Methoden, die nur einer kleinen Untermenge einer
Klasse dienen, kann uns nützliche Hinweise auf Verbesserungsmöglichkeiten lie­
fern. Doch der Hauptanstoß für Aktionen sollte von den Systemänderungen selbst
kommen. Wenn die Sql -Klasse als logisch vollständig eingestuft wird, müssen wir
uns keine Gedanken über die Trennung der Verantwortlichkeiten machen. Wenn
wir die u pdate-Funktionalität in absehbarer Zukunft nicht brauchen, sollten wir
Sql in Ruhe lassen. Doch sobald wir vor der Aufgabe stehen, eine Klasse zu öffnen,
sollten wir überlegen, wie wir unser Design verbessern können.
Was wäre, wenn wir eine Lösung wie die in Listing IO.IO ins Auge fassen würden?
Jede Sq 1 -Methode mit einer öffentlichen Schnittstelle aus dem vorhergehenden Lis­
ting 10.9 wurde durch Refactoring in eine separate abgeleitete Klasse der Sq 1 -Klasse
umgewandelt. Beachten Sie, dass p r i vate Methoden, wie etwa va l u e s l i st, direkt

186
10.3
Änderungen ei nplanen

dort hin gewandert sind, wo sie gebraucht werden. Das gemeinsame private Ver­
halten wurde in zwei Utility-Klassen, Where und Co 1 umnl i st, isoliert.

Listing 10.10: Ein Satz geschlossener Klassen


abst ract publ i c cl ass Sq l {
publ i c Sql ( St r i ng tabl e , Col umn [ ] col umns)
abs t ract publ i c S t r i ng gene rate () ;
}

publ i c c l ass C reateSql extends Sq l {


publ i c C reateSq l ( St r i ng tabl e , Col umn [] col umn s )
@Ove r r i de publ i c S t r i ng g e n e rate ()
}

publ i c c l ass Sel ectSql extends Sql {


publ i c Sel ectSq l ( St r i ng tabl e , Col umn [] col umns)
@Ove r ri de publ i c S t r i ng gene rate ()
}

publ i c c l a s s I n s e rtSql extends Sql {


publ i c I n s e rt Sq l ( St r i ng tabl e , Col umn [] col umns , Obj ect [ ] fi e l d s )
@Ove r ri d e publ i c St r i ng gene rate ()
p r i vate St r i ng val u e s li s t (Obj ect [ ] fi el ds , fi nal Col umn [ ] col umns )
}

publ i c cl as s Sel ectWi thC r i t e r i aSql extends Sql {


publ i c Sel ectWi thC r i t e r i aSql (
S t r i ng tabl e , Col umn [ ] col umn s , C ri t e ri a c ri te r i a)
@Ove r r i de publ i c S t r i ng gene rate ()
}

publ i c c l ass Sel ectWi thMatchSql extends Sql {


publ i c Sel ectWi thMatchSq l (
St r i ng tabl e , Col umn [] col umns , Col umn col umn , St ri ng patt e r n )
@Ove r r i de publ i c St r i ng generat e ()
}

publ i c c l ass Fi ndByKeySq l extends Sq l


publ i c Fi ndByKeySq l (
S t ri ng tabl e , Col umn [ ] col umn s , S t ri ng keyCol umn , S t r i ng keyVal u e )
@Ove r r i de publ i c S t r i ng gene r at e ()
}

publ i c cl ass P repared ! n s e rtSql extends Sql {


publ i c Prepared!nse rtSql ( St r i ng tabl e , Col umn [] col umn s )
@Ove r r i de publ i c St r i ng gene rat e () {
p r i vate S t ri ng pl acehol de r l i s t (Col umn [ ] col umns)
}
Kapitel 1 0
Klassen

publ i c c l ass Whe r e {


publ i c Whe re (St r i n g c r i t e r i a)
publ i c St ri ng gene rate ()
}

publ i c c l ass Col umnLi s t {


publ i c Col umnLi st (Col umn [ ] col umns)
publ i c St ri ng gen e rate ()
}

Der Code in den einzelnen Klassen ist jetzt außergewöhnlich einfach. Der Zeitauf­
wand, um eine Klasse zu verstehen, ist praktisch aufnull gesunken. Das Risiko, dass
eine Funktion eine andere beschädigen könnte, ist auch fast null. Und es ist leichter
geworden, alle Komponenten dieser Lösung zu testen, da die Klassen j etzt alle von­
einander isoliert sind.
Und was gleichermaßen wichtig ist: Wenn die update-Anweisungen hinzugefügt
werden sollen, muss keine vorhandene Klasse geändert werden! Die Logik für die
Erstellung von u pdate-Anweisungen wird einfach in eine neue Unterklasse von
Sql namens UpdateSql eingefügt. Anderer Code in dem System wird durch diese
Änderung nicht beschädigt.
Unsere umstrukturierte Sq l -Logik repräsentiert die beste aller Welten. Sie unter­
stützt das SRP. Sie unterstützt ebenfalls ein anderes Schlüsselprinzip des 00-Klas­
sendesigns, nämlich das Open-Closed-Prinzrp oder OCP [PPP]: Klassen sollten offen
für Erweiterungen, aber geschlossen für Änderungen sein. Unsere umstrukturierte
Sq l -Klasse ist offen für neue Funktionalitäten, die durch Unterklassen zur Verfü­
gung gestellt werden. Doch diese Änderungen können erfolgen, während alle ande­
ren Klassen geschlossen bleiben. Wir fügen einfach unsere UpdateSql -Klasse zu
dem System hinzu.
Die Struktur eines Systems sollte so beschaffen sein, dass wir sie so wenig wie mög­
lich ändern müssen, wenn wir neue Funktionen hinzufügen oder vorhandene
ändern. Idealerweise sind neue Funktionen einfach Erweiterungen des Systems,
die den vorhandenen Code nicht ändern.

Änderu ngen isol ieren

Anforderungen ändern sich, deshalb ändert sich der Code. Wir haben bei der Ein­
führung in die 00 gelernt, dass es konkrete Klassen gibt, die Implementierungs­
details (Code) enthalten, und abstrakte Klassen, die nur Konzepte repräsentieren.
Eine Client-Klasse, die von konkreten Details abhängt, ist gefährdet, wenn sich diese
Details ändern. Wir können Interfaces und abstrakte Klassen einführen, die dazu
beitragen, die Auswirkungen dieser Details zu isolieren.
Dependencies von konkreten Details verursachen zusätzliche Probleme beim Tes­
ten des Systems. Wenn wir eine Po rtfo l i o-Klasse erstellen und die von einem

1 88
1 0-3
Änderungen ei nplanen

externen TokyoStoc kExchange-API abhängt, um den Wert des Portfolios zu


berechnen, werden unsere Testfälle von der Volatilität eines solchen Lookups
(Datenabrufs) beeinflusst. Es ist schwer, einen Test zu schreiben, wenn wir alle fünf
Minuten eine andere Antwort erhalten!
Anstatt Portfol i o so zu konzipieren, dass die Klasse direkt von TokyoStoc kEx­
change abhängt, erstellen wir ein Interface, StockExchange, dass eine einzige
Methode deklariert:

publ i c i nt e rface StockEx change {


Money c u r rent P r i c e (St ri ng symbo l ) ;
}

Wir konzipieren TokyoStockExchange so, dass diese Klasse dieses Interface im­
plementiert. Wir sorgen auch dafür, dass der Konstruktor von Po rtfol i o eine
StockExchange-Referenz als Argument übernimmt:

publ i c Portfol i o {
p r i vate StockExchange exchan ge ;
publ i c Portfo l i o ( StockExch ange exchange) {
t h i s . exchange =exchange ;
}
II
}

Jetzt kann unser Test eine testbare Implementierung des StockExchange-Interface


erstellen, die die To kyoStoc kExchange-Klasse emuliert. Diese Testimplementie­
rung hält den gegenwärtigen Wert aller Symbole fest, die wir beim Testen verwen­
den. Wenn unser Test den Kauf von fünf Microsoft-Aktien für unser Portfolio
demonstriert, codieren wir die Testimplementierung so, dass sie immer 100 OS­
Dollar pro Microsoft-Aktie zurückgibt. Unsere Testimplementierung des StockEx­
change-Interface ist auf ein einfaches Tabellen-Lookup geschrumpft. Dann können
wir einen Test schreiben, der 500 US-Dollar als Gesamtwert des Portfolios erwartet.

publ i c c l ass Po rtfo l i oTes t {


p r i vate Fi xedStockExchangeStub exchang e ;
p r i vate Portfol i o po rtfol i o ;

@Befo r e
p rotected voi d setU p () th rows Excepti o n {
exchange = n ew Fi xedStockExchangeSt u b ( ) ;
exchange . fi x ( " M S FT" , 100) ;
po rtfol i o =n ew Portfol i o (exchange) ;
}

@Tes t
publ i c voi d G i v en Fi veMS FTTotal Shou l dße SOO () t h rows Excepti on {
po rtfol i o . add ( S , " M S FT" ) ;

189
Kapitel 1o
Klassen

As s e r t . asse rtEqual s ( SOO , portfo l i o . val u e () ) ;


}
}

Wenn ein System so weit entkoppelt ist, dass es auf diese Weise getestet werden
kann, ist es auch flexibler und kann leichter wiederverwendet werden. Die schwache
Kopplung bedeutet, dass die Elemente unseres Systems besser voneinander und
von Änderungen isoliert sind. Durch diese Isolierung ist es leichter, alle Elemente
des Systems zu verstehen.
Indem wir so die Kopplung minimieren, sorgen wir dafür, dass unsere Klassen ein
anderes Prinzip des Klassendesigns erfüllen, das so genannte Dependency-Inversion­
Prinzip (DIP; Prinzip der Abhängigkeitsumkehr; [PPP]) . Im Wesentlichen fordert
das DIP, dass unsere Klassen von Abstraktionen, nicht von konkreten Details
abhängen sollten.
Unsere Po rtfol i o-Klasse ist jetzt nicht mehr von den Implementierungsdetails
der TokyoStockExchang e-Klasse, sondern von dem Stoc kEx c h ange-Interface
abhängig. Das StockExchange-Interface repräsentiert das abstrakte Konzept, den
aktuellen Kurs eines Symbols (einer Aktie) abzurufen. Diese Abstraktion isoliert alle
speziellen Details des Kursabrufs, einschließlich seines Herkunftsortes.
Kapitel 1 1

Systeme

von Dr. Kevin Dean

»Komplexität tötet. Sie saugt den Entwicklern das Leben aus und erschwert
das Planen, Erstellen und Testen von Produkten.«
- Ray Ozzie, CTO, Microsoft Corporation

1 1 .1 Wie baut man eine Stadt?


Könnten Sie alle Details selbst gestalten? Wahrscheinlich nicht. Selbst die Verwal­
tung einer vorhandenen Stadt ist zu viel für eine Person. Dennoch funktionieren
Städte (meistens) . Sie funktionieren, weil Städte über Teams von Entwicklern ver­
fügen, die spezielle Aspekte der Stadt verwalten: die Wasserversorgung, die Strom­
versorgung, den Verkehr, die Einhaltung der Gesetze, Bauvorschriften usw. Einige
dieser Entwickler sind für das Gesamtbild verantwortlich, andere kümmern sich um
die Details.
Kapitel 11
Systeme

Städte funktionieren auch deshalb, weil sie geeignete Abstraktionsebenen und Teil­
bereiche entwickelt haben, die es einzelnen Personen und den von ihnen verwalte­
ten »Abteilungen (Kammern, Ämtern)« ermöglichen, effizient zu arbeiten, auch
ohne das Gesamtbild zu verstehen.
Obwohl Software-Teams oft ähnlich aufgebaut sind, enthalten die Systeme, an
denen sie arbeiten, häufig nicht dieselbe Trennung von Zuständigkeiten und Ab­
straktionsebenen. Sauberer Code hilft uns, dies auf den unteren Abstraktionsebe­
nen zu erreichen. In diesem Kapitel wollen wir überlegen, wie wir aufden höheren
Abstraktionsebenen, der Systemebene, sauber bleiben können.

1 1 .2 Konstru ktion u nd Anwend u ng eines Systems trennen


Zunächst müssen wir erkennen, dass Konstruktion und Anwendung ganz verschie­
dene Prozesse sind. Während ich dies schreibe, blicke ich durch mein Fenster in
Chicago aufeine Baustelle, auf der ein neues Hotel errichtet wird. Heute ist es ein
unverkleideter Betonkasten mit einem Baukran und einem Fahrstuhl, der an einer
Außenwand befestigt ist. Die geschäftigen Ingenieure und B auarbeiter tragen alle
schwere Schutzhelme und Arbeitskleidung. In etwa einem Jahr wird das Hotel fer­
tig sein. Der Kran und der Aufzug werden verschwunden sein. Das Gebäude wird
sauber und die Wände werden mit Glasfenstern verkleidet und attraktiv angestri­
chen sein. Die Personen, die sich dann dort aufhalten werden, werden ganz anders
aussehen.
Software-Systeme sollten den Startup-Prozess, wenn die Anwendungsobjekte
konstruiert und die Abhängigkeiten »verdrahtet« werden, von der Laufzeit­
Logik trennen, die nach dem Startup die Kontrolle übernimmt.
Der Startup-Prozess ist ein Cancern (Belang, Verantwortlichkeit, Zuständigkeit) , der
bei jeder Anwendung geregelt sein muss. Es ist der erste Cancern, den wir in diesem
Kapitel untersuchen werden. Die Trennung der Concerns (Trennung der Verantwort­
lichkeiten) gehört zu den ältesten und wichtigsten Design-Techniken unserer
Zunft.

Leider behandeln die meisten Anwendungen diesen Concem nicht separat. Der
Code für den Startup-Prozess wird ad hoc geschrieben und mit der Laufzeit-Logik
vermischt. Hier ist ein typisches Beispiel:

publ i c S e rvi ce getSe rvi c e ( ) {


i f ( s e rvi ce == nul l )
s e rv i c e = new MySe rvi ceimpl ( . . . ) ; II I n den mei s ten Fäl l e n aus rei ­
chend?
return s e rvi c e ;
}

Dies ist das Idiom der Lazy InitializationjEvaluation, das mehrere Vorteile bietet:
Der zusätzliche Aufwand für die Konstruktion eines Objekts fallt erst dann an, wenn

1 92
1 1 .2
Konstruktion und Anwendung eines Systems trennen

das Obj ekt tatsächlich verwendet wird. Folglich kann das Startu.p entsprechend
schneller erfolgen. Außerdem sorgen wir dafür, dass niemals n u l l zurückgegeben
wird.
Doch jetzt gibt es eine fest eincodierte Dependency (Abhängigkeit) von MySe rvi c e ­
I m p l und allem, was deren Konstruktor benötigt (was ich ausgelassen habe) . Ohne
diese Dependencies können wir den Code nicht kompilieren, selbst wenn wir zur
Laufzeit kein Objekt dieses Typs verwenden!
Das Testen kann ein Problem sein. Wenn My Se rvi c e imp l ein schwergewichtiges
Objekt ist, müssen wir dafür sorgen, dass dem s e rv i c e- Feld ein geeignetes Test
Double [Mezzaroso7] oder Mock Object zugewiesen wird, bevor diese Methode wäh­
rend der Unit-Tests aufgerufen wird. Weil wir die Konstruktionslogik mit der nor­
malen Laufzeit-Verarbeitung vermengt haben, sollten wir alle Ausführungspfade
testen (zum Beispiel auch den n u l l -Test und seinen Block). Die Methode hat also
zwei Verantwortlichkeiten, das heißt mehr als eine Aufgabe, und verstößt damit im
Kleinen gegen das Single-Responsibility-Prinzip.
Was vielleicht am schlimmsten ist: Wir wissen nicht, ob MySe rvi ceimpl in allen
Fällen das richtige Objekt ist. Der Kommentar soll diese Unklarheit zum Ausdruck
bringen. Warum muss diese Klasse mit dieser Methode den globalen Kontext ken­
nen? Können wirjemals wirklich wissen, welches Objekt an dieser Stelle richtig ist?
Kann es überhaupt sein, dass ein Typ für alle möglichen Kontexte richtig ist?
Eine Anwendung der Lazy Initialization ist natürlich kein ernstes Problem. Doch
Anwendungen enthalten normalerweise viele Instanzen kleiner derartiger Setup­
Idiome. Deshalb ist die globale Setup-Strategie (falls es eine gibt) über die Anwen­
dung verstreut. Sie ist gering modularisiert und enthält oft eine beträchtliche Dup­
lizierung.
Wenn wir wirklich sorgfältig gut strukturierte und robuste Systeme erstellen wollen,
sollten wir uns niemals dazu hinreißen lassen, die Modularität durch bequeme Idi­
ome zu gefahrden. Der Startu.p-Prozess der Obj ekt-Konstruktion und -Verknüp­
fung bildet keine Ausnahme. Wir sollten diesen Prozess von der normalen Laufzeit­
Logik trennen und für die Anwendung einer globalen, konsistenten Strategie zur
Auflösung der Haupt-Dependencies sorgen.

Tren n u ng in main

Eine Möglichkeit, die Konstruktion von der Anwendung zu trennen, besteht darin,
einfach alle Aspekte der Konstruktion nach ma i n oder in Module zu verschieben, die
von mai n aufgerufen werden, und das restliche System unter der Annahme zu kon­
zipieren, dass alle Objekte konstruiert und korrekt verknüpft worden sind (siehe
Abbildung II.I).

1 93
Kapitel n
Systeme

2:run(oo)
main appllcation

l
1 :b� ild
'
1 . 1 : COI'IStruc1 co: Conligured
Builder
<<ereates>> Object

Abb. 1 1 .1 : D i e Konstruktion n a c h m a i n verlagern

Der Kontrollfluss ist leicht nachzuvollziehen. Die mai n-Funktion erstellt die für das
System benötigten Objekte und übergibt sie dann an die Anwendung, die sie ein­
fach benutzt. Beachten Sie, in welche Richtung die Dependency-Pfeile die Barriere
zwischen mai n und der Anwendung überschreiten. Sie weisen alle in eine Rich­
tung, die von mai n wegzeigt Dies bedeutet, dass die Anwendung nichts über mai n
oder den Konstruktionsprozess weiß. Sie nimmt einfach an, dass alles korrekt
erstellt worden ist.

Factaries

Natürlich ist manchmal die Anwendung selbst verantwortlich dafür, wann ein 0bjekt
erstellt wird. Beispielsweise muss die Anwendung in einem Auftragsverwaltungs­
system die Li n e item-Instanzen (Positionen) eines Auftrags erstellen. In diesem Fall
können wir der Anwendung mit dem Abstract Factmy-Pattern [GOF) die Kontrolle
über den Zeitpunkt der Erstellung von Auftragspositionen geben, aber die Details die­
ser Konstruktion von dem Anwendungscode trennen (siehe Abbildung 11.2 ) .

run(factory)
maln OrderProcesslng

/ l\
<<creates»

<<interface>>
UnehemFactory
lmplementation -{> UnehemFactory
+ makelinehem

Unehem
<<Creates>>

Abb. 1 1 .2: Abtrennung der Konstruktion mit Factory

Beachten Sie, dass auch hier alle Dependencies von mai n weg auf die O rd e r P ro ­
c e s s i n g-Anwendung zeigen. Dies bedeutet, dass die Anwendung von den Details

1 94
1 1 .2
Konstruktion und Anwendung eines Systems trennen

der Erstellung von Li n eitem-Objekten entkoppelt ist. Die entsprechende Fähigkeit


ist in der L i n e i temFacto rylmpl ementati on eingekapselt, die sich auf der mai n ­
Seite der Trennlinie befindet. Dennoch hat die Anwendung die vollständige Kon­
trolle über den Zeitpunkt, zu dem Li n e i tem Instanzen erstellt werden, und kann
-

sogar anwendungsspezifische Konstruktar-Argumente übergeben.

Dependency I njection

Eine leistungsstarke Technik, um die Konstruktion von der Anwendung zu trennen,


ist die Dependency Injection (DI; Inj ektion der Abhängigkeit) , die Anwendung der
Inversion ofControl (loC; Umkehrung der Kontrolle) auf das Dependency-Manage­
ment ([Fowler] u.a.). Die Inversion of Control verlagert untergeordnete Verantwort­
lichkeiten von einem Objekt auf andere Objekte, die dem jeweiligen Zweck dienen,
und unterstützt damit das Single-Responsibility-Prinzip. Im Kontext des Depen­
dency-Managements sollte ein Objekt nicht die Verantwortung dafür tragen,
Dependencies selbst zu instanzieren, sondern es sollte diese Verantwortung an
einen anderen »Zuständigen« Mechanismus delegieren und damit die »Kontrolle
umkehren«. Weil das Setup ein globaler Concern ist, sollte dieser zuständige
Mechanismus normalerweise die »mai n«-Routine oder ein spezieller, diesem
Zweck dienender Container sein.
J N DI-Lookups sind eine »partielle« Implementierung der DI, in der ein Objekt
einen Verzeichnis- Server auffordert, einen » Service« bereitzustellen, der einem
speziellen Namen entspricht.

MySe rvi ce myServi ce = (MyServi ce) (jndi Context . 1 ookup ( " NameOfMyServi ce " ) ) ;

Das aufrufende Objekt hat keine Kontrolle über die Art von Objekt, das tatsächlich
zurückgegeben wird. (Natürlich muss es das entsprechende Interface implemen­
tieren.) Dennoch löst das aufrufende Objekt aktiv die Abhängigkeit auf.
Echte Dependency In j ection geht einen Schritt weiter. Die Klasse unternimmt keine
direkten Schritte, ihre Abhängigkeiten aufzulösen; sie ist vollständig passiv. Statt­
dessen stellt sie Setter-Methoden oder Konstruktar-Argumente (oder beides) zur
Verfügung, mit denen die Dependencies injiziert werden. Während der Konstruk­
tion instanziert der DI-Container die erforderlichen Objekte (normalerweise auf
Anfrage) und benutzt die bereitgestellten Konstruktar-Argumente oder Setter­
Methoden, um die Dependencies zu verknüpfen. Welche abhängigen Objekte tat­
sächlich verwendet werden, wird durch eine Konfigurationsdatei oder per Pro­
gramm in einem speziellen Konstruktionsmodul festgelegt.
Das Spring Framework ist der bekannteste DI-Container für Java. (Siehe [Spring].
Es gibt auch ein Spring.NET Framework.) Sie definieren in einer XML-Konfigura­
tionsdatei, welche Objekte verknüpft werden sollen, und fordern dann spezielle
Objekte namentlich in Ihrem Java-Code an. Etwas später finden Sie ein Beispiel.

1 95
Kapitel 1 1
Systeme

Aber was ist mit den Vorzügen der Lazy Initialization? Dieses Idiom ist auch mit DI
manchmal nützlich. Erstens: Die meisten DI-Container konstruieren ein Objekt
nicht, bevor es nicht gebraucht wird. Zweitens: Viele dieser Container stellen
Mechanismen zur Verfügung, um Factaries aufzurufen oder Proxies zu konstruie­
ren, die für eine Lazy Evaluation und ähnliche Optimierungen verwendet werden
können. (Sie dürfen nicht vergessen, dass Lazy InitializationjEvaluation nur eine
Optimierungstechnik ist, die vielleicht zu früh angewendet wird.)

1 1 .3 Aufwärtsskalieru ng
Städte sind gewachsene Dörfer und diese wiederum gewachsene Ansiedlungen.
Zuerst sind die Straßen schmal und praktisch nicht vorhanden; dann werden sie
gepflastert; dann werden sie mit der Zeit verbreitert. Kleine Gebäude werden durch
größere Gebäude ersetzt, leere Grundstücke bebaut. Einige dieser Gebäude werden
später von Wolkenkratzern abgelöst.
Zunächst gibt es keine Versorgung mit Strom, Gas oder Wasser, kein Abwässersys­
tem und kein Internet (oh Schreck!). Diese Dienste werden nach und nach auf- und
ausgebaut, wenn die Bevölkerung wächst und die Bebauungsdichte zunimmt.

Dieses Wachstum verläuft oft nicht schmerzlos. Wie oft haben Sie Stoßstange an
Stoßstange an einer Baustelle eines »Verbesserungsprojekts« im Stau gestanden
und sich gefragt: »Warum hat man das nicht gleich richtig gebaut!«?
Aber anders wäre es gar nicht möglich gewesen. Wer kann die Kosten für den Aus­
bau einer sechsspurigen Schnellstraße mitten durch eine Kleinstadt rechtfertigen,
die ein Wachstum erwartet? Wer wünscht sich eine solche Straße durch seine Stadt?

Es ist ein Mythos, dass wir Systeme »gleich beim ersten Mal richtig« erstellen kön­
nen. Stattdessen sollten wir nur die heutigen Stories (Geschichten) implementieren,
dann refaktorisieren und das System erweitern, um morgen neue Stories zu im­
plementieren. Dies ist die Essenz der iterativen und inkrementeilen Agilität. Test
Driven Development (TOD; testgesteuerte Entwicklung), Refaktorisierung und der
saubere Code, der dadurch erstellt wird, sorgen dafür, dass dies auf Code-Ebene
funktioniert.
Aber was ist mit der Systemebene? Verlangt die Systemarchitektur keine Voraus­
planung? Sicher kann sie nicht schrittweise vom Einfachen zum Komplexen wach­
sen, oder etwa doch?
Software-Systeme sind im Vergleich zu physikalischen Systemen einzigartig.
Ihre Architekturen können schrittweise wachsen, wenn wir eine geeignete
Trennung der Concems beachten.
Dies wird durch die flüchtige Natur von Software-Systemen ermöglicht, wie Sie
gleich sehen werden. Zunächst wollen wir ein Gegenbeispiel einer Architektur
betrachten, bei der die Concerns nicht sauber getrennt sind.
1 1 .3
Aufwärtsskalierung

Bei den ursprünglichen EJBI- und E]B2-Architekturen waren die Concerns nicht
sauber getrennt. Dadurch wurden unnötige B arrieren für ein organisches Wachs­
tum aufgebaut. Betrachten Sie etwa eine Entity Bean für eine persistente Bank­
Klasse. Eine Entity Bean ist eine speicherresidente Repräsentation relationaler
Daten, anders ausgedrückt: einer Tabellenzeile.
Zuerst musste man ein lokales (in process) oder fernes (separates JVM-) Interface
definieren, das die Clients verwenden sollten. Listing II.I zeigt ein mögliches loka­
les Interface:

Listing 11.1: Ein lokales Interface von E)B2 für eine Bank-EJ B
package com . exampl e . banki ng ;
i mpo rt j ava . u t i l . Co l l ecti o n s ;
i mpo rt j avax . ej b . * ;

publ i c i nte rface Ban klocal extends j ava . e j b . EJ B Local Obj ect {
S t r i ng getSt reetAd d r l ( ) th rows EJ BExcept i on ;
St r i ng getSt reetAdd r2 () th rows E J B Except i on ;
S t r i ng getCi t y () th rows EJ BExcepti on ;
S t r i ng getStat e ( ) th rows EJ BExcept i on ;
S t r i ng getZi pCode () th rows EJ BExcepti on ;
voi d s etSt reetAdd r l ( St r i n g s t reetl) th rows EJ BExcept i on ;
voi d s etSt reetAdd r2 (St ri ng s t reet2 ) th rows EJ BExcept i on ;
voi d s etCi t y ( S t r i ng ci ty) th rows EJ BExcepti on ;
voi d s etState (St r i ng state) th rows EJ BExcepti on ;
voi d s etZi pCod e ( St r i ng zi p) th rows EJ BExcepti on ;
Col l ec t i o n getAccount s () th rows EJ BExcepti on ;
voi d s etAcco u n t s ( Co l l ecti on accou nts) t h rows E J B Except i on ;
voi d addAcco u n t (Accou ntDTO accountDTO) th rows EJ BExcepti on ;
}

Ich habe mehrere Attribute der Adresse der Bank und eine Co l l ect i o n von Konten
gezeigt, die der Bank gehören und deren Daten jeweils durch eine separate
Account-EJB gehandhabt werden würden. Listing 11.2 zeigt die entsprechende
Implementierungsklasse der Bank-Bean.

Listing 11 .2: Die entsprechende E)B2-Entity-Bean-lmplementierung


package com . exampl e . ban k i ng ;
i mport j ava . u t i l . Co l l ect i on s ;
i mport j avax . ej b . * ;

publ i c abstract c l ass Ban k i mp l ements j avax . ej b . Enti tyßean {


II Geschäft s l ogi k . . .
publ i c abst ract S t r i ng getSt reetAd d r l ( ) ;
publ i c abst ract S t r i ng getSt reetAd d r2 () ;
publ i c abst ract S t r i ng getCi ty() ;
publ i c abst ract S t r i ng getStat e ( ) ;
publ i c abstract S t r i ng getZi pCode ( ) ;
publ i c abstract voi d setStreetAdd r l ( St r i ng s t reetl) ;

1 97
Kapitel n
Systeme

publ i c abst ract voi d setSt reetAdd r2 (St ri ng s t reet2) ;


publ i c abst ract voi d setCi ty ( S t r i ng c i ty) ;
publ i c abst ract voi d setState ( St ri ng s tate ) ;
publ i c abst ract voi d setZi pCode ( St ri ng z i p ) ;
publ i c abst ract Col l ecti on getAccou nts () ;
publ i c abst ract voi d setAccou n t s (Col l ecti on accounts ) ;
publ i c voi d addAccou n t (AccountDTO accoun tDTO) {
I n i t i alContext cont ext = n ew I n i t i al Con text () ;
Accou n t Homelocal accoun tHome = context . l ooku p ( " Accoun tHomelocal " ) ;
Acco u n t local acco u n t = accountHome . c reate (accoun tDTO) ;
Col l ecti on accounts = getAcco u n ts () ;
accou n t s . add (accou n t) ;
}
II E J B - Contai n e r - Logi k
publ i c abst ract voi d s etid (Integer i d) ;
publ i c abst ract I n tege r getid ( ) ;
p u bl i c Integer ej bCreate ( I n tege r i d) { . . . }
publ i c voi d ej bPostC reate (Intege r i d) { . . . }
II Der Re s t m u s s te i mpl emen t i e r t we rden , wa r abe r gewöh n l i ch l ee r :
publ i c voi d setEnti tyCo n text (Enti tyCon text ctx) { }
publ i c voi d un setEnti tyCon text () { }
publ i c voi d ej bActi vat e ( ) { }
publ i c voi d ej bPas s i vate() { }
publ i c voi d ej bload () { }
publ i c voi d ej bSto r e ( ) { }
publ i c voi d ej bRemove () { }
}

Ich habe das entsprechende Loca 1 Horne-Interface nicht gezeigt. Es handelt sich im
Wesentlichen um eine Factory zum Erstellen von Objekten. Auch mögliche Abfra­
gemethoden von Bank, die man hinzufügen könnte, sind nicht angegeben.
S chließlich musste man eine oder mehrere XML-Deployment-Deskriptoren schrei­
ben, um die Details des Objekt-relationalen Mappings aufeinen persistenten Spei­
cher, das gewünschte Transaktionsverhalten, Sicherheitseinschränkungen usw. zu
beschreiben.
Die Geschäftslogik ist eng an den EJB2-Anwendungs-»Container« gekoppelt. Sie
müssen Unterklassen von Container-Typen bilden und viele Lifecycle-Methoden
zur Verfügung stellen, die von dem Container benötigt werden.
Wegen dieser Kopplung an den schwergewichtigen Container sind isolierte Unit­
Tests schwierig. Es ist erforderlich, den Container durch ein Mock-Objekt zu erset­
zen, was schwierig ist, oder viel Zeit damit zu verschwenden, EJBs und Tests auf
einem echten Server zu deployen. Wegen der engen Kopplung ist eine Wiederver­
wendung außerhalb der E] B 2-Architektur praktisch unmöglich.
Schließlich wird sogar die objektorientierte Programmierung untergraben. Eine
Bean kann nichts von einer anderen erben. Beachten Sie die Logik, um ein neues
1 1 .3
Aufwärtsskalierung

Konto hinzuzufügen. Bei EJB2-Beans werden üblicherweise »Data Transfer


Objects« (DTOs; Datentransferobjekte) definiert, die im Wesentlichen aus
»st ructs« ohne Verhaltensweisen bestehen. Dies führt normalerweise zu redun­
danten Typen, die hauptsächlich dieselben Daten speichern; und es wird Standard­
Code benötigt, um Daten von einem Objekt zu einem anderen zu kopieren.

Cross-Cutti ng Concerns

In einigen Bereichen kommt die EJ B2-Architektur nahe an eine echte Trennung von
Concerns heran. Beispielsweise wird das gewünschte Transaktions-, Sicherheits­
und ein Teil des Persistenzverhaltens unabhängig vom Sourcecode in den Deploy­
ment-Deskriptoren deklariert.
Beachten Sie, dass Concems wie Persistenz oft über die natürlichen Objektgrenzen
einer Domäne hinweggreifen. Im Allgemeinen wollen Sie alle Ihre Objekte mit der­
selben Strategie dauerhaft speichern, etwa mit einem speziellen DBMS (Daten­
bank-Management-System) und nicht in flachen Dateien. Sie wollen dabei
bestimmte Namenskonventionen für Tabellen und Spalten einhalten, eine konsis­
tente Transaktionssemantik verwenden usw.
Theoretisch können Sie Ihre Persistenzstrategie mit gekapselten Modulen konzi­
pieren. Doch in der Praxis müssen Sie im Wesentlichen identischen Code, mit dem
Sie die Persistenzstrategie implementieren, auf viele Objekte verteilen. Für dieses
Phänomen wurde die Bezeichnung Cross-Cutting Concems geprägt (wörtlich »quer­
schneidende Belange«; Probleme, die unabhängig vom Einzelobjekt in jedem
Objekttyp gelöst werden müssen, zum Beispiel speichern oder drucken). Auch hier
kann das Persistenz-Framework modular sein; und auch unsere Bereichslogik
könnte isoliert eine modulare Struktur haben. Das Problem ist die feinkörnige Über­
schneidung der Domänen (Bereiche) .
Tatsächlich nahm die Methode, wie die EJB-Architektur mit Persistenz, Sicherheit
und Transaktionen das Aspect-oriented Programming (AOP, Aspektorientierte Pro­
grammierung) vorweg, einen Allzweckansatz, um die Modularität auch bei Cross­
Cutting Concerns zu realisieren. Allgemeine Informationen über Aspkete und die
AOP finden Sie in [AO S D] . [AspectJ] und [Colyer] liefern AspectJ-spezifische Infor­
mationen.
In der AOP spezifizieren modulare Konstrukte, die so genannten Aspekte, an wel­
chen Punkten des Systems das Verhalten in einer konsistenten Weise implemen­
tiert werden soll, um einen speziellen Cancern zu unterstützen. Diese Spezifikation
erfolgt durch Einbettung bestimmter Deklarationen und Anweisungen in den Pro­
grammcode.
Betrachten wir Persistenz als Beispiel. Sie deklarieren, welche Objekte und Attribute
(oder Patterns von Objekten und Attributen) gespeichert werden sollen, und dele­
gieren dann die Persistenz-Tasks an Ihr Persistenz-Framework. Das AOP-Frame­
work ändert das Verhalten des Target-Codes nicht-invasiv, was bedeutet, dass Sie den

1 99
Kapitel 1 1
Systeme

Sourcecode des Targets manuell nicht ändern müssen. Schauen wir uns drei
Aspekte oder aspektähnliche Mechanismen in Java an.

1 1 .4 J ava-Proxies
Java-Proxies eignen sich in einfachen Situationen, etwa um Methodenaufrufe in
einzelne Objekte oder Klassen einzuhüllen. Doch die dynamischen Proxies, die in
dem JD K zur Verfügung gestellt werden, funktionieren nur mit I nterfaces. Für Klas­
sen-Proxies müssen Sie ein Library zur Byte-Code-Manipulation verwenden, wie
etwa CGLIB, ASM oder Javassist (siehe [CGLIB], [ASM] oder [Javassist]).
Listing 11.3 zeigt das Gerüst für ein JDK-Proxy, das eine Persistenz-Unterstützung
für unsere Ban k-Anwendung zur Verfügung stellt. Es werden nur die Methoden dar·
gestellt, um die Liste der Konten abzurufen und zu speichern.

Listing 1 1 .3: J DK-Proxy-Beispiel


II Ban k . j ava (supp res s i ng package names . . . )
i mport j ava . ut i l s . * ;

II Di e Abst rakti on e i n e r Bank


publ i c i nte rface Ban k {
Col l ecti on<Account> getAccou n t s () ;
voi d setAccoun t s (Col l ec t i o n <Acco u n t> accounts) ;
}

II Ban kimpl . j ava


i mport j ava . u ti l s . * ;

II Das " P l ai n Ol d J ava Obj ect" ( POJO) , das di e Abst ra kti on i mpl eme n t i e r t
publ i c c l ass Bankimpl i mp l ements B a n k {
p ri vate Li st<Accoun t> acco u nts ;

publ i c Col l e cti on<Account> g e tAccounts ( ) {


retu rn accounts ;
}
publ i c voi d setAccount s (Col l ecti on<Acco u n t> accou nts) {
th i s . accounts = n ew A r r ayli s t<Account> () ;
fo r (Account accou nt : acco u n t s ) {
th i s . accounts . add (accou nt) ;
}
}
}

II BankProxyHandl e r . j ava
i mport j ava . l ang . refl ect . * ;
i mport j ava . ut i l . * ;

II " Invocat i onHandl e r " , d e r von dem P roxy-API benöti gt wi rd

200
1 1 .4
J ava-Proxies

publ i c cl ass BankP roxyHandl e r i mp l ements Invocati onHand l e r {


p r i vate Bank ban k ;

publ i c BankHandl e r (Bank bank) {


th i s . ba n k = ban k ;
}

II Met hode , di e i n I nvocat i onHandl e r defi n i e r t i s t


publ i c Obj ect i nvoke (Object p r ox y , Method method , Obj ect [ ] args)
th rows Th rowabl e {
S t r i ng met hodName = method . getName () ;
i f (methodName . eq u al s ( " getAccoun t s " ) ) {
ban k . setAccounts (getAccou n t s F romDatabas e () ) ;
ret u r n ban k . getAccounts () ;
} e l s e i f (methodName . eq u al s ( " setAccounts " ) ) {
ban k . s etAcco u n ts ( (Col l ecti on<Account>) a rg s [O] ) ;
setAccou n tsToDatabas e (ban k . getAccounts ( ) ) ;
return n u l l ;
} el se {

}
}

II Zahl rei ch e Detai l s h i e r . . .


p rotected Col l e cti on<Account> getAccou n t s F romDatabase () { . . . }
p rotected voi d s etAccountsToDatabas e (Col l ecti on<Account> acco u n t s ) { . . . }
}

II An ande re r Stel l e

Bank bank = (Bank) P roxy . new P roxyinstanc e (


Ban k . cl as s . getCl ass loade r () ,
n ew Cl as s [ ] { B an k . c l ass } ,
new BankProxyHandl e r ( n ew Ban kimpl () ) ) ;

Wir definieren ein Interface, Ban k , das von dem Proxy eingehüllt (engl. wrapped)
wird, und ein Plain-Old Java Object (POJO), Ban k lmp l , das die Geschäftslogik im­
plementiert. (Wir kommen in Kürze auf POJOs zurück.)
Das Proxy-API benötigt ein I n vocati onHan d l e r-Objekt, das es aufruft, um die
Aufrufe von Ban k-Methoden zu implementieren, die an das Proxy gerichtet werden.
Unser Ban k P roxyHan d l e r benutzt das Java Reflection API, um die generischen
Methodenaufrufe den entsprechenden Methoden in Bank lmpl zuzuordnen, usw.
Der Code hier ist ziemlich umfangreich und selbst in diesem einfachen Fall relativ
kompliziert. (Ausführlichere Beispiele für das Proxy-API und Anwendungsbei­
spiele finden Sie beispielsweise in [Goetz] .) Der Einsatz einer der Libraries zur Byte­
Manipulation ist ähnlich schwierig. Der Umfang und die Komplexität dieses Codes
sind zwei Nachteile von Proxies. Dadurch wird es schwer, sauberen Code zu erste!-

201
Kapitel 1 1
Systeme

len! Außerdem stellen Proxies keinen Mechanismus zur Verfügung, um systemweit


interessante »Ausführungspunkte(( zu definieren, was für eine echte AOP-Lösung
erforderlich ist. (AOP wird manchmal mit den Techniken verwechselt, mit denen
es implementiert wird, wie etwa die Methoden-Interception und das »Wrapping((
mittels Proxies. Der wirkliche Wert eines AOP- Systems basiert aufseiner Fähigkeit,
systemische Verhaltensweise klar und knapp zu beschreiben.)

1 1 .5 Reine Java-AOP-Frameworks
Glücklicherweise kann der meiste Proxy-Standardcode automatisch mit Werkzeu­
gen erstellt und bearbeitet werden. Proxies werden intern in mehreren Java-Frame­
works (beispielsweise Spring AOP und J Boss AOP) verwendet, um Aspekte in
reinem Java zu implementieren. (Siehe [Spring] und [JBoss]. »Reines Java(( bedeutet
Java ohne AspectJ .) In Spring schreiben Sie Ihre Geschäftslogik als Plain-Old Java
Objects. POJOs sind ausschließlich auf Ihre Domäne fokussiert. Sie haben keine
Dependencies von Enterprise Frameworks (oder anderen Domänen) . Deshalb sind
sie konzeptionell einfacher und leichter zu testen. Wegen der relativen Einfachheit
können Sie auch die entsprechenden Benutzer-Stories leichter korrekt implemen­
tieren. Ihr Code wird wartungsfreundlicher und kann bei zukünftigen Stories bes­
ser erweitert werden.
Die erforderliche Anwendungsinfrastruktur, einschließlich der Cross-Cutting Con­
cerns wie Persistenz, Transaktionen, Sicherheit, Caching, Ausfallsicherheit usw. ,
wird mithilfe deklarativer Konfigurationsdateien oder APis i n die Programme ein­
gebaut. In vielen Fällen spezifizieren Sie tatsächlich Spring- oder J Boss-Library­
Aspekte, wobei das Framework die mechanische Anwendung der Java-Proxies oder
Byte-Code- Libraries für den Benutzer transparent handhabt. Diese Deklarationen
steuern den Dependency Injection Container (DI-Container) , der die Hauptobjekte
auf Anforderung instanziert und miteinander verknüpft.
Listing 11.4 zeigt ein typisches Fragment der Konfigurationsdatei app . xm l von
Spring V2.5 (adaptiert von h t t p : //www . th e s e rve rs i de . com/art i c l e s /
art i c l e . t s s ? l =Int rotoSpri ng2 5):

Listing 1 1 .4: Konfigurationsdatei von Spring 2.X


<beans>

<bean i d= " appDataSou rce"


c l ass="org . apache . commons . dbcp . Basi cDataSou rce"
dest roy-method= " c l ose"
p : d ri v e rCl as sName= " com . mysql . j dbc . D ri ve r "
p : u r l = " j d bc : mysql : //l ocal host : 3 3 06/mydb "
p : us e rname= " me " />

<bean i d= " ban kDataAccessOb j e c t "


c l ass=" com . exampl e . banki ng . p e r s i stence . Ban kDataAcce s sObj ect"

202
1 1 .5
Reine J ava-AOP-Frameworks

p : dataSou rce- ref= " appDataSou rce " />

<bean i d= " bank"


cl ass="com . exampl e . banki ng . model . Bank"
p : dataAcces sObj ect- ref=" ban kDataAcces sObj e c t " />

</beans>

Jede »bean« ist einer Figur einer verschachtelten »Russischen Puppe« vergleichbar.
Ein Domänenobjekt (Bank) wird von einem Proxy, einem Data Access Object (DAO;
Datenzugriffs-Objekt) , eingehüllt, das seinerseits von einer JDBC-Driver-Data­
Source eingehüllt ist (siehe Abbildung IL3) -

AppDataSource

Ban kDataAcessObject
client
I! I Bank
I
I

Abb. 1 1 .3: Die »Russische Pu ppe« der Dec o r at o r s

Der Client glaubt, er würde getAccou n t s () eines Bank -Objekts aufrufen; tatsäch­
lich redet er aber mit der äußeren Schicht verschachtelter Decorator-Objekte [GOF],
die das grundlegende Verhalten des Ban k-POJOs erweitern. Wir könnten andere
Decorators für Transaktionen, Caching und andere Funktionen hinzufügen.
In der Anwendung werden einige Zeilen benötigt, um den DI-Container nach den
Top-Level-Objekten des Systems zu fragen, die in der XM L-Datei spezifiziert sind.

Xml BeanFactory bf =
n ew Xml Bean Facto r y (new Cl ass PathResource ( " app . xml " , getCl ass () ) ) ;
Bank bank = (Bank) bf . getBean ( " ban k " ) ;

Weil nur so wenige Zeilen von Spring-spezifischem Java-Code benötigt werden, ist
die Anwendungfast vollständig von Spring entkoppelt. Damit sind alle Probleme der
engen Systemkopplung wie bei EJB2 nicht existent.
Obwohl XM L wortreich und schwer zu lesen ist, ist die »Policy«, die in diesen Kon­
figurationsdateien festgelegt wird, einfacher als die komplizierte Proxy- und Aspekt­
Logik, die unsichtbar bleibt und automatisch erstellt wird. (Das Beispiel könnte
noch durch Mechanismen vereinfacht werden, die dem Prinzip Convention over
Con.figuration, Konvention über Konfiguration, folgen und Java-5-Annotationen ver­
wenden, um den Umfang der ausdrücklich anzugebenden Verknüpfungslogik zu
reduzieren.) Diese Art von Architektur ist so überzeugend, dass Frameworks wie
Spring zu einer vollständigen Ü berarbeitung des EJ B-Standards führten. EJB3 folgt

203
Kapitel 11
Systeme

zu einem großen Teil dem Spring-Modell der deklarativen Unterstützung von


Cross-Cutting Concerns mit XM L-Konfigurationsdateien undjoder Java-s-Annota­
tionen.
Listing rr.5 zeigt unser Ban k-Objekt umgeschrieben in EJB3 (adaptiert von htt p : I I
www . onj ava . comlpu blalo n j avai 2 006IOSI17Istandard i z i ng-wi th-ej b 3 -
j ava- p e r s i s tence-ap i . h tml ) .

Listing 1 1 .5: Eine EBJ3·Bank-EJB


package com . exampl e . banki ng . model ;
i mport j avax . pe r s i stence . * ;
i mport j ava . ut i l . A r rayl i s t ;
i mport j ava . uti l . Co l l ect i on ;

@Enti ty
@Tabl e (name = " BANK S " )
publ i c cl ass Ban k i mpl ement s j ava . i o . S e r i al i zabl e {
@Id @Gene ratedVal u e (s t rategy=Gen e rati onType . AUTO)
p r i vate i nt i d ;

@Embeddabl e // Ei n " i n l i ned" Obj ekt i n d e r DB-Zei l e von Ban k


publ i c cl ass Add ress {
p rotected St ri ng s t reetAd d r l ;
p rotected St ri ng s t reetAddr2 ;
p rotected S t r i ng ci ty ;
p rotected St ri ng state ;
p rotected St ri ng z i pCode ;
}

@Embedded
p r i vate Add ress add re s s ;

@OneToMan y (cascade = CascadeType . ALL , fetch = FetchType . EAGER ,


mappedBy= " ba n k " )
p ri vate Col l ecti on<Account> accounts = n e w A r rayl i st<Account> () ;

publ i c i n t get i d ( ) {
retu rn i d ;
}

publ i c voi d setid (i nt i d) {


thi s . i d = i d ;
}

publ i c voi d addAccount (Accou n t account) {


accou n t . setBa n k ( th i s) ;
accou nts . add (account) ;
}

publ i c Col l ecti on<Accou n t> getAccount s () {

204
1 1 .6
Aspect)-Aspekte

retu rn accounts ;
}

publ i c voi d s etAccounts (Co l l ecti on<Account> accounts) {


t h i s . accounts = accou n t s ;
}
}

Dieser Code ist viel sauberer als der ursprüngliche E J B2-Code. Einige E ntity-Details
sind immer noch vorhanden, und zwar in den Annotationen. Doch weil sich keine
dieser Informationen außerhalb von Annotationen befindet, ist der Code sauber
und klar und deshalb leicht zu testen, zu warten usw.
Einige oder alle Persistenz-Informationen in den Annotationen können aufWunsch
in XM L- Deployment- Deshiptoren ausgelagert werden, um wirklich reine PO J Os zu
erhalten. Wenn sich die Details des Persistenz-Mappings nur selten ändern, ziehen
es viele Teams möglicherweise vor, die Annotationen zu behalten. Dies hat aber
erheblich weniger schädliche Nachteile als das invasive Verhalten von EJB2.

1 1 .6 Aspectj-Aspekte
Schließlich ist die AspectJ-Sprache (siehe [AspectJ] und [Colyer]) das funktions­
reichste Werkzeug zur Trennung von Concerns. AspectJ ist eine Erweiterung von
Java, die eine ��erstklassige« Unterstützung zur Modularisierung von Aspekten bie­
tet. Die reinen Java-Ansätze von Spring AOP und J Boss AOP reichen in 8o bis 9 0
Prozent der Fälle aus, i n denen Aspekte am nützlichsten sind. Doch AspectJ stellt
einen sehr reichhaltigen und leistungsstarken Werkzeugsatz für die Trennung von
Concerns zur Verfügung. Der Nachteil von AspectJ liegt darin, dass Sie mehrere
neue Tools, Sprachkonstrukte und Anwendungsidiome lernen müssen.
Die Probleme der Übernahme sind etwas kleiner geworden, seit vor kurzem die
»Annotation-Form« von AspectJ eingeführt worden ist, bei der Aspekte mit Java-s­
Annotationen in reinem Java-Code definiert werden können. Außerdem enthält das
Spring Framework eine Reihe von Funktionen, die das Einfügen von annotations­
basierten Aspekten für ein Team mit begrenzter AspectJ- Erfahrung erheblich
erleichtern.
Ein komplette Beschreibung von AspectJ sprengt den Rahmen dieses Buches.
Näheres finden Sie in [AspectJ] . [Colyer] und [Spring].

1 1 .7 Die Systemarchitektur testen


Die Wirksamkeit der Trennung von Concerns durch aspektorientierte Ansätze kann
gar nicht überbetont werden. Wenn Sie Ihre Anwendungslogik mit POJOs schrei­
ben können, die auf Code-Ebene von allen architektonischen Concerns entkoppelt

205
Kapitel 1 1
Systeme

sind, können Sie Ihre Architektur wirklich testgesteuert entwickeln. Sie können sie
nach Bedarf vom Einfachem zum Komplexen hin ausbauen und je nach Anforde­
rung neue Technologien einbauen. Es ist kein Big Design Up Front (BDUF; »Großes
Design vor der Arbeit«) erforderlich. Tatsächlich ist BDUF sogar schädlich, weil es
die Anpassung an Änderungen behindert. Denn es gibt einen psychologischen
Widerstand dagegen, frühere Aufwendungen zu verwerfen. Außerdem schränken
frühere Entscheidungen über die Architektur das nachfolgende Denken über das
Design ein. (BDUF darf nicht mit der guten Praxis verwechselt werden, vorher ein
Design zu erstellen. BDUF bedeutet, alles und jedes zu designen, bevor die Imple­
mentierung überhaupt beginnt.)
Architekten von Gebäuden müssen ein B D UF leisten, weil es nicht machbar ist,
radikale Änderungen einer großen physischen Struktur durchzuführen, wenn die
Konstruktion über die ersten Anfänge hinausgekommen ist. Dennoch gibt es auch
hier beträchtliche iterative Anstrengungen und Diskussionen über Details. Selbst
nachdem der Bau längst begonnen hat. Obwohl Software über eine eigene Physik
verfügt, ist es ökonomisch machbar, die Struktur radikal zu ändern ,falls die Struktur
der Software ihre Concerns wirksam trennt. (Der Terminus software physics, Software­
physik, wurde zum ersten Mal von [Kolence] verwendet.)

Dies bedeutet, dass wir ein Software-Projekt mit einer »naiv einfachen«, aber
hübsch entkoppelten Architektur starten, schnell funktionierende Benutzer-Stories
liefern und dann die Infrastruktur ausbauen können, wenn wir das System auf­
wärtsskalieren. Einige der größten Websites der Welt haben eine sehr hohe Verfüg­
barkeit und ein hohes Leistungsverhalten erzielt, indem sie gekonnt und flexibel
Daten-Caching, Sicherheitsfunktionen, Virtualisierung usw. einsetzen können,
weil sie auf minimal gekoppelten Designs basieren, die aufjeder Abstraktionsebene
und in dem Geltungsbereich entsprechend einfach sind.
Natürlich bedeutet dies nicht, dass wir ohne Zielvorstellungen und Steuerungsmit­
tel in ein Projekt einsteigen. Wir haben einige Erwartungen an den allgemeinen Gel­
tungsbereich, die Ziele und den Plan für das Projekt sowie an die allgemeine
Struktur des fertigen Systems. Doch wir müssen unsere Fähigkeit bewahren, das
System an neue Umstände anpassen zu können.

Die frühe EJB-Architektur ist nur eines der vielen bekannten APis, die eng gekop­
pelte Komponenten verwendeten und die Trennung von Concerns unnötig
erschwerten. Selbst gut konzipierte APis können zu viel des Guten anbieten, wenn
es nicht wirklich benötigt wird. Ein gutes API sollte die meiste Zeit über hauptsäch­
lich unsichtbar sein, damit sich das Team vor allem auf die Implementierung der
Benutzer-Stories konzentriert. Ist dies nicht der Fall, behindern architektonische
Einschränkungen die effiziente Lieferung des optimalen Werts an den Kunden.
Zusammenfassend können wir also sagen:
Eine optimale Systemarchitektur besteht aus modularisierten Concem-Domä­
nen, die jeweils mit Plain Old Java Objects (oder anderen Objekten) imple-

206
1 1 .8
Die Entscheidungsfindung optimieren

mentiert werden. Die verschiedenen Domänen werden durch minimal


invasive Aspekte oder aspektähnliche Tools integriert. Diese Architektur kann,
wie der Code, testgesteuert entwickelt werden.

1 1 .8 Die Entscheidu ngstind u ng optim ieren


Modularität und Trennung von Concerns ermöglichen eine dezentralisierte Verwal­
tung und Entscheidungsfindung. Bei hinreichend großen Systemen, egal ob Stadt
oder Software-Proj ekt, kann keine einzelne Person alle Entscheidungen treffen.
Wir wissen alle, dass es am besten ist, die Verantwortung an die am besten qualifi­
zierten Personen zu delegieren. Oft vergessen wir, dass es auch am besten ist, Ent­
scheidungen bis zum letzten möglichen Moment aufzuschieben. Dies ist keine Faulheit
oder Verantwortungslosigkeit, sondern ermöglicht es uns, Entscheidungen mit
dem bestmöglichen erreichbaren Informationsstand zu treffen. Eine voreilige Ent­
scheidung wird immer mit einem suboptimalen Wissensstand getroffen. Wir ver­
fügen über sehr viel weniger Kundenfeedback haben weniger über das Projekt
nachgedacht und wissen weniger über unsere lmplementierungsoptionen, wenn
wir zu schnell entscheiden.
Die Agilität, die ein PO]O-System mit modularisierten Concerns bietet,
ermöglicht es uns, optimale, ]ust-in-Time-Entscheidungen zu treffen, die auf
dem aktuellen Wissensstand basieren. Die Komplexität dieser Entscheidungen
wird ebenfalls reduziert.

1 1 .9 Standards weise anwenden, wen n sie nachweisbar


einen Mehrwert bieten
Dem Bau von Gebäuden zuzuschauen, ist ein bewundernswerter Anblick, weil die
Geschwindigkeit, mit der heute neue Gebäude (selbst mitten im Winter) errichtet
werden, außerordentlich hoch ist und mit der heutigen Technologie ungewöhnliche
Designs möglich sind. Die Bauindustrie ist eine reife Branche mit hochgradig opti­
mierten Bauteilen, Methoden und Standards, die sich unter ständigem Druck über
Jahrhunderte hinweg entwickelt haben.
Viele Teams verwendeten die EJB2-Architektur, weil sie ein Standard war, selbst
wenn leichgewichtigere und geradlinigere Designs ausgereicht hätten. I ch habe
erlebt, wie Teams von verschiedenen stark beworbenen Standards besessen wurden
und die Implementierung von Werten für ihre Kunden aus den Augen verloren.
Standards machen es leichter, Ideen und Komponenten wiederzuverwenden,
Entwickler mit einschlägiger Erfahrung zu rekrutieren, gute Ideen einzukap­
seln und Komponenten zu verknüpfen. Doch der Prozess der Erstellung von
Standards kann manchmal zu lange dauern, als dass die Branche warten

207
11
Kapitel
Systeme

könnte; und einige Standards verlieren den Kontakt mit den wirklichen
Bedürfnissen der Zielgruppe.

1 1 .1 0 Systeme brauchen domänenspezifische Sprachen


Die Bauindustrie hat, wie die meisten Branchen, eine eigene Fachsprache mit
einem reichhaltigen Vokabular, Idiomen und Patterns entwickelt, die wesentliche
I nformationen klar und präzise vermittelt. In der Software-Branche war die Arbeit
von [Alexander] besonders einflussreich. Bei der Software-Entwicklung wurde in
jüngerer Zeit das Interesse an der Erstellung von Domain-Specific Languages (DSL;
domänenspezifische Sprache) wiederbelebt. ( Siehe zum Beispiel [DSL]. [JMock] ist
ein gutes Beispiel für ein Java-API, das eine D S L erstellt.) D S Ls sind separate, kleine
Skriptsprachen oder APis in Standardsprachen, in denen man Code so schreiben
kann, dass er sich wie ein strukturierter Prosatext eines Domänenexperten liest.
Eine gute D S L minimiert die »Kommunikationslücke« zwischen einem Domänen­
konzept und dem Code, der es implementiert, ähnlich wie agile Praktiken die Kom­
munikation in einem Team und mit den Stakeholdern des Projekts optimieren.
Wenn Sie Bereichslogik in derselben Sprache implementieren, die ein Domänen­
experte verwendet, ist die Gefahr geringer, dass Sie die Logik falsch implementie­
ren.
Eine effiziente Anwendung von D S Ls hebt das Abstraktionsniveau über Code-Idi­
ome und Design-Patterns hinaus. Sie ermöglicht es dem Entwickler, den Zweck des
Codes auf der geeigneten Abstraktionsebene auszudrücken.
Domain-Specific Languages ermöglichen es, alle Abstraktionsebenen und alle
Domänen der Anwendung als PO]Os auszudrücken, und zwar von der abs­
traktformulierten Policy bis hin zu den konkreten Details.

1 1 .1 1 Zusa m menfassung
Auch Systeme müssen sauber sein. Eine invasive Architektur überwältigt die
Bereichslogik und verschlechtert die Agilität. Wenn die Bereichslogik verdunkelt
wird, leidet die Qualität, weil Bugs schwerer zu lokalisieren und Stories schwerer
zu implementieren sind. Wenn die Agilität leidet, nimmt die Produktivität ab und
die Vorteile der TDD gehen verloren.
Der Zweck sollte auf allen Abstraktionsebenen klar sein. Dies können Sie nur errei­
chen, wenn Sie POJOs schreiben und andere lmplementierungs-Concerns nicht­
invasiv mit aspektähnlichen Mechanismen realisieren.

Egal, ob Sie Systeme oder einzelne Module designen, dürfen Sie niemals vergessen:
Verwenden Sie die einfachste funktionsfähige Lösung!

208
Kapitel 1 2

Emergenz

von jeff Langr

Emergenz (lat. emergere: auftauchen, hervorkommen, sich zeigen) ist die


spontane Herausbildung von Phänomenen oder Strukturen auf der Makro­
ebene eines Systems auf der Grundlage des Zusammenspiels seiner Elemente.
Dabei lassen sich die emergenten Eigenschaften des Systems nicht offensicht­
lich aufEigenschaften der Elemente zurückführen, die diese isoliert aufweisen.
( Wikipedia: http : I /de . wi ki pedi a . o rg/wi ki / Eme rgenz)

1 2.1 Saubere Software d u rch emergentes Design


Was wäre, wenn es vier einfache Regeln gäbe, deren Befolgung Ihnen helfen
könnte, brauchbare Designs zu erstellen? Was wäre, wenn Sie durch Befolgung die­
ser Regeln Einsichten in die Struktur und das Design Ihres Codes bekämen, die es
Ihnen erleichtern würden, Prinzipien wie etwa SRP und DIP anzuwenden?
Was wäre, wenn diese vier Regeln die Emergenz eines guten Designs unterstützten?

20 9
Kapitel 1 2
Emergenz

Viele Entwickler sind der Auffassung, dass die vier Regeln aus Simple Design von
Kent Beck [XPE] eine wesentliche Hilfe bei der Erstellung eines brauchbaren Soft­
ware-Designs sind.
Laut Kent ist ein Design ))einfach«, wenn es folgende Bedingungen (A.d. Ü . : engl.
unglücklich rules, ))Regeln«) erfüllt:
• Es besteht alle Tests.

• Es enthält keine Duplizierungen.

• Es verkörpert die Absicht der Programmierer.

• Es minimiert die Anzahl der Klassen und Methoden.

Die Regeln sind nach Wichtigkeit geordnet. Die wichtigste steht am Anfang.

1 2.2 Einfache Design- Regel 1 : Alle Tests bestehen


Vor allem muss ein Design ein System erzeugen, das den gewollten Zweck erfüllt.
Ein System kann auf dem Papier ein perfektes Design haben, aber wenn nicht auf
einfache Weise geprüft werden kann, ob das System tatsächlich zweckerfüllend
funktioniert, ist der gesamte Papieraufwand fraglich.
Ein System, das umfassend getestet ist und jederzeit alle Tests besteht, ist ein test­
bares System. Diese Aussage ist offensichtlich, aber wichtig. Systeme, die nicht test­
bar sind, können nicht verifiziert werden.
Man könnte fordern, dass ein System, das nicht verifiziert werden kann, niemals
produktiv eingesetzt werden sollte.
Glücklicherweise drängt uns das Bemühen, unsere Systeme testbar zu machen, zu
einem Design, bei dem unsere Klassen klein und einem einzigen Zweck dienen. Es
ist eben einfacher, Klassen zu testen, die dem SRP folgen. Je mehr Tests wir schrei­
ben, desto stärker bemühen wir uns, Code zu schreiben, der einfacher zu testen ist.
Deshalb unterstützt unser Bemühen, komplett testbare Systeme zu erstellen, die
Entwicklung eines besseren Designs.
Eine starke Kopplung erschwert das Schreiben von Tests. Hier ist der Gedanken­
gang ähnlich: Je mehr Tests wir schreiben, desto mehr bemühen wir uns, die Kopp­
lung zu minimieren, und wenden folglich desto häufiger Prinzipien wie DIP und
Techniken wie Dependency Injection, Interfaces und Abstraktionen an.
Unsere Designs werden noch besser.
Bemerkenswerterweise führt das Befolgen einer einfachen und offensichtlichen
Regel, die besagt, dass wir Tests schreiben und unsere Systeme laufend damit testen
sollen, dazu, dass unser System quasi automatisch bestimmte Hauptziele des objekt­
orientierten Designs, schwache Kopplung und starke Kohäsion, erfüllt.
Tests zu schreiben, führt zu besseren Designs.

21 0
1 2. 3
Einfache Design-Regeln 2-4: Refactoring

1 2.3 Einfache Design-Regel n 2-4: Refactoring


Die Tests geben uns die Möglichkeit, unseren Code und unsere Klassen sauber zu
halten. Wir erreichen dies, indem wir den Code schrittweise refaktorisieren. Wir
fügen einige Codezeilen hinzu, halten dann inne und überdenken das neue Design.
Haben wir es gerade verschlechtert? Wenn ja, säubern wir es und führen unsere
Tests aus, um uns zu vergewissern, dass wir nichts beschädigt haben.
Die Tatsache, dass wir diese Tests haben, nimmt uns die Furcht, wir könnten den Code
durch die Bereinigung beschädigen!
Bei der Refaktorisierung können wir unser gesamtes Wissen über gutes Software­
Design einsetzen. Wir können die Kohäsion verstärken, die Kopplung verringern,
Zuständigkeiten trennen, Systemaufgaben in Module auslagern, unsere Funktio­
nen und Klassen verkleinern, bessere Namen verwenden usw. An dieser Stelle wen­
den wir auch die anderen drei Regeln des einfachen Designs an: Duplizierten Code
eliminieren, die Ausdrucksstärke des Codes verbessern und die Anzahl der Klassen
und Methoden minimieren.

1 2.4 Keine Duplizieru ng


Die Duplizierung ist der Hauptfeind eines guten System-Designs. Sie bedeutet
zusätzliche Arbeit, zusätzliche Risiken und zusätzliche unnötige Komplexität.

Duplizierung manifestiert sich in viele Formen. Natürlich sind identische Codezei­


len eine Form der Duplizierung. Ähnlich aussehende Codezeilen können oft so
umformuliert werden, dass sie sich noch ähnlicher werden und ein Refactoring
leichter durchzuführen ist. Duplizierung kann auch in anderen Formen wie etwa
der Duplizierung der Implementierung auftreten. Beispielsweise könnte eine Col­
lection-Klasse zwei Methoden enthalten:

i nt s i ze () { }
boo l ean i s Empty() { }

Wir hätten beide Methoden separat implementieren können. Die i s Empty­


Methode könnte einen booleschen Wert und s i ze einen Zähler überwachen. Wir
könnten diese Duplizierung eliminieren, indem wir i s Empty mit der Definition
von s i ze verbinden:

boo l ean i s Empty() {


return 0 == s i ze () ;
}

Die Erstellung eines sauberen Systems erfordert den Willen, Duplizierung zu eli­
minieren, selbst wenn nur wenige Codezeilen betroffen sind. Betrachten Sie bei­
spielsweise den folgenden Code:

21 1
Kapitel 12
Emergenz

publ i c voi d scal eToOneD i me n s i o n (


fl oat d e s i redDi mensi on , fl o at i mageDi men si on) {
i f (Math . abs (desi redDi men si on - i mageDi me nsi on) < e r rorTh resho l d)
retu rn ;
fl oat scal i ngFacto r = d e s i redDi me n s i on I i mageDi mens i on ;
s cal i ng Facto r = (fl oat) (Math . fl oo r (scal i ngFacto r �
100) * 0 . 01f) ;

Rende redOp newlmage = ImageUti l i ti es . getScal edlmag e (


i mage , scal i ng Facto r , s cal i ngFacto r ) ;
i mage . di spose () ;
System . gc () ;
i mage = newlmage ;
}

publ i c s ynch roni zed voi d rotat e ( i n t d e g rees) {


Rende redOp newlmage = ImageUti l i t i es . getRotatedlmage (
i mage , degrees ) ;
i mage . di spose () ;
System . gc () ;
i mage = newlmage ;
}

Um dieses System sauber zu halten, sollten wir die geringe Duplizierung in den
Methoden sca l eToün eDi men s i on und rotate eliminieren:

publ i c voi d scal eToOneDi mensi o n (


fl oat desi redDi men s i on , fl oat i mageDi men s i o n ) {
i f (Math . abs (desi redDi men s i on - i mageDi men s i on) < e r ro rTh reshol d)
retu r n ;
fl oat scal i ng Facto r = desi redDi me n s i o n I i mageDi mens i on ;
scal i ng Facto r =
(fl oat) (Math . fl oo r (scal i ng Facto r * 100) * 0 . 01f) ;

repl aceimage (ImageUti l i ti es . getScal edimage(


i mage , scal i ngFactor , scal i ngFactor) ) ;
}

publ i c sync h ro n i zed voi d rotate ( i n t degrees) {


repl aceimage(ImageUt i l i ti es . getRotatedimage ( i mage , degrees) ) ;
}

p r i vate voi d repl aceimage(RenderedOp newlmage) {


i mage . di spose() ;
System . gc () ;
i mage = newlmage ;
}

Wenn wir Gemeinsamkeiten auf dieser sehr winzigen Ebene extrahieren, beginnen
wir, Verstöße gegen das SRP zu erkennen. Deshalb könnten wir eine neu extrahierte
Methode in eine andere Klasse einfügen. Dadurch wird die Sichtbarkeit der Methode
verbessert. Ein anderes Teammitglied könnte eine Gelegenheit erkennen, die neue

21 2
1 2 -4
Keine Duplizierung

Methode weiter zu abstrahieren und in einem anderen Kontext wiederzuverwenden.


Diese >>Wiederverwendung im Kleinen« kann zu einer erheblichen Verringerung der
Systemkomplexität führen. Wiederverwendung im Kleinen zu verstehen, ist ein
wesentlicher Aspekt, um Wiederverwendung im Großen zu erzielen.
Das Template Method-Pattern [GOF] ist eine gebräuchliche Technik, um Duplizie­
rung aufi höheren Ebenen zu eliminieren. Ein Beispiel:

publ i c cl ass Vacati o n Po l i cy {


publ i c voi d acc r u eU SDi vi s i onVacati o n ( ) {
II Code z u r Berechnung d e r U r l aubstage a u fg rund de r Arbe i t s st u nden
II . . .
II Code z u r Garant i e des US-Mi ndest u r l aubs
II . . .
II Code fü r d i e B e rechnung des U rl aubsl ohns
II . . .
}

publ i c voi d accr u eEUDi v i s i onVacat i on () {


II Code z u r B e rechnung d e r U r l aubs tage aufg ru nd d e r Arbe i t s s t u nden
II . . .
II Code z u r Garanti e des EU-Mi ndes t u r l aubs
II . . .
II Code fü r d i e B e rech n u ng des U rl aubs l oh n s
II
}
}

Der Code in acc rueUSDi v i s i onVacat i on und acc r ue EUDi v i s i onVacat i on ist
zu großen Teilen gleich; die Ausnahme ist die Berechnung des gesetzlichen Min­
desturlaubs. Dieser Teil des Algorithmus hängt vom Mitarbeitertyp ab.
Mit dem Template Method-Pattern können wir die offensichtliche Duplizierung eli­
minieren:

abst ract publ i c cl ass Vacati o n Pol i cy {


publ i c voi d accrueVacati on () {
cal cul ateBaseVacati onHou r s () ;
al t e r Fo rl egal Mi n i mums () ;
appl yToPay rol l () ;
}

p r i vate voi d cal cul ateBaseVacat i onHou r s () { I * . . . * I } ;


abstract protected voi d al te rForlegal M i n i mums() ;
p r i vate voi d appl yToPayrol l () { /* . . . */ } ;
}

publ i c cl ass U SVacati onPol i cy extends Vacati on Pol i cy {


@Ove r r i de p rotected voi d al t e r Fo rlegal Mi n i mums () {
II US- spezi fi s c h e Logi k

21 3
Kapitel 1 2
Emergenz

}
}

publ i c c l ass EUVacat i o n Po l i cy extends Vacati o n Po l i cy {


@Ove r r i de p rotected voi d a l te r Fo r legal Mi ni mums () {
II EU- spez i fi sche Logi k
}
}
}

Die Unterklassen füllen das »Loch« in dem ac c ru eVacati On-Algorithmus aus und
liefern die einzigen Informationen, die nicht dupliziert wurden.

1 2.5 Ausdrucksstärke
Die meisten Entwickler haben schon mit verwickeltem Code gearbeitet. Viele haben
auch selbst verwickelten Code produziert. Es ist leicht, Code zu schreiben, den man
selbst versteht, weil man zu dem Zeitpunkt, an dem man ihn schreibt, tief in das
betreffende Problem eingetaucht ist. Andere Entwickler, die den Code warten müs­
sen, verstehen ihn nicht so gründlich.
Die Hauptkosten eines Software-Projekts werden durch die langfristige Wartung
verursacht. Um potenzielle Defekte bei Änderungen zu vermeiden, müssen wir ver­
stehen können, was ein System tut. Da Systeme immer komplexer werden, brau­
chen Entwickler immer mehr Zeit, um die Systeme zu verstehen, und die Gefahren
von Missverständnissen werden immer größer. Deshalb sollte Code die Absichten
seines Autors möglichst deutlich ausdrücken. Je klarer der Autor seinen Code
schreiben kann, desto weniger Zeit müssen andere aufwenden, um seinen Zweck
zu verstehen. Dadurch werden Defekte und Wartungskosten reduziert.
Sie können Ihre Zwecke durch geeignete Namen ausdrücken. Wenn wir den Namen
einer Klasse oder Funktion lesen, wollen wir keine Überraschung erleben, wenn wir
seine Verantwortlichkeiten entdecken.
Sie können Ihren Code auch ausdrucksstärker machen, indem Sie Ihre Funktionen
und Klassen klein halten. Kleine Klassen und Funktionen sind normalerweise leich­
ter zu benennen, zu schreiben und zu verstehen.
Sie können Ihren Code auch ausdrucksstärker machen, indem Sie sich an die Stan­
dardnomenklatur halten. So dreht es sich etwa bei Design-Patterns hauptsächlich
um Kommunikation und Ausdruckskraft. Mit standardmäßigen Pattern-Namen,
wie etwa Command oder Visitor, in den Namen der Klassen, die diese Patterns im­
plementieren, können Sie anderen Entwicklern Ihr Design knapp und treffend mit­
teilen.

21 4
1 2.6
M i nimale Klassen und M ethoden

Gut geschriebene Unit-Tests sind ebenfalls ausdrucksstark Tests dienen haupt­


sächlich auch als Beispiele für Dokumentationszwecke. Wer unsere Tests liest,
sollte schnell verstehen können, worum es in einer Klasse geht.
Aber die wichtigste Methode, ausdrucksstarken Code zu schreiben, besteht darin,
es auszuprobieren. Allzu oft bringen wir unseren Code zum Laufen und wenden uns
dann dem nächsten Problem zu, ohne genügend zu fragen, ob der Code für den
nächsten Leser aussagestark genug ist. Vergessen Sie nicht: Wahrscheinlich sind
Sie der Nächste, der den Code lesen wird.
Seien Sie deshalb ein wenig stolz aufihr Können. Verbringen Sie ein wenig Zeit mit
Ihren Funktionen und Klassen. Wählen Sie bessere Namen, zerlegen Sie große
Funktionen in kleinere und kümmern Sie sich ganz allgemein um Ihre Produkte.
Sorgfalt ist eine wertvolle Ressource.

1 2.6 M i nimale Klassen u nd Methoden


Selbst Konzepte, die so grundlegend sind wie die Eliminierung der Duplizierung,
die Ausdrucksstärke des Codes und das S RP, können übertrieben werden. Bei unse­
rem Bemühen, unsere Klassen und Methoden zu verkleinern, erstellen wir mögli­
cherweise zu viele winzige Klassen und Methoden. Deshalb schreibt diese Regel vor,
dass wir auch die Anzahl unserer Funktionen und Klassen niedrig halten sollen.
Eine hohe Anzahl von Klassen und Methoden ist manchmal die Folge eines sinn­
losen Dogmatismus. Betrachten Sie beispielsweise einen Codierstandard, der dar­
auf besteht, dass man für jede Klasse ein I nterface erstellen soll. Oder betrachten
Sie Entwickler, die darauf bestehen, dass Felder und Verhaltensweisen immer in
Daten-Klassen und Verhaltens-Klassen getrennt werden müssen. Auch Sie sollten
sich solchen Dogmen widersetzen und einen pragmatischeren Ansatz verfolgen.
Unser Ziel besteht darin, das Gesamtsystem klein und zugleich die Anzahl unserer
Funktionen und Klassen klein zu halten. Denken Sie jedoch, dass diese Regeln die
niedrigste Priorität unter den Regeln des einfachen Designs haben. Deshalb ist es
zwar wichtig, die Anzahl der Klassen und Funktionen klein zu halten, wichtiger ist
jedoch, Tests zu schreiben, Duplizierung zu eliminieren und ausdrucksstarken
Code zu schreiben.

12.7 Zusa m menfassung


Gibt es einen Satz von einfachen Techniken, die die Erfahrung ersetzen können?
Sicher nicht. Andererseits sind die Techniken (Regeln) , die in diesem Kapitel und
in diesem Buch beschrieben werden, die Quintessenz vieler Jahrzehnte Program­
miererfahrung der Autoren. Die Prinzipien des einfachen Designs zu befolgen, hilft
Entwicklern, bewährte Prinzipien und Patterns anzuwenden, für deren Aneignung
sie sonst Jahre benötigen würden.

215
Kapitel 1 3

N eben läufigkeit

von Brett L. Schuchert

»Objekte sind Abstraktionen der Verarbeitung. Threads sind Abstraktionen


von Zeitabläufen.«
-]ames 0. Coplien (in einer privaten Mitteilung)
Saubere nebenläufige Programme zu schreiben, ist schwer - sehr schwer. Es ist viel
leichter, Code zu schreiben, der in einem einzigen Thread ausgeführt wird. Es ist
ebenfalls leicht, Multithreaded-Code zu schreiben, der an der Oberfläche gut aus­
sieht, aber in einer tieferen Ebene Defekte hat. Solcher Code funktioniert so lange,
bis das System unter Stress gerät.
In diesem Kapitel beschreiben wir, warum die nebenläufige Programmierung erfor­
derlich ist und welche Schwierigkeiten mit ihr verbunden sind. Dann geben wir
mehrere Empfehlungen für den Umgang mit diesen Schwierigkeiten und das
Schreiben von sauberem nebenläufigen Code. Wir schließen mit Problemen, die
beim Testen von nebenläufigem Code auftreten können.

21 7
Kapitel 13
Nebenläufigkeit

Saubere Nebenläufigkeit ist ein komplexes Thema, das ein eigenes Buch verdient.
In diesem Buch können wir nur einen Überblick über dieses Thema geben. Es wird
in diesem Kapitel eingeführt und in Anhang A, Nebenläufigkeit I I, vertieft. Wenn Sie
zunächst nur wissen wollen, worum es bei Nebenläufigkeit geht, reicht dieses Kapi­
tel für Sie im Moment aus. Wollen Sie tiefer in das Thema einsteigen, sollten Sie
auch den Anhang lesen.

1 3.1 Waru m Nebenläufigkeit?


Nebenläufigkeit ist eine Entkopplungsstrategie. Sie hilft das, was getan wird, davon
zu entkoppeln, wann es getan wird. Bei single-threaded Anwendungen sind das
Was und das Wann so stark gekoppelt, dass der Status der gesamten Anwendung
oft durch einen Blick auf den Stack-Backtrace bestimmt werden kann. Ein Program­
mierer, der ein solches System debuggt, kann einen Breakpoint oder eine Folge von
Breakpoints setzen und kennt den Zustand des Systems, das er mithilfe der Break­
points untersucht.
Das Was von dem Wann zu entkoppeln, kann sowohl den Durchsatz als auch die
Struktur einer Anwendung erheblich verbessern. Die Struktur einer solchen ent­
koppelten Anwendung ähnelt eher einer Menge kleiner zusammenarbeitender
Computer als einer großen Hauptschleife. Dadurch kann das System transparenter
gemacht werden; und die Zuständigkeiten können besser verteilt werden.
Betrachten Sie beispielsweise das Standard-»Servlet«-Modell von Webanwendun­
gen. Diese Systeme laufen unter dem Schirm eines Web- oder EJB-Containers, der
einen Teil der Nebenläufigkeit für Sie verwaltet. Die Servlets werden asynchron aus­
geführt, wenn Webanfragen eingehen. Der Servlet-Programmierer muss nicht alle
eingehenden Anfragen verwalten. Im Prinzip lebt jede Servlet-Ausführung in ihrer
eigenen kleinen Welt und ist von allen anderen Servlet-Ausführungen entkoppelt.
Natürlich wäre dieses Kapitel nicht erforderlich, wenn es so leicht wäre. Tatsächlich
ist die Entkopplung, die der Web-Container leistet, bei Weitem nicht perfekt. Serv­
let-Programmierer müssen sehr sorgfältig arbeiten, damit ihre nebenläufigen Pro­
gramme korrekt sind. Dennoch bietet das Servlet-Modell erhebliche strukturelle
Vorteile.
Aber die Strukturverbesserung ist nicht das einzige Motiv für die Einführung der
Nebenläufigkeit Einige Systeme haben Auflagen für ihr Antwortverhalten und
ihren Durchsatz, die häufig manuell codierte nebenläufige Lösungen erfordern.
Betrachten Sie beispielsweise einen single-threaded Informationsaggregator, der
Informationen von vielen verschiedenen Websites sammelt und daraus einer täg­
liche Zusammenfassung erstellt. Weil das System single-threaded ist, werden alle
Websites nacheinander abgefragt. Eine Website wird erst fertig bearbeitet, bevor die
nächste in Angriff genommen wird. Der tägliche Lauf muss in weniger als 24 Stun­
den ausgeführt werden. Doch je mehr Websites hinzugefügt werden, desto mehr

218
l J.l
Warum Nebenläufigkeit?

Zeit wird benötigt, bis mehr als 24 Stunden benötigt werden, um alle Daten zu sam­
meln. Die Single-Thread-Lösung führt zu langen Wartezeiten an den Websockets,
während die 1/0 abgewickelt wird. Wir könnten das Leistungsverhalten mit einem
multithreaded Algorithmus verbessern, der mehr als eine Website gleichzeitig
besucht.
Oder betrachten Sie ein System, das immer nur einen Benutzer auf einmal abfertigt
und nur eine Sekunde Zeit pro Benutzer benötigt. Bei wenigen Benutzern reagiert
dieses System recht flott; aber wenn die Anzahl der Benutzer zunimmt, werden die
Antwortzeiten des Systems länger. Kein Benutzer möchte in einer Schlange hinter
r 50 anderen warten! Wir könnten die Antwortzeit dieses Systems verbessern, indem
wir viele Benutzer nebenläufig (parallel, gleichzeitig) abfertigen.
Oder betrachten Sie ein System, das große Datenmengen verarbeitet, aber erst dann
eine komplette Lösung liefert, nachdem es alle Daten verarbeitet hat. Vielleicht
könnten Teilmengen der Daten auf verschiedenen Computern parallel verarbeitet
werden.

Mythen u nd fal sche Vorstellu ngen

Die Beispiele sollten gezeigt haben, dass es überzeugende Gründe dafür gibt,
nebenläufig zu programmieren. Doch wie bereits erwähnt: Nebenläufigkeit ist
schwer. Wenn Sie nicht sehr sorgfältig arbeiten, können Sie einige sehr hässliche
Situationen produzieren. Betrachten Sie die folgenden verbreiteten Mythen und
Fehlvorstellungen:
• Nebenläufigkeit verbessert immer das Leistungsverhalten. Nebenläufigkeit kann das
Leistungsverhalten manchmal verbessern, aber nur wenn es zahlreiche Warte­
zeiten gibt, die von mehreren Threads oder Prozessoren genutzt werden könn­
ten. Keine Situation ist trivial.

• Das Design ändert sich nicht, wenn man nebenläufige Programme schreibt. Tatsäch­
lich kann sich das Design eines nebenläufigen Algorithmus stark von dem ei­
nes single-threaded Systems unterscheiden. Die Entkopplung des Was vom
Wann hat normalerweise einen riesigen Einfluss auf die Struktur des Systems.

• Nebenläufigkeitsprobleme zu verstehen, ist nicht wichtig, wenn man mit einem Con­
tainer wie etwa einem Web- oder E]B-Container arbeitet. Tatsächlich sollten Sie ge­
nau wissen, was Ihr Container macht, um Probleme mit nebenläufigen Up­
dates und Deadlocks, die später in diesem Kapitel beschrieben werden, mög­
lichst zu vermeiden.

Hier sind einige Tipps für das Schreiben nebenläufiger Software:


• Nebenläufigkeit verlangt einen gewissen Verwaltungsaufwand, der das Leistungs­
verhalten etwas verschlechtert und zusätzlichen Code erfordert.

• Korrekte Nebenläufigkeit ist komplex, selbst bei einfachen Problemen.

21 9
Kapitel 13
Nebenläufigkeit

• Nebenläufigkeit-Bugs sind normalenveise nicht rq�roduzierbar; deshalb werden sie


oft als einmalige Erscheinungen (kosmische Strahlung, Glitches usw.) abge­
schrieben und nicht, wie es erforderlich wäre, als echte Defekte behandelt.

• Nebenläufigkeit erfordert oft eine grundlegende Anderung der Design-Strategie.

1 3.2 Herausforderungen
Was macht die nebenläufige Programmierung so schwierig? Betrachten Sie die fol­
gende triviale Klasse:

publ i c c l ass X {
p r i vate i nt l astidUsed ;
publ i c i nt getNextid () {
ret u rn ++l astidUsed ;
}
}

Angenommen, wir erstellten eine Instanz von X, setzen das 1 astidUs ed-Feld auf
42 und nutzen dann die Instanz in zwei Threads, die beide die Methode getNext­
Id () aufrufen. Dann gibt es drei mögliche Ergebnisse:
• Thread eins erhält den Wert 43, Thread zwei erhält den Wert 44, 1 astidUsed
ist 44-

• Thread eins erhält den Wert 44, Thread zwei erhält den Wert 43, 1 astidUsed
ist 44-

• Thread eins erhält den Wert 43, Thread zwei erhält den Wert 43, 1 astidUsed
ist 43·

Das überraschende dritte Ergebnis (siehe den Abschnitt Tiefer graben in Anhang
A.2) tritt ein, wenn die beiden Threads in Konflikt geraten. Dies passiert, weil es
viele mögliche Pfade gibt, auf denen die beiden Threads diese Zeile des Java-Codes
durchlaufen können und einige dieser Pfade falsche Ergebnisse erzeugen. Wie viele
verschiedene Pfade gibt es? Um diese Frage wirklich zu beantworten, müssen Sie
verstehen, was der Just-in-Time-Compiler mit dem generierten Byte-Code anstellt
und was das Memory-Model von Java als atomar betrachtet.

Eine schnelle Antwort: Wenn wir nur den generierten Byte-Code betrachten, gibt es
12.870 verschiedene mögliche Ausführungspfade, wie diese beiden Threads inner­
halb der getNextid-Methode ausgeführt werden können. ( Eine detailliertere Ana­
lyse finden Sie in Anhang A.2 im Abschnitt Mögliche Ausjührungspfade.) Wenn der
Typ von 1 astidUs ed von i n t in 1 ong geändert wird, wächst die Anzahl der mög­
lichen Pfade auf 2.704-156. Natürlich erzeugen die meisten dieser Pfade gültige
Ergebnisse. Das Problem liegt darin, dass einige falsche Ergebnisse generieren.

220
Prinzipien einer defensiven Nebenläufigkeitsprogrammierung
13-3 1
1 3·3 Prinzipien einer defensiven Nebenläufigkeits-
.
I
program m1eru ng
Die folgenden Abschnitte enthalten mehrere Prinzipien und Techniken, mit denen
Sie Ihre Systeme gegen Probleme mit nebenläufigem Code schützen können.

Si ngle-Responsibility- Pri nzip

Das Single-Responsibility-Prinzip (SRP; Prinzip der einzigen Verantwortlichkeit;


[PPP]) besagt, dass eine MethodejKlassejKomponente nur einen einzigen Grund
haben sollte, sich zu ändern. Das Design von nebenläufigen Programmen ist kom­
plex genug. Änderungen durch nebenläufigen Code sollten nicht mit Änderungen
durch den restlichen Code vermengt werden. Leider sind die Details einer Neben­
läufigkeitsimplementierung allzu oft direkt in anderen Produktionscode eingebet­
tet. Hier sind einige Punkte, die Sie beachten sollten:
• Nebenläufiger Code hat einen eigenen Lebenszyklus mit Entwicklung, A.nderung und
Feinschliff.
• Nebenläufiger Code ist mit besonderen Problemen verbunden, die eine andere Form
und oft auch einen höheren Schwierigkeitsgrad haben als nicht nebenläufiger
Code.

• Die Anzahl der Fehlermöglichkeiten bei schlecht geschriebenem nebenläufi­


gen Code macht die Programmierung auch ohne die zusätzliche Last des ande­
ren Anwendungscodes schwer genug.

Empfehlung: Trennen Sie den nebenläufigen Code von dem anderen Code (siehe das Cli­
entjServer-Beispiel in Anhang A.1).

Korollar: Beschränken Sie den G ü ltigkeitsbereich von Daten

Wie wir gesehen haben, können zwei Threads, die dasselbe Feld eines gemeinsam
genutzten Objekts modifizieren, in Konflikt geraten und ein unerwartetes Verhal­
ten auslösen. Eine Lösung besteht darin, einen kritischen Abschnitt in dem Code, der
auf das gemeinsam genutzte Objekt zugreift, mit dem Schlüsselwort synch ron i ­
zed zu schützen.
Es ist wichtig, die Anzahl solcher kritischen Abschnitte einzuschränken. Je mehr
Stellen gemeinsam genutzter Daten aktualisiert werden können, desto wahrschein­
licher werden folgende Ereignisse:
• Sie vergessen, eine oder mehrere dieser Stellen zu schützen, wodurch praktisch
der gesamte Code defekt wird, der die gemeinsam genutzten Daten modifiziert.

• Der erforderliche Aufwand für den wirksamen Schutz aller Stellen wird praktisch
verdoppelt (ein Verstoß gegen das DRYP, Don't-repeat-Yourself-Prinzip; [PRAG]).

221
Kapitel 13
Nebenläufigkeit

• Es wird noch schwerer werden, die Quelle von Fehlern zu finden, die bereits
schwer genug zu finden sind.
Empfehlung: Nehmen Sie sich die Datenkapselung zu Herzen; schränken Sie den Zugriff
auf alle gemeinsam genutzten Daten nachhaltig ein.

Korollar: Arbeiten Sie m it Kopien der Daten

Eine gute Methode, die gemeinsame Nutzung von Daten zu vermeiden, besteht
darin, sie ganz zu umgehen. In einigen Situationen können Sie Objekte kopieren
und als read-only behandeln. In anderen Fällen könnte es möglich sein, Objekte zu
kopieren, Ergebnisse aus mehreren Threads in diese Kopien zu sammeln und dann
die Ergebnisse in einem einzigen Thread zusammenzuführen.

Falls sich eine leichte Möglichkeit anbietet, sollten Sie die gemeinsame Nutzung
von Objekten vermeiden. Der Code wird wahrscheinlich sehr viel weniger Probleme
verursachen. Vielleicht machen Sie sich Gedanken über die Kosten, um all die
zusätzlichen Objekte zu erstellen. Es lohnt sich, durch Experimente herauszufin­
den, ob dies wirklich ein Problem ist. Doch wenn es möglich ist, mit Kopien von
Objekten eine Synchronisierung zu vermeiden, heben die Einsparungen des Auf­
wands für intrinsische Locks wahrscheinlich die Kosten für die zusätzliche Erstel­
lung der Objekte und die Garhage Collection wieder auf.

Korollar: Threads sol lten voneinander so u nabhängig


wie möglich sei n

Wenn Sie nebenläufigen Code schreiben, sollten Sie j eden Thread möglichst so
schreiben, als existierte er in seiner eigenen Welt, ohne Daten mit einem anderen
Thread zu teilen. Jeder Thread verarbeitet eine Client-Anfrage, wobei alle erforder­
lichen Daten aus einer nicht mit anderen Threads geteilten Quelle stammen und
als lokale Variablen gespeichert werden. Dadurch verhalten sich diese Threads so,
als wären sie die einzigen Threads auf der Welt und als gäbe es keine Synchronisa­
tionsanforderungen.
Beispielsweise erhalten alle von HttpServl e t abgeleiteten Klassen alle Informati­
onen als Parameter, die in den doGet- und doPost-Methoden übergeben werden.
Dadurch verhält sich jedes Servlet so, als hätte es eine eigene Maschine. Solange der
Code in dem Servlet nur lokale Variablen verwendet, besteht keine Gefahr, dass das
Servlet Synchronisationsprobleme verursacht. Natürlich verwenden die meisten
mit Servlets arbeitenden Anwendungen irgendwann gemeinsam genutzte Ressour­
cen wie etwa Datenbankverbindungen.
Empfehlung: Versuchen Sie, Daten in unabhängige Teilmengen zu zerlegen, die von
unabhängigen Threads, möglicherweise in verschiedenen Prozessoren, verarbeitet werden
können.

222
1 3·4
Lernen Sie I h re Library kennen

13.4 Lernen Sie I h re Library kennen


Java 5 enthält gegenüber früheren Versionen viele Verbesserungen für die Entwick­
lung von nebenläufigem Code. Sie müssen mehrere Dinge beachten, wenn Sie
nebenläufigen Code in Java 5 schreiben:
• Verwenden Sie die zur Verfügung gestellten thread-sicheren Collections.

• Verwenden Sie das Executor Framewerk zur Ausführung unzusammenhän­


gender Tasks.

• Verwenden Sie nicht blockierende Lösungen, falls möglich.

• Mehrere Library-Klassen sind nicht thread-sicher.

Thread-sichere Collections

In der Anfangszeit von Java schrieb Doug Lea das bahnbrechende Buch Concurrent
Programming in Java [Lea99]. Zusätzlich zu dem Buch entwickelte er mehrere
thread-sichere Collections, die später in das j ava . uti 1 . con c u r rent-Package des
JDK übernommen wurden. Die Collections in diesem Package können gefahrlos in
multithreaded Situationen verwendet werden und zeigen ein gutes Leistungsver­
halten. Tatsächlich arbeitet die Concu r re n tHas hMap-lmplementierung in fast allen
Situationen besser als die HashMap. Sie ermöglicht auch gleichzeitige nebenläufige
Lese- und Schreibvorgänge und verfügt über Methoden, die gebräuchliche zusam­
mengesetzte Operationen unterstützen, die sonst nicht thread-sicher sich. Wenn
die Deployrnent-Umgebung Java 5 verwendet, sollten Sie mit Con c u r re ntHas hMap
beginnen.
Mehrere andere Klassen unterstützen ebenfalls nebenläufigen Code. Einige Bei­
spiele:

Reentrantlock Ein Lock, das in einer Methode gesetzt und in einer anderen aufgeho-
ben werden kann.

Semaphore Eine Implementierung der klassischen Semaphore, ein Lock mit


einem Zähler.

CountDown latch Ein Lock, das eine Anzahl von Ereignissen abwartet, bevor es alle auf es
wartenden Threads freigibt. Dadurch haben alle Threads eine faire
Chance, etwa zur gleichen Zeit zu starten.

Empfehlung: Studieren Sie die Klassen, die Ihnen zur Veifügung stehen. Bei Java sollten
Sie sich mit java . uti 7 . concurrent, java . uti 7 . concurrent . a toma r, java . uti 7 .
concurren t . 7ocks vertraut machen.

223
Kapitel 13
Nebenläufigkeit

13.5 Lernen Sie I h re Ausfü h ru n gsmodelle kennen


Es gibt mehrere Methoden, Verhalten in einer nebenläufigen Anwendung aufzu­
teilen. Zunächst müssen Sie einige grundlegende Definitionen kennen:

Bound Resources Ressourcen fester Größe oder Anzahl, die in einer nebenläufigen
(gebundene Res· Umgebung verwendet werden. Beispiele sind Datenbankverbindungen
sourcen) und Lesej Schreib-Puffer fester Größe.

Mutual- Exclusion Nur ein Thread kann gleichzeitig auf gemeinsam genutzte Daten oder
(gegenseitiger eine gemeinsam genutzte Ressource zugreifen.
Ausschluss)

Starvation Ein Thread oder eine Gruppe von Threads wird außerordentlich lange
(Verhungern) oder für immer daran gehindert, weiterzuarbeiten. Wenn beispiels·
weise schnell ablaufende Threads immer zuerst bedient werden,
könnte dies dazu führen, dass lange laufende Threads verhungern,
wenn die schnell laufenden Threads kein Ende nehmen.

Deadlock Zwei oder mehr Threads warten gegenseitig auf das Ende desjder
jeweils anderen. Jeder Thread hat eine Ressource, die von demfden
anderen benötigt wird, und keiner kann aufhören, bevor er nicht die
fehlende Ressource bekommt.

Livelock Threads versuchen , im Gleichschritt zu laufen, stehen sich dabei aber


gegenseitig im Weg. Aufgrund von Resonanz versuchen sie immer wei·
ter, Fortschritte zu machen, kommen aber für außerordentlich lange
Zeit oder niemals voran.

Mit dieser Definitionen können wir jetzt die verschiedenen Ausführungsmodelle


beschreiben, die in der nebenläufigen Programmierung verwendet werden.

Erzeuger-Verbraucher

Das Erzeuger-Verbraucher-Problem (http : I /de . wi ki pedi a . o rg/wi ki / E rzeuge r ­


Ve rb rauch e r - P robl em; engl. Producer-Consumer) zählt z u den klassischen Proble­
men der Prozess- Synchronisation. Einer oder mehrere Erzeuger-Threads erstellen
ein Produkt und fügen es in einen Puffer oder eine Queue ein. Einer oder mehrere
Verbraucher-Threads rufen das Produkt aus der Queue ab und ergänzen es. Die
Queue zwischen den Erzeugern und Verbrauchern ist eine gebundene Ressource.
Dies bedeutet, dass die Erzeuger auf einen freien Platz in der Queue warten müs­
sen, bevor sie schreiben können, und die Verbraucher warten müssen, bis die
Queue etwas Konsumierbares enthält. Die Koordination zwischen den Erzeugern
und Verbrauchern mittels der Queue erfordert den Austausch von Signalen zwi­
schen den Erzeugern und Verbrauchern. Die Erzeuger schreiben in die Queue und
signalisieren, dass die Queue nicht mehr leer ist. Die Verbraucher lesen aus der
Queue und signalisieren, dass die Queue nicht mehr voll ist. Beide müssen mögli­
cherweise auf ein Signal warten, dass sie fortfahren können.

224
lJ.s
Lernen Sie I h re Ausführungsmodelle kennen

Leser-Schreiber

Das Leser-Schreiber-Problem (engl. Readers-Writers) zählt ebenfalls zu den klassischen


Problemen der Prozess-Synchronisation. Wenn Sie über eine gemeinsam genutzte
Ressource verfügen, die hauptsächlich als Informationsquelle für Leser dient, aber
gelegentlich von Schreibern aktualisiert wird, ist der Durchsatz ein Problem. Wird
der Durchsatz betont, kann es verhungern und eine Ansammlung überholter Infor­
mationen kann eintreten. Lässt man Aktualisierungen zu, kann der Durchsatz
beeinträchtigt werden. Leser und Schreiber so zu koordinieren, dass die Leser nicht
lesen, während der Schreiber aktualisiert, und umgekehrt, ist ein schwieriger
Balanceakt. Schreiber neigen dazu, viele Leser für lange Zeitspannen zu blockieren,
und verursachen so Probleme mit dem Durchsatz.
Die Herausforderung liegt darin, die Anforderungen der Leser und der Schreiber
so auszugleichen, dass eine korrekte Operation gewährleistet, ein vernünftiger
Durchsatz erzielt und Verhungern vermieden wird. Eine einfache Strategie besteht
darin, Schreiber warten zu lassen, bis keine Leser da sind, bevor der Schreiber eine
Aktualisierung durchführen darf. Doch wenn es Dauerleser gibt, verhungern die
Schreiber. Gibt es dagegen Schreiber mit häufigen Aktualisierungen und erhalten
sie Priorität, leidet der Durchsatz. Hier besteht das Problem also darin, die geeignete
Balance zu finden und Probleme mit den nebenläufigen Updates zu vermeiden.

Phi losophen problem

Das Philosophenproblem (http : I /de . wi k i pedi a . o rg/wi ki /Phi l osophenp rob-


1 em; engl. Dining-Philosophers-Problem) ist ein weiteres klassisches Problem der Pro­
zess-Synchronisation. Stellen Sie sich vor, dass mehrere Philosophen an einem
runden Tisch sitzen. Links neben jedem Philosophen liegt eine Gabel. In der Mitte
des Tisches steht eine große Schüssel Spaghetti. Die Philosophen denken nach, bis
sie hungrig sind. Wenn sie hungrig sind, nehmen sie die Gabeln links und rechts
neben sich auf und essen. Ein Philosoph kann nur essen, wenn er zwei Gabeln hat.
Hat einer der Philosophen zu seiner Rechten oder Linken bereits eine der benötig­
ten Gabeln in der Hand, muss der hungrige Philosoph warten, bis der Nachbar mit
dem Essen fertig ist und seine Gabeln wieder hinlegt. Nachdem ein Philosoph
gegessen hat, legt er beide Gabeln wieder auf den Tisch und wartet, bis er wieder
hungrig ist.
Wenn Sie Philosophen durch Threads und Gabeln durch Ressourcen ersetzen,
beschreibt dieses Problem viele Unternehmensanwendungen, in denen Prozesse
um Ressourcen konkurrieren. Bei einem nachlässigen Design können derartig kon­
kurrierende Systeme Deadlocks, Livelocks, Durchsatzabfalle und Effizienzverluste
erleiden.

Die meisten Nebenläufigkeitsprobleme, denen Sie begegnen werden, werden wahr­


scheinlich eine Variante dieser drei Probleme sein. Studieren Sie die einschlägigen

225
Kapitel 13
Nebenläufigkeit

Algorithmen und wenden Sie sie in Ihren eigenen Lösungen an, damit Sie auf diese
Nebenläufigkeitsprobleme vorbereitet sind.
Empfehlung: Studieren Sie die grundlegenden Algorithmen und ihre Anwendung in
Lösungen.

1 3.6 Achten Sie auf Abhängigkeiten zwischen


synch ronisierten Methoden
Abhängigkeiten zwischen synchronisierten Methoden in nebenläufigem Code ver­
ursachen subtile Bugs. Java enthält das Konstrukt synch roni zed, das eine einzelne
Methode schützt. Doch da dieselbe gemeinsam genutzte Klasse mehr als eine syn­
e h roni zed Methode enthält, ist Ihr System möglicherweise falsch konzipiert (siehe
den Abschnitt Abhängigkeiten zwischen Methoden können nebenläufigen Code beschä­
digen in Anhang A-4) .
Empfehlung: Vermeiden Sie es, mehr als eine Methode auf ein gemeinsam genutztes
Objekt anzuwenden.
Manchmal müssen Sie mehr als eine Methode auf ein gemeinsam genutztes Obj ekt
anwenden. In diesem Fall gibt es drei Möglichkeiten, korrekten Code zu schreiben:
• Clientbasiertes Locking - Der Client sollte den Server sperren, bevor die erste
Methode aufgerufen wird, und dafür sorgen, dass das Lock den Code ein­
schließt, der die letzte Methode aufruft.

• Serverbasiertes Locking - Erstellen Sie im Server eine Methode, die den Server
sperrt, alle Methoden aufruft und dann die Sperre aufhebt. Lassen Sie den Cli­
ent die n ew-Methode aufrufen.

• Adapted Server - Erstellen Sie eine intermediäre Komponente, die die Sperre
ausführt. Dies ist eine Variante des serverbasierten Lockings, wenn der ur­
sprüngliche Server nicht geändert werden kann.

1 3.7 Halten Sie synchronisierte Absch n itte klei n


Das Schlüsselwort synch roni zed setzt ein Lock (Sperre) . Alle Code-Abschnitte, die
durch dasselbe Lock geschützt werden, werden garantiert nur von einem Thread
gleichzeitig ausgeführt. Locks sind teuer, weil sie Verzögerungen und zusätzlichen
Verwaltungsaufwand verursachen. Deshalb sollten Sie in Ihrem Code synch roni ­
zed-Anweisungen nur sparsam verwenden. Andererseits müssen kritische
Abschnitte geschützt werden. (Ein kritischer Abschnitt ist ein Code-Abschnitt, der
nur dann korrekt ausgeführt werden kann, wenn nicht gleichzeitig mehrere
Threads auf ihn zugreifen.)
Deshalb sollte Ihr Code so wenig kritische Abschnitte wie möglich enthalten.

226
1 3 .8
Korrekten Shutdown-Code zu schreiben, ist schwer

Einige naive Programmierer versuchen, dies zu erreichen, indem sie ihre kritischen
Abschnitte sehr groß wählen. Doch wenn die Synchronisation über den minimalen
kritischen Abschnitt hinaus vergrößert wird, nehmen die Konflikte zu und die Leis­
tung verschlechtert sich (siehe den Abschnitt Den Durchsatz verbessern in Anhang
A. s ) .
Empfehlung: Halten Sie Ihre synchronisierten Abschnitte so klein wie möglich.

1 3.8 Korrekten Shutdown-Code zu schreiben, ist schwer


Ein System zu schreiben, das aktiv bleiben und für immer laufen soll, unterscheidet
sich von einem System, das eine Zeit lang arbeitet und dann kontrolliert abgeschal­
tet wird.
Ein kontrolliertes Abschalten ist oft schwer zu realisieren. Zu den üblichen Proble­
men zählen Deadlocks (siehe auch Anhang A.6), bei denen Threads auf Signale war­
ten, die nie gesendet werden.
Stellen Sie sich beispielsweise ein System mit einem Parent-Thread vor, der meh­
rere Child-Threads hervorbringt und dann darauf wartet, dass sie alle beendet wer­
den, bevor er seine Ressourcen freigibt und sich abschaltet. Was passiert, wenn
einer der Child-Threads in einen Deadlock gerät? Der Parent-Thread wartet ewig,
und das System wird niemals abgeschaltet.
Oder betrachten Sie ein ähnliches System, das angewiesen wurde, sich abzuschalten.
Der Parent-Thread weist alle seine Child-Threads an, ihre Tasks abzubrechen und
sich zu beenden. Aber was passiert, wenn zwei Children als Producer-Consumer­
Paar zusammenarbeiten? Nehmen Sie an, der Producer empfange das Signal von
dem Parent und schalte sich schnell ab. Der Consumer könnte auf eine Nachricht
von dem Producer gewartet haben und in einem Zustand blockiert sein, in dem er
das Shutdown-Signal nicht empfangen kann. Er könnte aufewig weiter aufden Pro­
ducer warten und verhindern, dass der Parent auch beendet werden kann.
Situationen wie diese sind durchaus nicht selten. Wenn Sie also nebenläufigen Code
schreiben müssen, bei dem es auch um ein kontrolliertes Abschalten geht, müssen
Sie damit rechnen, viel Zeit darin zu investieren, den Shutdown korrekt zu pro­
grammieren.
Empfehlung: Sie sollten bereitsfrüh über einen Shutdown nachdenken und ihn möglichst
früh zum Laufen bringen. Wenn Sie warten, dauert es immer länger. Studieren Sie vor­
handene A�gorithmen, weil diese Aufgabe wahrscheinlich schwerer ist, als Sie denken.

1 3.9 Threaded-Code testen


Die Korrektheit von Code zu beweisen, ist unpraktisch. Tests können keine Korrekt­
heit garantieren. Doch gute Tests können das Risiko vermindern. Dies gilt vor allem

227
Kapitel 13
Nebenläufigkeit

für Lösungen, die mit einem einzigen Thread arbeiten. Sobald zwei oder mehr
Threads denselben Code verwenden und dieselben Daten gemeinsam nutzen, wird
alles sehr viel komplexer.
Empfehlung: Schreiben Sie Tests, die Probleme aufdecken können, und führen Sie sie
dann mit verschiedenen Programm- und Systemkonfigurationen und Lasten aus. Sollten
Tests scheitern, suchen Sie nach der Ursache und beheben Sie sie. Ignorieren Sie ein Schei­
tern nicht nur deshalb, weil die Tests bei einer nachfolgenden Ausführung bestanden wer­
den.
Es gibt zahlreiche Faktoren, die Sie berücksichtigen müssen. Hier sind einige stär­
ker aufgegliederte Empfehlungen:
• Behandeln Sie gelegentliche Fehler als potenzielle Threading-Probleme.

• Bringen Sie zunächst Ihren Nonthreaded-Code zum Laufen.

• Machen Sie Ihren Threaded-Code pluggable.

• Machen Sie Ihren Threaded-Code anpassbar.


• Führen Sie den Code mit mehr Threads als Prozessoren aus.

• Führen Sie den Code auf verschiedenen Plattformen aus.

• Instrumentieren Sie Ihren Code, um Fehler zu provozieren.

Behandeln Sie gelegentl ich auftretende Feh ler als


potenzielle Threadi ng-Probleme

Threaded-Code bringt Komponenten zum Scheitern, die »einfach nicht scheitern


können«. Die meisten Entwickler haben kein intuitives Verständnis dafür, wie
Threading mit anderem Code interagiert (die Autoren eingeschlossen) . Bugs in
Threaded-Code können ihre Symptome einmal in tausend oder in einer Million
Ausführungen zeigen. Versuche, Abläufe zu reproduzieren, können frustrierend
sein. Dies verleitet Entwickler oft dazu, einen Fehler aufkosmische Strahlen, einen
Hardware-Glitch oder ein anderes einmaliges Ereignis zu schieben. Am besten
gehen Sie davon aus, dass derartige ))einmalige Ereignisse« nicht existieren. Je län­
ger Sie sie ignorieren, desto mehr Code wird auf einem potenziell fehlerhaften
Ansatz aufgebaut.
Empfehlung: Behandeln Sie Systemfehler nicht als »einmalige Ereignisse«.

Bringen Sie erst den N onthreaded-Code z u m Laufen

Dies mag offensichtlich sein, aber es schadet nicht, diesen Punkt extra zu betonen.
Sorgen Sie dafür, dass der Code außerhalb der Threads funktioniert. Im Allgemei­
nen bedeutet dies, dass Sie POJOs erstellen, die von Ihren Threads aufgerufen wer­
den. Die POJOs wissen nichts von den Threads und können deshalb außerhalb der

2.28
1 3 ·9
Threaded-Code testen

mit Threads arbeitenden Umgebung getestet werden. Je größer der Anteil Ihres Sys­
tems ist, den Sie in solche POJOs einfügen können, desto besser.
Empfehlung: Versuchen Sie nicht, nicht Thread-bezogene Bugs und Thread-bezogene
Bugs gleichzeitig zu beheben. Sorgen Sie dafür, dass Ihr Code außerhalb von Threadsfunk­
tioniert.

M achen Sie I h ren Th readed-Code pluggable

Schreiben Sie nebenläufigen Code so, dass er in mehreren Konfigurationen ausge­


führt werden kann:
• Ein Thread, mehrere Threads, bei der Ausführung variierend

• Threaded-Code, der sowohl mit echten Daten als auch mit Test-Doubles inter-
agieren kann
• Ausführung mit Test-Doubles, die variabel schnell oder langsam laufen

• Tests so konfigurieren, dass die Anzahl der Iterationen beliebig ist

Empfehlung: Machen Sie Ihren threadbasierten Code besonders pluggable, damit Sie ihn
in verschiedenen Konfigurationen ausführen können.

Schreiben Sie a n passbaren Th readed-Code

Normalerweise wird die richtige Balance von Threads nur durch Versuch und Irr­
tum gefunden. Sie sollten sich gleich am Anfang überlegen, wie Sie das Leistungs­
verhalten Ihres Systems unter verschiedenen Konfigurationen messen können.
Treffen Sie Vorkehrungen, damit Sie die Anzahl der Threads leicht anpassen kön­
nen, auch während das System läuft. Erwägen Sie auch, Möglichkeiten zu schaffen,
dass sich das System selbst je nach Durchsatz und Nutzung optimiert.

Den Code m it mehr Th reads als Prozessoren ausfü h ren

Dinge passieren, wenn das System zwischen Tasks wechselt. Um den Task-Wechsel
anzuregen, sollten Sie den Code mit mehr Threads ausführen, als Prozessoren oder
Kerne vorhanden sind. Je häufiger Ihre Tasks gewechselt werden, desto wahrschein­
lich stoßen Sie auf Code, der einen kritischen Abschnitt verpasst oder einen Dead­
lock verursacht.

Den Code aufverschiedenen Plattformen a usfü h ren

2007 entwickelten wir einen Kurs über die nebenläufige Programmierung. Die
Kursentwicklung erfolgte hauptsächlich unter OS X. Das Seminar wurde mit Win­
dows XP in einer VM präsentiert. Tests, die geschrieben wurden, um Fehlerbedin­
gungen zu demonstrieren, scheiterten in einer XP-Umgebung nicht so häufig wie
unter OS X.

229
Kapitel 13
Nebenläufigkeit

In allen Fällen war der zu testende Code bekanntermaßen defekt. Dies bestätigt nur
die Tatsache, dass verschiedene Betriebssysteme unterschiedliche Threading-Poli­
cies haben, die die Ausführung des Codes beeinflussen. Multithreaded-Code verhält
sich in verschiedenen Umgehungen unterschiedlich. (Wussten Sie, dass das Threa­
ding-Modell in Java kein präemptives Threading garantiert? Moderne Betriebssys­
teme unterstützen präemptives Threading; deshalb erhalten Sie es »kostenlos«.
Doch selbst dann wird es von der JVM nicht garantiert.) Sie sollten Ihre Tests in jeder
potenziellen Deployment-Umgebung ausführen.
Empfehlung: Führen Sie nebenläufigen Codefrüh und oft aufallen Zielplattfonnen aus.

Code-Scheitern d u rch I n stru me ntieru ng provozieren

Defekte in nebenläufigem Code sind normalerweise verborgen und lassen sich


durch einfache Tests oft nicht aufdecken. Tatsächlich bleiben sie bei der normalen
Verarbeitung verborgen. Sie treten nur alle paar Stunden oder Tage oder sogar
Wochen auf!
Der Grund, warum Threading-Bugs so selten auftreten können und so schwer zu
reproduzieren sind, liegt darin, dass nur sehr wenige der vielen Tausend möglichen
Ausführungspfade durch einen verletzbaren Abschnitt tatsächlich scheitern. Deshalb
kann die Wahrscheinlichkeit für das Scheitern eines Ausführungspfades erstaunlich
gering sein. Dies macht das Erkennen und das Debugging sehr schwierig.

Wie können Sie Ihre Chancen verbessern, solche seltenen Ereignisse einzufangen?
Sie können Ihren Code instrumentieren und ihn zwingen, in verschiedenen Rei­
henfolgen zu laufen, indem Sie Aufrufe an Methoden wie Obj ect . wa i t ( ) ,
Obj ect . s l eep () , Obj ect . y i el d () und Obj ect . p r i o r i ty () hinzufügen.
Jede dieser Methoden kann die Reihenfolge der Ausführung beeinflussen und des­
halb die Chancen für die Entdeckung eines Mangels verbessern. Es ist besser, wenn
defekter Code so früh und so oft wie möglich scheitert.
Es gibt zwei Optionen für Code-Instrumentierung:
• Manuelle Codierung

• Automatisiert

M a n uelle Codierung

Sie können Aufrufe von wai t ( ) , s l eep () , yi e l d () und p r i o r i ty () manuell in


Ihren Code einfügen. Wenn Sie einen verzwickten Code-Bereich testen, könnte dies
genau die richtige Maßnahme sein.
Hier ist ein Beispiel für diese Vorgehensweise:

publ i c synch ron i zed St ri ng next U rl OrNu l l () {


i f (hasNext ( ) ) {
S t ri ng u rl = u rl Gene rato r . n ext () ;
1 3-9
Threaded-Code testen

Th read . yi e l d () ; // fü r Te stzwec ke ei ngefügt


u pdateHasNext () ;
retu rn u rl ;
}
retu r n n ul l ;
}

Der eingefügte Aufruf von yi e 1 d() ändert die Ausführungspfade des Codes und
führt möglicherweise dazu, dass er an Stellen scheitert, die er zuvor problemlos pas­
siert hatte. Wenn der Code scheitert, liegt das nicht an dem zusätzlichen Aufrufvon
y i e 1 d() , sondern er war bereits vorher defekt; und jetzt wurde dieser defekte
Bereich aufgedeckt. (Dies ist nicht ganz richtig. Da die JVM kein präemptives Threa­
ding garantiert, könnte ein bestimmter Algorithmus immer unter einem OS funk­
tionieren, das Threads nicht präemptiv verarbeitet. Das Umgekehrte ist ebenfalls
möglich, allerdings aus anderen Gründen.)
Bei diesem Ansatz gibt es mehrere Probleme:
• Sie müssen die entsprechenden Stellen für diese Maßnahme manuell suchen.

• Woher wissen Sie, wo Sie den Aufruf einfügen müssen und welche Art von
Aufruf Sie verwenden sollten?

• Wird solcher Code in eine Produktionsumgebung übernommen, verlangsamt


er der Ausführung unnötigerweise.

• Es ist ein Schrotschuss-Ansatz. Vielleich entdecken Sie Mängel, vielleicht auch


nicht. Tatsächlich stehen die Chancen gegen Sie.

Wir brauchen eine Methode, um diese Maßnahmen beim Testen, aber nicht in der
Produktion durchzuführen. Außerdem brauchen wir eine Möglichkeit, Konfigura­
tionen zwischen verschiedenen Ausführungen schnell und leicht zu ändern, um
unsere Chancen zu verbessern, Fehler zu finden.
Wenn wir unser System in POJOs, die nichts über Threading wissen, und Klassen
zerlegen, die das Threading steuern, wird es offensichtlich leichter sein, die ent­
sprechenden Stellen für die Instrumentierung des Codes zu lokalisieren. Darüber
hinaus könnten wir viele verschiedene Test-Jigs erstellen, die die POJOs mit ver­
schiedenen Kombinationen von s 1 eep, y i e 1 d usw. aufrufen.

Automatisiert

Sie könnten Tools wie etwa Aspect-Oriented Framework, CGLIB oder ASM einset­
zen, um Ihren Code per Programm zu instrumentieren. Beispielsweise könnten Sie
eine Klasse mit einer einzigen Methode verwenden:

publ i c c l ass Th read J i gg l ePo i n t {


publ i c stat i c voi d j i gg l e () {
}
}
Kapitel 13
Nebenläufigkeit

Sie könnten dann Aufrufe dieser Klasse an verschiedenen Stellen in Ihrem Code
einfügen:

p u b l i c syn ch roni z ed St r i ng n ex t U r l OrNul l () {


i f ( h asNext () ) {
Th read J i g l e Poi n t . j i ggl e ( ) ;
St r i ng u rl = u r l Gene rato r . next() ;
Th read J i g l e Poi nt . j i ggl e ( ) ;
updateHasNext () ;
Th read J i g l e Poi nt . j i ggl e ( ) ;
ret u r n u rl ;
}
ret u r n n ul l ;
}

Jetzt verwenden Sie einen einfachen Aspekt, der zufällig eine der Aktionen »do
nothing«, »sleeping« oder »yielding« auswählt.
Oder nehmen Sie an, die Th read J i gg 1 ePoi n t-Klasse habe zwei Implementierun­
gen. Die erste implementiert j i g g l e, um nichts zu tun, und wird in der Produktion
benutzt. Die zweite generiert eine Zufallszahl, um zwischen »sleeping«, »yielding«
oder einfach »falling through« auszuwählen. Wenn Sie nun Ihre Tests tausend Mal
mit Random-Jiggling ausführen, finden Sie möglicherweise einige Mängel. Werden
die Tests bestanden, können Sie wenigstens sagen, dass Sie sorgfältig gearbeitet
haben. Auch wenn dieser Ansatz etwas simplifizierend ist, könnte er eine vernünf­
tige Option darstellen, wenn ausgefeiltere Tools nicht zur Verfügung stehen.
Es gibt ein Tool namens ConTest (http : //www . a l phawo rks . i bm . com/tech/
contest) von IBM, das, allerdings mit erheblich mehr Finesse, Ähnliches leistet.
Der Punkt ist, dass der Code so »durchgerüttelt« wird, dass die Threads zu verschie­
denen Zeiten in unterschiedlicher Reihenfolge ausgeführt werden. Die Kombina­
tion aus brauchbaren Tests und Jiggling kann Ihre Chancen, Fehler zu finden,
erheblich verbessern.
Empfehlung: Venvenden Sie ]iggling-Strategien, um Fehler auszumerzen.

1 3.10 Zusam menfass u n g

E s ist schwer, korrekten nebenläufigen Code zu schreiben. Einfach nachvollziehba­


rer Code kann zum Albtraum werden, wenn mehrere Threads und gemeinsam
genutzte Daten ins Spiel kommen. Wenn Sie nebenläufigen Code schreiben müs­
sen, sollte Ihr Code unbedingt sauber sein; andernfalls müssen Sie mit subtilen und
kaum reproduzierbaren Fehlern rechnen.
Zuallererst sollten Sie das Single-Responsibility-Prinzip beachten. Zerlegen Sie Ihr
System in POJOs, die den Thread-bezogenen Code von dem T:hread-freien Code
trennen. Achten Sie darauf; dass Sie beim Testen von T:hread-bezogenen Code nur
13.10
Zusammenfassung

diesen und nichts sonst testen. Anders ausgedrückt: Ihr Thread-bezogener Code
sollte klein und fokussiert sein.
Sie müssen die möglichen Quellen für Nebenläufigkeitsprobleme kennen: mehrere
Threads, die gemeinsam genutzte Daten verarbeiten oder einen gemeinsamen Res­
sourcen-Pool nutzen. Grenzfälle, wie etwa ein sauberes Herunterfahren oder das
Beenden einer Schleifen-Iteration, können besonders verzwickt sein.
Studieren Sie Ihre Library und die grundlegenden Algorithmen. Sie müssen ver­
stehen, wie die Library und die Algorithmen die Lösung spezieller Probleme unter­
stützen.
Lernen Sie, wie Sie die Code-Bereiche finden, die gesperrt werden müssen, und
sperren Sie sie. Sperren Sie keine Code-Bereiche, die nicht gesperrt werden müs­
sen. Vermeiden Sie es, einen gesperrten Abschnitt von einem anderen aus aufzu­
rufen. Dazu müssen Sie gründlich verstehen, was und was nicht gemeinsam
genutzt wird. Halten Sie die Menge der gemeinsam genutzten Objekte und die Gel­
tungsbereiche der Nutzungen so klein wie möglich. Passen Sie die Designs der
Objekte mit gemeinsam genutzten Daten an die Clients an, anstaU diese zu zwin­
gen, den Nutzungsstatus dieser Daten zu verwalten.
Probleme lassen sich nicht vermeiden. Probleme, die nicht frühzeitig auftreten,
werden oft als einmalige Ereignisse abgeschrieben. Diese so genannten One-offs
treten nur unter Last oder zu scheinbar zufälligen Zeitpunkten auf. Deshalb müs­
sen Sie Ihren Thread-bezogenen Code unter vielen Konfigurationen wiederholt und
laufend aufvielen Plattformen ausführen können. Testbarkeit, die sich natürlicher­
weise aus der Befolgung der Drei Gesetze der TDD ergibt, schließt eine Plug-Fähig­
keit ())Konfigurierbarkeit«) in einem Umfang ein, der die für die Ausführung des
Codes unter vielen Konfigurationen erforderliche Unterstützung bietet.
Sie können Ihre Chancen, fehlerhaften Code zu finden, erheblich verbessern, wenn
Sie sich die Zeit nehmen, Ihren Code zu instrumentieren. Sie können dies manuell
tun oder eine automatisierte Technologie einsetzen. Investieren Sie frühzeitig in
diese Technologie. Sie sollten threadbasierten Code so lange wie möglich ausfüh­
ren, bevor Sie ihn produktiv einsetzen.
Wenn Sie einen sauberen Ansatz befolgen, steigen Ihre Chancen auf einen Erfolg
erheblich.

233
Kapitel 1 4

Sch rittweise Verfei neru ng

Fallstudie eines Parsers fü r Befehlszeilenargumente

Dieses Kapitel enthält eine Fallstudie der schrittweisen Verfeinerung. Sie lernen ein
Modul kennen, das gut angelegt ist, aber dann nicht skaliert. Denn sehen Sie, wie
ein Refactoring durchgeführt und das Modul bereinigt wird.
Die meisten Entwickler müssen dann und wann Befehlszeilenargumente parsen.
Ohne ein geeignetes Utility durchlaufen sie dann einfach das Array von Strings, das
an die mai n-Funktion übergeben wird. Es gibt mehrere brauchbare Utilities aus ver­
schiedenen Quellen, aber keines leistet genau das, was ich haben möchte. Deshalb
beschloss ich natürlich, mein eigenes zu schreiben. I ch nenne es Args.
Args ist sehr leicht anzuwenden. Sie konstruieren einfach ein Args-Objekt mit den
Input-Argumenten und einem Format-String. Dann fragen Sie die Args-Instanz
nach den Werten der Argumente ab. Betrachten Sie das folgende einfache Beispiel:

Listing 1 4.1: Einfaches Beispiel von Args


publ i c stat i c voi d mai n (S t r i n g [ ] a rg s ) {
t ry {
Args arg = new Args ( " l , p# , d * " , args) ;

235
Kapitel 14
Schrittweise Verfeinerung

boo l ean l oggi ng = a rg . getßool ean ( ' l ' ) ;


i n t po rt = arg . getint ( ' p ' ) ;
S t r i ng di rectory =
a rg . getS t r i ng ( ' d ' ) ;
execu teAppl i ca t i o n ( l oggi ng , port , di recto ry) ;
} catch (ArgsExcepti on e) {
System . ou t . p r i ntf("A rgument e r r o r : %s\n " , e . e r ro rMe s s age ( ) ) ;
}
}

Sie sehen, wie einfach dies ist. Wir erstellen einfach mit zwei Parametern eine
Instanz der Args-Klasse. Der erste Parameter ist der Format- oder Schema-String:
" l , p# , d '� . " Er definiert drei Befehlszeilenargumente: Das erste, l , ist ein boole­
-

sches Argument; das zweite, - p, ist ein Integer-Argument; das dritte, - d , ist ein
String-Argument. Der zweite Parameter des Args-Konstruktors ist einfach das
Array mit den Befehlszeilenargumenten, die an ma i n übergeben werden.
Wenn der Konstruktor zurückkehrt, ohne eine A r g s Excepti on auszulösen, wurde
die eingehende Befehlszeile geparst, und die Args-Instanz steht für Abfragen
bereit. Mit Methoden wie g et Boo 1 ean, geti ntege r und getStri ng können wir die
Werte der Argumente mit ihren Namen abfragen.
Wenn es ein Problem mit dem Format-String undjoder den Befehlszeilenargumen­
ten gibt, wird eine Args Except i on ausgelöst. Mit der e r rorMe s s age-Methode der
Ausnahme können wir eine genauere Beschreibung des Fehlers abrufen.

1 4.1 Args-I m plementieru ng


Listing 14-2 zeigt die Implementierung der A rg s-Klasse. Bitte lesen Sie sie sehr
sorgfältig. Ich habe hart an dem Stil und der Struktur gearbeitet und hoffe, dass sie
sich als Vorlage eignet.

Listing 14.2: Args . j ava


package com . obj ectmento r . uti l i ties . args ;

i mport stati c com . obj ectmento r . uti l i ti e s . args . ArgsExcepti on . Erro rCode . * ;
i mport j ava . uti l . * ;

publ i c c l ass Args {


pri vate Map<Character , ArgumentMarshal e r> marshal ers ;
pri vate Set<Character> args Found ;
pri vate Li stiterator<Stri ng> cu rrentArgument ;

publ i c Args (String schema , Stri ng [] args) th rows ArgsExcepti on {


marshal ers = new HashMap<Character , ArgumentMarshal e r> () ;
=
argsFound new HashSet<Characte r> () ;

parseSchema(schema) ;
1 4. 1
Args-lmplementierung

parseArgumentStri ngs (Arrays . asLi st(args)) ;


}

pri vate voi d parseSchema (Stri ng schema) th rows ArgsExcepti on {


for (Stri ng el ement : schema . spl i t (" , " ) )
i f (el ement . l ength() > 0)
parseSchemaEl ement(el ement . trim() ) ;
}

pri vate voi d parseSchemaEl ement(Stri ng element) th rows ArgsExcepti on {


char el ement!d =el ement . charAt(O) ;
Stri ng el ementTai l =el ement . substri ng(l) ;
val i dateSchemaEl ementid (el ementid) ;
i f (el ementTai l . l ength() =
0)
marshal ers . put(el ement!d , new BooleanArgumentMarshal e r () ) ;
el se i f (el ementTai l . equal s ( " · " ) )
marshal ers . put(el ement!d , new Stri ngArgumentMarshal er()) ;
e l se i f (el ementTai l . equal s ( "#") )
marshal ers . put(el ement!d , new IntegerArgumentMarshal e r () ) ;
e l se i f (el ementTai l . equal s ( "##"))
marshal ers . put(el ement!d , new Doubl eArgumentMarshal er()) ;
el se i f (el ementTai l . equal s ( " [*] " ) )
marshal ers . put(el ement!d , new Stri ngArrayArgumentMarshal e r () ) ;
e l se
th row new ArgsExcepti on (INVALID_ARGUMENT_FORMAT , el ement!d , el ementTai l ) ;
}

pri vate voi d val i dateSchemaEl ementid(char el ement!d) th rows ArgsExcepti on {


i f ( ! Characte r . i slette r(el ement!d))
th row new ArgsExcepti on(INVALID_ARGUMENT_NAME , el ement!d , nul l ) ;
}

pri vate voi d parseArgumentStri ngs (Li st<Stri ng> argsli st) th rows ArgsException
{
for (cur rentArgument = argsli st . l i st!terator() ; cu rrentArgument . hasNext() ; )
{
Stri ng argStri ng = cu rrentArgument . next() ;
i f (argStri ng . startsWi t h ( " - " ) ) {
parseArgumentCharacte rs (argStri ng . substri ng(l) ) ;
} el se {
currentArgument . previous() ;
break;
}
}
}

pri vate voi d parseArgumentCharacte rs (Stri ng argChars) th rows ArgsException {


for ( i nt i =
0 ; i < argChars . l ength () ; i ++)
parseArgumentCharacter (argChars . charAt (i ) ) ;

237
Kapitel14
Schrittweise Verfeinerung

pri vate voi d parseArgumentCharacte r (char argChar) th rows ArgsException {


ArgumentMarshal e r m = marshal ers . get(argChar) ;
i f (m == nul l ) {
th row new ArgsExcepti on (UNEXPECTED_ARGUMENT , argChar, nul l ) ;
} el se {
argsFound . add (argChar) ;
t ry {
m . set(cu rrentArgument) ;
} catch (ArgsException e) {
e . setErrorArgumentid(argChar) ;
th row e ;
}
}
}

publ i c bool ean has(char arg) {


return argsFound . contai ns (arg) ;
}

publ i c i nt nextArgument() {
return currentArgument . nextindex ( ) ;
}

publ i c bool ean getBool ean(char arg) {


return Bool eanArgumentMarshal e r . getVal ue(marshal e rs . get(arg) ) ;
}

publ i c Stri ng getStri ng(char arg) {


return Stri ngArgumentMarshal e r . getVal ue(marshal e rs . get(arg) ) ;
}

publ i c i nt getlnt (char arg) {


return IntegerArgumentMarshal e r . getVal u e (marshal e rs . get(arg) ) ;
}

publ i c doubl e getDoubl e(char arg) {


return Doubl eArgumentMarshal e r . getVal ue(marshal ers . get(arg) ) ;
}

publ i c Stri ng [] getStri ngArray (char arg) {


return Stri ngAr rayArgumentMarshal e r . getVal ue(marshal ers . get(arg)) ;
}
}

Beachten Sie, dass Sie den Code von oben bis unten lesen können, ohne viel her­
umzuspringen oder vorauszuschauen. Der einzige Abschnitt, bei dem Sie voraus­
schauen müssen, ist die Definition von ArgumentMa r s h a 1 e r, den ich absichtlich
1 4 .1
Args-lmplementierung

ausgelassen habe. Nachdem Sie den Code sorgfaltig gelesen haben, sollten Sie ver­
stehen, was das A rgumentMa r s h a l e r Interface ist und was seine abgeleiteten Klas­
-

sen tun. Einige dieser Klassen werden jetzt in den Listings 14.3 bis 14.6 gezeigt.

Listing 14.3: ArgumentMarshal e r . j ava


publ i c i nte rface A rg umentMarshal e r {
voi d s e t ( I te rator<S t r i ng> cu r re n tArgument) t h rows A rgs Except i on ;
}

Listing 14.4: Bool eanArgumentMarshal er . j ava


publ i c cl ass Bool eanArgumen tMa r s h al e r i mpl eme n t s A rgumen tMa rshal e r {
p r i vate bool ean bool eanVal u e = fal se ;

publ i c voi d set (Ite rato r<S t r i ng> cu r r e n tArgume n t ) t h rows ArgsExcepti on {
bool eanVal u e = t ru e ;
}

publ i c s tati c bool ean getVal u e (ArgumentMa r s hal e r am) {


i f (am ! = n u l l && am i n stan ceof Bool eanA rgumentMa rshal e r )
r e t u r n ( (Bool eanA rgumen tMa rshal e r ) am) . bool eanVal u e ;
el se
retu rn fal se ;
}
}

Listing 14.5: Stri ngArgumentMarshal er . j ava


i mpo rt stati c com . objectme n to r . u t i l i ti es . args . A r g s Excepti on . E r ro rCode . * ;

publ i c cl ass S t r i ngArgume n tMa rshal e r i mpl ements Argumen tMarshal e r {


p r i vate S t r i ng s t r i ngVal ue = '"' ;

publ i c voi d s e t ( I te rato r<St ri ng> c u r r e n tA rgument) t h rows A rg s Excepti on {


t ry {
s t ri ngVal ue = c u r rentA rgume nt . next ( ) ;
} catch (NoSu c h E l ementExcepti on e) {
th row new ArgsExcepti on (MISSING_STRING) ;
}
}

publ i c stati c S t ri ng getVal u e (Argumen tMa rshal e r am) {


i f (am ! = n u l l && am i n stanceof S t r i ngArg ume n tMa rshal e r)
retu rn ( (S t r i ngArgumentMa r s h al e r) am) . st r i ngVal u e ;
el s e
" " ·
retu r n .

}
}

239
Kapitel 14
Schrittweise Verfeinerung

Listi n g 14.6: IntegerArgumentMarsha 1 e r . j ava


i mpo rt s tati c com . obje ctmentor . uti l i ti e s . args . Args Excepti on . E r ro rCode . * ;

publ i c cl ass I n tegerA rgumentMa r s h al e r i mpl ements A rg umentMa r s hal e r {


p r i vate i nt i n tVal ue = 0 ;

publ i c voi d set ( I te rato r<St ri ng> c u r rentA rgume n t) th rows A rg s Excepti on {
St ri ng paramete r = n u l l ;
t ry {
pa ramete r = c u r ren tArgumen t . next () ;
i n tVal ue = I n tege r . p a r s e i n t (paramete r) ;
} catch (NoS u c h E l eme n t Excepti on e) {
th row new Args Except i o n (MISSING_INTEGER) ;
} catch (Numbe r Fo rmatExcepti on e) {
th row new A r g s E xcept i on (INVALID_INTEGER , pa ramete r) ;
}
}

publ i c s tati c i n t getVal ue (ArgumentMa rshal e r am) {


i f (am ! = n u l l && am i n s tanceof Intege rArgumentMa r s hal e r)
r e t u r n ( ( In tege rA rg umen tMa r s hal e r) am) . i ntVal ue ;
el se
retu rn 0 ;
}
}

Die anderen von A rg umentMa r s h a 1 e r abgeleiteten Klassen wiederholen einfach


dieses Pattern für dou b l es- und S t r i n g-Arrays und würden dieses Kapitel nur
überfrachten. Ich überlasse sie Ihnen zur Übung.
Möglicherweise bereiten Ihnen einige andere Informationen Schwierigkeiten: die
Definition der Fehlercode-Konstanten. Sie sind in der Args Excepti on-Klasse ent­
halten (Listing 14-7) .

Listi ng 14.7: ArgsExcepti on . j ava


i mport s tati c com . obje ctmento r . uti l i ti es . a rg s . A rg s Excepti on . E r ro rCode . * ;

publ i c c l as s A rg s Excepti on extends Excepti on {


p ri vate char e r rorArgume n t i d = ' \0 ' ;
p ri vate S t r i ng e r ro rParame t e r = n ul l ;
p ri vate E r ro rCode e r ro rCode = O K ;

publ i c Args Excepti o n () { }

publ i c A rg s Excepti on (St r i ng m e s s age) { s upe r (mes sage) ; }

publ i c A rg s Excepti on ( E r ro rCod e e r ro rCode) {


t h i s . e r ro rCode = e r ro rCode ;
}
1 4. 1
Args-l m plementierung

publ i c A r g s Except i on ( E r ro rCode e r ro rCod e , S t r i ng e r rorPa rameter) {


t h i s . e r ro rCode = e r ro rCode ;
=
t h i s . e r ro r Pa ramete r e r ro r Paramete r ;
}

publ i c Args Except i on ( E r ro rCode e r r o rCod e ,


char e r ro rA rgumentid , S t r i ng e r ro r Pa rameter) {
=
t h i s . e r ro rCode e r ro rCode ;
t h i s . e r ro r Pa ramet e r = e r ro r Pa ramete r ;
t h i s . e r ro rArgumentid = e r ro rA rg umen t i d ;
}

publ i c char getEr rorArgumentid () {


ret u r n e r rorArgumen t i d ;
}

publ i c voi d set E r ro rArgumen tid ( c h a r e r rorArgumentid) {


t h i s . e r ro rA rgumentid =
e r ro rA rgumen tid ;
}

publ i c S t r i ng get E r ro rParamete r () {


retu rn e r ro rParamete r ;
}

publ i c voi d setE r ro r Pa ramete r ( St r i ng e r ro r Paramete r) {


t h i s . e r ro r Pa ramete r = e r ro r Pa ramete r ;
}

publ i c E r r o rCode get E r rorCode ( ) {


retu rn e r ro rCode ;
}

publ i c voi d set E r r o rCode ( E r ro rCode e r ro rCode) {


t h i s . e r ro rCode = e r ro rCode ;
}

publ i c St ri ng e r rorMessag e ( ) {
swi tch (e r ro rCode) {
case OK :
retu rn "TILT : Shou l d not get h e re . " ;
case UNEXPECTED_ARGUMENT :
retu rn S t r i ng . format ("Argume n t -%c u nexpected . " , e r ro rA rg umentid) ;
case MISSING_STRING :
retu rn S t r i ng . format ( " Co u l d not fi nd s t r i ng pa rameter fo r -%c . " ,
e r ro rA rgumentid) ;
case INVALID_INTEGE R :
ret u r n S t r i ng . format ( "A rgument -%c expects a n i ntege r b u t was ' %s ' . " ,
e r ro rA rg umentid , e r ro r Pa ramete r) ;
Kapitel 14
Schrittweise Verfeinerung

case M I SSI NG_INTEGER :


retu rn S t r i ng . format ( "Cou l d not fi nd i ntege r pa ramete r for -%c . " ,
e r rorArgumentid) ;
case I NVALID_DOUB L E :
retu rn S t r i ng . format ( "Argument -%c expects a doubl e b u t was ' %s ' . " ,
e r ro rArgume n t i d , e r ro r Pa rame t e r) ;
case MISSING_DOUB L E :
retu rn S t r i ng . format ( "Cou l d not fi n d doub l e pa ramete r for -%c . " ,
e r rorArgumen t i d ) ;
case I NVAL ID_ARGUMENT_NAME :
retu rn S t r i ng . fo rmat ( " ' %c ' i s not a val i d a rgument n ame . " ,
e r rorArgumentid) ;
case I NVALID_ARGUMENT_FORMAT :
retu rn S t r i ng . fo rmat ( " ' %s ' i s not a val i d a rgument format . " ,
e r ro r Pa r amete r) ;
}
1111 .
ret u r n '

publ i c enum E r rorCode {


OK , I NVALID_ARGUMENT_FORMAT , U N EX P ECTED_ARGUMENT , I NVALID_ARGUMENT_NAME ,
MISSI NG_STRING ,
MISSI NG_INTEGER , INVALID_I NTEGER ,
MISSI NG_DOUB L E , INVALID_DOUB L E }
}

Es ist bemerkenswert, wie viel Code erforderlich ist, um die Details dieses einfachen
Konzepts zu realisieren. Einer der Gründe dafür liegt darin, dass wir eine besonders
wortreiche Sprache verwenden. Java erfordert als statische typisierte Sprache zahl­
reiche Wörter, um das Typensystem zu befriedigen. In Sprachen wie Ruby, Python
oder Smalltalk wäre dieses Programm viel kleiner. (Vor Kurzem schrieb ich dieses
Programm in Ruby um. Es war nur I/7 so groß und etwas besser strukturiert.)
Bitte lesen Sie den Code noch einmal. Achten Sie besonders auf die Benennung der
Dinge, die Größe der Funktionen und die Formatierung des Codes. Wenn Sie ein
erfahrener Programmierer sind, haben Sie möglicherweise hier und dort etwas am
Stil oder der Struktur auszusetzen. Insgesamt hoffe ich jedoch, dass Sie zu dem
Schluss kommen, dass dieses Programm gut geschrieben ist und eine saubere
Struktur hat.
Beispielsweise sollte es offensichtlich sein, wie man einen neuen Argumenttyp, wie
etwa ein Datum oder eine komplexe Zahl, hinzufügen kann. Es sollte auch klar sein,
dass dafür nur ein trivialer Aufwand erforderlich ist. Kurz gesagt: Sie sollten einfach
nur eine neue abgeleitete Klasse von ArgumentMarsha 1 e r, eine neue getXXX-Funk­
tion und eine neue case-Anweisung in der parseSch emaEl eme nt-Funktion benö­
tigen. Wahrscheinlich sollte es auch einen neuen ArgsExcepti on . E r ro rCode und
eine neue Fehlermeldung geben.
1 4 .2
Args: der Rohentwurf

Wie habe ich d ies gemacht?

Ich möchte Sie gleich am Anfang beruhigen. Ich habe dieses Programm in seiner
gegenwärtigen Form nicht einfach aus dem Stand geschrieben. Und was noch wich­
tiger ist: Ich erwarte nicht, dass Sie saubere und elegante Programme in einem
Durchgang schreiben können. Wenn wir überhaupt etwas in den letzten Jahrzehn­
ten gelernt haben, dann eines: Programmierung ist eher ein Handwerk als eine Wis­
senschaft. Um sauberen Code zu schreiben, müssen Sie zunächst schmutzigen
Code schreiben und ihn dann bereinigen.
Dies sollte Sie nicht überraschen. Wir haben diese Wahrheit schon in der Grund­
schule gelernt, als unsere Lehrer (normalerweise vergebens) versuchten, uns bei­
zubringen, erst ins Unreine zu schreiben. Idealerweise sollten wir erst ein rohes
Konzept schreiben und dieses in mehreren Schritten immer weiter bis zur endgül­
tigen Version verfeinern. Saubere Ergebnisse, so die Botschaft, wären die Folge
einer schrittweisen Verfeinerung.
Die meisten Programmieranfänger (wie die meisten Grundschüler) befolgen die­
sen Rat nicht besonders eifrig. Sie glauben, das Hauptziel bestehe darin, das Pro­
gramm zum Laufen zu bringen. Sobald es »funktioniert«, wenden sie sich der
nächsten Aufgabe zu und lassen das »funktionierende« Programm in dem Zustand
zurück, in dem sie es schließlich »zum Laufen« brachten. Die meisten erfahrenen
Programmierer wissen, dass dies professioneller Selbstmord ist.

14.2 Args: der Rohentwurf


Listing 14.8 zeigt eine frühere Version der A r g s -Klasse. Sie »funktioniert«. Und sie
ist chaotisch.

Listing 14.8: Arg s . j ava (erster Entwurf)


i mport j ava . text . ParseExcepti on ;
i mport j ava . uti l . * ;

publ i c cl ass Args {


p ri vate St ri ng schema ;
p ri vate Stri ng [] args ;
p ri vate bool ean val i d = t ru e ;
p ri vate Set<Character> unexpectedArguments = new TreeSet<Characte r>() ;
p ri vate Map<Charact e r , Bool ean> bool eanArgs =

new HashMap<Characte r , Bool ean> () ;


p ri vate Map<Characte r , St ri ng> s t ri ngArgs = new HashMap<Characte r , Stri ng> O ;
pri vate Map<Characte r , Integer> i ntArgs = new Has hMap<Characte r , Integer>() ;
pri vate Set<Character> argsFound = new HashSet<Characte r>() ;
pri vate i nt c u r rentArgument ;
pri vate char e r rorArgumentid = ' \0 ' ;
p ri vate St ri ng e r rorParameter = "TILT" ;
p ri vate E r rorCode errorCode = E r rorCode . OK ;

243
Kapitel 14
Schrittweise Verfeinerung

p r i vate enum E r ro rCode {


OK , MISSING_STRING , MISSING_INTEGER , INVALID_INTEGER , UNEXPECTED_ARGUMENT }

publ i c Args (St ri ng schema , S t r i n g [ ] a rgs) t h rows ParseExcept i on {


t h i s . schema =s chema ;
t h i s . args = args ;
val i d = p a r s e ( ) ;
}

p ri vate bool ean pars e ( ) t h rows Pars eExcepti on {


i f ( s c h ema . l ength () == 0 && a rgs . l ength == 0)
retu rn t rue ;
parseSchema ( ) ;
t ry {
parseArgume n t s () ;
} catch (ArgsExcept i on e) {
}
retu rn val i d ;
}

p ri vate bool ean parseSchema() t h rows P a r s e Except i on {


fo r (St ri ng el ement : schema . s pl i t ( " , " ) ) {
i f (el ement . l e n g t h ( ) > 0) {
S t r i ng t ri mme d E l eme n t = el ement . t ri m () ;
parse SchemaEl eme n t ( t r i mmedEl eme n t ) ;
}
}
retu rn t r ue ;
}

p r i vate voi d parse SchemaEl emen t ( S t r i ng el ement) t h rows Pa r seExcepti on {


char el eme n t i d = el emen t . c h a rAt(O) ;
St ri ng el ementTai l = el ement . s u b s t r i ng ( l) ;
val i dateSchemaEl eme n ti d (el ementid ) ;
i f (i s Bool ean Sc h emaEl eme n t ( e l ementTai l ) )
parseBool ean SchemaEl eme n t ( e l eme n ti d ) ;
el se i f (i s S t r i ngSchemaEl ement (el ementTai l ) )
parseSt ri ngSchemaEl ement ( e l eme n t i d ) ;
el se i f (i s i nt ege rSchemaEl eme n t (e l ementTai l ) ) {
p a r s e i n tegerSchemaEl ement ( e l ementid) ;
} e l se {
t h row new P a r s e Excepti on (
S t r i ng . fo rmat ( "Argume n t : %c has i nval i d format : %s . " ,
el eme ntld , el ementTai l ) , 0) ;
}
}

p ri vate voi d val i dateSchemaEl eme n t i d ( ch a r el ementld) th rows ParseExcepti on {


14 2
Args: der Rohentwurf

i f ( ! Ch aracte r . i slette r (el ementld) ) {


th row new ParseExcepti on (
" Bad characte r : " + el ement!d + " i n Args format : " + s c h ema , 0) ;
}
}

p r i vate voi d pars eBool ean S c h emaEl ement ( c h a r el ementld) {


bool eanArg s . put (el ementld , fal s e ) ;
}

p r i vate voi d parselntege rSchemaEl ement ( c h a r e l ementld) {


i ntA rgs . put (el ementld , 0) ;
}

p r i vate voi d parseSt r i ngSch emaEl ement ( c h a r el ementld) {


s t r i ngArgs . put (el ementld , " " ) ;
}

p r i vate bool ean i sS t r i ngSchemaEl ement (St r i ng el ementTai l ) {


r e t u r n e l ementTai l . eq u al s ( " * " ) ;
}

p r i vate bool ean i s Bool ean S c h emaEl ement ( St r i ng el ementTai l ) {


retu r n el ementTai l . l engt h ( ) == 0;
}

p r i vate bool ean i si n t e g e r Sc h emaEl ement ( S t r i ng e l ementTai l ) {


retu rn el ementTai l . eq ual s ( "# " ) ;
}

p ri vate bool ean parseArgume n t s ( ) th rows ArgsExcepti on {


=
fo r (cu r rentArgument 0 ; c u r rentA rgument < args . l ength ; c u r re ntArgument++)
{
St ri ng a rg = arg s [ c u r rentArgument] ;
parseArgument (arg) ;
}
r e t u r n t ru e ;
}

p r i vate voi d parseArgument (Stri ng arg) t h rows ArgsExcepti on {


i f (arg . startsWi t h ( " - " ) )
parseEl ements ( a rg) ;
}

p r i vate voi d parseEl ements (Stri ng arg) t h rows ArgsExcepti on {


fo r (i nt i =
1 ; i < a rg . l ength () ; i ++)
parseEl ement (arg . c h a rAt (i ) ) ;
}

24 5
Kapitel 14
Schrittweise Verfeinerung

p ri vate voi d parseEl emen t (char a rgChar) t h rows A r g s Excepti on {


i f ( setArgume n t (a rgCha r) )
a rg s Fou nd . ad d (a rgChar) ;
el se {
u nexpectedA rgume n t s . add (argChar) ;
e r ro rCode = E r ro rCode . UN EXPECTED_ARGUMENT ;
val i d = fal se ;
}
}

p r i vate boo l ean setArgume n t ( c h a r argChar) th rows A rg s Except i o n {


i f (i s Bool eanA rg (argChar) )
setBoo l eanAr g ( a rgCh a r , t ru e) ;
el se i f (i s S t r i ngArg(argChar) )
setStri ngArg ( a rgCha r) ;
el se i f (i s i n tA rg ( a rgChar) )
s e t i n tArg (argCha r) ;
el se
ret u r n fal s e ;

retu r n t rue ;
}

pri vate bool ean i sintArg (char argChar) { retu rn i ntArgs . contai nsKey (argChar) ; }

p ri vate voi d setin tArg (char argChar) th rows ArgsExcepti o n {


cu r rentArgume n t++ ;
St ri ng pa rame t e r = n ul l ;
t ry {
pa ramet e r = args [ c u r re n tArgument] ;
i ntArgs . p u t ( argCh a r , new Integer (pa ramet e r) ) ;
} catch (Ar rayindexOutOfBou n d s Excepti on e) {
val i d = fal s e ;
e r ro rArgumentid = a rgCh a r ;
e r ro rCode = E r ro rCode . MISSING_INTEGER ;
t h row new Args Excepti on () ;
} catch (Numbe r Fo rmatExcepti o n e) {
val i d = fal se ;
e r ro rArgume n t i d = a rgCh a r ;
e r ro r Parameter = paramet e r ;
e r ro rCode = E r r o rCode . INVALID_INTEGE R ;
th row new A r g s Excepti o n ( ) ;
}
}

p ri vate voi d setSt ri n gA r g ( c h a r a rgChar) t h rows Args Excepti on {


cu r rentArgument++ ;
t ry {
s t r i ngArgs . put ( a rgCh a r , arg s [c u r re ntArgument] ) ;
1 4 .2
Args: der Rohentwurf

} catch (A r rayindexOutOfBou n d s Except i on e) {


val i d = fal se ;
e r rorArgumentid = a rgCh a r ;
e r ro rCode = E r ro rCode . MISSING_STRI NG ;
th row new A rgs Except i o n () ;
}
}

p ri vate bool ean i s St ri ngArg (char a rgCh a r) {


return s t r i ngArg s . contai n sKey (argCh a r ) ;
}

p r i vate voi d s etßool eanArg (char a rgCh a r , bool ean val ue) {
bool eanArg s . put (argCh a r , val ue) ;
}

p ri vate bool ean i s ßool eanArg (char a rgCha r) {


retu r n bool eanArg s . contai n sKey (a rgCh a r ) ;
}

publ i c i nt c a r d i nal i t y ( ) {
ret u r n a r g s Found . s i ze () ;
}

pub l i c Stri ng u sage () {


i f (schema . l engt h ( ) > 0)
ret u r n [ + schema + )
" - " " " ;
else
11 11 .
retu r n '

pub l i c Stri ng e r r o rMes sage () th rows Excepti on {


swi tch ( e r ro rCode) {
case OK :
th row new Except i o n ( "TILT : Shoul d not get h e re . " ) ;
case UNEXPECTED_ARGUMENT :
retu r n u n expectedArgumentMes s age () ;
case MISSING_STRING :
retu r n S t r i ng . format ("Cou l d not fi nd s t r i ng pa rameter for -%c . " ,
e r r o rA rgument i d ) ;
case INVALID_INTEGER :
retu rn St ri ng . fo rmat ( "A rgument -%c expects an i ntege r but was ' %s ' . " ,
e r ro rArgume n ti d , e r ro r Paramete r) ;
case MISSING_INTEGE R :
ret u r n Stri ng . format ( "Coul d not fi nd i ntege r paramete r f o r -%c . " ,
e r ro rA rgument i d ) ;
}
11 11 .
retu rn '

247
Kapitel 14
Sch rittweise Verfeinerung

pri vate Stri n g u nexpectedA rgumentMe s s age () {


Stri ngBu ffe r message = new S t r i n g B u ffe r ( " A rgume n t ( s ) - " ) ;
fo r (char c : u n expectedArgume nts) {
mes sage . appe n d (c) ;
}
mes sage . appen d ( " u n expected . " ) ;

retu rn message . toStri n g ( ) ;


}

p r i vate bool ean fal se ifNu l l (Bool ean b) {


ret u r n b ! = n u l l && b ;
}

p r i vate i nt ze roifNu l l ( Intege r i ) {


re t u r n i == n u l l ? 0 : i ;
}

pri vate Stri ng b l ankifNul l (St r i ng s) {


ret u r n s == n u l l ? '"'
: s;
}

p u b l i c S t r i n g getS t r i ng (char a rg) {


ret u r n bl a n k i fNu l l ( s t r i ngArgs . get (arg) ) ;
}

p u b l i c i nt get! nt (char arg) {


ret u r n z e roifNul l (i ntArg s . get (arg) ) ;
}

publ i c bool ean getBool ean (char a rg) {


ret u r n fal sei fNu l l ( bool eanA rgs . get (arg) ) ;
}

p u b l i c bool ean has (char arg) {


ret u r n arg s Found . contai n s (arg) ;
}

p u b l i c bool ean i sVal i d () {


retu rn va 1 i d ;
}

p r i vate c l as s Arg s Except i o n extends Except i o n {


}
}

Ich hoffe, dass Sie zunächst wie folgt aufdiese Masse von Code reagiert haben: »Auf
jeden Fall bin ich froh, dass er den Code nicht in dieser Form gelassen hat!« Wenn
14 2
Args: der Rohentwurf

Sie dieses Gefühl haben, sollten Sie daran denken, wie andere Entwickler auf Code
reagieren, den Sie in der Form eines Rohentwurfs hinterlassen haben.
Wahrscheinlich ist »Rohentwurf« noch die freundlichste Bezeichnung für diesen
Code. Es handelt sich offensichtlich um ein unfertiges Produkt. Die reine Anzahl
der Instanzvariablen ist abschreckend. Die seltsamen Strings wie "TILT " , die
HashSets und T r eeSets und die t ry cat ch c at c h Blöcke ergeben zusammen
- - -

einen Misthaufen.
Ich wollte keinen Misthaufen schreiben. Tatsächlich versuchte ich, alle Dinge mög­
lichst vernünftig zu ordnen. Sie können dies wahrscheinlich an meiner Wahl der
Funktions- und Variablennamen und der Tatsache ablesen, dass das eine grobe
Struktur hat. Doch ganz offensichtlich ist mir das Problem entglitten.
Das Chaos entstand allmählich. Frühere Versionen waren längst nicht so hässlich.
So zeigt etwa Listing 14.9 eine frühere Version, in der nur boolesche Argumente
funktionierten.

Listing 14.9: Args . j ava (nur boolesche Argumente)


package com . obj ectmento r . uti l i ti e s . getopt s ;

i mport j ava . u ti l . * ;

p u b l i c c l as s Args {
p r i vate S t r i ng s c h ema ;
p r i vate S t r i ng [] a rg s ;
p r i vate bool ean val i d ;
p r i vate Set<Character> unexpectedA rguments = new T r e eSet<C h a ract e r > ( ) ;
p r i vate Map<Ch a ract e r , Bool ean> bool eanArgs =
new HashMap<Characte r , Bool ean> () ;
p r i vate i n t n umbe rOfArguments 0;=

publ i c Args (St r i ng s ch ema , S t r i n g [ ] a r g s ) {


t h i s . s chema = s c h em a ;
th i s . args = args ;
val i d = pa r s e () ;
}

p u b l i c bool ean i sVal i d () {


retu rn va 1 i d ;
}

p r i vate boo l ean pa r s e () {


i f (schema . l ength () == 0 && args . l ength == 0)
retu r n t r u e ;
parseSchema () ;
parseArguments ( ) ;
ret u r n u n expectedArgumen ts . s i ze ( ) == 0 ;
}

249
Kapitel 14
Schrittweise Verfei nerung

p r i vate bool ean parseSchema() {


"
fo r (St r i ng el emen t : s chema . spl i t ( " , ) ) {
parseSc hemaEl emen t (el ement) ;
}
ret u r n t r u e ;
}

p ri vate voi d pars eSchemaEl emen t (Stri ng el ement) {


i f (el ement . l ength ( ) == 1) {
parseBool ean S c h emaEl ement ( e l ement) ;
}
}

p r i vate voi d parseBool eanSchemaEl ement ( S t r i ng el ement) {


c h a r c = el ement . c h a rAt(O) ;
i f (Cha racte r . i s lette r (c ) ) {
bool eanArg s . pu t ( c , fal se) ;
}
}

p r i vate bool ean parseArguments ( ) {


fo r (St r i ng arg : a r g s )
parseA rgumen t (arg) ;
r e t u r n t rue ;
}

p r i vate voi d par seArgumen t ( St r i ng arg) {


i f (arg . startsWi t h ( " - " ) )
parseEl ements (arg) ;
}

p r i vate voi d parseEl ements ( S t r i ng arg) {


fo r ( i nt i = 1 ; i < arg . l ength ( ) ; i ++)
parseEl ement (arg . ch a rAt (i ) ) ;
}

p r i vate voi d parseEl eme n t ( c h a r a rgChar) {


i f (i s Bool ean (argCh a r ) ) {
n umbe rOfArg uments++ ;
setBool eanA r g ( a rgCh a r , t rue) ;
} el se
unexpectedA rguments . add (argChar) ;
}

p ri vate voi d setBool eanArg (char a rgCh a r , bool ean val ue) {
bool eanArgs . pu t (argCh a r , val u e) ;
}

p ri vate bool ean i s Bool ean (char a rgChar) {


ret u r n bool eanArg s . con tai n s K ey (argChar) ;
1 4 .2
Args: der Rohentwurf

publ i c i n t card i nal i ty ( ) {


retu rn n umbe rOfArguments ;
}

publ i c S t r i ng u s ag e ( ) {
i f (schema . 1 ength () > 0)
retu rn " - [ "+schema+" ] " ;
el se
11 1 1 .
retu rn '

publ i c S t r i ng e r rorMes sage () {


i f (unexpectedA rguments . s i ze ( ) > 0) {
r e t u r n u n expec tedA rgumentMessag e ( ) ;
} el se
1111 .
return '

p r i vate S t r i ng un expectedArgumentMe s s ag e ( ) {
S t r i ngBuffe r me ssag e =
n ew S t r i n g B u ffe r ( " A rgume n t ( s ) - " ) ;
fo r ( c h a r c : un expectedA rgume n t s ) {
message . append ( c ) ;
}
message . append ( " u nexpected . " ) ;

retu rn mes sage . toSt r i n g ( ) ;


}

publ i c bool ean getBool ean ( c h a r a rg ) {


retu rn bool eanA rg s . ge t (a rg) ;
}
}

Obwohl es an diesem Code viel zu bemängeln gibt, ist er wirklich nicht so schlecht.
Er ist kompakt und leicht zu verstehen. Doch in dem Code sind die Keime des spä­
teren Misthaufens leicht zu erkennen. Es ist ziemlich klar, wie daraus das spätere
Chaos entstand.
Beachten Sie, dass das spätere Chaos nur zwei zusätzliche Argumenttypen enthält:
St r i ng und i nteger. Das Hinzufügen von nur zwei weiteren Argumenttypen hatte
einen erheblichen negativen Einfluss auf den Code. Ich machte aus einem einiger­
maßen wartbaren Programm ein Gebilde, bei dem ich mit zahlreichen Bugs und
Wanzen rechnen musste.
Ich fügte die beiden Argumenttypen schrittweise hinzu. Zuerst fügte ich das
St r i ng-Argument hinzu und erhielt das folgende Programm:
Kapitel 14
Schrittweise Verfeinerung

Listing 14.10: Args . j ava (boolesche Argumente und String-Argumente)


package com . ob j e ctmento r . ut i l i ti es . getopt s ;

i mport j ava . text . ParseExcept i on ;


i mpo rt j ava . uti l . * ;

publ i c c l as s Args {
p r i vate S t r i ng s chema ;
p r i vate S t r i ng [ ] a rgs ;
p r i vate boo l ean val i d = t rue ;
p r i vate Set<C h a racter> unexpectedArguments = new TreeSe t<Cha racte r> ( ) ;
p r i vate Map<Cha racte r , Boo l ean> boo l eanArgs =
new Has hMap<Cha racte r , Boo l ean> () ;
p r i vate Map<C h a rac te r , S t r i ng> s t r i ngA rgs =
new Has hMap<C h a racte r , S t r i ng> () ;
p r i vate Set<Character> args Found = new HashSet<C h a racte r > ( ) ;
p r i vate i nt cu r re n tArgument ;
p r i vate c h a r e r ro rArgument = ' \0 ' ;

enum E r ro rCode {
OK , MISSING_STRING }

p r i vate E r ro rCode e r ro rCode = E r ro rCode . OK ;

publ i c Args (St r i ng s chema , S t r i ng [] args) t h rows ParseExcept i on {


thi s . s chema =s chema ;
t h i s . args= a rgs ;
val i d = pars e ( ) ;
}

p r i vate bool ean parse () t h r ows Parse Excepti on {


i f (s chema . l ength () == 0 && args . l ength == 0)
return true ;
pa rseSc hema () ;
parseArguments () ;
return val i d ;
}

p r i vate bool ean pa rseSchema() t h r ows ParseExcept i on {


for ( S t r i ng e l ement : s chema . s pl i t ( " , " ) ) {
i f (el ement . l ength () > 0) {
St ri ng t r i mmedEl ement= e l ement . t ri m () ;
parseSchemaEl ement (tri mmedEl ement) ;
}
}
retu r n t rue ;
}

p r i vate voi d parseSchemaEl ement (St r i n g e l ement) t h rows ParseExcept i on {


c h a r e l ementid = e l ement . c h a rA t (O) ;
S t r i ng e l emen tTai l = e l ement . su b s t r i n g ( l) ;
1 4 .2
Args: der Rohentwurf

val i dateSchemaEl ementid (el ementid) ;


i f ( i sBool eanSchemaEl ement (el ementTai l ) )
parse Bool eanSchemaEl ement (el ementid) ;
e l s e i f (i s S t r i ngSchemaEl ement (el ementTai l ) )
parseStri ngSchemaEl ement (el ementid) ;
}

pri vate voi d val i dateSchemaEl ementid (cha r el ement!d) th rows Pa rseExcepti on {
i f ( ! Ch a racte r . i s lette r (e l ement!d) ) {
th row new ParseExcept i o n (
" Bad c ha r acte r : " + e l ement!d + " i n Args fo rmat : " + s chema , 0) ;
}
}

p r i vate voi d p a rs eSt ri ngSchemaEl ement (char el ement!d) {


st ri ngArgs . put (e l ement!d , " " ) ;
}

p r i vate bool ean i s S t r i ngSchemaEl ement (St ri ng e l ementTai l ) {


retu rn e l ementTai l . equal s ( " · " ) ;
}

p r i vate boo l ean i s Bool eanSchemaEl ement ( S t r i ng e l ementTai l ) {


retu rn e l ementTai l . l engt h () == 0 ;
}

p r i vate voi d parseBool e anSchemaEl ement ( c h a r e l ement!d) {


bool eanArg s . put(el ementid , fal se) ;
}

p r i vate boo l ean pa rseArguments () {


=
fo r (cu r re ntArgument 0 ; cu r re n tArgument < args . l ength ; cu r rentArgument++)
{
St ri ng arg = args [cu r rentArgument] ;
parseArgument (arg) ;
}
retu rn t rue ;
}

p r i vate voi d pa rseArgument (St ri ng arg) {


i f (arg . sta rt sWi t h ( " - " ) )
parseEl emen t s ( a rg) ;
}

p r i vate voi d parseEl ements (Stri ng arg) {


fo r ( i n t i = 1 ; i < a rg . l ength () ; i ++)
parseEl emen t (a rg . c h a rAt (i ) ) ;
}

p r i vate voi d parseEl ement (char argChar) {


i f ( setArgument ( a rgChar) )

2 53
Kapitel 14
Schrittweise Verfeinerung

args Found . add ( a rgCha r ) ;


el se {
unexpectedA rgument s . add (argCh a r ) ;
val i d = fal s e ;
}
}

p ri vate bool ean setA rgume n t ( c h a r a rgCha r ) {


bool ean set = t r u e ;
i f ( i sBoo l ean (argC h a r) )
setBool eanA r g ( a rgCh a r , t r u e ) ;
el se i f (i s St r i ng ( a rgCha r ) )
setSt ri ngArg (a rgCha r , " " ) ;
el se
s e t = fal se ;

retu r n set ;
}

p r i vate voi d setSt ri ngA rg ( c h a r a rgCh a r , S t r i ng s) {


c u r rentA rgument++ ;
try {
s t r i ngArgs . pu t (argCh a r , a rg s [ c u r rentArgument] ) ;
} catch (A r rayindexOu tOfBoun d s Except i on e) {
val i d = fal s e ;
e r rorA rg ument = a rgCh a r ;
e r ro rCode =
E r ro rCode . MI S S I NG_STRING ;
}
}

p r i vate bool ean i sS t r i ng ( c h a r a rgCh ar) {


r e t u r n s t r i ngArgs . contai nsKey (a rgCh ar) ;
}

p r i vate voi d setBool eanArg (char a rgCh a r , bool ean val ue) {
bool eanArg s . p u t (argCha r , val ue) ;
}

p r i vate boo l ean i s Bool ean (char a rgCha r ) {


retu r n boo l eanArg s . contai n s K e y ( a rgCh a r ) ;
}

publ i c i nt cardi nal i t y () {


r e t u r n a r g s Fo u nd . s i ze () ;
}

publ i c S t r i ng u s ag e ( ) {
i f (schema . l ength () > 0)
retu r n "- [ " + s chema + " ] " ;
e l se
11 11 .
retu rn '

254
1 4 .2
Args: der Rohentwurf

publ i c St ri ng e r ro rMessag e ( ) th rows Except i o n {


i f (unexpectedArguments . s i ze () > 0) {
return u n expectedArgumentMessage ( ) ;
} el se
swi tch ( e r ro rCode) {
case MISSING_STRING :
retu r n S t ri ng . format (00Cou l d not fi nd s t r i ng pa ramete r fo r -%c . 00 ,
e r rorA rgument) ;
case OK :
throw n ew Except i o n ( 00TILT : Shou l d not get h e re . 00 ) ;
}
" " ·
return '

p r i vate S t r i ng u nexpectedA rgumentMess age ( ) {


S t ri ngßuffe r mes s ag e = new S t r i ngßuffe r ( 00Argument (s ) - 00) ;
fo r (ch a r c : unexpectedA rguments) {
messag e . append (c) ;
}
mes sage . append ( oo u nexpected . 00 ) ;

retu rn mes s ag e . to S t r i n g ( ) ;
}

publ i c boo l ean getßoo l ean ( c h a r arg) {


retu rn fal seifNu l l (boo l eanArgs . get ( a rg) ) ;
}

p r i vate bool ean fal seifNu l l (Boo l ean b) {


retu rn b == n u l l ? fal se : b ;
}

p u b l i c S t r i ng getSt r i n g (c h a r a rg) {
retu rn b l an kifNu l l ( s t r i ngArgs . ge t (arg) ) ;
}

p r i vate St r i ng bl a n kifNu l l (Stri ng s) {


oo oo
retu rn s == n u l l ? : s;
}

p u b l i c boo l ean has (char arg) {


retu rn a rg s Fo u nd . contai n s ( a rg ) ;
}

p u b l i c boo l ean i sVal i d () {


retu rn val i d ;
}
}

255
Kapitel 14
Schrittweise Verfeinerung

Sie können erkennen, dass der Code meiner Kontrolle entglitt. Er war noch nicht
schrecklich, aber das Chaos wurde sicherlich größer. Er bildet zwar einen Haufen,
aber noch keinen Misthaufen. Ich musste noch den i ntege r-Argumenttyp hinzu­
fügen, um wirklich einen Misthaufen zu erhalten.

Deshalb hörte ich auf

Ich musste mindestens noch zwei weitere Argumenttypen hinzufügen und konnte
erkennen, dass sie alles noch viel schlimmer machen würden. Wenn ich stur drauf­
los gearbeitet hätte, hätte ich den Code wahrscheinlich zum Laufen bringen kön­
nen; aber ich würde ein Chaos hinterlassen, das für etwaige Wartungsarbeiten zu
groß gewesen wäre. Wenn die Struktur dieses Codes überhaupt wartbar sein sollte,
war jetzt der Zeitpunkt gekommen, sie zu korrigieren.
Deshalb hörte ich auf, Funktionen hinzuzufügen, und begann, ein Refactoring durch­
zuführen. Da ich gerade die S t r i ng- und i ntege r-Argumente hinzugefügt hatte,
wusste ich, dass für jeden Argumenttyp neuer Code an drei hauptsächlichen Stellen
eingefügt werden musste. Erstens wurde für jeden Argumenttyp eine Methode zum
Parsen seines Schema-Elements benötigt, um die Has hMap für diesen Typ auszuwäh­
len. Zweitens musste jeder Argumenttyp in den Befehlszeilen-Strings geparst und in
seinen wahren Typ umgewandelt werden. Drittens benötigte jeder Argumenttyp eine
getXXX-Methode, um ihn als echten Typ an einen Aufrufer zurückzugeben.
Viele verschiedene Typen, alle mit ähnlichen Methoden - das hört sich für mich wie
eine Klasse an. Und so wurde das ArgumentMars hal e r-Konzept geboren.

ü ber i n krementeile Entwickl u ng

Eine der besten Methoden, ein Programm zu ruinieren, besteht darin, im Namen
der Verbesserung massive Änderungen an seiner Struktur vorzunehmen. Einige
Programme erholen sich nie von solchen »Verbesserungen«. Das Problem liegt
darin, dass es sehr schwer ist, das Programm dazu zu bringen, wie vor der »Ver­
besserung« zu arbeiten.
Um dies zu vermeiden, arbeite ich nach den Prinzipien der Test Driven Development
(TDD). Eine der zentralen Doktrinen dieses Ansatzes fordert, dass das System jeder­
zeit lauffähig sein muss. Anders ausgedrückt: Bei TD D darf ich das System nicht so
ändern, dass es nicht mehr läuft. Nach jeder Änderung muss es wie zuvor arbeiten.
Um dies zu erreichen, benötige ich eine Suite automatisierter Tests, die ich jederzeit
nach Gusto ausführen kann und die verifizieren, dass sich das Verhalten des Sys­
tems nicht geändert hat. Für die Args-Klasse hatte ich eine Suite von Unit- und
Acceptance-Tests erstellt, während ich den Misthaufen zusammenbaute. Die Unit­
Tests waren in Java geschrieben und wurden von J Unit verwaltet. Die Acceptance­
Tests bestanden aus Wiki-Seiten in FitNesse. Ich konnte diese Tests jederzeit aus­
führen. Wurden sie bestanden, war ich sicher, dass das System meinen Spezifika­
tionen entsprechend funktionierte.
1 4 .2
Args: der Rohentwurf

Deshalb machte ich mich daran, eine große Anzahl winziger Änderungen vorzu­
nehmen. Jede Änderung brachte die Struktur des Systems dem A rg ume n tMa rsha-
1 e r-Konzept näher. Dennoch blieb das System nach jeder Änderung lauffahig.
Zuerst fügte ich am Ende des Misthaufens das Gerüst des A rgumentMa r s h a 1 1 e r
ein (Listing r4.rr).

Listing 14.11: ArgumentMarshal e r am Ende von Args . j ava eingefügt


p r i vate cl ass ArgumentMa rshal e r {
p r i vate boo l e an boo l e anVal u e = fal s e ;

publ i c voi d setßoo l ean (bool ean val ue) {


boo l eanVal u e = val ue ;
}

publ i c bool ean getBoo l ean ( ) { re t u r n b oo l e anVa l u e ; }


}

p r i vate c l ass Bool eanArgumentMa rshal e r extends A rgumentMa rs hal e r {


}

p r i vate c l ass S t r i ngA rgume ntMa rshal e r extends ArgumentMarshal e r {


}

p r i vate c l ass Intege rArgumentMa rsha l e r extends ArgumentMa rshal e r {


}
}

Damit wurde der Code sicher nicht beschädigt. Dann nahm ich die einfachste Ände­
rung vor, die so wenig wie möglich kaputtmachen konnte. Ich änderte die H as hMap
für die booleschen Argumente so, dass sie einen A rg umen tMa r s h a l e r übernahm.

p r i vate Map<Cha racte r , ArgumentMarshal e r> boo l eanArgs =

new Has hMap<C h a r acte r , ArgumentMarshal er> () ;

Dadurch wurde einige Anweisungen ungültig, die ich schnell korrigierte.

p r i vate voi d parseßool eanSchemaEl emen t ( char e l ement!d) {


boo l eanArgs . pu t ( e l ementid , new Bool eanArgumentMarsha l e r () ) ;
}

p r i vate voi d s etBool eanArg (char a rgCh a r , bool ean val ue) {
boo l ean A rgs . ge t (argChar) . setBool ean (val ue) ;
}

publ i c boo l ean getßoo l ean ( c h a r a rg) {


retu rn fal s ei fN u l l (boo l e anArgs . get (arg) . getBool ean () ) ;
}

2 57
Kapitel 14
Sch rittweise Verfeinerung

Beachten Sie, wie diese Änderungen genau die Stellen betreffen, die ich weiter
vorne erwähnt habe: p a r s e , s e t und get für den Argumenttyp. Doch selbst diese
kleine Änderung brachte leider einige Tests zum Scheitern. Schauen Sie sich get­
Boo l ean sorgfaltig an: Wenn Sie die Funktion mit y aufrufen, aber es kein y­
1 1 ,

Argument gibt, dann gibt bool eanA rgs . get ( ' y ) den Wert n u l l zurück und die
1

Funktion löst eine N u l l Poi n t e r Except i o n aus. Die fal s eifNul l -Funktion war
als Schutz gegen diese Situation verwendet worden, aber meine Änderung hatte
diese Funktion irrelevant gemacht.
Eine schrittweise Entwicklung verlangte, dass ich dieses Problem schnell beheben
musste, bevor ich weitere Änderungen vornahm. Tatsächlich war die Korrektur
nicht zu schwierig. Ich musste nur die Prüfung auf n u l l verschieben. Ich musste
nicht mehr die boolesche Größe n u l l prüfen, sondern den A rg umentMa r s hal l e r.
Zuerst entfernte ich den Aufrufi von fa l s e i fN u l l aus der getßoo l ean-Funktion.
Er war jetzt nutzlos, deshalb eliminierte ich auch die Funktion selbst. Die Tests
scheiterten immer noch aufi dieselbe Weise; deshalb war ich ziemlich sicher, dass
ich keine neuen Fehler eingeführt hatte.

p u b l i c bool ean getßool ean (char arg) {


r e t u r n bool eanArgs . ge t (a rg) . getBool ean () ;
}

Als Nächstes zerlegte ich die Funktion in zwei Zeilen und fügte den A rgumentMa r ­
s hal l e r i n eine separate Variable namens a rg umentMa r s h al l e r ein. Mir gefiel der
lange Variablenname nicht; er war höchst redundant und machte die Funktion un­
übersichtlich. Deshalb kürzte ich ihn in am [Ns].

publ i c bool ean getßoo l ean (char a rg) {


Args . ArgumentMarshal e r am =
b ool eanArg s . get (arg) ;
retu rn am . getßool ean () ;
}

Dann fügte ich die Prüfung aufi n u l l ein.

publ i c bool ean getßool ean (char a rg) {


A rg s . A rgumentMar shal e r am = b ool eanArgs . get (arg) ;
retu rn am ! = nul l && am . getßool ean () ;
}

14.3 Stri ng-Argumente


Das Einfügen der St ri ng-Argumente ähnelte dem der boo l ean-Argumente. Ich
musste die H as hMap ändern und die Funktionen parse, s e t und g e t zum Laufen
bringen. Der folgende Code sollte keine Überraschungen enthalten, außer viel­
leicht, dass ich die gesamte Marshalling-Implementierung in die Basisklasse A r g u ­
mentMars h al l e r einfüge, anstatt sie auf die abgeleiteten Klassen z u verteilen.
14 3
String-Argum ente

p ri vate Map<Characte r , ArgumentMarshal e r> s t ri ngArgs


new HashMap<Ch a ract e r , ArgumentMarshal er> () ;

p ri vate voi d parseStri ngSc hemaEl emen t ( c h a r el ementid) {


s t ri ngA rgs . put (el ementid , new Stri ngArgumentMarshal er()) ;
}

p ri vate voi d setSt ri ngArg (char a rgChar) th rows A r g s Except i o n {


c u r rentA rgument++ ;
try {
s t ri ngArgs . ge t ( a rgChar) . setSt r i ng (args [cu r re n tA rgument] ) ;
} catch (Ar rayi ndexOu tOfBou ndsExcepti o n e ) {
val i d = fal se ;
e r rorArgumentid = argCh a r ;
e r ro rCode= E r ro rCode . MISSING_STRING ;
th row new A rgs Excepti on () ;
}
}

publ i c St ri ng getSt ri ng ( c h a r arg) {


Args . ArgumentMarshal er am = s t ri ngArgs . ge t (a rg) ;
retu rn am == nul l ? " "
: