![]() |
Comelio GmbH
|
Comelio-Blog > C#.NET > Kompositum-Muster Entwurfsmuster (Design Patterns) in C#.NET: Kompositum
Design Patterns: Kompositum / Composite in C#.NETEin weiteres Muster, das wir als Beispiel für den Einsatz von Schnittstellen vorstellen wollen, ist das Kompositum-Muster. Hier soll das Ziel umgesetzt werden, dass man Einheiten und zusammengesetzte Einheiten / Komposita unterscheidet. Das heißt, Objekte können wiederum andere Objekte enthalten. Dies ist, wenn man einen Warenkorb mit verschiedenen Produkt-Objekten zum Vergleich heranzieht, zunächst nicht so eine herausragende Besonderheit. Dies liegt an der grundsätzlichen Verschiedenheit von Warenkorb und Produkt. Eine weitere Voraussetzung, die in Situation, in welcher das Muster zum Einsatz kommen kann, zutreffen muss, ist die gleichartige Verarbeitung vom Kompositum und den einzelnen Einheiten. Im Beispiel der geometrischen Körper kann man sich vorstellen, dass man eine Gruppe von geometrischen Objekten besitzt, die ebenfalls solche Methoden wie errechneFlaeche unterstützen sollen. Ob man also die Fläche von einem einzigen Objekt ausrechnen lassen will, oder ob diese Rechnung für alle Objekte der Gruppe durchgeführt werden soll, lässt sich über das Muster einrichten. Dazu muss ja lediglich im Kompositum die gleiche Methode implementiert werden wie im einzelnen Objekt. Dabei ist die Implementierung grundlegend verschieden, weil das Kompositum alle enthaltenen Objekte durchläuft und die entsprechende Methode aufruft, doch der Rückgabewert und seine Bedeutung für die Anwendung (Fläche, Kantenlänge, Volumen) sind die gleichen. Zunächst wird also eine Schnittstelle benötigt, die sowohl für Einheiten als auch für Komposita passende Komponenten fordert. Dies ist zum einen die bereits bekannte Methode errechneFlaeche, zum anderen sind dies zwei neue Methode für das Hinzufügen und Entfernen von einzelnen Objekten. An diesen beiden Methoden ist interessant, dass sie jeweils Objekte vom Typ der Schnittstelle, also von sich erwarten. Hier liegt eine reflexive Beziehung vor, welche die Zusammensetzung von Objekten unterstützt. public interface IKoerper {
// Eigenschaften
int laenge { get; }
int breite { get; }
int hoehe { get; }
int anzahl { get; }
// Methoden
double errechneFlaeche();
void hinzufuegen(IKoerper koerper);
void entfernen(IKoerper koerper);
}
Schnittstelle für ein Kompositum
Die Klasse Quadrat hat zunächst den erwarteten Aufbau, weswegen wir den Quelltext etwas zusammengestaucht haben, damit er übersichtlicher wird. Da sie die gerade vorgeführte Schnittstelle mit den beiden besonderen Methoden zum Hinzufügen und Entfernen von Objekten implementieren muss, sind diese natürlich auch vorhanden, wobei sie allerdings keine sinnvolle Aktion durchführen können. Dies kann man entweder durch einen leeren Anweisungsblock ausdrücken, was allerdings nicht so gelungen ist, durch einen booleschen Rückgabewert, der jedoch in der Schnittstelle nicht gefordert wurde, oder durch eine Ausnahme. Dies ist eine Fehlermeldung, auf deren Verwendung wir im nächsten Kapitel eingehen werden. Diese Ausnahme ist zwar eine gute Lösung, doch stellt sie eigentlich einen Schönheitsfehler dar, welchen wir im nächsten Beispiel beheben werden. Methoden nur zu implementieren, damit einer Schnittstelle Genüge getan wird und um einige Methoden sowohl auf Einheiten wie auf Zusammensetzungen auszuführen, ist nicht gerade hübsch aufgebaute Software. public class Quadrat : IKoerper {
// Felder
private int _laenge;
// Eigenschaften
public int laenge { get { return _laenge; } }
public int hoehe { get { return 0; } }
public int breite { get { return _laenge; } }
public int anzahl { get { return 1; } }
// Methoden
public Quadrat(int laenge) {
_laenge = laenge;
}
public double errechneFlaeche() {
return laenge * laenge;
}
public void hinzufuegen(IKoerper koerper){
throw new Exception("Kein Kompositum.");
}
public void entfernen(IKoerper koerper){
throw new Exception("Kein Kompositum.");
}
}
Einzelne Einheit
Die Klasse Kompositum dagegen kann mit den gerade kritisch beäugten Methoden hinzufuegen und entfernen dagegen überaus wichtige Aktionen durchführen, nämlich tatsächlich Objekte vom Typ IKoerper (Quadrate, Rechtecke, Quader) in eine private ArrayList schreiben bzw. aus dieser wieder löschen. Hier macht auch die Eigenschaft anzahl Sinn, die in den anderen Klassen immer nur den Wert 1 zurücklieferte, weil natürlich eine Einheit nur aus einem Objekt besteht, ein Kompositum allerdings aus mehreren. Neben den Methoden zum Bearbeiten der Objektsammlung gibt es auch noch die Methode errechneFlaeche, welche sich durch eine ganz andere Implementierung auszeichnet als in den Klassen für Einheiten vom Typ IKoerper. Da die Sammlung als solche keine Fläche hat, sondern nur die in ihr untergebrachten Objekte, müssen in einer foreach-Schleife die einzelnen Objekte durchlaufen werden. Da sie alle die gleiche Schnittstelle implementieren, lässt sich ihr polymorphes Verhalten nutzen und die jeweilige errechneFlaeche-Methode aufrufen, um die Werte als Summe zurückzugeben. public class Kompositum : IKoerper {
// Felder
private ArrayList _kompositum;
// Eigenschaften
public int laenge { get { return 0; } }
public int hoehe { get { return 0; } }
public int breite { get { return 0; } }
public int anzahl {
get { return _kompositum.Count; }
}
// Methoden
public Kompositum() {
_kompositum = new ArrayList();
}
public double errechneFlaeche() {
double flaeche = 0;
foreach (IKoerper koerper in _kompositum) {
flaeche += koerper.errechneFlaeche();
}
return flaeche;
}
public void hinzufuegen(IKoerper koerper){
_kompositum.Add(koerper);
}
public void entfernen(IKoerper koerper){
_kompositum.Remove(koerper);
}
}
Zusammensetzung
Ein einfacher Test zeigt die Funktionstüchtigkeit der gesamten Konstruktion. Zunächst erstellt man wenigstens zwei Objekte vom Typ Quadrat und Rechteck, die dann zu einem Ganzen zusammengefügt werden. Dieses Kompositum wiederum soll nun die Summe aller Flächen und damit seine eigene Fläche zurückgeben. Da die Kantenlängen sehr klein gewählt wurden, lässt sich die entstehende Summe leicht als richtig überprüfen. Quadrat quadrat = new Quadrat(3);
Rechteck rechteck = new Rechteck(3, 4);
Kompositum kompositum = new Kompositum();
kompositum.hinzufuegen(quadrat);
kompositum.hinzufuegen(rechteck);
ausgabe.Text += kompositum.anzahl + ": ";
ausgabe.Text += quadrat.errechneFlaeche() + " + " +
rechteck.errechneFlaeche() + " = ";
ausgabe.Text += kompositum.errechneFlaeche();
Verwendung von Einzelstücken und Zusammensetzungen
Man erhält als Ausgabe: 2: 9 + 12 = 21. Einige der Entwurfsmuster weisen überaus charakteristische UML-Diagramme auf. Teilweise wie beim Singleton-Muster sind sie aufgrund der Übersichtlichkeit und Selbstreflexivität gut zu erkennen, teilweise aufgrund von eher selten erscheinenden Beziehungen wie hier im Kompositum. Auf Basis einer gemeinsamen Schnittstelle implementiert man so viele Klassen für Einheiten und Komposita wie einem notwendig erscheint. Für die Komposita gibt es eine weitere Aggregationsverbindung zur Schnittstelle, mit deren Hilfe ausgedrückt wird, dass das Kompositum Objekte vom Typ der Schnittstelle erwartet und speichern kann.
Kompositum-Muster Obwohl das Design elegant ist und das Muster eine Lösung für viele Probleme bietet, die bei der Abbildung von baumartigen Strukturen (Ordnern, Objektgruppen, XML-Elementen) hilfreich sein können, so ist das Auslösen von Ausnahmen doch insoweit ein Problem, weil man außen immer darauf gefasst sein muss, dass man gar nicht mit einer Zusammensetzung, sondern mit einer Einheit arbeitet. Dieses Problem konnte beim Experiment nicht auftreten, weil hier die verschiedenen Objekte statisch gebunden wurden. Man hat zunächst ein Quadrat, dann ein Rechteck und zum Schluss erst ein Kompositum erstellt. Man wäre eigentlich gar nicht darauf gekommen, einem Rechteck ein Quadrat hinzuzufügen. Dies liegt daran, weil man die Objektnatur an den Klassennamen eigentlich sehr gut erkennen kann. Nicht immer ist dies der Fall, weil nicht immer die Sammlungen so deutlich darauf hinweisen. In diesem Fall lohnt sich ein etwas anderer Aufbau. Dieser verhindert nicht, dass man wenigstens den Objekttyp testen muss, um seine Natur herauszufinden, doch dies ist möglicherweise einfacher als mit einem try- und catch-Block jeweils die Sammlungsmodifikation durchzuführen. Ein kleiner Wermutstropfen bleibt also auch bei der jetzt vorgestellten Lösung, allerdings spart man sich bei der Erstellung unnötige Methoden und kann außen bei der Verwendung mit einer einfachen Fallunterscheidung arbeiten. Die Lösung sieht so aus, dass man einfach zwei Schnittstellen erstellt, von denen die erste alles enthält, was Einheiten und Sammlungen gemein ist. Die zweite dagegen enthält nur solche Komponenten, die speziell für Sammlungen interessant ist. Durch den hierarchischen Aufbau sind dennoch die verschiedenen für Einheiten notwendigen Komponenten auch in den basierend auf der zweiten Schnittstelle erstellten Klassen enthalten. Die Quelltexte sind stark gekürzt, um die wesentlichen Änderungen zwischen der ersten und zweiten Version aufzuzeigen. public interface IKoerper {
// Eigenschaften
// Methoden
double errechneFlaeche();
}
public interface IKoerperKompositum : IKoerper {
// Methoden
void hinzufuegen(IKoerper koerper);
void entfernen(IKoerper koerper);
}
Getrennte Schnittstellen
Die Benutzung beider Schnittstellen erfolgt so, dass solche Klassen für Einheiten wie Quadrat, Rechteck oder Quader die IKoerper-Schnittstelle implementieren, während die zweite Schnittstelle IKoerperKompositum von Sammlungen implementiert wird. public class Quadrat : IKoerper {
// Felder und Eigenschaften
// Methoden mit Konstruktor und errechneFlaeche
// ohne hinzufuegen und entfernen
}
public class Kompositum : IKoerperKompositum {
// Felder mit _kompositum und Eigenschaften
// Methoden mit Konstruktor und errechneFlaeche
// sowie hinzufuegen und entfernen
}
Geordneter Klassenaufbau
Im UML-Diagramm erkennt man den für das Kompositum-Muster charakteristischen Aufbau, wobei nun allerdings die Schnittstelle für Sammlungen an der Stelle der ehemaligen Kompositum-Klasse sitzt und sowohl die Implementierung einer Schnittstelle als auch die Aggregationsbeziehung mit der gleichen Schnittstelle aufweist. Erst von dieser Schnittstelle ausgehend, schließen sich Sammlungsklassen an, von denen genau eine existiert, die für die Sammlungen notwendigen Methoden wie hinzufuegen und entfernen implementieren.
Erweitertes Kompositum
Seminare
|