27
1021 17 Refactorings Bei der Entwicklung und Pflege von Software ist das Gesetz der Entropie (der zuneh- menden Unordnung) aus der Physik zu beobachten. Man kennt Ähnliches aber auch aus dem täglichen Leben. In einer Wohnung nimmt die Unordnung ständig zu, wenn man nicht von Zeit zu Zeit aufräumt. Auf Software übertragen gilt, dass sich im Lau- fe der Zeit durch Änderungen die Struktur und Lesbarkeit derart verschlechtern kann, dass sich Fehlerbehebungen oder Erweiterungen immer schwieriger in den bestehen- den Sourcecode integrieren lassen. Das Thema Wartbarkeit ist beim professionellen Programmieren aber sehr wichtig, denn oftmals hat man eine große Sourcecode-Basis zu pflegen und zu erweitern. Nur selten kommt man in den Genuss, ein System voll- ständig neu entwerfen zu dürfen. Und auch in diesem Fall wandelt sich die Situati- on schnell, wenn man nicht fortlaufend Qualitätssicherung betreibt und kontinuierlich Überarbeitungen durchführt. Wurde dies versäumt, so hilft – wie im realen Leben – nur eine gründliche Aufräumaktion oder gar ein Umzug. Letzteres entspräche auf die Soft- ware übertragen einer kompletten Neuimplementierung. Eine solche ist aufgrund des hohen Risikos, zu scheitern, jedoch meistens keine Alternative. Es verbleibt die Aufräu- maktion, die Umbaumaßnahmen im Sourcecode, sogenannten Refactorings, entspricht. Diese sollen problembehafteten Sourcecode derart verändern, dass dieser besser ver- ständlich, lesbar und auch leichter wartbar wird. Martin Fowler definiert den Begriff Refactoring folgendermaßen: »Refactoring is a change made to the internal structure of a software component to make it easier to understand and cheaper to modify without changing the observable behavior of that software component« [24]. Dieses Kapitel stellt verschiedene in der Praxis erprobte Überarbeitungen von Sourcecode vor. In Abschnitt 17.1 betrachten wir ein einführendes Beispiel. Beim Über- arbeiten des Sourcecodes ist ein Standardvorgehen hilfreich, das ich in Abschnitt 17.2 beschreibe. Dieses sorgt zunächst mit einigen Vorbereitungsmaßnahmen und der Be- achtung von Coding Conventions (vgl. Kapitel 19) dafür, dass die weitere Bearbeitung und gewünschte Transformation leichter fallen. Je nach erkannter Schwachstelle kann man gemäß den Schritt-für-Schritt-Anleitungen aus Abschnitt 17.4 vorgehen. Nicht bei allen davon handelt es sich im strengen Sinne um Refactorings, da diese mitunter das nach außen sichtbare Verhalten leicht ändern und somit die obige Forderung nicht strikt einhalten. Allerdings gibt es auch Meinungen, die die Aussage auf relevantes Verhalten lockern. Unabhängig von der genauen Auslegung spreche ich der Einfach- heit halber immer von Refactorings. Natürlich müssen derartige Änderungen mit Vor- sicht und Sorgfalt ausgeführt werden, um dadurch keine neuen Fehler einzuführen oder

Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

  • Upload
    others

  • View
    0

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1021

17 Refactorings

Bei der Entwicklung und Pflege von Software ist das Gesetz der Entropie (der zuneh-menden Unordnung) aus der Physik zu beobachten. Man kennt Ähnliches aber auchaus dem täglichen Leben. In einer Wohnung nimmt die Unordnung ständig zu, wennman nicht von Zeit zu Zeit aufräumt. Auf Software übertragen gilt, dass sich im Lau-fe der Zeit durch Änderungen die Struktur und Lesbarkeit derart verschlechtern kann,dass sich Fehlerbehebungen oder Erweiterungen immer schwieriger in den bestehen-den Sourcecode integrieren lassen. Das Thema Wartbarkeit ist beim professionellenProgrammieren aber sehr wichtig, denn oftmals hat man eine große Sourcecode-Basiszu pflegen und zu erweitern. Nur selten kommt man in den Genuss, ein System voll-ständig neu entwerfen zu dürfen. Und auch in diesem Fall wandelt sich die Situati-on schnell, wenn man nicht fortlaufend Qualitätssicherung betreibt und kontinuierlichÜberarbeitungen durchführt. Wurde dies versäumt, so hilft – wie im realen Leben – nureine gründliche Aufräumaktion oder gar ein Umzug. Letzteres entspräche auf die Soft-ware übertragen einer kompletten Neuimplementierung. Eine solche ist aufgrund deshohen Risikos, zu scheitern, jedoch meistens keine Alternative. Es verbleibt die Aufräu-maktion, die Umbaumaßnahmen im Sourcecode, sogenannten Refactorings, entspricht.Diese sollen problembehafteten Sourcecode derart verändern, dass dieser besser ver-ständlich, lesbar und auch leichter wartbar wird. Martin Fowler definiert den BegriffRefactoring folgendermaßen: »Refactoring is a change made to the internal structure ofa software component to make it easier to understand and cheaper to modify withoutchanging the observable behavior of that software component« [24].

Dieses Kapitel stellt verschiedene in der Praxis erprobte Überarbeitungen vonSourcecode vor. In Abschnitt 17.1 betrachten wir ein einführendes Beispiel. Beim Über-arbeiten des Sourcecodes ist ein Standardvorgehen hilfreich, das ich in Abschnitt 17.2beschreibe. Dieses sorgt zunächst mit einigen Vorbereitungsmaßnahmen und der Be-achtung von Coding Conventions (vgl. Kapitel 19) dafür, dass die weitere Bearbeitungund gewünschte Transformation leichter fallen. Je nach erkannter Schwachstelle kannman gemäß den Schritt-für-Schritt-Anleitungen aus Abschnitt 17.4 vorgehen. Nicht beiallen davon handelt es sich im strengen Sinne um Refactorings, da diese mitunter dasnach außen sichtbare Verhalten leicht ändern und somit die obige Forderung nichtstrikt einhalten. Allerdings gibt es auch Meinungen, die die Aussage auf relevantesVerhalten lockern. Unabhängig von der genauen Auslegung spreche ich der Einfach-heit halber immer von Refactorings. Natürlich müssen derartige Änderungen mit Vor-sicht und Sorgfalt ausgeführt werden, um dadurch keine neuen Fehler einzuführen oder

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

D3kjd3Di38lk323nnm

Page 2: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1022 17 Refactorings

das Programmverhalten grundlegend zu ändern. Dazu ist es sinnvoll, Veränderungen inkleinen, überschaubaren Schritten durchzuführen und für eine Absicherung durch UnitTests zu sorgen (vgl. Kapitel 20).

17.1 Refactorings am BeispielAnhand eines von mir so in der Praxis gefundenen Beispiels möchte ich darstellen, wel-che Möglichkeiten zur Vereinfachung sich mit Refactorings ergeben können. Folgendestatische Methode isNumber(String) der Klasse NumberUtilsV1 prüft auf naiveWeise, ob ein übergebener String eine ganze Zahl darstellt. Dazu wird vor allem dieMethode Character.isDigit(char) verwendet:

public static boolean isNumber(final String value){

if (Character.isDigit(value.charAt(0))){

for (int i = 1, n = value.length(); i < n; i++){

if (!(Character.isDigit(value.charAt(i)))){

return false;}

}}else{

return false;}return true;

}

In der Methode finden zwei if-Abfragen statt. Zunächst wird das erste Zeichen geprüft.Nur wenn dieses eine Ziffer ist, wird anschließend in einer for-Schleife beginnend abIndex 1 der isDigit(char)-Vergleich wiederholt, bis entweder das Ende des Stringserreicht oder das betrachtete Zeichen keine Ziffer ist. Durch etwas Sourcecode-Analyseerkennen wir, dass die Methode isNumber(String) folgende Probleme enthält:

1. Unerwartete Exception bei leerer Eingabe – Wenn die Eingabe nicht mindes-tens ein Zeichen enthält, wird eine java.lang.StringIndexOutOfBounds-

Exception ausgelöst. Die Ursache ist der indizierte Zugriff per charAt(0), ohnezuvor die Länge des Parameters value zu überprüfen. Ein solches Verhalten istzu vermeiden. Dies gilt im Speziellen, wenn die Werte aus einer Benutzereingabestammen.

2. Fehleranfällig für null – Bei Übergabe eines null-Werts löst die Methodeerst bei der Verarbeitung und dem Aufruf von charAt(0) eine NullPointer-

Exception aus. Öffentliche Methoden sollten gemäß Design by Contract (vgl.Abschnitt 3.1.5) ihre Vorbedingungen sicherstellen und dazu vor der eigentlichenVerarbeitung die Eingabeparameter auf Gültigkeit prüfen. Damit wird eine Störungder Abarbeitung durch fehlerhafte Übergabewerte vermieden.

Page 3: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.1 Refactorings am Beispiel 1023

3. Zu kompliziert – Die initiale if-Bedingung und das if innerhalb der for-Schleifesorgen für eine weitere Schachtelungsebene und sind konträr zueinander formuliert.Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dannfragen, wieso die Schleife mit 1 und nicht bei 0 startet.

4. Missverständlicher Name bzw. unklares Verhalten – Der Methodenname is-

Number(String) suggeriert die Möglichkeit der Verarbeitung beliebiger Zahlen.Momentan werden aber weder Zahlen mit Vorzeichen noch solche mit Nachkom-mastellen unterstützt.

5. Viele return-Anweisungen – Für eine derart kurze Methode existieren mit dreireturn-Anweisungen schon recht viele Ausgänge.

Um die Funktionalität zu prüfen und mögliche Fehler aufzudecken, entwickeln wir ei-nige elementare Unit Tests. Dabei können uns die eben ermittelten obigen Kritikpunktehelfen, mögliche Testfälle zu identifizieren und diese Schwachstellen für die Zukunftauszuschließen. Das ist nützlich, weil wir auch einige kleinere Änderungen und Erwei-terungen realisieren wollen. Damit beginnen wir jedoch erst, nachdem wir ein Sicher-heitsnetz aus diversen Testfällen erstellt haben. Wir folgen hier der in Abschnitt 20.2.1beschriebenen testgetriebenen Entwicklung.

Problem 1: Unerwartete Exception bei leerer Eingabe

Bevor wir uns dem eigentlichen Problem bei leeren Eingaben zuwenden, erstellenwir einige Funktionstests: Wir prüfen die Verarbeitung einer gültigen Eingabe, etwa"12345", und eines fehlerhaften Werts, etwa "ABC". Im ersten Fall erwarten wir trueund im zweiten false als Ergebnis. Dies lässt sich mithilfe der Methoden assert-

True(boolean) und assertFalse(boolean) wie folgt ausdrücken:

@Testpublic void testValidNumberInput(){

assertTrue(NumberUtilsV1.isNumber("12345"));}

@Testpublic void testInvalidInput(){

assertFalse(NumberUtilsV1.isNumber("ABC"));}

Randfälle prüfen: Eingabe der Länge 0 und 1 Weil die obigen zwei Testsbestanden werden, prüfen wir nun auch zwei Randfälle, nämlich eine leere Eingabeund eine Eingabe mit nur einer Ziffer. Unsere Voranalyse hat schon aufgedeckt, dass esdabei Probleme gibt. Wir schreiben nun passende Unit Tests, die das bestätigen. In derZukunft soll die Methode für eine leere Eingabe keine StringIndexOutOfBounds-Exception auslösen, sondern den Wert false für die Aussage »keine Zahl« liefern.Eine einzelne Ziffer gilt als Zahl. Das Ganze lässt sich wie folgt absichern:

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 4: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1024 17 Refactorings

@Testpublic void testNumberInputLength0(){

assertFalse(NumberUtilsV1.isNumber(""));}

@Testpublic void testNumberInputLength1(){

assertTrue(NumberUtilsV1.isNumber("1"));}

Wie erwartet, schlägt der Test testNumberInputLength0() mit leerer Eingabe fehl.

Korrektur Bevor wir weitere Tests ergänzen, korrigieren wir die Funktionalität. Wirfügen folgende Sicherheitsprüfung am Anfang der Methode hinzu:

if (value.isEmpty()){

return false;}

Damit werden dann die Unit Tests wieder bestanden. Allerdings ist unser Nutzcodedurch die Abfrage und das weitere return etwas komplizierter geworden, aber wirleben erstmal damit. Darum kümmern wir uns, sobald wir das Sicherheitsnetz aus UnitTests ausgebaut haben. Dadurch können dann Änderungen an der inneren Struktur mitmehr Vertrauen in die Korrektheit ausgeführt werden.

Problem 2: Fehleranfällig für null

In der initialen Analyse haben wir erkannt, dass die Eingabe von null unbehandelt istund eine NullPointerException auslöst – im Originalcode allerdings nicht durcheine Parameterprüfung, die auch eine IllegalArgumentException nutzen könnte,sondern erst beim Zugriff value.charAt(0) durch die JVM beim Dereferenzieren.Wünschenswert ist eine explizite Behandlung und ein Hinweistext, der mitteilt, welcherParameter null war. Der Test ist etwas komplizierter zu formulieren:

@Testpublic void testNullInput(){

try{

NumberUtilsV1.isNumber(null);fail(); // es wird als Reaktion eine Exception erwartet

}catch (final Exception ex){

// NullPointerException wird als Reaktion erwartetassertTrue(ex instanceof NullPointerException);// Teste die Existenz eines Textes => keine StandardexceptionassertFalse(StringUtils.isEmpty(ex.getMessage()));

}}

Page 5: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.1 Refactorings am Beispiel 1025

Bei der Abarbeitung erwarten wir, dass eine Exception auftritt. Normalerweise könn-te man dazu die Notation @Test(expected=NullPointerException.class) nut-zen. Das geht hier nicht, da wir zudem prüfen wollen, ob ein Hinweistext in der Excep-tion hinterlegt ist. Das erwarten wir, wenn Exceptions aus dem eigenen Programm her-aus ausgelöst werden. Nur wenn dort passende Hinweise und Kontextinformationenbereitgestellt werden, ist eine sinnvolle Fehlerbehandlung möglich. Im Gegensatz dazusind die Texte leer, wenn Exceptions durch die JVM automatisch beim Dereferenziereneiner null-Referenz ausgelöst werden.

Wir verwenden die Methode fail() für den Fall, dass fälschlicherweise keineException geworfen und isNumber(null) normal terminieren würde. Beim Auftre-ten einer Exception prüfen wir auf die erwartete NullPointerException, sodass derUnit Test als fehlgeschlagen interpretiert wird, falls eine andere Exception auftritt. Ab-schließend testet die Methode isEmpty(String) der Klasse StringUtils aus Apa-che Commons Lang1, ob ein Hinweistext hinterlegt ist. Und, wie erwartet, schlägt auchdieser Test zunächst fehl.

Korrektur Wir haben in Kapitel 6 die Bibliothek Google Guava2 und deren Utility-Klasse Preconditions zum Sicherstellen von Vorbedingungen kennengelernt. Wirfügen eine Sicherheitsprüfung am Anfang der Methode hinzu:

public static boolean isNumber(final String value){

Preconditions.checkNotNull(value, "parameter ’value’ must not be null");

if (value.isEmpty()){

return false;}if (Character.isDigit(value.charAt(0))){

for (int i = 1, n = value.length(); i < n; i++){

if (!(Character.isDigit(value.charAt(i)))){

return false;}

}}else{

return false;}return true;

}

Damit werden dann die Unit Tests wieder bestanden. Allerdings ist unser Nutzcodedurch die Abfrage nochmals etwas komplizierter geworden. Da wir mittlerweile einrecht gutes Sicherheitsnetz erstellt haben, ist es nun an der Zeit, den Nutzcode – sofernmöglich – ein wenig aufzuräumen.

1http://commons.apache.org/2https://code.google.com/p/guava-libraries/

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 6: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1026 17 Refactorings

Problem 3: Zu kompliziert

Wenn wir uns die Methode und insbesondere die Schleife und dortigen if-Abfragenanschauen, ist das schon einigermaßen kompliziert. Bei genauem Hinsehen fällt auf,dass man Vereinfachungen erreichen kann: Die abgefragten Bedingungen sind seman-tisch gleich, aber negiert. Somit lassen sich die beiden if-Abfragen zusammenfassen,indem die if-Startbedingung in die if-Abfrage der for-Schleife integriert wird. AlsFolge kann die Schleife bei 0 gestartet werden.

Auch die Formulierung der for-Schleife ist uns zu kompliziert. Zur Vereinfachungentfernen wir die Zuweisung n = value.length() aus dem Initialisierungsteil derfor-Schleife, die zur Optimierung des Vergleichs i < n diente.3 Dies schreiben wirkürzer als i < value.length(). Das erhöht die Lesbarkeit und Verständlichkeit. Ins-gesamt ergibt sich folgende Korrektur:

public static boolean isNumber(final String value){

Preconditions.checkNotNull(value, "parameter ’value’ must not be null");

if (value.isEmpty()){

return false;}for (int i = 0; i < value.length(); i++){

if (!(Character.isDigit(value.charAt(i)))){

return false;}

}return true;

}

Wir führen die Unit Tests aus und diese bestätigen uns, dass alle bisher akzeptiertenZahlen weiterhin gültig sind.

Problem 4: Missverständlicher Name bzw. unklares Verhalten

Nach Rücksprache mit dem Kunden oder Requirements Engineer wird deutlich, dassdie Methode isNumber(String) neben positiven selbstverständlich auch negativeGanzzahlen verarbeiten können sollte. Eventuell wird später sogar eine Erweiterungauf Gleitkommazahlen gewünscht.

Wie bisher gehen wir testgetrieben vor, d. h., wir erstellen zuerst den Testfall. Die-ser sollte fehlschlagen, da die Funktionalität noch nicht existiert. Daraufhin korrigierenwir die Funktionalität. Gerade bei Sourcecode, den man nicht so gut kennt und in demkleinere Erweiterungen oder Verbesserungen zu realisieren sind, ist dieses Vorgehenhilfreich. Nicht so empfehlenswert finde ich dieses Vorgehen, wenn man ganz genauweiß, was man realisieren möchte, und das Design und die Implementierung schon im

3Normalerweise ist das nicht notwendig, weil Optimierungen auf dieser feingranularen Ebenedurch die JVM automatisch erfolgen (vgl. Kapitel 22).

Page 7: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.1 Refactorings am Beispiel 1027

Kopf hat. Dann können die Sprünge zwischen Test und Codierung stören. Genauer geheich darauf nochmals in Kapitel 20 ein.

Nach diesem kleinen gedanklichen Ausflug kommen wir wieder zu der Erweite-rung zurück. Wir wollen die Verarbeitung von Vorzeichen prüfen und nutzen die Werte"+4711" und "-4711" als Eingaben, die in beiden Fällen das Ergebnis true liefernsollten, was wir folgendermaßen prüfen:

@Testpublic void testNumberPositive_PlusSignShouldBeAccepted(){

assertTrue("plus sign should be accepted", NumberUtils.isNumber("+4711"));}

@Testpublic void testNumberNegative_MinusSignShouldBeAccepted(){

assertTrue("minus sign should be accepted", NumberUtils.isNumber("-4711"));}

Weil wir die Implementierung mittlerweile ziemlich gut kennen, wissen wir, dass keineVorzeichen unterstützt werden. Wie erwartet, werden demnach beide Tests auch nichtbestanden. Aber durch das mittlerweile erworbene Know-how fällt die Korrektur derMethode nicht schwer:

public static boolean isNumber(final String value){

Preconditions.checkNotNull(value, "parameter ’value’ must not be null");

if (value.isEmpty()){

return false;}// Verarbeite VorzeichenString number = value;if (value.startsWith("-") || value.startsWith("+")){

number = value.substring(1, value.length());}// Weitere Prüfung auf Zahl wie zuvorfor (int i = 0; i < number.length(); i++){

if (!(Character.isDigit(number.charAt(i)))){

return false;}

}return true;

}

Zwar werden nun alle Tests bestanden, die Komplexität des Applikationscodes hat aberschon wieder zugenommen. Spätestens jetzt sollten wir uns fragen, ob es nicht eineadäquate Funktionalität im JDK oder externen Bibliotheken gibt. Dadurch kann mansich in der Regel einige Arbeit sparen. Immerhin haben wir nun einen guten Satz an UnitTests. Damit können wir dann auch gleich das letzte Problem der eingangs aufgestelltenMängelliste angehen. Schauen wir einmal, was möglich ist.

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 8: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1028 17 Refactorings

Problem 5: Viele return-Anweisungen

Die Methode isNumber(String) sollte auf Ganzzahlen prüfen. Statt diese Funktio-nalität, wie initial geschehen, selbst zu programmieren, bietet sich der Einsatz der Java-Bibliotheken zum Parsen von Zahlen an. Da unsere Tests bereits recht ausgereift sind,müssen wir hier nichts ergänzen. In der Methode isNumber(String) verwenden wirnun die Methode Integer.parseInt(String) (vgl. Abschnitt 4.2.2). Durch derenEinsatz können im Gegensatz zu der eigenen Realisierung sowohl positive als auchnegative Zahlen verarbeitet werden.

public static boolean isNumber(final String value){

Preconditions.checkNotNull(value, "parameter ’value’ must not be null");

try{

Integer.parseInt(value); // Rückgabe ignorieren, nur prüfenreturn true;

}catch (final NumberFormatException ex){

return false;}

}

Wir nutzen wieder unsere mittlerweile auf 7 Testfälle angewachsene Testklasse, um diekorrekte Funktionalität zu prüfen. Alle Unit Tests werden bestanden. Ziehen wir einFazit und schauen auf mögliche Erweiterungen.

Was haben wir erreicht? Die Intention und Realisierung der Methode ist nundeutlich klarer, da nicht mehr auf Basis einzelner Zeichen geprüft wird. Vielmehr er-folgt auf logischer Ebene eine Umwandlung in eine Zahl. Die Lesbarkeit hat dadurchenorm zugenommen. Wir kommentieren zudem die bewusst fehlende Auswertung desRückgabewerts von Integer.parseInt(String), um möglicherweise aufkommen-de Fragen anderer Entwickler sofort zu klären.

Einschränkungen und mögliche Erweiterungen Die obige Realisierung istdeutlich klarer und besser verständlich als die Originalmethode sowie alle zuvor ge-zeigten Zwischenschritte.

Allerdings gibt es doch noch eine Kleinigkeit zu bedenken bzw. zu bemängeln:Durch die Prüfung mit Integer.parseInt(String) wird der Wertebereich aufInteger.MIN_VALUE bis Integer.MAX_VALUE begrenzt, also in etwa auf ± 2 Mil-liarden. Für größere Wertebereiche können wir auf die Wrapper-Klasse Long zurück-greifen. Um Gleitkommazahlen zu unterstützen, können wir die Klassen Float bzw.Double verwenden. All dies führt jedoch zu Veränderungen im nach außen sichtbarenVerhalten der Methode, weil dadurch weitere Eingabewerte erlaubt sind. Wir akzep-tieren hier die Erweiterung des Wertebereichs, müssen dabei aber die Anmerkungen

Page 9: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.1 Refactorings am Beispiel 1029

im folgenden Hinweis »Vorsicht selbst bei minimalen Modifikationen am Verhalten«bezüglich der Änderung von Verhalten bedenken.

Hinweis: Vorsicht selbst bei minimalen Modifikationen am Verhalten

In einigen Schritten wurde durch die initiale Prüfung sowie den Einsatz von Biblio-theksfunktionen zum Parsing minimal etwas am nach außen veröffentlichten Verhal-ten verändert. So etwas übersieht man leicht. Allerdings können dadurch Problemedurch Inkompatibilitäten verursacht werden. Betrachten wir dies im Detail.

Auswirkungen der Korrekturen für Problem 1 Die ursprüngliche Methodehat bei Übergabe leerer Eingaben eine StringIndexOutOfBoundsExceptionausgelöst. Die neue Realisierung greift gar nicht erst indiziert zu, wenn der überge-bene Text leer ist, sondern es wird direkt false zurückgegeben.

Auswirkungen der Korrekturen für Problem 4 und 5 Durch die Akzeptanzvon Vorzeichen sowie den Einsatz von Bibliotheksfunktionen beim Parsing werdennun je nach gewählter Realisierung auch Vorzeichen und Nachkommastellen unter-stützt. Die ursprüngliche Methode hat lediglich Ganzzahlen ohne Vorzeichen, dafüraber beliebiger Länge, als Zahl erkannt. Dies war höchstwahrscheinlich nicht alsFeature gedacht, sondern war vermutlich eher ein »Unfall«.

Kompatibilität und Einsatz von Bibliotheksfunktionen

Die Kompatibilität zum ursprünglichen Verhalten kann entscheidend sein, wenn mannicht alle Aufrufer im Zugriff hat und somit nicht weiß, ob vielleicht einer von diesendie Information »keine Zahl« durch Abfangen einer StringIndexOutOfBounds-Exception ermittelt. Andere Aufrufer könnten beliebig lange Ziffernfolgen als Zah-len auswerten wollen.

Weil Ersteres meiner Ansicht nach ein Designfehler ist, werde ich hier keine Lö-sung angeben. Die Auswertung beliebig langer Ziffernfolgen ist ein denkbarer An-wendungsfall. Hätte die Aufgabe der ursprünglichen Methode isNumber(String)tatsächlich darin bestanden, nur eine derartige Prüfung durchzuführen, kann mandies elegant und kompatibel zu allen Unit Tests mit der Klasse java.math.Big-Integer wie folgt realisieren:

public static boolean isNumber(final String value){

Preconditions.checkNotNull(value, "parameter ’value’ must not be null");

try{

new BigInteger(value); // nur prüfenreturn true;

}catch (final NumberFormatException ex){

return false;}

}

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 10: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1030 17 Refactorings

17.2 Das StandardvorgehenDas einleitende Beispiel hat Ihnen einen ersten Eindruck von Refactoring-Schritten ver-mittelt. Um reproduzierbare Ergebnisse zu erreichen und mehr Sicherheit zu haben, hal-ten wir uns an eine Art Checkliste zur Überarbeitung von Sourcecode. Diese beschreibtdas bereits angesprochene Standardvorgehen, das folgende Schritte umfasst:

1. Testen – Führe vorhandene Unit Tests aus. Im Speziellen sollte dies kontinuierlichnach jedem der folgenden Schritte geschehen.

2. Coding Conventions anwenden – Beachte die später in Abschnitt 19.3 vorgestell-ten Coding Conventions:

n Formatiere den Sourcecoden Mache, falls möglich, die Übergabeparameter finaln Definiere, falls möglich, lokale Variablen final

n Sorge für verständliche Konstanten-, Variablen- und Methodennamen

3. In Einzelbestandteile zerlegen – Zerlege, wenn notwendig und sinnvoll, einekomplexere Programmstelle zunächst mithilfe (des mehrmaligem Einsatzes) desBasis-Refactorings EXTRACT METHOD, das in Eclipse im Menü REFACTOR –>EXTRACT METHOD oder über das Tastaturkürzel ALT+SHIFT+M erreichbar ist,in handhabbare und sinnvolle kleinere Bestandteile.

4. Unit Tests erstellen – Nutze, sofern die Anforderungen von der Fachseite oder demKunden oder aus einem Requirements-Dokument bekannt sind, diese, um darausTestfälle zu gestalten. Ansonsten schreibe für zuvor extrahierte Methoden entspre-chende Unit-Test-Methoden:

n Für normale Eingabenn Für ungültige Eingabenn Für Rand- oder Spezialfälle

5. Aufräumen – Räume den Sourcecode auf:

n Entferne unbenutzte Variablen und unbenutzte Methodenn Entferne alte, unnötige oder (mittlerweile) falsche Kommentare

6. Vereinfachen – Reduziere die Komplexität:

n Entferne duplizierten Sourcecode, erzeuge gegebenenfalls Hilfsmethodenn Vereinfache Bedingungenn Füge bei Bedarf erklärende Kommentare ein – oftmals sollte man zunächst

sprechende Bezeichner für Attribute und Methoden in Betracht ziehen.

7. Konkrete Refactorings durchführen – Verbessere den Sourcecode durch denEinsatz eines für den erkannten Schwachpunkt passenden Refactorings aus demRefactoring-Katalog aus Abschnitt 17.4.

Page 11: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.2 Das Standardvorgehen 1031

Die einzelnen Schritte müssen nicht sklavisch exakt in dieser Reihenfolge abgearbeitetwerden. Es ist durchaus möglich, die Reihenfolge zu variieren und einige Schritte aus-zulassen oder auch mehrfach auszuführen. Um ein Gespür für die Vorgehensweise beimEinsatz in der Praxis zu bekommen, wollen wir die obigen Schritte an einem weiterenkurzen Beispiel nachvollziehen.

Beachten Sie bitte, dass ich sowohl im folgenden Beispiel als auch bei der Vor-stellung der Refactorings auf eine detaillierte Darstellung der eigentlich notwendigenUnit Tests verzichten werde, um so den Fokus auf die eigentliche Transformation zulegen. In der Praxis entspricht die Vorgehensweise jedoch dem zuvor gezeigten einlei-tenden Beispiel, in dem Unit Tests zur Absicherung erstellt und eingesetzt wurden.

Beispiel

Als Ausgangsbasis dient folgender überarbeitungswürdiger catch-Block, den ich tat-sächlich – abgesehen von Details – so in Produktionscode vorgefunden habe:

catch (final Fault aF){

final String stSrc = aF.getSource();final String stErr = aF.getFaultCode();if (stErr != null && stErr.equalsIgnoreCase("FileNotFound")){

stErrorMsg = aF.getFaultString() + ": " + stSrc;}else{

stErrorMsg = aF.getFaultString() + ": " + stSrc;}

}

Wir führen folgende Schritte (Nummerierung laut Standardvorgehen) durch:

Schritt 2: Coding Conventions anwenden Befolgt man Namenskonventionenführt das zu mehr Lesbarkeit. Es fällt auf, dass in diesem Sourcecode-Abschnitt eini-ge Präfixe in den Variablennamen verwendet werden. Wie später in Abschnitt 19.3.1genauer beschrieben, stellen Präfixe in Variablennamen eher ein Relikt aus alten Ta-gen dar, weil damals die IDEs noch nicht in der Lage waren, Informationen aus demSourcecode on the fly zu extrahieren und etwa Attribute farblich anders darzustellen.Heutzutage sollte man Präfixe selten verwenden oder besser ganz vermeiden – beimÜberarbeiten eines bestehenden Programmteils ist es aber ratsam, nicht alles umzu-krempeln und dem vorhandenen Stil einigermaßen zu folgen, sofern dies nicht (allzusehr) gegen die eigenen vorgegebenen Konventionen verstößt. Im Beispiel gilt, dass einPräfix nutzlos ist, wenn kein sinnvoller Variablenname folgt wie hier für die VariableaF. Diese wird daher in fault umbenannt. Solche Namensänderungen sind für lokaleVariablen immer möglich. Wir benennen die Variable stErr in strErr um. Aus stSrcwird strSource. Beide Male wird das merkwürdige Präfix st dabei zu str für String-variablen. Das Beibehalten der Kürzel ist ein Zugeständnis, nicht allzu viel am Stil zuändern. Zudem wird strErr durch Umbenennung in strFaultCode besser lesbar

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 12: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1032 17 Refactorings

und verständlich. Die Namensähnlichkeit mit dem privaten Attribut stErrorMsg unddie daraus resultierende Verwechselungsgefahr existiert nach dieser Namensänderungnicht mehr. Das Attribut wird außerdem durch den Einsatz der this-Notation explizitals solches gekennzeichnet und mit dem Präfix str zu strErrorMsg. Es lässt sichsomit visuell gut von den lokalen Variablen abgrenzen.4

catch (final Fault fault){

final String strSource = fault.getSource();final String strFaultCode = fault.getFaultCode();if (strFaultCode != null && strFaultCode.equalsIgnoreCase("FileNotFound")){

this.strErrorMsg = fault.getFaultString() + ": " + strSource;}else{

this.strErrorMsg = fault.getFaultString() + ": " + strSource;}

}

Nicht in jedem Fall ist eine Korrektur von Namen derart möglich. Private Attributekönnen problemlos umbenannt werden – sofern darauf nicht per Reflection zugegriffenwird. Im nachfolgenden Text dieses Kapitels gehe ich auf den Spezialfall Reflectionund Refactorings kaum mehr explizit ein. Die Änderung der Namen öffentlicher Attri-bute kann Änderungen in einsetzenden Klassen erfordern und ist daher möglicherweiseproblematisch. Eine Verbesserung der Kapselung erreicht man mit dem RefactoringREDUZIERE DIE SICHTBARKEIT VON ATTRIBUTEN (vgl. Abschnitt 17.4.1), das aufdem Basis-Refactoring ENCAPSULATE FIELD basiert. Als Folge kann bei Bedarf dasDesign geändert werden. Statt eines Attributs kann man eine Delegation nutzen oderden Wert dynamisch berechnen – das bietet sich etwa für die Eigenschaft Alter einerPerson an, das sich aus dem aktuellen Datum und dem Geburtstag ergibt.

Schritt 6: Vereinfachen In diesem einfachen Beispiel findet man eine exakte Du-plikation der Anweisungen im if- und else-Anweisungsblock. Zwar ist das realerAnwendungscode, aber meistens ist eine derart extreme Duplikation selten und eher alsWiederholung von Teilabschnitten zu beobachten. In diesem Beispiel kann eine enormeVereinfachung erzielt werden, weil die Auswertung der Bedingung überflüssig ist. DerAnweisungsblock muss dadurch lediglich einmal notiert werden:

catch (final Fault fault){

final String strSource = fault.getSource();final String strFaultCode = fault.getFaultCode();

this.strErrorMsg = fault.getFaultString() + ": " + strSource;}

4Allerdings besitzen moderne IDEs mittlerweile eine sehr ausgefeilte farbliche Darstellungvon Programmelementen, wodurch eine gute visuelle Trennung möglich wird, ohne dass mandazu this nutzen muss.

Page 13: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1033

Schritt 5: Aufräumen Als Folge dieser Vereinfachungen wird ersichtlich, dass dieVariable strFaultCode nun unbenutzt und damit überflüssig ist. Damit kann sie ent-fallen. Auch die Variable strSource ermittelt nur Daten aus dem übergebenen Fault-Objekt. Wir schreiben einfacher Folgendes:

catch (final Fault fault){

this.strErrorMsg = fault.getFaultString() + ": " + fault.getSource();}

Fazit

Das Resultat ist beeindruckend: Aus zehn Zeilen Sourcecode ist eine Zeile geworden.Ähnlich wie sich Bad Smells ausbreiten, wenn man unachtsam ist, gibt es glücklicher-weise auch einen gegenteiligen Effekt: Je mehr man für Klarheit und Struktur sorgt,desto leichter fallen weitere Verbesserungsmaßnahmen.

Jedoch haben wir ein Problem nicht entfernen können: Nach wie vor gibt es eineZuweisung an das Attribut strErrorMsg im catch-Block. Ein solcher Seiteneffektist zu vermeiden, da es an unerwarteter Stelle im Programm zu Zustandsänderungenkommt. Durch Refactorings wollen wir normalerweise das nach außen sichtbare Pro-grammverhalten nicht verändern, sondern lediglich die innere Struktur verbessern.An dieser Stelle können wir im Sourcecode einen Kommentar mit einem Hinweis aufdiesen Seiteneffekt einfügen und das Ganze später nochmal prüfen bzw. überarbeiten.

17.3 Kombination von Basis-RefactoringsBevor wir uns einen Katalog einiger komplexerer Refactorings – und zum Teil genaugenommen sogar leicht verhaltensverändernder Transformationen des Sourcecodes –anschauen, wollen wir zunächst an einem Beispiel verschiedene durch die IDE unter-stützte Basis-Refactorings, wie sie Martin Fowler in seinem Buch »Refactoring: Impro-ving the Design of Existing Code« [24] beschreibt, betrachten. Die Basis-Refactoringszeichnen sich dadurch aus, dass sie oftmals keine Modifikation am sichtbaren Verhaltenvornehmen. Die hohe Kunst ist es, die Schritte so klein und sicher zu gestalten, dass esdabei möglichst selten zu Kompilierfehlern oder anderweitigen Problemen kommt. Da-mit es klappt, muss man sich der Refactoring-Automatiken aus der IDE bedienen. DieWahrscheinlichkeit für Komplikationen steigt, wenn man eher freihändig refaktorisiert.

17.3.1 Refactoring-Beispiel: Ausgangslage und ZielAls Ausgangsbasis dient eine Utility-Klasse TimeStampUtils mit einer MethodecreateTimeStampString(). Schauen wir zunächst auf einen Aufruf eines Nutzers:

final String timeStamp = TimeStampUtils.createTimeStampString(currentPeriod,frequency);

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 14: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1034 17 Refactorings

Weil der Aufruf unproblematisch scheint, betrachten wir nun die statische öffentlicheMethode an sich, um mögliche Schwachpunkte zu erkennen:

public static String createTimeStampString(final ExtTimePeriod currentPeriod,final ComplexFrequency frequency)

{final DateTime start = currentPeriod.getDateTime();final int divisor = frequency == ComplexFrequency.P1M ? 1 : 3;final String addition = frequency == ComplexFrequency.P1M ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Ein erster Blick zeigt eine vermeintlich einfache Realisierung, die Jahresangaben ge-folgt von Monat oder Quartal ausgeben soll.5 Das Ganze ist recht kurz, aber viel-leicht durch die Abfragen mit dem ?-Operator ein wenig unübersichtlich. Problemati-scher ist jedoch, dass die Methode ungewünschte Abhängigkeiten auf die zwei KlassenExtTimePeriod und ComplexFrequency besitzt, die aus einem externen Package(external) stammen. Ein genauerer Blick offenbart zusätzlich folgende Probleme:

n Die Methode scheint für beliebige Frequenzen des Typs ComplexFrequency aus-gelegt zu sein, Tatsächlich ist sie es aber nicht, denn durch einen versteckten Logik-fehler wird alles außer der Frequenz monatlich auf Quartale abgebildet. Dadurchkönnen nur Monats- oder Quartalswerte korrekt verarbeitet werden.

n Es ist unklar, welches der gewünschte Rückgabewert ist. Gerade im Bereich vonDatumsarithmetik findet man 0- oder 1-basierte Werte: Startet getMonthOfYear()also mit 0 oder 1? Und wieso erfolgt eine Subtraktion von 1?

Für die nachfolgenden Refactorings steht zunächst die Auflösung der Abhängigkeitenim Fokus. Auf die beiden anderen Details der Verarbeitung gehe ich später ein.

Definition des Ziels

Die Methode createTimeStampString(ExtTimePeriod, ComplexFrequency)

soll nun mithilfe von Basis-Refactorings so umgestaltet werden, dass nur Abhängig-keiten auf Standards wie Joda-Time6 oder besser noch JDK-Klassen bestehen und derSourcecode verständlicher wird. Bevor wir mit den Umbauarbeiten beginnen, erstellenwir ein UML-Klassendiagramm von der Ausgangslage und insbesondere auch einemmöglichen Zieldesign. Das beides ist in Abbildung 17-1 dargestellt. Das gezeigte Zielist nicht ganz starr, sondern eher ein Anhaltspunkt, da man beim Entwickeln gewöhn-lich immer noch kleinere Änderungen vornimmt. Das ist auch der Grund, warum wirhier keine Typparameter in den Signaturen angeben.

5Dabei wird auf null-Prüfungen verzichtet, weil es sich um eine interne Hilfsklasse handeltund wir uns hier auf die Refactoring-Schritte konzentrieren wollen.

6Bis JDK 8 ist das vermutlich die beste Wahl, wenn man Datumsarithmetik ausführen muss.Online verfügbar unter http://www.joda.org/joda-time/.

Page 15: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1035

Abbildung 17-1 Refactoring der Methode createTimeStampString()

Wir wollen nun folgende Schritte ausführen, um die angemerkten Probleme zu beseiti-gen und das dargestellte Ziel zu erreichen:

n Auflösen der Abhängigkeiten – In einem ersten Schritt wollen wir die Ab-hängigkeiten zum Package external auflösen, indem wir einen enum namensSupportedFrequencies als Ersatz für ComplexFrequency einführen und an-stelle der Klasse ExtTimePeriod die Klasse DateTime aus Joda-Time nutzen.

n Vereinfachungen – Einige der Berechnungen in der Methode sind etwas komplexund nicht gut zu lesen. Wir werden ein paar Vereinfachungen vornehmen.

n Verlagern von Funktionalität – Abschließend schauen wir, wie wir durch einekleine Änderung von Zuständigkeiten für mehr Klarheit im Design sorgen.

17.3.2 Auflösen der AbhängigkeitenUm den Sourcecode klarer und besser verständlich zu gestalten, werden wir folgendeRefactorings, deren Tastaturkürzel in Eclipse in Klammern notiert sind, nutzen:7

n EXTRACT LOCAL VARIABLE (ALT+SHIFT+L)n EXTRACT METHOD (ALT+SHIFT+M)n INLINE (ALT+SHIFT+I)n CHANGE METHOD SIGNATURE (ALT+SHIFT+C)

7Oftmals bietet die in Eclipse integrierte QUICK-FIX-Funktionalität, die man durch CTRL+1aufruft, die Möglichkeit, die ersten drei Refactorings direkt auszuführen.

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 16: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1036 17 Refactorings

Schritt 1: Hilfsvariable einführen (EXTRACT LOCAL VARIABLE)

Zum leichteren Verständnis wird die zu bearbeitende Methode nochmals gezeigt:

public static String createTimeStampString(final ExtTimePeriod currentPeriod,final ComplexFrequency frequency)

{final DateTime start = currentPeriod.getDateTime();final int divisor = frequency == ComplexFrequency.P1M ? 1 : 3;final String addition = frequency == ComplexFrequency.P1M ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Als Erstes selektieren wir den Ausdruck ComplexFrequency.P1M und nutzen dasRefactoring EXTRACT LOCAL VARIABLE (ALT+SHIFT+L), um die lokale VariableisMonthly zu extrahieren, wodurch sich der Sourcecode vereinfachen lässt:

public static String createTimeStampString(final ExtTimePeriod currentPeriod,final ComplexFrequency frequency)

{final DateTime start = currentPeriod.getDateTime();final boolean isMonthly = frequency == ComplexFrequency.P1M;final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Schritt 2: Abhängigkeit zur Frequenz entfernen (EXTRACT METHOD)

Als Nächstes wollen wir die Abhängigkeit zur Klasse ComplexFrequency auflösen.Wir erzeugen dazu eine separate Methode createTimeStampString(), jedoch mitanderer Signatur. Damit wir diese aus der Originalmethode extrahieren können, ordnenwir die Zeilen um und nutzen dazu die Tastaturkürzel ALT+UP/DOWN. Damit ver-schieben wir die boolesche Variable start direkt zu der ersten Verwendung, also vordie Definition von value. Die Variable isMonthly schieben wir ganz nach oben anden Methodenanfang:8

public static String createTimeStampString(final ExtTimePeriod currentPeriod,final ComplexFrequency frequency)

{final boolean isMonthly = frequency == ComplexFrequency.P1M;final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final DateTime start = currentPeriod.getDateTime();final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

8Bitte beachten Sie, dass diese Umordnung hier zu keiner Verhaltensänderung führt, aber dassdas in der Praxis z. B. wegen möglicherweise versteckter Seiteneffekte nicht immer so ist.

Page 17: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1037

Danach selektieren wir alle Zeilen nach der Definition von isMonthly und setzen dasRefactoring EXTRACT METHOD (ALT+SHIFT+M)9 ein. Damit entsteht eine gleich-namige Methode mit der Sichtbarkeit public, die als Parametertyp boolean stattComplexFrequency besitzt.

@Deprecatedpublic static String createTimeStampString(final ExtTimePeriod currentPeriod,

final ComplexFrequency frequency){

final boolean isMonthly = frequency == ComplexFrequency.P1M;return createTimeStampString(currentPeriod, isMonthly);

}

public static String createTimeStampString(final ExtTimePeriod currentPeriod,final boolean isMonthly)

{final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final DateTime start = currentPeriod.getDateTime();final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Die unerwünschte Abhängigkeit zur Klasse ComplexFrequency wurde damit auf-gelöst – allerdings durch einen booleschen Parameter, was meistens kein gutes Designist. Später komme ich darauf zurück und wir beheben auch diese Schwachstelle.

Indem wir die ursprüngliche Methode als @Deprecated markieren, signalisierenwir, dass zukünftige Nutzer stattdessen die neu erstellte Methode verwenden sollten.Für die bisherigen Aufrufer hat sich nichts geändert.

Schritt 3: Inlining des Methodenaufrufs (INLINE)

Da wir die Abhängigkeit zur Klasse ComplexFrequency in der Utility-Klasse elimi-nieren wollen, sollte überall die neu erstellte statt der alten Methode eingesetzt werden.

Die Aufrufstellen sind ähnlich zu folgender:

final String timeStamp = createTimeStampString(currentPeriod, frequency);

Die Aufrufstellen könnten wir zwar von Hand korrigieren, aber es ist sinnvoller undweniger fehleranfällig, dazu die in die IDE integrierten Refactorings zu nutzen.10 Dazumarkieren wir den Namen der ursprünglichen Methode und nutzen dann das Refacto-ring INLINE (ALT+SHIFT+I). Dieses transformiert alle Aufrufstellen folgendermaßen:

final boolean isMonthly = frequency == ComplexFrequency.P1M;final String timeStamp = createTimeStampString(currentPeriod, isMonthly);

9Dabei können die Tastaturkürzel ALT+SHIFT+LEFT/RIGHT/DOWN hilfreich sein.10Allerdings sollten wir uns dabei bewusst sein, dass sich die Verarbeitungsreihenfolge durch

das INLINE ändern kann.

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 18: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1038 17 Refactorings

Optional kann die nicht mehr benötigte Methode automatisch gelöscht werden, wo-durch nur noch die zuvor extrahierte, neue Methode in der Utility-Klasse verbleibt.Die bisher durchgeführten Änderungen lassen erahnen, dass sich für Umgestaltungendie Nutzung von Refactoring-Automatiken anbietet, um die Wahrscheinlichkeit fürFehler zu reduzieren und für konsistente Änderungen zu sorgen.

Schritt 3 (optional): Inlining der Hilfsvariablen (INLINE)

Die Aufrufstellen sehen nun nach Schritt 3 – unter anderem durch den booleschen Über-gabeparameter – etwas ungelenk aus, was wir später noch mit einer Designänderungadressieren werden. Zunächst könnte man in einem weiteren Schritt statt der lokalenVariablen den Ausdruck direkt als Methodenparameter angeben. Dabei hilft wiederumdas Refactoring INLINE (ALT+SHIFT+I), diesmal für die Variablendeklaration. ZumAusführen ist die Variable isMonthly zu selektieren:

final boolean isMonthly = frequency == ComplexFrequency.P1M;final String timeStamp = createTimeStampString(currentPeriod, isMonthly);

Durch das Refactoring INLINE wird der Aufruf folgendermaßen abgewandelt:

final String timeStamp = createTimeStampString(currentPeriod,frequency == MyFrequency.P1M);

Jedoch reduziert dieser Schritt mitunter die Verständlichkeit und Lesbarkeit.

Schritt 4: Abhängigkeit zur Klasse ExtTimePeriod entfernen

Kommen wir wieder zu der eigentlichen Methode createTimeStampString() zu-rück. Unser Ziel besteht darin, die Abhängigkeiten aufzulösen, nun die auf die KlasseExtTimePeriod. Dabei hilft uns ein scharfer Blick auf die Methode und das Refac-toring EXTRACT METHOD: Abgesehen von der ersten Zeile der Methode selektierenwir den Rest und extrahieren eine gleichnamige Methode. Die Utility-Klasse sieht wiefolgt aus, nachdem wir die alte Methode noch als @Deprecated markiert haben:

@Deprecatedpublic static String createTimeStampString(final MyTimePeriod currentPeriod,

final boolean isMonthly){

final DateTime start = currentPeriod.getDateTime();return createTimeStampString(isMonthly, start);

}

public static String createTimeStampString(final boolean isMonthly,final DateTime start)

{final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Page 19: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1039

Wie schon zuvor, hat das Refactoring keine Auswirkungen auf bisherige Nutzer, außer,dass diese nun durch das Hinzufügen von @Deprecated auf unsere Änderungen in derUtility-Klasse aufmerksam gemacht werden.

Schritt 5: Für konsistente Parameterreihenfolge sorgen

Beim Extrahieren der Methode fällt uns auf, dass durch die Automatik die Parameter-reihenfolge vertauscht wurde und isMonthly nun der erste Parameter ist. Das wol-len wir korrigieren. Dazu nutzen wir das Refactoring CHANGE METHOD SIGNATURE

(ALT+SHIFT+C) und vertauschen die beiden Parameter, womit wir wieder eine konsis-tente Reihenfolge erzielen.

Schritte 6: Inlining des Methodenaufrufs

Wir führen weitere Aufräumarbeiten aus und selektieren die alte Methode und nutzendas Refactoring INLINE. Dadurch verdichten wir die Utility-Klasse:

public static String createTimeStampString(final DateTime start,final boolean isMonthly)

{final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Auch die Aufrufstelle wird automatisch durch die IDE angepasst:

final DateTime start = currentPeriod.getDateTime();final String timeStamp = createTimeStampString(start,

frequency == EasyFrequency.P1M);

Schritte 6 (optional): Inlining der Hilfsvariablen

Wenn gewünscht, kann man nochmals das Refactoring INLINE ausführen, um die Hilfs-variable zu entfernen und direkt in den Methodenaufruf zu integrieren:

final String timeStamp = createTimeStampString(currentPeriod.getDateTime(),frequency == EasyFrequency.P1M);

Zwischenfazit

Die erstellte Methode besitzt keine Abhängigkeiten auf externe Klassen mehr oderzumindest nur auf Klassen, die Standardbibliotheken wie Joda-Time entstammen. Da-mit lässt sich das Ganze viel einfacher mit Unit Tests überprüfen.

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 20: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1040 17 Refactorings

Tests

Bisher haben wir keine Unit Tests ausgeführt. Das lag zum einen daran, dass es keinegab, und zum anderen daran, dass wir bisher Refactorings nur so genutzt haben, dassdas nach außen sichtbare Verhalten sich nicht ändert. Bitte beachten Sie, dass dies selbstfür die Basis-Refactorings nicht immer gilt.11 Allerdings sind diese deutlich sicherer,als Änderungen von Hand auszuführen. In jedem Fall empfiehlt sich bei Umstruktu-rierungen die Ausführung von Unit Tests. Weil es noch keine gibt, erstellen wir hierexemplarisch zwei einfache Testfälle. In der Praxis sollten Utility-Klassen umfangrei-cher getestet werden, als es der Platz hier erlaubt:

@Testpublic void testCreateTimeStampString_Monthly(){

final boolean MONTHLY = true;assertEquals("2000-2", createTimeStampString(

new DateTime(2000, 2, 7, 0, 0), MONTHLY));assertEquals("2000-7", createTimeStampString(

new DateTime(2000, 7, 14, 0, 0), MONTHLY));}

@Testpublic void testCreateTimeStampString_Quarterly(){

final boolean QUARTERLY = false;assertEquals("2000-Q1", createTimeStampString(

new DateTime(2000, 2, 7, 0, 0), QUARTERLY));assertEquals("2000-Q3", createTimeStampString(

new DateTime(2000, 7, 14, 0, 0), QUARTERLY));}

Wir führen die Tests aus und sie zeigen – wie erwartet – Grün. Normalerweise würdenwir noch ein paar mehr Testfälle ergänzen. In diesem Kontext sollen uns aber diese zweireichen, um mögliche Probleme aufzuzeigen.

Unzulänglichkeit: Boolescher Parameter

Durch die Refactoring-Schritte haben wir zwar die Abhängigkeiten zum Packageexternal gelöst, jedoch – wie schon zuvor erwähnt – auch eine Unschönheit in un-sere öffentliche Schnittstelle eingefügt: einen booleschen Parameter. Was ist daran stö-rend? Aufrufer müssen dadurch immer genau wissen, was die Werte true bzw. falseausdrücken sollen. Dies ist jedoch nur durch Betrachten der Methode, nicht aber derAufrufstelle allein möglich.

Die Verwendung der booleschen Konstanten MONTHLY und QUARTERLY in denTests macht deutlich, dass sich ein weiteres Refactoring anbietet, um den booleschenParameter in der öffentlichen Schnittstelle zu eliminieren. In unserem Beispiel hatten

11Insbesondere gilt dies für das Refactoring CHANGE METHOD SIGNATURE, um Parameterumzuordnen, deren Typ zu ändern oder neue Parameter einzufügen, wodurch sich schnell Verhal-ten ändert. Ebenso kann ein Inlining problematisch sein, weil dadurch die Abarbeitungsreihen-folge leicht geändert wird. Man spricht bei dem Problem auch von »temporal coupling«.

Page 21: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1041

wir die Klassen im Zugriff und durften dort auch ändern. Das ist jedoch nicht immerder Fall, sodass man mitunter mit der Signatur leben muss. Wie kann man trotzdem fürbesser verständlichen Sourcecode sorgen? Schauen wir auf verschiedene Abhilfen.

Abhilfen ohne Änderungen der Signatur Wie wir es beim Erstellen der Testskennengelernt haben, kann man zwei Konstanten mit sprechenden Namen definieren:

public static final boolean MONTHLY = true;public static final boolean QUARTERLY = false;

Oftmals besser lesbar ist es, eine Hilfsmethode wie folgt zu implementieren:

private static boolean isMonthly(final ComplexFrequency frequency){

return frequency == ComplexFrequency.P1M;}

Bei der zweiten Variante verbleibt allerdings die Abhängigkeit von Aufrufern an dasPackage external, was aber möglicherweise akzeptabel ist.

Es gibt eine weitere Möglichkeit, die so elegant in der Nutzung und offensicht-lich ist, dass man sie leicht übersieht: Man definiert zwei Methoden mit sprechendemNamen, die jeweils den erwarteten Wert zurückgeben:12

private static boolean monthly(){

return true;}

private static boolean quarterly(){

return false;}

Die gezeigten Varianten lösen das eigentliche Problem nicht, lindern jedoch ein wenigdie »API-Schmerzen«. Das ist für die Fälle praktisch, in denen man die Schnittstelleder Klasse nicht ändern kann, die Aufrufe aber klarer gestalten möchte. Der Aufruf fürmonatlich würde wie folgt aussehen:

final String timeStamp = createTimeStampString(currentPeriod.getDateTime(),monthly());

Abhilfen mit Änderungen der Signatur Wenn man auf die Klassen Zugriff hatund die Methode ändern kann, bietet sich die Definition eines enums an:

public enum SupportedFrequencies{

MONTHLY, QUARTERLY;}

12Das kann man ganz hervorragend auch für andere Rückgabetypen außer boolean machen,solange die Wertemenge nicht zu groß wird.

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 22: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1042 17 Refactorings

Damit können wir die Signatur der Methode wie folgt abändern und eine HilfsvariableisMonthly einführen:

static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{final boolean isMonthly = frequency == SupportedFrequencies.MONTHLY;final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Diese Lösung ist für Aufrufer klarer, was ein sehr wichtiger Punkt ist, da damit auch dieBenutzbarkeit verbessert wird. Allerdings fängt das Ganze intern an, unübersichtlich zuwerden. Es wird höchste Zeit, nach ein paar Vereinfachungen Ausschau zu halten.

17.3.3 VereinfachungenIn diesem Abschnitt sehen wir uns zwei Arten von Vereinfachungen an. Zunächst ent-zerren wir die Anweisungen und die etwas komplexere Logik. Daraus ergeben sichweitere Möglichkeiten, die Formel zur Berechnung an sich zu vereinfachen.

Vereinfachung der Anweisungen

Die Komplexität innerhalb der Methode entsteht vor allem dadurch, dass hier die zweiFälle »Monatlich« und »Quartalsweise« ineinander verwoben behandelt werden:13

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{final boolean isMonthly = frequency == SupportedFrequencies.MONTHLY;final int divisor = isMonthly ? 1 : 3;final String addition = isMonthly ? "" : "Q";final int value = ((start.getMonthOfYear() - 1) / divisor + 1);

return start.getYear() + "-" + addition + value;}

Teilen wir das Ganze doch einfach so auf, dass wir abhängig von isMonthly zweiWertebelegungen erhalten. Versuchen wir schrittweise dorthin zu kommen.

Als Vorbereitung führen wir das Refactoring INLINE für die Variable value aus,um den Ausdruck zur String- und Wertekonkatenation zusammenzuführen.

Den ternären Operator (?-Operator) kann man in eine if-else-Anweisung um-wandeln. Dazu nutzen wir das Tastaturkürzel CTRL+1 für QUICK FIX und wählen dortREPLACE CONDITIONAL WITH ’IF-ELSE’. Das machen wir für beide ?-Operatoren.Damit ergibt sich folgende Variante der ursprünglichen Methode, wobei die entstehen-den if-else-Anweisungen allerdings (noch) keine Blöcke sind:

13Potenziell eine Verletzung des Single Responsibility Principle (SRP) (vgl. Abschnitt 3.5.3).

Page 23: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1043

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{final boolean isMonthly = frequency == SupportedFrequencies.MONTHLY;final int divisor;if (isMonthly)

divisor = 1;else

divisor = 3;final String addition;if (isMonthly)

addition = "";else

addition = "Q";

return start.getYear() + "-" + addition + ((start.getMonthOfYear() - 1)/ divisor + 1);

}

Das Ergebnis sieht ein wenig chaotisch aus und scheint in die falsche Richtung zugehen, da viel mehr Zeilen entstanden sind. Lassen Sie sich nicht entmutigen. Die Testszeigen, dass sich das Verhalten der Methode nicht geändert hat. Wir sind wohl auf demrichtigen Weg. Insbesondere sind die einzelnen Abfragen viel weniger komplex.

Jetzt wollen wir die jeweiligen Zeilen für die beiden Bedingungen zusammen grup-pieren. Voraussetzung dazu ist aber, dass wir die if-else-Anweisungen in Blöckeumwandeln. Dazu selektieren wir das if und nutzen wiederum das TastaturkürzelCTRL+1, was uns nun die Option CHANGE ’IF-ELSE’ STATEMENTS TO BLOCKS an-bietet, die wir wählen. Danach ordnen wir die Zeilen um, indem wir die Zeilen ausden jeweiligen Bedingungen gruppieren. Als Folge entsteht im unteren Teil ein leeresif-else-Gebilde, was wir entfernen. Es ergibt sich folgende Methode:

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{final boolean isMonthly = frequency == SupportedFrequencies.MONTHLY;final int divisor;final String addition;if (isMonthly){

divisor = 1;addition = "";

}else{

divisor = 3;addition = "Q";

}

return start.getYear() + "-" + addition + ((start.getMonthOfYear() - 1)/ divisor + 1);

}

Der Sourcecode ist erneut länger geworden, aber zumindest sind die logischen Einhei-ten gruppiert. Bevor wir vereinfachen können, wird es noch etwas unübersichtlicher undwir benötigen auch etwas Handarbeit, um die inverse Variante des Basis-Refactorings

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 24: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1044 17 Refactorings

CONSOLIDATE DUPLICATE CONDITIONAL FRAGMENT14 durchzuführen. Normaler-weise will man damit duplizierte Elemente in if-Zweigen zu einer Anweisung amEnde zusammenfügen. Hier machen wir das Gegenteil und duplizieren die return-Anweisung, um sie dann in jedem if-else-Zweig bereitzustellen:

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{final boolean isMonthly = frequency == SupportedFrequencies.MONTHLY;final int divisor;final String addition;if (isMonthly){

divisor = 1;addition = "";return start.getYear() + "-" + addition + ((start.getMonthOfYear() - 1)

/ divisor + 1);}else{

divisor = 3;addition = "Q";return start.getYear() + "-" + addition + ((start.getMonthOfYear() - 1)

/ divisor + 1);}

}

Wir machen weiter. Die Variablen divisor und addition sind eigentlich überflüssig,da sie jeweils nur einfache Konstanten enthalten. Wir können nun die Werte für beideVariable direkt im Sourcecode ersetzen. Je nach verwendeter IDE müssen wir etwasHandarbeit leisten, um die in den Zweigen jeweils konstanten Werte in die return-Anweisungszeile zu integrieren sowie die überflüssigen Variablendeklarationen zu ent-fernen:

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{if (frequency == SupportedFrequencies.MONTHLY){

return start.getYear() + "-" + ((start.getMonthOfYear() - 1) / 1 + 1);}else{

return start.getYear() + "-Q" + ((start.getMonthOfYear() - 1) / 3 + 1);}

}

Hinweis: Viele Zwischenschritte

Diese vielen kleinen Schritte für diesen einfachen Sourcecode-Abschnitt sehenmöglicherweise übertrieben aus. Allerdings bietet das den Vorteil, das man sichin jedem Einzelschritt auf das Wesentliche konzentrieren kann.

14Details finden Sie in Martin Fowlers Buch »Refactoring: Improving the Design of ExistingCode« [24].

Page 25: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.3 Kombination von Basis-Refactorings 1045

Wenn die Programmabschnitte komplexer werden, dann profitiert man am meistenvon kleinen Schritten, die im Falle eines Irrwegs bei Bedarf auch leicht zurück-genommen werden können. Flüchtigkeitsfehler lassen sich dadurch eher vermeidenals bei »Freihand«-Refactorings.

Vereinfachung der Berechnung in mehreren Schritten

Wenn man sich die Ausdrücke anschaut, sollte zumindest im ersten Fall eine Verein-fachung möglich sein. Damit wir nicht abgelenkt werden, schauen wir hier wirklichnur auf den Ausdruck der Monatsberechnung an sich, wobei die äußere Klammerungwegen der Stringkonkatenation nicht weiter betrachtet wird:

(start.getMonthOfYear() - 1) / 1 + 1

Die Division / 1 ist nutzlos Eine Division durch 1 ändert das Ergebnis nicht, machtaber den Ausdruck komplizierter und den Sourcecode schlechter lesbar. Also entfernenwir diese Division und erhalten folgende Vereinfachung:

(start.getMonthOfYear() - 1) + 1

Weitere Schritte 1 Aufgrund der Vereinfachung ergeben sich neue Möglichkeiten.In der Praxis sieht man es immer mal wieder, dass Ausdrücke eher zu viel geklammertsind. In diesem Fall ist die äußere Klammerung um die Subtraktion überflüssig undwird entfernt:

start.getMonthOfYear() - 1 + 1

Weitere Schritte 2 In einem letzten Schritt kann die Berechnung - 1 + 1 ent-fallen, weil sie den Wert 0 ergibt und hier somit nutzlos ist. Damit verbleibt für dieBerechnung der Monate nur noch der Aufruf start.getMonthOfYear() und wirkönnen die Methode wie folgt vereinfachen:

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{if (frequency == SupportedFrequencies.MONTHLY){

return start.getYear() + "-" + start.getMonthOfYear();}else{

return start.getYear() + "-Q" + ((start.getMonthOfYear() - 1) / 3 + 1);}

}

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1

Page 26: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

1046 17 Refactorings

Komplexität der Berechnung für Quartale Die Berechnung des Quartals istkomplexer, was sich kaum vermeiden lässt. In der hier genutzten Klasse DateTime

aus der Bibliothek Joda-Time beginnt die Zählung der Tage und Monate jeweils bei 1,wie es der menschlichen Denkweise entspricht. Die gezeigten Berechnungen für dasQuartal bilden die Werte von 1–12 durch die Subtraktion von 1 auf 0–11 ab, wodurchdie Division durch 3 einen Wertebereich von 0–3 liefert. Die Addition von 1 ergibtdann den Wertebereich 1–4. Allerdings liest sich das / 3 + 1 potenziell falsch. Umdie Berechnung klarer zu gestalten, kann man die Addition von 1 nach vorne ziehen:

return start.getYear() + "-Q" + (1 + (start.getMonthOfYear() - 1) / 3);

Fazit

Wenn man bedenkt, mit welch kompliziertem Ausdruck wir ins Rennen gestartet sind,ist das Erreichte beeindruckend. Bei einem Blick auf die ursprüngliche Berechnung wä-re weder eine Aussage möglich gewesen, was das Resultat denn nun genau ist, noch,ob man die Berechnung vereinfachen kann. Durch unseren letzten Schritt ist keine Ver-einfachung für die Monatsberechnung mehr nötig und das Ergebnis offensichtlich. Nurdie Quartalsberechnung ist eben etwas komplizierter.

17.3.4 Verlagern von FunktionalitätWenn wir uns die zuvor verbesserte Methode createTimeStampString(DateTime,SupportedFrequencies) genauer ansehen, erkennen wir, dass sie im if-else je-weils Funktionalität enthält, die stark mit dem enum SupportedFrequencies ver-bunden ist. Diesen haben wir beim Erstellen der Unit Tests zur Verbesserung des leichtunhandlichen APIs definiert. Es bietet sich nun an, noch mehr Funktionalität dorthin zuverlagern. Dabei nutzen wir, dass ein enum Attribute und Methoden besitzen kann undLetztere sogar überschrieben werden können:

public enum SupportedFrequencies{

MONTHLY{

public String createTimeStampString(final DateTime start){

return start.getYear() + "-" + start.getMonthOfYear();}

},

QUARTERLY{

public String createTimeStampString(final DateTime start){

return start.getYear() + "-Q" + (1 + (start.getMonthOfYear() - 1) / 3);}

};

public abstract String createTimeStampString(final DateTime start);}

Page 27: Der Weg zum Java-Profi - dpunkt.de · Bei flüchtigem Hinsehen könnte man die initiale Prüfung übersehen und sich dann fragen, wieso die Schleife mit 1 und nicht bei 0 startet

17.4 Der Refactoring-Katalog 1047

Die Utility-Klasse enthält nur noch folgende Methode:

public static String createTimeStampString(final DateTime start,final SupportedFrequencies frequency)

{return frequency.createTimeStampString(start);

}

Tatsächlich sprechen nur noch Rückwärtskompatibilitätsgründe dafür, die Utility-Klasse überhaupt noch beizubehalten. Ansonsten könnte man die Funktionalität durchdirekte Aufrufe an die jeweilige enum-Konstante realisieren.

17.4 Der Refactoring-KatalogDieser Abschnitt stellt einige Refactorings sowie Transformationen als Schritt-für-Schritt-Anleitungen vor. Diese sollen dabei helfen, ein spezielles Problem auf eine de-finierte Art und Weise zu beheben. Einige Refactorings lassen sich zu »High-Level-Refactorings« kombinieren. Ein Beispiel dafür ist die als MINIMIERE ZUSTANDS-ÄNDERUNGEN beschriebene Kombination aus Abschnitt 17.4.5.

Im folgenden Text spreche ich häufig der Einfachheit halber von get()- undset()-Methoden. Diese müssen nicht immer mit einem solchen Präfix anfangen, son-dern es sind ganz allgemein Accessor- und Mutator-Methoden gemeint.

17.4.1 Reduziere die Sichtbarkeit von AttributenBekanntlich stellen Attribute den internen Zustand eines Objekts dar. Dieser sollte vonaußen nicht durch direkte Zugriffe auf diese Attribute sichtbar oder sogar änderbar sein.Dem OO-Grundgedanken der Datenkapselung (vgl. Abschnitt 3.1) folgend, ist es dasZiel bei diesem Refactoring, eine direkte Änderbarkeit von Attributen auf ein Minimumzu reduzieren. Im Idealfall können Details der Implementierung ohne Rückwirkungenauf Nutzer geändert werden. Beispielsweise können Daten entweder als Attribut gehal-ten, bei Bedarf berechnet oder aus einer externen Quelle gelesen werden. Als Voraus-setzung sollte die Sichtbarkeit von Attributen möglichst weit eingeschränkt werden.

Schauen wir dazu auf die in Abbildung 17-2 als Klassendiagramm visualisierteKlasse Person, die die zwei Attribute name und age besitzt und die Ausgangsbasisfür dieses Refactoring darstellt. Die Attribute sind zu Demonstrationszwecken public

(’+’) und protected (’#’).

Abbildung 17-2 Ausgangszustand

Michael Inden, Der Weg zum Java-Profi, dpunkt.verlag, ISBN 978-3-86490-203-1