Training - Beratung - Projektarbeiten

www.David-Tielke.de

Softwarearchitektur: Komponenten Teil 1 - Was wir von anderen Industrien lernen können.

imageWenn man erfahrene Entwickler fragt, wie man große und Komplexe Softwareprojekte umsetzt, bekommt man meistens zwei Dinge zu hören: Schichten und Komponenten. Beiden ist gemeinsam, dass sie dazu dienen, innerhalb meines Softwaresystems klare Grenzen zwischen Funktionalitäten zu ziehen und sie in Module aufzuteilen mit all den einhergehenden Vorteilen wie besserer Wiederverwendbarkeit, Wartbarkeit, Testbarkeit und – vor allem – der besseren Verteilbarkeit auf einzelne Entwickler.

Herr Ober da liegt ein Haar neben meiner Suppe…

Schaut man über den Tellerrand hinaus in andere Ingenieursbereiche erkennt man, dass dort identische Probleme existieren – und gelöst wurden: durch Modularität. Modular ist etwas, was in sich eine geschlossene Einheit bildet und über verschiedene definierte Schnittstelle angesprochen wird und seinerseits die Fähigkeiten anderer Module ebenfalls über definierte Schnittstellen nutzt. Für Benutzer eines Moduls ist deren innerer Aufbau meist vollkommen unwichtig. Betrachtet man das Modul einer Backstraße zum backen von Brot, so ist klar: Es benötigt Mehl, Hefe und Wasser und produziert Brot, mehr muss ich von Außen nicht wissen um damit Brot herzustellen. Angeschlossen an ein Mehlsilo, ein Hefesilo und eine Wasserleitung kann es also los gehen. Klar wachsen Backstraßen nicht auf Bäumen, sie müssen von irgendjemandem gebaut werden. Bei dem Bau solch eines Systems, schauen wir uns die Lösung zu zwei Problemen an, die wir auch in der Softwareentwicklung haben:

  1. Wie und wo fangen wir bei großen Systemen an zu entwickeln?
  2. Wie können wir die Arbeit auf mehrere Personen aufteilen?

Aus dem Bereich der Mathematik und der Informatik kennt man die Taktik des “Divide And Conquer” (Teile und herrsche), welches besagt das man ein großes Problem in viele kleinere Probleme unterteilt. Das macht man so lange bis alle kleinen Probleme lösen werden können und somit das Gesamtproblem gelöst ist.

Fangen wir also an unser “Problem” aufzuteilen: Eine Backstraße besteht (vermutlich, Leser der Backindustrie mögen mich bitte korrigieren) aus den Untermodulen Teigverarbeitung, Teigherstellung und Backofen wobei in das Modul Teigverarbeitung die Eingabe von Mehl, Hefe und Wasser erhält.

image

Wie dabei dieser Teig erzeugt wird, wie die Rohlinge geformt werden oder wie gebacken wird – das interessiert den Benutzer von Außen nicht. Hauptsache an den Schnittstellen steht das definierte Ergebnis mit den definierten Eigenschaften bereit.

Nun ist so eine Teigherstellungsmaschine (die in der Backstraße enthalten ist) auch kein einfaches Modul und so ist es ebenfalls aus Untermodulen zusammengesetzt, die – ihr werdet es nicht glauben – über fest definierte Schnittstellen miteinander kommunizieren (gähn…)

image

Mangels Maschinenbaustudium vermute ich das solch ein Modul aus einem Mixer (zum vermengen der Zutaten) und einem Kneter (der alles zu einem Teig knetet) bestehen wird. Dieses Spiel könnte man nun so lange treiben, bis man bei Schrauben und Kabeln der einzelnen Module angekommen ist.

image

Haben wir den überschaubaren Level der Schrauben und Kabel erreicht, kann losgebaut werden. Die Module auf unterster Ebene können auf die Ingenieure verteilt werden, da sie ja keine anderen Komponenten brauchen um fertiggestellt zu werden. Aber auch an den Modulen der oberen Schichten kann gebaut werden (Halterungen, Rohre, Kabel, usw.) weil deren Arbeit ebenfalls unabhängig ist – auch von Untermodulen. Denn deren Ausmaße und Schnittstellen sind je genau definiert – die Ingenieure müssen dies nur beim Bau berücksichtigen. Wurde unsere Beispiel sauber durchspezifiziert, kann an 6 Stellen losgebaut werden.

Und wie man sich die Brötchen, die aus der Backstraße kommen, mit der Softwareentwicklung verdienen kann, indem man sich genau das Prinzip zu nutze macht, schauen wir uns im nächsten Teil an… Weil jetzt habe ich Hunger!

Test Driven Development mit .NET und C#–Tipp #2: Abhängigkeiten auflösen mit Stubs

Das Testen von Code ist eine wichtige und – für Entwickler – richtig coole Angelegenheit. Doch Testen ist weitaus mehr, als ein Testrunner und ein paar Attribute für Klassen und Methoden. In der Reihe “TDD Tipps” versuche ich regelmäßig Probleme zu erläutern, die beim Testen auftauchen und mit einem praktischen Hinweis einen Lösungsweg aufzuzeigen.

Unit-Tests sollen das Verhalten von nur einem einzigen Modul, für uns in diesem Fall einer Klasse, testen. Aber warum? Nehmen wir an wir testen die Methode Foo() in einer Klasse A die eine Referenz auf eine Klasse B hat. Im Laufe der Methode Foo() aus Klasse A wird die Methode Bar() aus Klasse B aufgerufen. Wenn wir jetzt A.Foo() testen, wird Code aus B.Bar() ausgeführt. Also ist das Ergebnis unseres Tests nicht mehr alleine von A, sondern auch von B abhängig und damit hängt auch der Testerfolg von A.Foo() von B.Bar() ab. Was hier noch nach einem einfachen Problem aussieht, wird spätestens zu einem Problem, wenn B eine externe Ressource nutzen würde, wie einen Webdienst, einen Mailserver, Datenbankserver o.ä. Aus dem und vielen anderen Gründen, sollte man in seinem Design immer so wenig Abhängigkeiten wie möglich haben. Was in der Theorie und in den zahlreichen Demos in Büchern und Konferenzen immer ganz einfach und simpel aussieht, gestaltet sich in der Praxis als teilweise sehr kompliziert. Wenn man ein Klassendesign entwirft, hat man an einigen Stellen zwangsläufig Abhängigkeiten.

Abbildung 1 verdeutlich das Problem noch einmal:

image

Abbildung 1: Klassendesign das getestet werden soll.

Würden wir nun einen Test für die Methode Foo() ) der Klasse A schreiben und dort ein Objekt der Klasse A instanziieren, würde dieses automatisch ein Objekt der Klasse B erzeugen, da A.Foo() ja deren Funktionalität nutzt.

Um solche Abhängigkeiten aufzulösen, bietet sich der Einsatz von Interfaces an. Mit nur einem Interface haben wir die Klassen voneinander entkoppelt und die Abhängigkeit zwischen A und B aufgelöst.image

Abbildung 2: Die Klassen A und B sind durch das Interface entkoppelt.

Zusätzlich übernimmt jetzt nicht mehr A selbst die Instanziierung von B, sondern wir injizieren dem Objekt von A mittels Konstruktorparameter b ein Objekt, das die Schnittstelle IB implementiert. Dadurch können wir von außen bestimmen, welches Objekt die Funktionalität IB.Bar() übernimmt, die wir von A.Foo() aus aufrufen.

Man stelle sich vor ,dass B.Bar() eine Datenbank aufruft. Gibt es dort einen Fehler, löst diese Methode eine InvalidOperationException aus. Unsere Methode A.Foo(), die ja B.Bar() aufruft, soll auf diese Exception mit einer CustomException reagieren. Wenn wir das mit dem Design aus Abbildung 1 testen möchten, müssten wir eine Datenbank aufsetzen, diese Tabellen in der Datenbank so manipulieren, dass in der Methode B.Bar() die gewünschte Exception auftritt und dann den Test für A.Foo() durchführen. Wir müssen quasi ein Szenario für B.Bar() simulieren, obwohl wir nur A.Foo() testen wollen. Das ist hier bereits verwirrend und in größeren Systemen führt das zu unglaublich kompliziertem Testcode den nach ein paar Tagen der Tester selbst nicht mehr versteht.

Mit dem Design aus Abbildung 2 ist das überhaupt kein Problem: In unserem Testprojekt legen wir einen sogenannten Stub an. Stub heißt, das er ein “dummer” Stellvertreter einer Interfaceimplementierung ist, der fest definierte Werte zurückgibt.

public class IBExceptionStub : IB
{
    public void Bar()
    {
        throw new InvalidOperationException("...");
    }
}

Listing 1: Ein Stub für einen Test der von B.Bar() eine Exception erwartet.

Schreiben wir nun also einen Test, der prüfen möchte, ob A.Foo() die erwartete CustomException auch tatsächlich auslöst, brauchen wir nur noch folgenden Code zu schreiben:

[TestMethod]
[ExpectedException(typeof(CustomException))]
public void Foo_DatabaseNotAvailable_CustomException()
{
    IB ib = new IBExceptionStub();
    A a = new A(ib);

    a.Foo();
}

Listing 2: Der Test für A.Foo() nutzt den ExceptionStub um einen Fehlerfall von B.Bar() zu simulieren.

Damit haben wir zwei Punkte erreicht:

Erstens können wir unterschiedlichste Verhalten von A in Abhängigkeit von Verhalten in B testen. Möchten wir z.B. ein Verhalten von A.Foo testen, wenn von B.Bar() aus der Datenbank ein ungültiger User geladen wird, schreiben wir einfach einen kleinen Stub.

Zweitens das Design ist wesentlich flexibler geworden durch die Entkopplung durch das Interface. Das Design aus Abbildung 1 wäre nicht durch eine Klasse C ersetzbar, welche die Methode Bar() ebenfalls anbietet. Das zweite Design könnte dies ohne Probleme, wenn C das Interface IB implementieren würde.

Das Ganze funktioniert so natürlich nur wenn Constructorinjection möglich ist. Sollte das nicht möglich sein, nutzt man dazu das ServiceLocator-Pattern. Aber das schauen wir uns in einem anderen Tipp genauer an.

Alle Beiträge der Reihe “TDD Tipps” sind hier zu finden.

Test Driven Development mit .NET und C#–Tipp #1: Events testen

Das Testen von Code ist eine wichtige und – für  Entwickler – richtig coole Angelegenheit. Doch Testen ist weitaus mehr, als ein Testrunner und ein paar Attribute für Klassen uns Methoden. In der Reihe “TDD Tipps” versuche ich regelmäßig Probleme zu erläutern, die beim Testen auftauchen und mit einem praktischen Hinweis einen Lösungsweg aufzuzeigen.

Um Klassen untereinander zu benachrichtigen und voneinander zu entkoppeln, können Events genutzt werden. Wenn man Klassen mit Events testen möchte, muss natürlich auch das korrekte auslösen des Events getestet werden. Hat man kein entsprechendes Mock-Framework zur Hand, geht das auch relativ einfach mit Lambda-Expressions. Gegeben sei folgende Klasse Foo mit dem Event Bar (die so natürlich NICHT im Produktivcode auftauchen sollte):

public class Foo
{
    public event EventHandler Now;
    public void Bar()
    {
        Now(null, null);
    }
}

Listing 1: Die Klasse Foo mit dem Event Now, das getestet werden soll

Das Testen ist relativ einfach und simple:

[TestMethod]
public void Bar_EventSubscribed_EventIsRaised()
{
    bool isRaised = false;
    Foo foo = new Foo();
    foo.Now += (sender, args) => isRaised = true;
            
    foo.Bar();

    Assert.IsTrue(isRaised);
}

Listing 2: Die Methode, die testet, ob das Event ausgelöst wurde

Zunächst wird eine Variable isRaised deklariert und mit false initialisiert. Diese Variable erfasst, ob das Event ausgelöst wurde. Danach wird mit einem Lambda-Ausdruck dafür gesorgt, dass diese Variable auf true gesetzt wird, wenn das Event ausgelöst wird. Im Assert-Teil ist dann nur noch zu prüfen, ob diese Variable auf true gesetzt wurde.

Alle Beiträge der Reihe “TDD Tipps” sind hier zu finden.