4 Komponentenorientierung am Beispiel

Ein Beispiel soll nun zeigen, wie die komponentenorientierte Implementation einer ganz einfachen Anwendung aussieht.

4.1 Anforderungen

Implementiert werden soll die Kommandozeilenanwendung cat. Der aus der Unix Welt stammende Befehl cat gibt den Inhalt mehrerer Textdateien auf der Konsole aus.

  • Die Dateinamen werden als Kommandozeilenparameter übergeben.
  • Die Ausgabe der Dateien erfolgt in der Reihenfolge der Dateinamen.

Beispiele:

cat file1.txt

Gibt die Datei file1.txt Zeile für Zeile auf der Konsole aus.

cat file1.txt file2.txt file3.txt

Gibt den Inhalt der drei Dateien nacheinander auf der Konsole aus. Die Ausgabe erfolgt in der Reihenfolge, in der die Dateinamen angegeben sind.

4.2 Entwurf

Es gibt viele Möglichkeiten, diese Anforderungen umzusetzen. In jedem Fall sollte vor der Implementation ein Entwurf stehen. Die Implementation erfolgt in textueller Weise in einer Programmiersprache. Dabei geht es um sehr viele Details. Der Entwurf dagegen soll einen Blick auf eine abstraktere Form ermöglichen, in der die Details ganz bewusst noch nicht auftauchen. Nur in dieser abstrakten Form lässt sich über mögliche Umsetzungen der Anforderungen im Team diskutieren. Ferner liegt der Entwurf nicht in textueller sondern in grafischer Weise vor, was die Diskussion darüber gut unterstützen kann.

Wie man zu einem geeigneten Entwurf kommt, ist eine spannende Frage. Da mir jedoch in diesem Buch der Fokus auf die Arbeitsorganisation ganz wichtig ist, wird es hier nicht darum gehen, wie man zu einem Entwurf kommt. Die Frage, die sich im Folgenden stellt ist, wie man einen vorhandenen Entwurf auf Komponenten verteilt.

Die folgende Abbildung zeigt einen Entwurf für die Umsetzung der Anforderungen. In diesem Entwurf wird mit Klassen gearbeitet, die zueinander in Abhängigkeiten stehen. Die einzelnen Klassen sind jeweils für einen Aspekt der Anwendung verantwortlich.

Die Klasse Cat ist die zentrale Funktionseinheit dieses Entwurfs. Sie ist von drei weiteren Klassen abhängig. Anhand der Formen der Funktionseinheiten wird bereits deutlich, zu welcher Kategorie sie gehören:

  • UI ist ein Portal. Hier findet die Ausgabe an den Benutzer statt.
  • Kommandozeile ist ein Adapter. Mit diesem Adapter wird auf die Ressource Kommandozeilenparameter in der Umwelt des Systems zugegriffen.
  • Textdatei ist ein Adapter. Er ist für den Zugriff auf die Textdateien zuständig, die ebenfalls als Ressource in der Umwelt des Systems liegen.
  • Cat enthält die Logik des Systems.

Im Entwurf nicht dargestellt ist die Klasse Program, die für die Integration der restlichen Klassen zuständig ist. Ergänzt man den Entwurf um diese Klasse sowie die Abhängigkeiten, entsteht folgendes Bild:

Die Abbildung wird durch die Ergänzung der Abhängigkeiten sehr unübersichtlich. Das liegt daran, dass die Klasse Program von allen anderen Klassen abhängig ist. Sie muss alle diese Klassen instanzieren und die Abhängigkeiten dieser Klassen untereinander auflösen. Weil die Klasse Program typischerweise von allen anderen Klassen abhängig ist und die Abbildungen dadurch sehr unübersichtlich werden, lassen wir sie in zukünftigen Entwürfen weg.

4.3 Zerlegung in Komponenten

Nun liegt also ein Entwurf vor, in dem die einzelnen Aspekte des Systems auf Klassen verteilt wurden. Versetzen Sie sich nun einmal gedanklich in die Situation, dass ein Team mit mehreren Entwicklern jetzt mit der Implementation beginnen möchte. Die Herausforderung besteht darin, die gemeinsame Arbeit am System so zu organisieren, dass alle Entwickler gleichzeitig arbeiten können. Alle Klassen in ein und demselben Visual Studio Projekt anzulegen scheidet aus. Das würde dazu führen, dass alle Entwickler dasselbe Projekt öffnen und darin Klassen anlegen. Spätestens beim Übertragen in die Versionskontrolle käme es zu Problemen durch Mergekonflikte. Es liegt daher nahe, die Klassen auf mehrere Visual Studio Projekte zu verteilen. Das Beispiel ist bewusst klein gehalten, damit der Überblick gewahrt bleibt. Allerdings führt das nun dazu, dass die Zerlegung des Systems in Komponenten etwas übertrieben erscheinen mag. Lassen Sie sich davon nicht irritieren. In realen Systemen enthalten die Komponenten typischerweise mehr als eine Klasse.

Die folgende Abbildung zeigt, wie eine Zuordnung der Klassen zu Komponenten aussehen könnte. Ich habe für jede Klasse eine eigene Komponente vorgesehen. Geleitet hat mich dabei, dass die Klassen jeweils für völlig unterschiedliche Aspekte zuständig sind. Ich möchte vermeiden, dass unterschiedliche Aspekte eines Systems in einer Komponente zusammengefasst werden. Auf die Kriterien für das Zerlegen eines Entwurfs in Komponenten wird später noch detaillierter eingegangen.

Für die Bezeichnung der Komponenten verwende ich folgende Konvention:

  • Alle Namen der Komponenten beginnen mit dem Namen des Systems. In diesem Beispiel ist das cat.
  • Auf den Systemnamen folgt der Bezeichner für die Komponente, getrennt durch einen Punkt.
  • Die Bezeichnung der Komponenten erfolgt vollständig in Kleinbuchstaben. Für jede Komponente muss ein Visual Studio Projekt angelegt werden. Beim Anlegen eines Projektes übernimmt Visual Studio den Projektnamen als Default Namespace. Die Verwendung von Kleinbuchstaben hat den Vorteil, dass damit auch der Default Namespace in Kleinbuchstaben angelegt wird. Dadurch entstehen keine Konflikte zu Klassennamen.

Speziell die Konvention, Komponenten mit Kleinbuchstaben zu bezeichnen, sollten Sie unbedingt übernehmen. Bei einem komponentenorientierten System tritt häufig der Fall ein, dass eine Klasse genauso heißt, wie die Komponente. Da für jede Komponente ein Namespace angelegt wird, käme es immer zu einem Konflikt zwischen Namespace und Klassenname. Man müsste dann den Klassennamen jeweils durch den vorangestellten Namespace qualifizieren, also zum Beispiel folgendes schreiben:

var cat = new Cat.Cat();

Durch Namespaces in Kleinbuchstaben entfällt die Notwendigkeit, den Namespace vor den Klassennamen schreiben zu müssen.

4.4 Erstellen der Kontrakte

Für das Erstellen der Kontrakte ist es natürlich erforderlich, dass ein Entwurf vorliegt. Bislang sind in den Abbildungen zum Entwurf allerdings nur die Klassennamen gezeigt. Über welche Methoden die Klassen verfügen, geht daraus noch nicht hervor. Selbstverständlich ist das der zentrale Punkt eines Entwurfs: herauszufinden, welche Funktionalität benötigt wird und wie man sie auf Methoden, Klassen und Komponenten verteilt. Da das Beispiel überschaubar ist und der Fokus auf der Arbeitsorganisation mittels Komponenten liegt, werde ich hier nicht weiter ausführen, wie ich auf die Methoden gekommen bin. Entwurf ist Thema für ein anderes Buch.

4.4.1 Der Kontrakt ICat

1 namespace cat.contracts
2 {
3     public interface ICat
4     {
5         void Run();
6     }
7 }

Die Run Methode ist der Einstiegspunkt der Anwendung. Sie wird später in der Program.Main Methode des EXE-Projektes aufgerufen. Ihre Aufgabe ist die Integration der anderen Funktionseinheiten. Sie koordiniert den Aufruf der Methoden der anderen beteiligten Klassen.

4.4.2 Der Kontrakt IUi

1 using System.Collections.Generic;
2 
3 namespace cat.contracts
4 {
5     public interface IUi
6     {
7         void Ausgeben(IEnumerable<string> zeilen);
8     }
9 }

Die Komponente UI ist für die Ausgabe von Textzeilen zuständig. Sie verfügt dazu über die Methode Ausgeben, die eine Aufzählung von Strings als Parameter erhält und diese auf die Konsole ausgibt.

4.4.3 Der Kontrakt IKommandozeile

1 using System.Collections.Generic;
2 
3 namespace cat.contracts
4 {
5     public interface IKommandozeile
6     {
7         IEnumerable<string> Dateinamen();
8     }
9 }

Die Komponente Kommandozeile ist ein Adapter zu den Kommandozeilenparametern, die sich in der Umwelt des zu erstellenden Systems befinden. Sie verfügt über eine Methode Dateinamen, mit der alle Dateinamen, die auf der Kommandozeile übergeben wurden, ermittelt werden.

4.4.4 Der Kontrakt ITextdatei

1 using System.Collections.Generic;
2 
3 namespace cat.contracts
4 {
5     public interface ITextdatei
6     {
7         IEnumerable<string> Einlesen(string dateiname);
8     }
9 }

Der Zugriff auf den Inhalt der einzelnen Dateien erfolgt durch die Komponente Textdatei. Sie enthält die Methode Einlesen, die den gesamten Inhalt einer Datei als Aufzählung von Strings liefert.

4.5 Implementieren der Komponenten

Die Aufgabenstellung des Programms ist überschaubar. Daher sind die Komponenten nicht sehr umfangreich. Jede Komponente ist daher als einzelne Klasse realisiert. Das muss natürlich nicht immer so sein. In größeren Systemen bestehen Komponenten durchaus aus mehreren Klassen.

4.5.1 Die Komponente Cat

Aufgabe der Komponente Cat ist die Integration der drei Komponenten UI, Textdatei und Kommandozeile. Voraussetzung dafür ist, dass Cat die drei Komponenten kennt. Natürlich darf hier aber keine direkte Abhängigkeit zwischen den Komponenten entstehen. Cat muss die Dienste der anderen Komponenten in jedem Fall über den Kontrakt in Anspruch nehmen. Die Kontrakte werden in C# typischerweise durch Interfaces realisiert.

 1 using cat.contracts;
 2 
 3 namespace cat.cat
 4 {
 5     public class Cat : ICat
 6     {
 7         private readonly IUi ui;
 8         private readonly ITextdatei textdatei;
 9         private readonly IKommandozeile kommandozeile;
10 
11         public Cat(IUi ui, ITextdatei textdatei, IKomma\
12 ndozeile kommandozeile) {
13             this.ui = ui;
14             this.textdatei = textdatei;
15             this.kommandozeile = kommandozeile;
16         }
17 
18         public void Run() {
19             var dateinamen = kommandozeile.Dateinamen();
20             foreach (var dateiname in dateinamen) {
21                 var zeilen = textdatei.Einlesen(dateina\
22 me);
23                 ui.Ausgeben(zeilen);
24             }
25         }
26     }
27 }

Cat kann die benötigten Klassen nicht selbst instanzieren. Dazu wäre eine Referenz auf die Implementation der Komponenten erforderlich. Das würde die Komponentenorientierung ad absurdum führen. Aus diesem Grund werden die drei benötigten Komponenten dem Konstruktor von Cat als Parameter übergeben und in Feldern der Klasse abgelegt. Dadurch hat die Methode Run Zugriff auf die Komponenten und kann deren Methoden in der erforderlichen Weise aufrufen.

Am Beispiel der Komponente Cat kann man klar erkennen, dass Cat implementiert werden kann, ohne dass die drei anderen Komponenten bereits existieren. Lediglich die Kontrakte müssen vorliegen. Auf diese Weise wird eine Arbeitsorganisation im Team ermöglicht, die ein gleichzeitiges Arbeiten an den Komponenten zulässt.

4.5.2 Die Komponente UI

Die Ausgabe von Strings auf der Konsole ist die Aufgabe der Komponente UI. Ihre Methode Ausgeben erhält eine Aufzählung von Strings als Parameter. Darüber iteriert die Methode in einer Schleife und gibt jeden String mit Console.WriteLine auf die Konsole aus.

Keine große Sache; möglicherweise entsteht daher der Wunsch, diese Funktionalität im Hauptprogramm unterzubringen. Schließlich erscheint der Overhead für das Erstellen der Komponente relativ groß im Verhältnis zu den wenigen Zeilen Code, welche die Funktionalität erbringen. Doch es ist ganz wichtig, hier nicht die falschen Kriterien anzulegen. Die Anzahl der Codezeilen sollte kein Kriterium sein für die Frage, ob es sich „lohnt“ eine weitere Komponente zu erstellen. Stattdessen sollte die Frage im Vordergrund stehen, ob die Komponente dazu beiträgt, die Aspekte des Systems zu trennen. Und das ist hier bei der Komponente UI definitiv gegeben. Die Komponente UI isoliert den Aspekt der Ausgabe der Daten. Dieser Aspekt kann sich getrennt von anderen Aspekten des Systems verändern. Es könnte zum Beispiel der Wunsch entstehen, das System mit einer grafischen Oberfläche auszustatten. In diesem Fall wäre die Komponente UI zu ändern. Die anderen Komponenten sollten nicht geändert werden müssen. Andernfalls wäre das ein Hinweis darauf, dass die Aspekte nicht klar getrennt sind.

 1 using System;
 2 using System.Collections.Generic;
 3 using cat.contracts;
 4 
 5 namespace cat.ui
 6 {
 7     public class Ui : IUi
 8     {
 9         public void Ausgeben(IEnumerable<string> zeilen\
10 ) {
11             foreach (var zeile in zeilen) {
12                 Console.WriteLine(zeile);
13             }
14         }
15     }
16 }

4.5.3 Die Komponente Kommandozeile

Das Programm erhält die Namen der auszugebenden Textdateien als Parameter auf der Kommandozeile übergeben. Für den Zugriff auf diese Parameter ist die Komponente Kommandozeile zuständig. Ihre Methode Dateinamen liefert die Kommandozeilenparameter als Aufzählung von Strings.

Auch hier scheint die geringe Anzahl von Codezeilen dafür zu sprechen, die benötigte Funktionalität im Hauptprogramm unterzubringen. Zumal die Methode Program.Main, die zur Laufzeit als Einstiegspunkt in das Programm dient, die Kommandozeilenparameter als Methodenparameter übergeben kriegt. Doch auch hier geht es darum, die Aspekte zu trennen. Die Anforderungen könnten sich beispielsweise so ändern, dass die Dateinamen nicht über die Kommandozeile übergeben werden, sondern aus einer Steuerdatei gelesen werden sollen. In diesem Fall wäre lediglich die Komponente Kommandozeile von der Änderung betroffen. Das Trennen der Aspekte ist somit gut für die Evolvierbarkeit des Systems.

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using cat.contracts;
 5 
 6 namespace cat.kommandozeile
 7 {
 8     public class Kommandozeile : IKommandozeile
 9     {
10         public IEnumerable<string> Dateinamen() {
11             return Environment.GetCommandLineArgs().Ski\
12 p(1);
13         }
14     }
15 }

4.5.4 Die Komponente Textdatei

Das Lesen der Zeilen der Textdatei ist Aufgabe der Komponente Textdatei. Sie verfügt über eine Methode Einlesen, die den Dateinamen der einzulesenden Datei als Parameter erhält. Als Ergebnis liefert die Methode den Inhalt der Datei als Aufzählung von Strings.

 1 using System.Collections.Generic;
 2 using System.IO;
 3 using cat.contracts;
 4 
 5 namespace cat.textdatei
 6 {
 7     public class Textdatei : ITextdatei
 8     {
 9         public IEnumerable<string> Einlesen(string date\
10 iname) {
11             return File.ReadLines(dateiname);
12         }
13     }
14 }

4.6 Implementieren des Host

Die Funktionalität des gesamten Systems ist nun auf die oben beschriebenen Komponenten verteilt. Was nun noch fehlt, ist ein Host, der alle benötigten Komponenten referenziert und die benötigten Klassen instanziert. Schließlich muss der Einstiegspunkt des Systems, in diesem Fall die Methode Cat.Run, aufgerufen werden.

Das Visual Studio Projekt cat.application ist das einzige Projekt, das Referenzen auf die Komponentenimplementationen erhält. Alle anderen Projekte referenzieren lediglich die Kontrakte. Auf diese Weise wird erreicht, dass die Komponenten in beliebiger Reihenfolge und auch parallel entwickelt werden können. Die folgende Abbildung zeigt die Referenzen des Host Projekts.

Mit der Implementation des Host kann logischerweise erst begonnen werden, wenn alle Implementationen der Komponenten vorliegen. Allerdings müssen die Komponenten dazu nicht vollständig implementiert sein, sondern es genügt, die Visual Studio Projekte aufzusetzen. Die Implementationen der einzelnen Methoden können zunächst leer gelassen werden. Im Ergebnis kann dann mit der Arbeit am Host begonnen werden, da das Hostprojekt dann bereits alle benötigten Komponenten referenzieren kann.

 1 using cat.cat;
 2 using cat.kommandozeile;
 3 using cat.textdatei;
 4 using cat.ui;
 5 
 6 namespace cat.application
 7 {
 8     internal class Program
 9     {
10         private static void Main() {
11             var ui = new Ui();
12             var kommandozeile = new Kommandozeile();
13             var textdatei = new Textdatei();
14 
15             var cat = new Cat(ui, textdatei, kommandoze\
16 ile);
17 
18             cat.Run();
19         }
20     }
21 }

Der Host der Anwendung Cat instanziert zunächst die drei Klassen der Komponenten UI, Kommandozeile und Textdatei. Anschließend kann die Klasse Cat instanziert werden. Ihr werden im Konstruktor die drei anderen Instanzen der Klassen übergeben. Zum Abschluss ist nichts weiter zu tun, als mit cat.Run die Anwendung zu starten.

  1. Zur Theorie of Constraints siehe z.B. http://de.wikipedia.org/wiki/Theory_of_Constraints