Kann ich nicht einfach so umbauen?

Um es kurz zu machen: nein, einfach so umbauen geht gar nicht! Oft ist es verführerisch, weil man die Lösung ja schon im Kopf hat. ABER: man hat zwar die Lösung an einer speziellen Stelle, dennoch kann die Dokumentation durch die Lösung veralten, Tests können fehlschlagen und Abhängigkeiten zu anderen Codestellen können übersehen werden. Bevor man Änderungen vornimmt sollte man hinreichend prüfen, wo mögliche Fallstricke lauern und entsprechend behutsam vorgehen.

Auf die Fragestellung bin ich gekommen, weil zum Thema DTI immer wieder die Frage aufgetaucht ist, wie das denn ist, wenn ich einen Umbau auf bestehendem Code vornehmen muss. Vor allem, weil die Dokumentation dann veraltet, die ja wegen DTI schon da ist. Wie kann das funktionieren? Ganz vorneweg will ich dazu anmerken, dass das es KEIN DTI-Immanentes Problem ist! DTI verschärft es höchstens, weil die Dokumentation immer schon fertig ist (im Gegensatz zu Arbeitsweisen, bei denen die Dokumentation nie fertig wird).

Exkurs: Refactoring

In diesem Exkurs möchte ich kurz den Unterschied herausarbeiten, der zwischen Refactoring im allgemeinen besteht, und den in diesem Artikel behandelten Umbaumaßnahmen. Der Artikel betrachtet jeden Umbau, ob es nun ein Refactoring ist oder ein BugFix oder eine Funktionserweiterung. Ich denke nur, dass wir für jede Art von Umbaumaßnahmen vom Refactoring etwas lernen können.

Das Problem mit der Code-Degenerierung

Über Code-Degenerierung ist schon Vieles gesprochen worden und jeder Entwickler kennt es an tausend Beispielen aus seiner eigenen Praxis. Laut Wikipedia (siehe http://de.wikipedia.org/wiki/Degeneration) setzt sich der Begriff "Degeneration" aus "De-" (Ent-) und "Genus" (Art) zusammen, also "Entartung". Im allgemeinen Sprachgebrauch wird Degeneration nicht als Gegenteil von Generierung, Erzeugung, verwendet sondern eher als Rückbildung. Ich möchte im Rahmen dieses Artikel als Code-Degeneration den Vorgang bezeichnen, der stattfindet, wenn sich die Differenz zwischen Quellcode und seinem Idealzustand vergrößert.

Es ist uns allen klar, dass das geschieht, je mehr Änderungen in den Code eingebaut werden. Neu ist vielleicht die Erkenntnis, dass Code schon degeneriert, während man noch an der ersten Version schreibt. Aber geradezu revolutionär ist die Feststellung, dass Quellcode sogar dann degeneriert, wenn wir nicht daran arbeiten. Warum? Weil wir (hoffentlich) lernen und neue Möglichkeiten entdecken, dieses oder jenes Problem auf andere, bessere Weise zu lösen. Der schon geschriebene Code wächst auf diese Art üblicherweise nicht mit. Irgendwann später jedenfalls müssen wir doch wieder zu den Untaten aus unserer Vergangenheit zurückkehren und uns des Codes annehmen, der nicht mehr dem Ideal, das wir vor Augen haben, entspricht.

Vielleicht ist jetzt klar, warum ich in meiner Definition für Degeneration von einer Vergrößerung des Abstandes sprach. Weil sich nicht zwangsläufig der Quellcode verändern muss. Ich hebe das deshalb so heraus, weil ich hoffe, hiermit das Verständnis für "schlechten" Quellcode zu erhöhen. Derjenige, der für den Code verantwortlich zeichnet, wusste es zu jenem Zeitpunkt, als er ihn aus der Taufe hob, einfach nicht besser. Das ist keine Schande, sondern streicht nur heraus, dass der Mensch, jeder von uns, lernfähig ist.

Die Antwort, die einem als erstes auf das Degenerierungsproblem einfällt, ist Refactoring. Wahrscheinlich ist es auch das Letzte, was einem dazu einfällt. Ich - und dabei bin ich vermutlich nicht allein - kenne nämlich keine andere Möglichkeit. Aber können wir ständig Refactoring betreiben? Nein, natürlich nicht. Es ist ganz einfach nicht wirtschaftlich, sofort bei jeder neuen Entdeckung zwölf-und-zwanzig Programme daraufhin umzustellen. Manchesmal ist eine einzelne Entdeckung gar so umfangreich und tiefgreifend, dass man sie aus wirtschaftlicher Sicht gar nicht auf aktuell laufende Projekte anwenden kann. In so einem Fall kann man sich nur vornehmen, es beim nächsten Mal besser zu machen.

Der richtige Zeitpunkt

Wann aber ist ein guter Zeitpunkt für Refactoring? Meiner Ansicht nach gibt es zwei Zeitpunkte, zu denen wirtschaftlich sinnvoll ein Refactoring durchgeführt werden sollte: sofort nach Fertigstellung der Implementierung und unmittelbar vor dem Beginn einer Weiterentwicklung. Es gibt noch einen erwähnenswerten dritten Zeitpunkt, nämlich dann, wenn einem auffällt, dass die stattgefundene Degeneration problematisch wird. Dieser letzte Zeitpunkt ist immer - und ich meine IMMER - ungünstig, denn dieser Zeitpunkt wird von anderen Faktoren als von unserem Willen beeinflusst. Dadurch bringt er chaotische Verhältnisse mit sich und unsere Zeitplaunung durcheinander. Deshalb sollte idealerweise (aber was ist schon ideal? *seufz*) das Refactoring zu den erstgenannten beiden Zeitpunkten und so sorgfältig wie möglich durchgeführt werden.

Unmittelbar vor dem Beginn einer Weiterentwicklung bietet sich ein Refactoring deshalb an, weil ein Refactoring eine gute Möglichkeit darstellt, sich ganz neu (oder wieder) mit dem Code vertraut zu machen. Außerdem kann es sein, dass ein Refactoring notwendig ist um die neuen Anforderungen einzubauen.

Der Zeitpunkt "nach Fertigstellung der Implementierung" ist eigentlich irreführend, denn meiner Ansicht nach gehört das Refactoring des gerade entwickelten Codes zur Implementierung selbst dazu. Es ist wie das Aufräumen oder Saubermachen vor dem Wochenende in einem Handwerksbetrieb - es gehört in einer professionellen Entwicklerwerkstatt einfach dazu. Problematisch ist natürlich, dass niemand auf die Idee käme, für Werkstattaufräumen zu bezahlen. Aufräumen geht im Grunde immer zu Lasten der Werkstatt, ist aber trotzdem unerlässlich will man sich unnötige Kosten für verlorengegangenes, kaputtes oder verrottetes Werkzeug sparen.

Es gibt noch so eine Art "Gelegenheitsrefactoring", das man immer dann betreibt, wenn einem zufällig etwas auffällt und es sofort ändert. Zufällig meint, dass man beim Entwickeln einer neuen Funktionalität über ein Stück Code stolpert, der eine Vereinfachung verträgt aber nicht notwendigerweise braucht. Die Notwendigkeit erzwingt (jetzt noch) keine Veränderung, aber man hat die Gelegenheit dazu. Dieses Gelegenheitsrefactoring ist gut und wichtig und sollte unbedingt getan werden, damit es nicht später zu einem Problem wird. Im Folgenden möchte ich nicht weiter darauf eingehen, nur eins noch: wenn man dabei vom Hundertsten ins Tausende kommt - was oft bei schlecht gewartetem Code der Fall ist - sollte man irgendwo einen Schlussstrich ziehen und ein umfangreicheres Refactoring fix einplanen; eventuell sogar mehrere.

Wie, Refactoring?

An dieser Stelle scheint es mir gelegen, einmal zu umreißen, was Refactoring eigentlich meint. Refactoring bezeichnet den Umbau einer bestehenden Code-Basis ohne die beobachtbare Funktionalität zu ändern. Anders (und wahrscheinlich schwerer verständlich) ausgedrückt bedeutet dies eine Menge von Nulltransformationen, die über den Quellcode ausgeführt werden.

Konkret darf bei Refactoring der Ablauf verändert werden, innere Verantwortlichkeiten, Paketzugehörigkeiten, die Quellcode-Formatierung, innere Symbolnamen und so weiter. Entscheidender ist, was nicht verändert werden darf, nämlich keine Schnittstelle nach außen und kein von Außen erwartetes Verhalten. Systemtest dürfen also beispielsweise gar nicht angefasst werden, Integrationstests nicht für die Teile, die von Außen angesprochen werden, und bei Unittests müsste man theoretisch jeden einzelnen prüfen, ob er die Funktionalität einer inneren oder einer Schnittstellenmethode sicherstellt.

Martin Fowler fügt seiner Definition von Refactoring noch hinzu, dass es sich dabei um so kleine Änderungen handelt, dass eine einzelne davon es eigentlich gar nicht wert wäre, sie vorzunehmen. Zum Beispiel könnte ein Refactoring die Umbenennung der Indexvariable innerhalb einer for-Schleife sein, "index" statt "i". Ändert nichts an der Funktionalität, ändert nichts am Ablauf, Tests müssen gar nicht angepasst werden, ist in wenigen Sekunden vollbracht, und ist - solange man nur diese eine Stelle betrachtet - eigentlich unnötig. Bei konsequenter Anwendung, verspricht Fowler, ändern sich Lesbarkeit und Wartbarkeit aber enorm.

Wer näheres zu Refactoring erfahren möchte, kann bei Wikipedia (http://de.wikipedia.org/wiki/Refactoring) oder Martin Fowler (http://martinfowler.com/books.html#refactoring) nachschlagen.

Damit ist mein Exkurs zum Thema Refactoring auch schon beendet. Als Quintessenz würde ich für den weiteren Text zum Thema "Umbau mit DTI" drei Punkte festhalten und mitnehmen wollen:

Der Plan

Jeder Umbau beginnt mit Lesen. Und das ist im Grunde schon mal die erste Prüfung für meine Quellen: wenn ich mir schon schwer tue, das Zeugs zu lesen und zu verstehen, muss ich sofort den Rotstift ansetzen. Lesbarkeit und Verständlichkeit sind eine Grundvoraussetzung für Umbauten, anderenfalls brauche ich gar nicht erst mit größeren Umbauten anzufangen. Denn: wenn ich nicht verstanden habe, was ich umbaue, werde ich die Auswirkungen meiner Maßnahmen nicht vollständig erkennen und produziere nur weitere Fehler und Fehlerquellen.

Dieser erste Schritt wird mit einiger Sicherheit der am häufigsten Auszuführende sein, und er ist auch der Allerwichtigste. Allerdings würden genauere Erläuterungen den Rahmen sprengen und den Fokus verlagern, weshalb ich mich an dieser Stelle auf das bisher Gesagte beschränken möchte. Gehen wir also im Folgenden davon aus, dass all mein Code lesbar und verständlich ist.

Nun heißt es, einen Plan zu entwickeln. Der Plan enthält natürlich das Ziel, auf das ich hinarbeite. Es könnte sein, dass ich ein ressourcenschonenderes Speichermanagement betreiben, die Performance tunen, einige Kommunikationswege verändern, größere Erweiterbarkeit schaffen oder Anderes verbessern möchte. Weiterhin sollte der Plan in möglichst kleinen Schritten beschreiben, wie ich ans Ziel gelange. Der Plan muss nicht zwangsläufig schriftlich festgehalten werden. Aber um speziell bei größeren Umbauten nichts zu vergessen ist es sinnvoll, wenigstens mit ToDo-Kommentaren zu arbeiten.

Wichtig ist vor allem, dass man bei Umbauten in möglichst kleinen Schritten vorangeht. Wir wollen nicht vergessen, dass wir immer am offenen Herzen operieren und deshalb die Funktionstüchtigkeit des gesamten Systems auf dem Spiel steht. Je kleiner die Schritte, desto öfter kann die korrekte Funktionalität überprüft werden und umso geringer wird unsere Fehlererzeugungsrate ausfallen.

Die Anneliese - äh, Korrektur: die Analyse

Es müssen Art und Umfang der Änderung identifiziert werden. Die Kombination aus beidem bestimmt, wie rücksichtsvoll ich mit meiner geplanten Veränderung im Hinblick auf abhängige Systeme umgehen muss und daraus resultiert das Vorgehen, mit dem ich meine Änderung einbringe.

Ergänzung

Bei einer Ergänzung handelt es sich um eine Weiterentwicklung, wie sie auch schon die ganze Zeit während der Erstentwicklung stattgefunden hat. Der gesamte DTI-Artikel befasst sich damit.

Abhängigkeiten nach außen sind speziell dann zu beachten, wenn beispielsweise ein gefordertes Interface erweitert werden soll. Da hier die Implementierungen ja immer außerhalb des von mir kontrollierten Codes liegen (weil es ein gefordertes und kein implementiertes Interface ist), ist es besser, das ursprüngliche Interface zu belassen und mit einem weiteren Interface durch "extends" zu erweitern. Ein Beispiel soll das verdeutlichen.

/**

 * <p>Original-Interface mit der ursprünglich geforderten Funktionalität.</p>

 */

public interface Original {

 

    /**

     * <p>Eine Beispiel-Methode zur Verdeutlichung.</p>

     */

    void doOriginalBeispiel();

}

 

    /**

     * <p>Das Original wird um neu geforderte Methoden erweitert.</p>

     */

    public interface OriginalErweiterung extends Original {

 

    /**

     * <p>Eine neue Methode als Erweiterungsbeispiel.</p>

     */

    void doNeuesBeispiel();

}

 

Refactoring

Wie wir gelernt haben, ist das Refactoring eine Maßnahme zur Verbesserung der Code-Qualität ohne Änderung der Funktionalität. Deshalb dürften weder die Dokumentation noch die Tests zu ändern sein. Natürlich kann es vorkommen, dass bei einem Refactoring neue Packages, Klassen oder Methoden entstehen oder verschwinden. Letzterer Fall ist ohnehin problemlos. Und im ersteren Falle sollte man getrost nach DTI entwickeln: also zuerst das Neuentstandene dokumentieren, dann testen und zuletzt implementieren.

Das Angenehme bei Refactoring ist, dass der Umfang immer vollständig unter unserer Kontrolle liegt.

BugFix

Bei einem BugFix werde ich zunächst den Bug als solches Dokumentieren. Wie wirkt sich der Bug aus? In welchen Versionen taucht er auf? Unter welchen Umständen taucht er auf? Und so weiter. Dafür eignen sich am Besten natürlich einschlägige Werkzeuge. Anschließend ist die Fehlerquelle zu finden und auch diese sollte zu dem Bug dokumentiert werden. Danach müssen die Tests erstellt werden, die den Bug sichtbar machen indem sie fehlschlagen. Zuletzt kann man sich dem Fix selbst zuwenden und so lange entwickeln, bis alle Tests durchlaufen.

Auf jeden Fall ist es wichtig, sich mit dem Fix selbst zurückzuhalten, bis der Bug durch Tests sichtbar ist. Sonst kann es passieren, dass die Tests vergessen werden und der Bug in einer späteren Version wieder auftaucht.

Auch bei einem BugFix hat man die Annehmlichkeit, dass man kaum auf Abhängigkeiten achten muss. Im Gegenteil, alle freuen sich, wenn's plötzlich funktioniert!

Veränderung

Veränderungen sind sehr undankbar, denn man hat damit ziemlich viel Arbeit. Und wenn man Pech hat, muss man nach einer riesigen Veränderung die ganze Arbeit wieder wegschmeißen, weil es doch nicht wie vorgesehen funktioniert. Außerdem kommt es vor, dass die Veränderung mehr Umfang beansprucht, als man selbst in der Hand hält, und das ist gefährlich: ändere eine Public-Methode in einer Public-Klasse und Du zerstörst womöglich alle abhängigen Systeme.

Vor Veränderungen wird man sich nie ganz schützen können, aber es gibt Möglichkeiten, sie zu reduzieren. Die Einhaltung des Open-Closed-Principles ist eine Möglichkeit. Eine andere ist es, die Veränderung in eine Ergänzung zu verwandeln. Dazu gibt es zwei Möglichkeiten:

  1. die Erweiterung mit einer Kopie und
  2. die Ersetzung durch eine Kopie.

Exkurs: Open-Closed-Principle

Das OCP oder Open-Closed-Principle (von Robert C. Martin) besagt, dass eine Methode (oder Klasse oder Package) offen für Erweiterung sein soll, aber geschlossen für Veränderung. Er gibt einige Beispiele, wie Quellcode aussieht, der dieses Prinzip erfüllt. Er schlägt Design-Patterns vor, um das OCP zu erreichen. Und er erklärt, warum das OCP so wichtig ist.

Auf jeden Fall ist es das wert, dass man sich einmal mit seinen Schriften auseinandersetzt (z. B. in http://www.objectmentor.com/resources/articles/ocp.pdf). An dieser Stelle möchte ich lediglich herausstreichen, dass eine Veränderung bestehenden Codes immer ein Risiko birgt. Deshalb ist Code, der OCP entspricht, guter Code. DTI und die in diesem Artikel vorgeschlagene Umbautechnik sollen das Streben nach OCP erleichtern.

So, und wie mache ich das jetzt?

Wollen wir mal die beiden Strategien unter die Lupe nehmen.

Erweiterung mit einer Kopie

An dieser Stelle möchte ich das Beispiel vom Blauen Elephanten anwenden. Ihr wisst schon: "Die Sendung mit der Maus". Hat bestimmt schon jeder mal gesehen. Stellen wir uns folgenden Code vor:

/**

 * <p>Represents a blue elephant on the screen that is able to glow.</p>

 */

public class BlueElephant {

 

    ...

    private Color color = Color.DARK_BLUE;

 

    ...

 

    /**

     * <p>Makes the elephant glow in a soft blue light.</p>

     */

    public void glow() {

        System.out.println("Jetzt glow halt!");

        this.color = Color.SOFT_BLUE_LIGHT;

    }

 

    ...

}

 

Den Elephanten glühen lassen war gut für die eine Sendung. In der Nächsten soll der Elephant kurz flackern bevor er doch endlich leuchtet. Wir könnten die Methode glow() darauf anpassen, leider haben wir die Applikation SendungMitDerMaus-v1.0.0 schon zum Download angeboten. Wir haben keine Ahnung, wer sich darauf verlässt, dass der Blaue Elephant so schön blau glüht. Wir können diese Methode also nicht umbasteln ohne das Risiko, vielleicht jemanden zu verärgern. Daher erweitern wir die Klasse mit einer Kopie dieser Methode und vergeben einen schönen neuen Namen: glowAfterFlickering.

/**

 * <p>Represents a blue elephant on the screen that is able to glow.</p>

 */

public class BlueElephant {

 

    ...

    private Color color = Color.DARK_BLUE;

 

    ...

 

    /**

     * <p>Makes the elephant glow in a soft blue light.</p>

     *

     * @deprecated use <code>glowAfterFlickering</code> instead

     */

    public void glow() {

        System.out.println("Jetzt glow halt!");

        this.color = Color.SOFT_BLUE_LIGHT;

    }

 

    /**

     * <p>Makes the elephant flicker with a noise and finally glow in a

     * soft blue light.</p>

     */

    public void glowAfterFlickering() {

    }

 

    ...

}

 

Die schöne neue Methode kann ich jetzt wieder problemlos nach DTI entwickeln. Und wenn ich der Meinung bin, dass der Blaue Elephant einen dauerhaften Schaden davon getragen hat und in Zukunft nicht mehr so ohne Weiteres glühen kann, dann muss ich glow auf deprecated setzen. Einfach wegwerfen geht nicht, weil nicht alle notwendigen Änderungen unter meiner Kontrolle liegen.

Ersetzung durch eine Kopie

Der Blaue Elephant soll nochmals für ein Beispiel herhalten (der arme Kerl hat jetzt auch noch Schluckauf ...):

/**

 * <p>Represents a blue elephant on the screen that is able to glow but

 * hicoughs when doing so.</p>

 */

public class BlueElephant {

 

    ...

 

    /**

     * <p>Gives a noise.</p>

     */

    void sound() {

        this.soundEngine.hicough();

    }

 

    ...

 

    /**

     * <p>Makes the elephant flicker with a noise and finally glow in a

     * soft blue light.</p>

     */

    public void glowAfterFlickering() {

        this.flicker();

        this.sound();

        this.glow();

    }

 

    ...

}

 

Umfragen haben festgestellt, dass jeder Zuschauer vom Schluckauf angesteckt wird, deshalb soll das für alle Zeiten in ein Husten geändert werden. Der erste Schritt ist wie vorher die Kopie von sound, also eine neue Methode anzulegen, die beispielsweise soundAlternate heißt, die ich in altbewährter Manier wieder nach DTI entwickle. Die Methode sound ist übrigens deshalb package visible, damit sie von Tests aufgerufen werden kann, aber sonst nicht von anderen Packages aus direkt. Deshalb sollte soundAlternate auch package visibilty haben.

Die fertige Klasse sieht dann in etwa so aus:

/**

 * <p>Represents a blue elephant on the screen that is able to glow but

 * coughs when doing so.</p>

 */

public class BlueElephant {

 

    ...

 

    /**

     * <p>Gives a noise.</p>

     *

     * @deprecated use <code>soundAlternate</code> instead

     */

    void sound() {

        this.soundEngine.hicough();

    }

 

    /**

     * <p>Gives a noise.</p>

     */

    void soundAlternate() {

        this.soundEngine.cough();

    }

 

    ...

 

    /**

     * <p>Makes the elephant flicker with a noise and finally glow in a

     * soft blue light.</p>

     */

    public void glowAfterFlickering() {

        this.flicker();

        this.soundAlternate();

        this.glow();

    }

 

    ...

}

 

Da die Methode sound mit dem Schluckauf nicht mehr verwendet werden soll, wollen wir sie löschen. Da sie nur package visibility hat und wir das Package voll unter unserer Kontolle haben, können wir das auch getrost tun. Zu guter Letzt benennen wir soundAlternate noch nach sound um, denn eine Alternative macht ohne ein Original wenig Sinn. Am Ende noch das Resultat:

/**

 * <p>Represents a blue elephant on the screen that is able to glow but

 * coughs when doing so.</p>

 */

public class BlueElephant {

 

    ...

 

    /**

     * <p>Gives a noise.</p>

     */

    void sound() {

        this.soundEngine.cough();

    }

 

    ...

 

    /**

     * <p>Makes the elephant flicker with a noise and finally glow in a

     * soft blue light.</p>

     */

    public void glowAfterFlickering() {

        this.flicker();

        this.sound();

        this.glow();

    }

 

    ...

}

 

Die Ersetzung durch eine Kopie sollte mit Vorsicht betrachtet werden, denn es ändert sich auf jeden Fall das beobachtbare Verhalten. Sollte der Schluckauf aus irgendwelchen Gründen beibehalten werden, kann man diese Methode nicht ersetzen, wohl aber umbenennen.

Möglicherweise hat jetzt jemand Angst davor, dass zuviele Code-Doubletten entstehen. Diese Angst wird mit dem Wort "Kopie" in den Überschriften noch geschürt und ist in gewisser Weise berechtigt. Mein Tipp: Code-Doubletten vermeidet man am Besten dadurch, dass duplizierter Code in andere Methoden ausgelagert wird. Das ist eine Refactoring-Geschichte und gehört zum Aufräumen dazu. Weitere Ausführungen würden an dieser Stelle zu weit führen, aber wenn man sich über Code-Doubletten Gedanken macht, wird man nicht umhin können, sich auch über Verantwortlichkeiten Gedanken zu machen.

Okay ... ?

Wer sich jetzt wundert, wo bei dem ganzen Thema das DTI bleibt, der wundert sich zurecht. Es geht hier nicht um DTI sondern um Umbau. All die vorgestellten Weisheiten lassen sich auch anwenden, ohne DTI zu betreiben, deshalb existieren lediglich Hinweise an den Stellen, wo DTI wirksam wird. DTI ist eine Vorgehensweise zum Erstellen von Programmen, bei der zuerst die Dokumentation, dann die Tests und zuletzt die Implementierung erstellt werden. Beim Umbauen funktioniert DTI genauso, erst die Doku, dann die Tests und zuletzt die Implementierung anpassen. Weil aber anpassen ungleich gefährlicher ist als neu machen, widmet sich der vorliegende Artikel der Verwandlung von Umbau in Neuentwickeln. Und diese Technik ist es, die DTI mit Umbau und Refactoring verbindet.