Vererbung und objektorientiertes Design – Tip 34 “Unterscheiden Sie zwischen der Vererbung der Schnittstelle und der Implementierung”

Meyers gibt dem Programmierer in Tip 34 Merksätze an die Hand, was es bedeutet, rein virtuell, einfach virtuell und nicht virtuell zu vererben.

Wird eine Funktion rein virtuell vererbt, vererbt sie eine Schnittstelle. Die abgeleitete Klasse muss diese Funktion implementieren.

Wird einfach virtuell vererbt, erbt die abgeleitete Klasse eine Schnittstelle und eine Standardimplementierung. Wird die Implementierung in der abgeleiteten Klasse nicht spezialisiert, wird implizit auf die Standardimplementierung zurück gegriffen. Dabei muss man sich der Gefahr bewusst sein, dass solch eine Spezialisierung vergessen werden kann. Es gibt zwei Möglichkeiten, um zu erzwingen, dass die Standardimplementierung explizit benutzt wird. Die vererbende Klasse kann die zu vererbende Methode rein virtuell deklarieren und die Standardimplementierung dann als protected implementieren. Die erbende Klasse muss dann eine Implementierung schreiben und kann inline die geschützte Implementierung explizit aufrufen. Oder die Basisklasse implementiert direkt die rein virtuelle Methode. Die Implementierung ist dann allerdings öffentlich. Sie muss dann auch wie bei der vorangehenden Methode von der Basisklasse explizit aufgerufen werden.

Schließlich bedeutet nicht virtuell zu vererben, dass man eine Schnittstelle und eine verbindliche Implementierung vererbt. Die Basisklasse legt damit eine Invariante fest, die durch die abgeleitete Klasse tunlichst nicht neu definiert werden sollte.

Vererbung und objektorientiertes Design – Tip 32 “Modellieren Sie bei der öffentlichen Vererbung die Beziehung >ist ein<“

Das Open Closed Principle (OCP), welches zum Herz der objektorientierten Programmierung gehört, wird durch Abstraktion und Vererbung implementiert. Es besagt, dass Code offen für Erweiterung und geschlossen für Veränderung sein soll. Anstatt funktionierenden Code zu ändern, wird für ein neues Feature Code hinzugefügt.

Wenn z.B. eine Funktion geometrische Formen auf den Bildschirm malt und zuerst auf den Typ der Form prüfen muss, um die entsprechende spezielle Malfunktion aufzurufen, widerspricht dies dem OCP. Für jede neue geometrische Form, z.B. Kreis, Rechteck oder Ellipse, muss die Funktion angefasst und geändert werden. Gibt es noch andere Funktionen, wie die Berechnung des Flächeninhalts, gibt es schon zwei Stellen, die geändert werden müssen, um einen neuen Typ hinzuzufügen.

Das OCP wird hingegen beherzigt, indem ein Interface Shape erstellt wird, welches die abstrakten Methoden Draw und ComputeArea besitzt. Konkrete Klassen wie Ellipse und Circle implementieren nun diese Methoden. Eine Funktion, um verschiedene geometrische Formen der Basisklasse Shape zu malen, ruft nun für jedes Objekt die Methode Draw über das Interface auf. Wird eine neue Form ins Leben gerufen, braucht die Funktion nicht geändert zu werden. Stattdessen muss die neue Form das Interface implementieren.
Nun gibt es wiederum ein Prinzip, um diese öffentliche Vererbung zu regeln, das Liskov Substitution Principle (LSP):

Funktionen, die Zeiger auf Basisklassen benutzen, müssen in der Lage sein, Objekte von abgeleiteten Klassen zu benutzen, ohne sie zu kennen. Basisklassenzeiger müssen also durch Zeiger auf abgeleitete Objekte substituiert werden können.

Daraus folgt, dass abgeleitete Objekte sich gleich wie Basisobjekte verhalten müssen.

Zurück zu Effektiv C++ programmieren, Tip 32 Modellieren Sie bei der öffentlichen Vererbung die Beziehung “ist ein”. So geht denn C++ hin und kompiliert es, wenn Zeiger auf abgeleitete Objekte anstelle von Basisklassenzeigern verwendet werden. Resultat: Bei Ableitung muss eine “ist ein”-Beziehung mit einkalkuliert werden. Was “ist ein” bedeutet wird jedoch anhand des prominenten Beispiels Rechteck und Quadrat deutlich. Umgangssprachlich ist ein Quadrat ein Rechteck. Es verhält sich aber nicht so. Und “ist ein” bedeutet in der objektorientierten Programmierung “verhält sich wie”. Leite ich ein Quadrat vom Rechteck ab, möchte ich auch die Breite des Rechtecks ändern können, ohne die Höhe ändern zu müssen. Um den Quadrat genüge zu tun, muss ich aber beides ändern.

Dass öffentliche Vererbung “ist ein” bedeutet, gebietet also die objektorientierte Programmierung und wird durch C++ mit dem entsprechenden freizügigen Kompilierverhalten unterstützt.

Um LSP C++-seitig ferner zu garantieren, soll man keine öffentlich geerbten Namen verdecken (Tip 34), denn die abgeleitete Klasse würde sich anders verhalten als die Basisklasse.
Wenn z.B. in der Basisklasse B die Methode mf1() rein virtuell ist und die Methode mf1(int) einfach virtuell, überdeckt die Neudeklaration von mf1() in der öffentlich abgeleiteten Klasse den Namen mf1 und mf1(int) wird nicht geerbt. Deswegen ist man gezwungen, in der abgeleiteten Klasse in der public-Deklaration mf1(int) mit using B::mf1 sichtbar zu machen.

Bei privater Vererbung jedoch muss nicht alles geerbt werden.

Design und Deklaration

Die Tips Erleichtern Sie die korrekte und erschweren Sie die falsche Verwendung von Schnittstellen und Behandeln Sie Klassendesign als Typdesign haben meines Erachtens wenig mit C++ zu tun. Höchstens, dass man sich desto mehr über korrekte Verwendung des entwickelten Codes Gedanken machen muss, je komplexer die Sprache ist. Und die Ausdrucksmöglichkeiten von C++ suchen Ihresgleichen.

Auf Tip 20 Übergeben Sie Objekte lieber als Verweis auf const statt als Wert wird im Buch sehr oft als der Tip verwiesen, der die Leistungsfähigkeit von C++-Code entscheidend erhöht. Wenn man ein Objekt nämlich als Zeiger oder Referenz übergibt, müssen Kopierkonstruktor und Destruktor nicht bemüht werden. Und je nach Größe des Objekts und Häufigkeit der Übergabe, kann sich das sehr verflüssigend auf die Performance des Programms auswirken. Im Übrigen umgeht man bei Übergabe als Referenz dem Slicing-Problem. Wird ein Objekt einer abgeleiteten Klasse einer Funktion übergeben, die ein Objekt der Basisklasse entgegennimmt, wird innerhalb der Funktion auch nur ein Basisklassenobjekt konstruiert.
Anhand einer selbstgebastelten Klasse Rational erklärt Meyers aber, dass manchmal eine Übergabe als Objekt stattfinden muss. Wenn operator* ein Verweis auf ein lokal erzeugtes Objekt zurückgeben würde, würde dieses Objekt verrottet sein, wenn es die Funktion verlässt. Ein Zeiger auf ein auf dem Heap erstelltes Objekt kann ebenfalls nicht zurückgegeben werden, da eine Kette von Multiplikationen Speicherlöcher hervorrufen würde. Es gibt keine andere Alternative, als ein Objekt zurückzugeben.

Meyers Tip 22, Datenelemente private zu deklarieren ist Richtlinie in der objektorientierten Programmierung und hat nichts mit C++ zu tun. Solange man sich entscheidet, eine Klasse nicht als Datenstruktur zu programmieren, die ihre Datenelemente direkt öffentlich zur Verfügung stellt, sollte man die Implementierung von der Öffentlichkeit abkapseln, indem man den Zutritt zu den Datenelementen sperrt.

Tip 23, obgleich es auch hier um Objektorientiertheit geht, ist ein C++ Tip. Man soll nichtbefreundete Nichtelementfunktionen Elementfunktionen vorziehen. In C++ gibt es im Gegensatz zu C# oder Java globale Funktionen. Diese Eigenschaft lässt sich prima ausnutzen, um die Kapselung von Klassen zu erhöhen. Was hat eine Komforfunktion, die aus einer Sammlung von Aufrufen öffentlicher Methoden einer Klasse besteht, in der Klasse zu suchen? Um die Kohäsion, also den Zusammenhalt, einer Klasse zu erhöhen, sollten möglichst viele Methoden auf möglichst viele Datenelemente zugreifen. Komfortunktionen erhalten aber Zugriff auf private Elemente, den sie nicht benötigen.

Ressourcenverwaltung

Eine Ressource kann ein auf dem Heap angelegtes Objekt sein, dass nach Benutzung wieder gelöscht werden muss. Genauso müssen eine Datenbankverbindung oder ein Mutex vor Gebrauch gesperrt und nach Gebrauch wieder freigegeben werden. Zum Umgang mit Ressourcen empfiehlt sich das RAII (Resource Acquisition Is Initialization)-Prinzip. Gemäß diesem Prinzip wird eine angeforderte Ressource sofort bei Initialisierung in ein Objekt verpackt. Der Destruktor des Objekts kann dann zuverlässig garantieren, dass die Freigabe des Objekts korrekt geschieht. Zwei Standardklassen für die Ressourcenverwaltung sind std::tr1::auto_ptr und std::tr1::shared_ptr. Ein tr1::shared_ptr ist ein RCSP (Reference-Counting Smart Pointer), ein intelligenter Zeiger, der überwacht, wie viele Objekte auf eine Ressource zeigen, und automatisch diese Ressource löschen kann. tr1::shared_ptr kann bei Initiasierung auch eine Funktion mitgegeben werden, die aufgerufen wird, statt die Ressource zu löschen.

Beim Arbeiten mit Ressourcen verwaltenden Objekten ist auf richtiges Kopierverhalten zu achten. Im Allgemeinen sollte das Kopieren verhindert werden.

Konstruktoren, Destruktoren und Zuweisungsoperatoren

Man sieht schon, ich finde in diesem Buch wirklich jeden Satz lesenswert und jedes Detail wichtig. Nur wäre es nicht besonders effizient in bisheriger Detaildichte über das Buch zu berichten, deswegen ab jetzt kapitelweise.

Nett, dass angehender Vollprofi – für diese Leserschaft ist das Buch gedacht – noch kennenlernen darf, welche Funktionen C++ implizit generiert und aufruft, nämlich Standardkonstruktor, Kopierkonstruktor, Destruktor und Kopierzuweisungsoperator.

Um zu verhindern, dass solche Funktionen erzeugt werden, z.B. weil man ein Objekt nicht kopierbar machen möchte, deklariert man die entsprechenden Funktionen als private. Um das Kopieren zu verhindern sind dies Kopierkonstruktor und Kopierzuweisungsoperator. Bei fehlerhafter Verwendung erhält man einen Linkerfehler, da kein Code vorhanden ist, der die Deklaration definiert. Da bevorzugt wird, den Fehler so früh wie möglich zur Kompilierzeit aufzudecken, können die privaten Deklarationen in eine Basisklasse verschoben werden. Dies funktioniert dadurch, dass die implizit vom Compiler generierten Funktionen versuchen, ihre Gegenstücke in der Basisklasse aufzurufen.

Ein sehr nützlicher Tip sagt, dass man Destruktoren in polymorphen Basisklassen immer virtuell deklarieren soll. Das ist eigentlich kein Tip, sondern ein Muss. Denn ein schönes Design ist ja, eine Fabrikfunktion einzusetzen, die ein Objekt einer abgeleiteten Klasse zurückgibt. Dieses Objekt wird aber einem Basisklassenzeiger zugewiesen. Wird nun der Basisklassenzeiger benutzt, um das Objekt zu löschen, muss der Destruktor virtuell sein. Denn man will nicht, dass nur die Basisklassenanteile des Objekts zerstört werden.

Tip 9, während der Kontruktion der der Zerstörung keine virtuellen Funktionen aufzurufen, ist logisch und konnte ich an der Technik Fabrik-Methode extrahieren und überschreiben nachvollziehen.

Meyers Meinung nach muss Code, der mit Verweisen oder Zeigern auf mehrere Objekte desselben Typs arbeitet, in Betracht ziehen, dass die Objekte identisch sein können. Deshalb sollte man überprüfen, dass operator= sich bei der Zuweisung eines Objekes an sich selbst korrekt verhält.

Jeder Tip wie eine Tasse Wasser in der Wüste

Scott Meyers bescheidener Meinung nach ist sein Buch das 2. wichtigste C++-Buch nach Bjarne Stroustrups Die C++-Programmiersprache. Gleich danach kommt für ihn Design Patterns von Erich Gamma und Co, welches großen Einfluss auf den Gebrauch von C++ gehabt hatte.

Das am häufigsten gebrauchte Wort in seinem Ratgeber ist Kunde. Für Meyers sind die Kunden nicht nur die Programmierer, die den Code benutzen, sondern der Verfasser des Quelltextes selbst. Das überschneidet sich mit Robert C. Martins Feststellung in Clean Code,  dass man sich als Programmierer keineswegs ständig etwas Neues ausdenkt, sondern 9/10 des Tages damit verbringt, seinen eigenen Quelltext zu lesen. Denn natürlich besteht ja der Sinn im Programmieren, Dinge zu verfassen, die wieder verwendet werden.

Hier mal ein paar Beispiel-Tips:

Nach Möglichkeit const verwenden

In C++ gibt es vielfältige Möglichkeiten, const zu verwenden. Wenn man jetzt nicht nur noch darüber nachdenkt (mein Tip), was man alles als const deklarieren könnte, spricht nichts dagegen, es mehr und mehr einzusetzen. Und manche Programmierfehler werden dabei aufgedeckt. Defaultmäßige bitweise Konstanz von Klassenelementen kann nachträglich durch mutable in logische Konstanz aufgeweicht werden. Also nur Mut!

Objekte vor ihrer Verwendung initialisieren

Die Regeln, die definieren, wann ein Objekt initialisiert wird, sind sogar für Meyers zu kompliziert. Eine einfache Regel ist, einfach jedes Objekt vor Verwendung zu initialisieren. Nichtklassenelemente müssen manuell initialisiert werden. Bei Klassenelementen übernimmt diese Aufgabe der Konstruktor. Hier sollte allerdings die Elementinitialisierungsliste verfasst werden. Nur hier ist die Initialisierung ohne Ausnahme gewährleistet. Und man will sich ja über die Regeln keine Gedanken machen müssen. Zudem ist diese Initialisierung oft schneller als über den Umweg des Kopierzuweisungsoperators.
Da die Reihenfolge der Initialisierung nicht lokaler (außerhalb von Funktionen definierter) statischer Objekte in verschiedenen Übersetzungseinheiten (Übersetzungseinheit = Quelltextdatei + eingebundene Header) nicht definiert ist, ergeben sich Probleme wenn ein statisches Objekt ein anderes statisches Objekt benutzen will. Der Kniff besteht hier darin, eine Funktion zu schreiben, die die Referenz auf das statische Objekt zurück gibt. Dies ist eine einfache Anwendung des Singleton Pattern. Das globale statische Objekt wird durch Funktionskapselung lokal und für die Initialisierung von lokalen statischen Objekten übernimmt C++ vor deren Verwendung die Garantie.

Kompilierungsabhängigkeiten zwischen Dateien minimieren

C++ kann schlecht zwischen dem Klasseninterface und der Implementierung unterscheiden. Was in der Klasse Person als private deklariert ist, geht den Kunden nichts an, könnte man denken. Denn der benutzt ja nur die öffentlichen Funktionen. Ändern sich in der Deklaration aber private Daten oder Definitionen, muss alles neu kompiliert werden, was die geänderte Klasse benutzt. Ist die geänderte Klasse noch zentral, und man ist sich in seiner Implementierung noch nicht sicher, muss der Kunde ständig informiert werden, er möge doch erst mal alle Abhängigkeiten finden und gründlich neu kompilieren.

Die Lösung besteht darin, sich von Definitionen unabhängig zu machen und stattdessen von Deklarationen abzuhängen. Wo möglich sollten Klassen foreward deklariert werden. Wenn sich der Aufwand rechtfertigt, könnte man zwei Headerdateien für eine Übersetzungseinheit anlegen, eine mit der Deklaration und eine mit der Definition. Z.B. Bibliotheken, deren ‚Bücher‘ nun mal häufig wiederverwendet werden, sollte man so entwickeln.

Zwei weitere Möglichkeiten, die Abhängigkeit von Definitionen zu vermeiden, sind Handle-Klassen und Interfaces. Wenn die Klasse Person einen Zeiger auf ihre Implementierung (Pimple-Idiom) besitzt und diesen benutzt, um ihre öffentliche Schnittstelle zu implementieren, sind ihre Kunden nur von von ihrer Deklaration abhängig. Die Definition wurde in die Implementierungsklasse verlagert.

Dasselbe Ziel lässt sich mit einem Interface erreichen. Die Klasse Person mutiert zu einer abstrakten Basisklasse. In C++ kann diese im Gegensatz zu Java oder .NET eine Fabrikfunktion beinhalten, die eine konkrete Person erzeugt und einen Zeiger auf die abstrakte Person zurück liefert.

Fabrik-Methode extrahieren und überschreiben

Hier geht es um die Erzeugung von Objekten in Konstruktoren. Klasse WantToTest erzeugt im Konstruktor ein Wrapperobjekt der Klassen A und B. Klasse A wird mit 20 initialisiert, was 20 Datenbank-Verbindungen bedeutet.

Hier das Szenario in C++

#include <iostream>
#include <tr1/memory>

class A
{
  public:
    A(int n)
    : m_NumberOfDBConnections(n)
    {}

    A()
    : m_NumberOfDBConnections(0)
    {}

    int m_NumberOfDBConnections;
};

class B
{
  public:
    B(int i)
    : i(i)
    {}

    int i;
};
typedef std::tr1::shared_ptr<A> PtrA;
typedef std::tr1::shared_ptr<B> PtrB;

class AB
{
  private:
     PtrA m_A;
     PtrB m_B;

  public:
  AB(PtrA a, PtrB b)
  : m_A(a), m_B(b)
  {
  }

  AB ()
  {}

  A* GetA()
  {
    return m_A.get();
  }
};

typedef std::tr1::shared_ptr<AB> PtrAB;

class WantToTest
{
  public:

  WantToTest()
  {
    PtrA a (new A(10));
    PtrB b (new B(20));

    PtrAB ab ( new AB(a,b));

    m_AB = ab;

  }

  private:
    PtrAB m_AB;
};

Da in Tests keine Datenbank-Kommunikation vorkommen soll, wollen wir dies gerne verhindern. Wir wollen wie bei Extract Method and Overwrite Call, den Kontruktoraufruf extrahieren und überschreiben:

class WantToTestCreationExtracted
{
  public:

  WantToTestCreationExtracted()
  {
    m_AB = CreateAB();
  }

  PtrAB CreateAB()
  {
    PtrA a (new A(10));
    PtrB b (new B(20));

    PtrAB ab ( new AB(a,b));

    return ab;

  }

  AB* GetAB()
  {
    return m_AB.get();
  }

  private:
    PtrAB m_AB;
};

class CanTest: public WantToTestCreationExtracted
{
  PtrAB CreateAB()
  {
    PtrA a (new A(0));
    PtrB b (new B(0));

    PtrAB ab ( new AB(a,b));

    return ab;
  }
};

Folgender Test spuckt aber 10 aus:

int main()
{
  CanTest t;
  A* a = t.GetAB()->GetA();
  std::cout << (a->m_NumberOfDBConnections);
  return 0;
}

Das lässt sich damit erklären, dass C++ zum Zeitpunkt des Konstruktoraufrufs der Basisklasse keine abgeleitete Klasse kennt. So lautet denn Scott Meyers Tip 9 in Effektiv C++ programmieren auch, niemals während der Konstruktion oder der Zerstörung virtuelle Funktionen aufzurufen, da diese Funktionen nicht über die Basisklasse hinauskommen.

Aufruf extrahieren und überschreiben

Ein Pärchen von Methoden, die Feathers häufig benutzt, um Abhängigkeiten in zu testenden Klassen zu kappen,  sind Extract and Overwrite Call und Extract and Overwrite Factory Method. Während Ersteres in allen Sprachen funktioniert, geht Letzteres nicht in C++ und mich hat interessiert warum.

Beispiel zu Extract and Overwrite Call in C#:

class CannotTest //Derived from Monster class
{
  static public int GetCloudsCount()
  {
    return -20;
  }

  // 100 more methods
}

class WantToTest
{
  private int CloudsCount = 0;
  private int StarsCount = 0;

  public void InitValues()
  {
    StarsCount = 207;
    CloudsCount = CannotTest.GetCloudsCount();
  }
}

Die Klasse WantToTest ist von CannotTest abhängig, die sich nicht in einen Test hineinziehen lässt.
WantToTest wird in WantToTestExtracted Call umgeformt, indem der Methodenaufruf an die nicht testbare Klasse in eine Methode extrahiert wird:

class WantToTestExtractedCall
{
  private int CloudsCount = 0;
  private int StarsCount = 0;

  public void InitValues()
  {
    StarsCount = 207;
    CloudsCount = GetCloudsCount();
  }

  public int GetCloudsCount()
  {
    return CannotTest.GetCloudsCount();
  }
}

Nun wird die Klasse CanTest von WantToTestExtractedCall abgelitten (höhö, heißt ja eigentlich abgeleitet) und die extrahierte Methode wird überschrieben. Die Abhängigkeit ist gelöst.

class CanTest: WantToTestExtractedCall
{
  public int GetCloudsCount()
  {
    return 10;
  }
}

class Hello
{
    public static void Main(string[] args)
    {
      CanTest t = new CanTest();
      t.InitValues();

      Console.WriteLine (t.GetCloudsCount());
    }
}

Working Effectively with Legacy Code – Das Buch der Wandlungen

In seinem Vorwort bezeichnet Robert C. Martin, Autor von Clean Code, Legacy Code als verrotteten Code. Diese Verrottung komme zustande, weil Software nicht flexibel auf sich ändernde Anforderungen reagieren könne. Schließlich hat Software die Aufgabe, flexibel zu sein, sonst wäre es Hardware. Änderung ist ein großes Thema in der Softwareentwicklung, welches einkalkuliert werden muss. Aber jedes noch so gut durchdachte Design kann brüchig werden, weil man Änderungen nicht voraussehen kann. Während nun der tatsächliche Code in Richtung der neuen Anforderung geht, hinkt das Design hinterher und verrottet. Nun kann keiner den ersten Stein werfen. Keiner kann sich davon frei sprechen, solchen verrottenden Code entwickelt zu haben. Deshalb ist Feathers Working Effectively with Legacy Code der Renner des Jahrhunderts. Denn hier wird nicht ausgearbeitet, wie man es verhindert, sondern wie man solche Altlasten wieder zum Leben umkehrt.

Feathers selbst hingegen bezeichnet Legacy Code ganz pragmatisch als Code ohne Tests. Denn warum wird Code nicht einfach aufgrund sich ändernder Spezifikation geändert? Weil es ohne (möglichst automatisierte) Tests wie artistischer Seiltanz ohne Auffangnetz ist. So etwas macht man nur, wenn es unbedingt sein muss. Und das ist der Grund, warum in der Praxis so konservativ wie möglich geändert wird. Die Angst, etwas kaputt zu machen, ist einfach zu groß. Besser nicht refaktorisieren, besser keine neue Klasse, sind ja erst hundert Methoden drin. Strukturänderungen werden äußerst unübersichtlich, weil man gar nicht weiß, wie sich die Änderungen auswirken. Deswegen am Besten alles beim Alten lassen. In dem Buch geht es im Großen und Ganzen darum, wie man die vier Arten von Änderungen (Hinzufügen neuer Funktion, Fehlerkorrektur, Redesign und Optimierung der Ressourcennutzung) möglichst effektiv durchführt. Effektiv bedeutet hier, dass man für seine Änderungen möglichst viel Feedback durch automatisierte Tests bekommt.

Humorvollerweise bezeichnet Feathers die aktuelle industrielle Methode, Softwareänderungen durchzuführen, als Ändern und Beten statt als Testen und Ändern. Ändern und Beten vergleicht Feathers auch mit einem Chirurg, der vorsichtig mit einem Buttermesser operiert. Er könnte ja etwas falsch machen. Doch wer hätte zu einem solchen Chirurg vertrauen, nur weil er vorsichtig operiert, aber nicht die nötigen Änderungen macht?

Der Entwickler von Legacy-Systemen steckt in einem Dilemma. Einerseits braucht er Tests, um Änderungen durchzuführen. Andererseits kann er nur Tests einführen, wenn er Code verändert. Er muss Abhängigkeiten brechen, um einzelne Spaghetti vollständig aus dem Topf heraus in eine Testumgebung zu holen. Um Abhängigkeiten von verklebenden Spaghetti zu brechen, sollte man dann aber doch konservativ vorgehen. Im letzten Teil seines Buches gibt Feathers dem Leser einen entspechenden Katalog zum Brechen von Abhängigkeiten an die Hand.

Clean Code – Fazit

Erst zum Schluß, auf der allerletzten Seite gibt Robert C. Martin sich als testsüchtiger Programmierer zu erkennen. Martin ist bekennender Anhänger von Test Driven Development (TDD). Ich meine, er sollte von Anfang an darauf pochen, sich diese Praktik zu eigen zu machen. Ich habe selber erfahren, dass durch testgetriebene Entwicklung erst sauberer Code ermöglicht wird. Neulich hatte ich eine Anwendung, die ich nicht testgetrieben entwickelt hatte, nachträglich refaktorisiert, so dass ich sie testen konnte. Zuerst habe ich aus meinen zu testenden Klassen Funktionen extrahiert (indem ich die Signatur bewahrt habe – preserve Signature). Diese Funktionen malen in einer Zeichnung Pfeile und Markierungen und tun andere zeichnungsbezogene Dinge. Ich habe erkannt, dass sie in eine Klasse mit hoher Kohäsion zusammengehören. Danach habe ich das, was meine Anwendung eigentlich tut, hinter ein Interface verbannt. Ich bin einer der vielen Entwickler, die das Glück haben, mit einer Legacy Code-Basis zu arbeiten (Machen wir uns nichts vor: So geht es fast jedem Entwickler). Meine Anwendung kommuniziert jetzt mit dem System über die Implemtierung von diesem Interface. Für die Unit-Tests wird das Interface durch ein Fakeobjekt implementiert. Wieder entstand eine wunderbare Einheit. Durch dieses Refactoring erkannte ich auch, wie kompliziert ich entwickelt habe, da ich mich hauptsächlich mit dem “Wie” statt mit dem “Was” beschäftigt hatte. Das kann beim Test First Ansatz nicht passieren, da man ständig im Kopf hat, was man eigentlich machen will. Ich habe auch durch die nachträgliche Refaktorisierung und saubere Abkopplung meiner Anwendung erkannt, mit wie viel Aufwand ich tatsächlich das Legacy-System befriedigt habe und wie viel an unabhängiger Logik noch übrig blieb. Aber dadurch lasse ich mich nicht entmutigen. Ich habe so enorm viel dadurch gelernt und wie Feathers in Working Effectively with Legacy Code schreibt, entstehen zuerst Inseln, die getestet sind. Aus den Inseln wird Größeres, bis Kontinente daraus entstehen.

Ich bin sehr froh darüber, dass ich dieses Buch gelesen habe und es hat mir enorm viel gebracht. Ich habe das nötige Selbstvertrauen bekommen, selbst komplexeste Anwendungen stabil und zuverlässig entwickeln zu können. Komisch finde ich, dass es sehr viele Entwickler gibt, die sich um Codequalität nicht scheren. Hier wird meines Erachtens viel zu naiv daran gedacht, schnell ein Ergebnis zu fabrizieren. Ähnlich wie die chinesischen Spielzeugautos, die schön aussehen, aber auseinanderfallen, wenn ein Kind sie berührt. Dabei kann man gerade mit unsichtbarer Hintergrundarbeit mit wenig Aufwand sehr viel erreichen. Das schlagende Argument ist, dass ein Entwickler 9/10 des Tages nunmal nicht damit beschäftigt ist, ein neues Feature zu entwickeln, sondern seinen eigenen Quellcode zu lesen und zu warten. Es ist bekannt, dass in der Informationstechnik eine Technik die nächste jagt. Dass man da nicht überall dabei sein kann noch sollte, verstehe ich völlig. Aber die Maßstäbe, sauberen Code zu entwickeln sind schon viele Jahre alt. Hier geht es nämlich um Prinzipien, Muster und Praktiken, statt aus dem Boden schießende Technik. Ähnlich wie Design Patterns, die ihr 25. Jubiläum gefeiert haben und unabhängig von Computersprachen sind. Hier hat man eine ruhige tiefe Stelle im Meer weit unter der Oberfläche gefunden, eine Invariante in der Informationstechnik. Von hier aus hat man eine stabile Basis, sich vorwärts zu bewegen.