Newsfeed der it-agile

10/06/2008
Kategorie:
  • Refactoring, Konferenzen

— Stefan Roock

it-agile hatte zur JAX 2008 eine Refactoring-Aufgabe gestellt. Es ging darum, in einem vorgegebenen Beispielcode (elektronischen Terminplaner) ein größeres Refactoring vorzunehmen: Die Klasse Termin implementierte das Comparable-Interface. Der Code sollte so umgebaut werden, dass die Implementationsbeziehung entfällt und der Code stattdessen mit einem Comparator arbeitet.

Die Umbauten sollten dabei

  • mit möglichst wenig Schritten auskommen,
  • von denen möglichst wenige manuell und möglichst viele automatisiert sein sollten
  • und die den Code möglichst immer in einem grünen Zustand (keine Compilefehler, Tests laufen durch) hinterlassen sollten.

Aus diesen Kriterien haben wir für jede Lösung einen Score nach folgender Formel errechnet:

Score = 100 – AS – MS*10 – FS*100

AS = Anzahl automatisierter Schritte

MS = Anzahl manueller Schritte

FS = Anzahl Schritte mit Compilefehlern oder fehlerhaften Tests

Der höchste Score gewinnt. Das bedeutet, dass automatisierte Schritte um den Faktor 10 besser sind als manuelle. Der Grund dafür ist einfach: Bei jedem manuellen Schritt läuft man Gefahr, einen Fehler zu machen. Also sind automatisierte Schritte zu bevorzugen.

Wir möchten uns an dieser Stelle für die vielen hochwertigen Lösungsvorschläge bedanken. Die Qualität der Lösungen zeigt, dass viele Java-Entwickler die Refactoring-Fähigkeiten ihrer IDE sehr gut kennen und auch in der Lage sind, die automatisierten elementaren Refactorings zu zusammengesetzten Refactorings zu kombinieren.

Bei den eingereichten Lösungen kann man prinzipiell zwei Wege unterscheiden: den startorientierten und den zielorientierten.

Beim startorientierten Weg beginnt das Refactoring bei der compareTo-Methode der Klasse Termin. Diese wird schrittweise so umgebaut, dass sie nicht mehr direkt auf Member der Klasse Termin zugreift, sondern das Termin-Objekt als Parameter hineingereicht bekommt. Dann kann die Methode static gemacht werden. Damit ist die Voraussetzung dafür gegeben, dass man die Methode anschließend mit dem Move-Refactoring in eine neu geschaffene Comparator-Klasse verschieben kann. Dann muss man nur noch die Implementationsbeziehung von Termin zu Comparable entfernen. Die beste eingereichte Lösung zu diesem Weg besteht aus 9 Einzelschritten, von denen 3 manuell und 6 automatisiert sind (Score 64). Der Code ist nach jedem Einzelschritt grün.

Der zielorientierte Weg beginnt mit einer leeren Comparator-Klasse, in die mit dem Refactoring Inline-Method der Inhalt der compareTo-Methode aus der Klasse Termin transferiert wird. Dann muss man noch die Implementationsbeziehung von Termin zu Comparable entfernen und ist fertig. Die beste eingereichte Lösung zu diesem Weg besteht aus 6 Einzelschritten, von denen 4 manuell und 2 automatisiert sind (Score 58). Der Code ist nach jedem Einzelschritt grün. Diese Lösung (in leicht unterschiedlichen Varianten) haben Olaf Fricke (mit seiner zweiten eingereichten Lösung), Dominik Göpel und Eicke Godehardt eingereicht.

Damit hat Olaf Fricke mit seiner ersten, startorientierten Lösung den höchsten Score erreicht und den iPod gewonnen. Herzlichen Glückwunsch!

Nicht unerwähnt lassen wollen wir einen pfiffigen Trick, den Olaf Fricke und Ralf Kern angewendet haben: Ein Problem beim startorientierten Weg ist die Umstellung des Member-Zugriffs in der compareTo-Methode auf das Termin-Objekt per Parameter. Dabei ist es hinderlich, dass im Beispielcode die Member implizit und nicht über this.xxx angesprochen werden. Man muss den Code zuerst auf explizite Adressierung mit this umstellen, damit man this dann durch den Parameter ersetzen kann. Olaf Fricke und Ralf Kern haben eine Möglichkeit gefunden, genau das automatisiert durchzuführen:

  1. Extrahiere den kompletten Inhalt der compareTo-Methode in eine eigene compare-Methode mit dem Refactoring Extract-Method.
  2. Führe Refactoring Introduce-Indirection auf dem Aufruf der neuen compare-Methode durch und erzeuge compareIndirect. Anmerkung: Dieses Refactoring erzeugt eine neue statische Methode, die die compare-Methode aufruft.
  3. Jetzt führt man das Refactoring Inline-Method auf compare und auf compareIndirect durch. Dadurch hat man wieder den Ausgangszustand, nur dass alle Memberzugriffe in compareTo explizit über this stattfinden.

Da man beim startorientierten Weg ohnehin eine statische Methode zum Verschieben haben möchte, kann man sich bei der gegebenen Aufgabe das Inline-Method auf compareIndirect auch sparen.

Startorientierter Lösungsweg in normierter Fassung

  1. In Termin.compareTo: Extract local variable 'o2' from expression '((Termin)o)' – automatisiert
  2. Extract method 'private int compareTmp(Termin o2)' from 'de.itagile.jax2008.terminplaner.Termin.compareTo()' – automatisiert
  3. Introduce indirection for 'de.itagile.jax2008.terminplaner.Termin.compareTmp(...)'. Method name: 'compare' – automatisiert
  4. Inline method 'de.itagile.jax2008.terminplaner.Termin.compareTmp()' – automatisiert
  5. Erzeugung einer anonymen Inner-Comparator-Class in TerminRepository – manuell
        private SortedSet<Termin> _termine =  new TreeSet<Termin>(new Comparator<Termin>() {
            public int compare(Termin o1, Termin o2) { return Termin.compare(o1, o2); }
        }
  6. Entfernung des implements-Beziehung zu Comparable in Termin – manuell
  7. Inline method 'de.itagile.jax2008.terminplaner.Termin.compare()' – automatisiert
  8. Löschen der nicht mehr benötigten Methode compareTo in Termin - manuell

Über Schritt 5 kann man trefflich streiten. Sind es nicht eigentlich zwei Schritte, von denen der Erste (Erzeugen der anonymen Inner-Class mit Default-Implementation der compare-Methode) die Tests rot werden lässt? Leider war unsere Aufgabenstellen in dem Punkt wie groß genau ein Schritt sein darf nicht ganz eindeutig. Das werden wir nächstes Mal besser machen. Wir haben uns dafür entschieden, hier nur einen Schritt zu zählen, weil der gewählte Weg pragmatisch und trotzdem sicher ist.

Zielorientierter Lösungsweg in normierter Fassung

  1. erzeuge Klasse: "public class TerminComparator implements Comparator <Termin>"
    (Eclipse erzeugt automatisch eine leere Methode "compare") – automatisiert
  2. implementiere die Methode "compare" unter Benutzung von "Termin.compareTo()"   return arg0.compareTo (arg1); - manuell
  3. benutze den neuen Comparator als Argument für den Treeset Konstruktor: new TreeSet<Termin>(new TerminComparator ()) – manuell
  4. inline der Methode "compareTo" innerhalb von "TerminComparator.compare()" – automatisiert
  5. lösche in Termin.java "implements Comparable" – manuell
  6. lösche in Termin.java die Methode "compareTo" - manuell

Lust auf Mehr?

Das Refactoring existierenden Codes ist heute alltägliche Arbeit – egal, ob man agil vorgeht oder nicht. Früher oder später muss man existíerenden Code umbauen.

Die automatisierten Refactorings moderner Entwicklungsumgebungen unterstützen uns dabei. Allerdings ist es nicht immer einfach, einen sicheren Weg (ohne Compilefehler und ohne fehlschlagende Tests) zu finden. Wie man an den eingereichten Lösungen zur Refactoring-Aufgabe sieht, kann es ganz unterschiedliche Wege geben, die unterschiedlich effektiv sind.

Wir bieten mit dem zweitägigen Seminar Inkrementeller Entwurf sowie dem fünftägigen TDD-Camp zwei Schulungen an, die sich mit der Evolution von Softwaresystemen beschäftigen. Informieren Sie sich hier oder treten Sie mit uns in Kontakt