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.