Das Composite Design Pattern
Studienprojekt von Philipp Hauer. 2009 - 2010. ©
Strategy, Observer, Decorator, Factory Method, Abstract Factory, Singleton, Command, Composite, Facade, State
Literaturverzeichnis, Philipps Blog
Inhalt
Einführung
Wir wollen die Mitarbeiterhierarchie der Firma XY modellieren. Der Hierarchiebaum sieht folgendermaßen aus:
Offensichtlich gibt es "normale" Mitarbeiter und Abteilungsleiter, die andere Mitarbeiter (oder wiederum Abteilungsleiter) unter sich haben.
Welche Anforderungen werden an unser System gestellt?
- Jeder Mitarbeiter und Abteilungsleiter besitzt einen Namen und eine Telefonnummer (getName(), getTelefonnummer()). Darüber hinaus soll jeder Abteilungsleiter den Namen seiner Abteilung kennen (getAbteilung())
- Es sollen Operationen bereitgestellt werden, um Mitarbeiter in den Baum einzuordnen, zu holen oder zu entfernen (add(), getMitarbeiter(), remove()).
- Jeder Abteilungsleiter soll die Anzahl der Mitarbeiter/Abteilungsleiter in seiner Abteilung ausgeben können (getMitarbeiterAnzahl()).
- Die gesamte Hierarchie soll textuell ausgedruckt werden können (print() - dazu später mehr).
Ein erste naiver Entwurf ist folgender:
Wonach schreit dieser Entwurf förmlich? Genau, nach Vererbung. Doch bevor wir den Entwurf reparieren, wollen wir uns seine Schlechtigkeit mit all den Nachteilen in vollen Zügen anschauen.
- Codedopplung. Die Attribute für den Namen und die Telefonnummer, sowie die dazugehörigen Getter und Setter sind absolut redundant. Inkonsistenzen und aufwändige Wartbarkeit treten mit an 1 grenzender Wahrscheinlichkeit auf.
-
Fallunterscheidungen und enge Kopplung. Sowohl der Client als auch der Abteilungsleiter muss in seinem Code ständig zwischen Mitarbeitern und Abteilungsleitern unterscheiden. Dies erzeugt eine Menge an schwer wartbaren Code und verhindert Allgemeingültigkeit. Client und Abteilungsleiter sind hart an Implementierungen gekoppelt. Damit widersprechen wir einem wichtigem OO-Entwurfsprinzip:
Stütze dich auf Abstraktion, nie auf konkrete Klassen.
Das sei an einem Beispiel verdeutlicht. Wie würde der Client über den Baum wandern (traversieren), um die Gesamthierarchie auszugeben? Er müsste fortwährend zwischen Mitarbeitern und Abteilungsleitern differenzieren und sie getrennt behandeln:
Schlechte Ausgabe der Hierarchie:private void print(Abteilungsleiter leiter) { for(int i= 0; i < leiter.getMitarbeiterAnzahl(), i++){ Mitarbeiter ma = leiter.getMitarbeiter(i); System.out.println(ma.getName()+ ". Abteilung: "+ma.getAbteilung()); } for(int i= 0; i < leiter.getAbteilungsleiterAnzahl(), i++){ Abteilungsleiter al = leiter.getAbteilungsleiter(i); System.out.println(al.getName()+" ist Leiter von "+al.getAbteilung()); print(al);//Rekursiver Aufruf } }
Extrem fehleranfällig, unschön und unflexibel. Jede Operation über der gesamten Hierarchie wird zu einer Qual.
Der obige Entwurf ist also schlecht. Wie könnte eine bessere Modellierung aussehen?
Zunächst führen wir Abstraktion in Form der Superklasse Mitarbeiter ein, von der sowohl normale Mitarbeiter als auch Abteilungsleiter erben. In diesem Atemzug überarbeiten wir unser Vokabular: Fortan ist jeder Mitarbeiter (ob nun normaler Mitarbeiter oder Abteilungsleiter) auch ein Mitarbeiter - so wie in der Wirklichkeit. Dies zeigt, wie uns die Realität bei der OO-Modellierung helfen kann. Mitarbeiter, die keine Abteilung leiten, werden als atomare Mitarbeiter bezeichnet.
Wenden wir uns nun dem Abteilungsleiter zu. Muss er wirklich zwischen ihm unterstellten atomaren Mitarbeitern und Abteilungsleitern unterscheiden? Würde man nicht viel mehr Flexibilität gewinnen, wenn er sich stattdessen auf die Abstraktion Mitarbeiter stützt? Auf diese Weise kann er atomare Mitarbeiter und Abteilungsleiter gleich behandeln. Es ist ihm gleich, ob ihm unterstellte Mitarbeiter selbst Abteilungsleiter sind oder nur normale Mitarbeiter.
Dies ist besonders bei den Verwaltungsmethoden interessant und vereinfacht die Implementierung enorm, da die Aufrufe einfach an die Liste delegiert werden können.
class Abteilungsleiter extends Mitarbeiter{
private List<Mitarbeiter> mitarbeiter = new ArrayList<Mitarbeiter>();
public void add(Mitarbeiter ma){
mitarbeiter.add(ma);
}
public void remove(Mitarbeiter ma){
mitarbeiter.remove(ma);
}
public Mitarbeiter getMitarbeiter(int index){
return mitarbeiter.get(index);
}
}
Weiterhin wird auch der Clientcode wesentlich einfacher, denn in diesem muss ebenso keine Unterscheidung mehr getroffen werden (solange die Mitarbeiterschnittstelle ausreicht). Dieser Punkt wird im folgendem noch deutlicher.
Im nächsten Schritt wollen wir die Methoden print() und getMitarbeiterAnzahl() implementieren und zeigen, wie elegant diese realisiert werden können. Beginnen wir mit getMitarbeiterAnzahl(). Wir erweitern die Mitarbeiterschnittstelle um diese Methode und definieren getMitarbeiterAnzahl() abstrakt und zwingen folglich die Unterklassen zur Implementierung der Methode.
Was ist das gewünschte Verhalten? Ruft man getMitarbeiterAnzahl() auf einem atomaren Mitarbeiter auf, so gibt dieser 1 zurück. Klar, er hat ja niemanden unter sich. Auf einem Abteilungsleiter aufgerufen, soll die Methode die Anzahl all seiner Mitarbeiter (plus 1 für ihn selbst) ausgeben. Dies schließt untergeordnete Abteilungsleiter und dessen Mitarbeiter mit ein. Was wäre da einfacher als über die Mitarbeiterliste zu iterieren und auf jedem Mitarbeiter getMitarbeiterAnzahl() aufzurufen und die Ergebnisse zu kumulieren?
Hier spielen wir den großen Vorteil der Abstraktion aus. Der Abteilungsleiter kennt nur die Schnittstelle Mitarbeiter und ruft getMitarbeiterAnzahl() auf. Dank Polymorphie liefern atomare Mitarbeiter 1 und Abteilungsleiter rufen ihrerseits rekursiv getMitarbeiterAnzahl() auf ihren Mitarbeitern auf.
abstract class Mitarbeiter{
public abstract int getMitarbeiterAnzahl();
}
class AtomarerMitarbeiter extends Mitarbeiter{
public int getMitarbeiterAnzahl() {
return 1;
}
}
class Abteilungsleiter extends Mitarbeiter{
private List<Mitarbeiter> mitarbeiter = new ArrayList<Mitarbeiter>();
public int getMitarbeiterAnzahl() {
int summe = 1; //1 für sich selbst
for (Mitarbeiter ma : mitarbeiter) {
summe += ma.getMitarbeiterAnzahl();
}
return summe;
}
//Verwaltungsmethoden...
}
Was haben wir damit gewonnen? Eine sehr elegante und flexible Implementierung, die sich überaus einfach durch den Client bedienen lässt. Er muss nicht mehr manuell durch die Baumstruktur wandern, sondern überlässt dies der Baumstruktur selbst.
public static void main(String[] args) {
Mitarbeiter vorstand = new Abteilungsleiter();
//Mitarbeiterhierarchie unter vorstand aufbauen (add())
//Mitarbeiterhierarchie nutzen
System.out.println(vorstand.getMitarbeiterAnzahl());
}
Sehr schön. Nun zur letzten Anforderung - dem textuellem Ausdrucken der gesamten Hierarchie. Die Realisierung dieser print()-Methode gestaltet sich analog zu getMitarbeiterAnzahl(). Die allgemeine Mitarbeiterschnittstelle wird um die abstrakte Methode print() erweitert. Jeder Mitarbeiter liefern beim Aufruf seinen Namen, Telefonnummer und Abteilung. Abteilungsleiter müssen natürlich zusätzlich den print()-Aufruf an ihre unterstellten Mitarbeiter delegieren.
abstract class Mitarbeiter {
//Damit wir eine schöne Einrückung erhalten,
//nutzen wir einen Stringparameter, der den Abstand enthält
public abstract void print(String abstand);
/*
* Restlicher Code:
*/
public abstract int getMitarbeiterAnzahl();
private String name;
private int telefonNr;
//Konstruktor, in dem die Attribute gesetzt werden können
public Mitarbeiter(String name, int telefonNr) {
this.name = name;
this.telefonNr = telefonNr;
}
//Getter und Setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getTelefonNr() {
return telefonNr;
}
public void setTelefonNr(int telefonNr) {
this.telefonNr = telefonNr;
}
}
class AtomarerMitarbeiter extends Mitarbeiter {
//Hinweis: der Stringparameter dient lediglich der Einrückung
public void print(String abstand) {
System.out.println(abstand + getName() + ". Tel: " + getTelefonNr());
}
/*
* Restlicher Code:
*/
//Konstruktor
public AtomarerMitarbeiter(String name, int telefonNr) {
super(name, telefonNr);
}
//der Rest ist bekannt
public int getMitarbeiterAnzahl() {
return 1;
}
}
class Abteilungsleiter extends Mitarbeiter {
//Hinweis: der Stringparameter dient lediglich der Einrückung
public void print(String abstand) {
System.out.println(abstand + "Abteilungsleiter " + getName() + " (" + getAbteilung() + "). Tel: " + getTelefonNr());
for (Mitarbeiter ma : mitarbeiter) {
ma.print(abstand + " ");//Einrückung
}
}
/*
* Restlicher Code:
*/
private List<Mitarbeiter> mitarbeiter = new ArrayList<Mitarbeiter>();
private String abteilung;
public String getAbteilung() {
return abteilung;
}
public void setAbteilung(String abteilung) {
this.abteilung = abteilung;
}
//Konstruktor
public Abteilungsleiter(String name, String abteilung, int telefonNr) {
super(name, telefonNr);
this.abteilung = abteilung;
}
//Der Rest ist bekannt
public int getMitarbeiterAnzahl() {
int summe = 1; //1 für sich selbst
for (Mitarbeiter ma : mitarbeiter) {
summe += ma.getMitarbeiterAnzahl();
}
return summe;
}
//Verwaltungsmethoden...
public void add(Mitarbeiter ma) {
mitarbeiter.add(ma);
}
public void remove(Mitarbeiter ma) {
mitarbeiter.remove(ma);
}
public Mitarbeiter getMitarbeiter(int index) {
return mitarbeiter.get(index);
}
}
public static void main(String[] args) {
/*
* Firmenhierarchie einmalig aufbauen
*/
//Vertrieb
Abteilungsleiter al1 = new Abteilungsleiter("W. Fischer", "Vertrieb", 001);
al1.add(new AtomarerMitarbeiter("P. Meier", 123));
al1.add(new AtomarerMitarbeiter("I. Schulz", 112));
//Technologie
Abteilungsleiter al2 = new Abteilungsleiter("T. Kunz", "Technologie", 002);
al2.add(new AtomarerMitarbeiter("M. Rehberg", 223));
al2.add(new AtomarerMitarbeiter("O. Riedel", 212));
//Entwicklung
Abteilungsleiter al3 = new Abteilungsleiter("M. Hardbrot", "Entwicklung", 003);
al3.add(new AtomarerMitarbeiter("E.Schmidt", 323));
al3.add(al2);
//Vorstand
Abteilungsleiter vorstand = new Abteilungsleiter("A. Müller", "Vorstand", 004);
vorstand.add(al1);
vorstand.add(new AtomarerMitarbeiter("U. Temann", 442));
vorstand.add(al3);
/*
* Firmenhierarchie ausdrucken
*/
vorstand.print("");
}
Abteilungsleiter A. Müller (Vorstand). Tel: 4
Abteilungsleiter W. Fischer (Vertrieb). Tel: 1
P. Meier. Tel: 123
I. Schulz. Tel: 112
U. Temann. Tel: 442
Abteilungsleiter M. Hardbrot (Entwicklung). Tel: 3
E.Schmidt. Tel: 323
Abteilungsleiter T. Kunz (Technologie). Tel: 2
M. Rehberg. Tel: 223
O. Riedel. Tel: 212
Herzlichen Glückwunsch, wir haben so eben das Composite Design Pattern realisiert! Atomare Mitarbeiter und aggregierende Abteilungsleiter sind unter einer gemeinsamen Schnittstelle zusammengefasst, sodass sie durch den Client einheitlich behandelt werden können.
Das Klassendiagramm in seiner ganzen Pracht sieht wie folgt aus:
Der vollständige Code wurde bereits im Zuge der print()-Implementierung aufgelistet.
Durch den Einsatz des Composite Design Pattern haben wir viele Vorteile gewonnen.
- Einfache Bedienung. Der Client kann Operationen, die die gesamte komplexe Hierarchie betreffen, nur mit einem einzigen Aufruf anstoßen (print(), getMitarbeiterAnzahl()). Die Struktur kümmert sich rekursiv selbst um die korrekte Abarbeitung der Operation.
- Einheitliche Bedienung und Allgemeingültigkeit. Dabei ist es dem Client gleich, ob er nun seine Operationen auf einen normalen Mitarbeiter oder einem Abteilungsleiter ausführt, er arbeitet allein geben die Schnittstelle (print(), getMitarbeiterAnzahl(), getName(), getTelefonNr()). Die konkreten Klassen hinter der Schnittstelle kapseln die Logik, um die Operation korrekt auszuführen (beispielsweise durch Delegation an Untermitarbeiter). Damit ist der Client von dieser Logik befreit, muss keine Fallunterscheidung mehr zwischen den Typen machen und kann folglich schlanken, allgemeingültigen Code schreiben. Der Gedanke der Allgemeingültigkeit gilt natürlich genauso für den Abteilungsleiter, der ebenso nur auf der gemeinsamen Schnittstelle arbeitet.
- Erweiterbarkeit. Die bestehenden Typen in der Hierarchie (AtomarerMitarbeiter, Abteilungsleiter) können sehr einfach um weitere Typen/Klassen erweitert werden (z. B. Fachgruppenleiter, Vorstand). Dazu muss dieser neue Typ lediglich die gemeinsame Schnittstelle Mitarbeiter implementieren und kann fortan in der Struktur eingesetzt werden. Dabei bricht kein Code - jeder beim Abteilungsleiter noch beim Client.
Zum Schluss sei noch eindringlich auf die beiden Vorgehensmöglichkeiten bei Realisierung des Composite Design Pattern hingewiesen. In unserem Mitarbeiterbeispiel haben wir die Strategie der Sicherheit realisiert. Diese besagt, dass nur jene Methoden in die gemeinsame Schnittstelle aufgenommen werden sollten, die auch für atomare Mitarbeiter und Abteilungsleiter gleichermaßen sinnvoll sind. Damit werden die Verwaltungsmethoden (add(), remove() etc.) erst in der Abteilungsleiterklasse definiert.
Was hat das für Folgen? Der Client kann sicher sein, dass alle Methoden, die er auf einem Objekt vom Typ Mitarbeiter (gemeinsame Schnittstelle!) aufruft, wirklich sinnvoll ausgeführt werden. Klar: Ein atomarer Mitarbeiter kann ja keine weiteren Mitarbeiter mit add() aufnehmen, was einen solchen Aufruf sinnlos macht! Allerdings opfert man dabei ein Teil der angestrebten Allgemeingültigkeit, da nun in Fällen, in dem die Mitarbeiterhierarchie dynamisch um weitere Mitarbeiter erweitert werden soll, eine Downcast von Mitarbeiter zu Abteilungleiter notwendig ist. Anders kommt man nicht an die Verwaltungsmethoden. Folglich sind wieder Tests (instanceof) und Fallunterscheidungen notwendig. Da bei unserem Beispiel allerdings eher selten neue Mitarbeiter hinzugefügt werden sollen und mehr lesender Zugriff auf die Hierarchie ausgeführt wird, ist diese Variante gewählt worden.
Es zeigt sich, dass durch den Einsatz des Composite Patterns, ein hohes Maß an Flexibilität und Vereinfachung gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.
Nach dieser Einführung wird im folgenden Abschnitt das Composite Design Pattern formalisiert, näher analysiert und diskutiert.
Das Mitarbeiterbeispiel mit Composite Pattern Termini
Klasse | Composite Teilnehmer |
---|---|
Mitarbeiter | Component |
Atomarer Mitarbeiter | Leaf |
Abteilungsleiter | Composite |
Analyse und Diskussion
Gang Of Four-Definition
Composite:
"Füge Objekte zu Baumstrukturen zusammen, um Teil-Ganzes-Hierarchien zu repräsentieren. Das Kompositionsmuster ermöglicht es Klienten, einzelne Objekte sowie Kompositionen von Objekten einheitlich zu behandeln."
([GoF], Seite 239)
Beschreibung
Das Composite Entwurfsmuster ermöglicht es, eine verschachtelte (Baum)Struktur einheitlich zu behandeln, unabhängig davon, ob es sich um ein atomares Element oder um ein Behälter für weitere Elemente handelt. Der Client kann elegant mit der Struktur arbeiten.
Es wird eine gemeinsame Schnittstelle für die Elementbehälter (Composite, Kompositum; Aggregat, Knoten) und für die atomaren Elemente (Leaf, Blatt) definiert: Component. Diese Schnittstelle Component definiert die Methoden, die gleichermaßen auf Composites und auf Leafs angewandt werden sollen. Composites delegieren oft Aufrufe (operate()) an ihre Components, die atomare Leafs oder wiederrum zusammengesetzte Composites seien können.
Dies vereinfacht den Clientcode (beispielsweise beim Wandern/Traversieren durch die Struktur oder das Verwalten dieser), da der Client nicht mehr zwischen Composite und Leaf unterscheiden muss und allgemeingültigen Code schreiben kann.
//Achtung: Hier wird die Transparenz-Variante des Composite-Patterns realisiert
abstract class Component {
//gemeinsame Methode
public abstract void operation();
public void add(Component comp){
//Leere Defaultimplementierung.
//Für Leafs nicht sinnvoll.
//Werden nur von Composites überschrieben.
}
public void remove(Component comp){
//Leere Defaultimplementierung.
}
public Component getChild(int index){
//Leere Defaultimplementierung.
return null;
}
}
class Leaf extends Component{
public void operation() {
System.out.println("Ich bin ein Leaf!");
}
//ggf. Instanzvariablen und weitere Methoden...
}
class Composite extends Component{
//hier: Components als Liste vorgehalten
private List<Component> childComponents = new ArrayList<Component>();
//rekursiver Aufruf auf kindComponents, ggf. hinzufügen von eigener Logik
public void operation() {
System.out.println("Ich bin ein Composite. Meine Kinder sind:");
for (Component childComps : childComponents) {
childComps.operation();
}
}
//Überschreiben der Defaulimplementierung
public void add(Component comp){
childComponents.add(comp);
}
public void remove(Component comp){
childComponents.remove(comp);
}
public Component getChild(int index){
return childComponents.get(index);
}
}
public static void main(String[] args) {
/*
* Struktur aufbauen
*/
Composite rootComposite = new Composite();
rootComposite.add(new Leaf());
rootComposite.add(new Leaf());
Composite otherComposite = new Composite();
rootComposite.add(otherComposite);
otherComposite.add(new Leaf());
otherComposite.add(new Leaf());
otherComposite.add(new Leaf());
/*
* Einfache Nutzung der Struktur
*/
rootComposite.operation();
}
An dieser Stelle sei auf die zwei grundsätzlichen Vorgehensweise verweisen, die unter Variationen diskutiert werden. Sie betreffen die Breite der Schnittstelle Component, denn uns fällt bei Betrachtung des Klassendiagramms auf, dass nicht alle Methoden der Schnittstelle für Leafs sinnvoll sind.
Variationen
Breite der Component-Schnittstelle
Grundsätzlich gibt es zwei Strategien bei der Definition der Componentschnittstelle: Transparent und Sicherheit.
Transparenz
Die Componentschnittstelle wird breit definiert und ist damit die Summe aus den Leaf- und Compositemethoden. Nun sind viele der Verwaltungsmethoden des Composite (add(), remove()) nicht für Leafs sinnvoll. Zu einem Leaf können keine weiteren Leafs hinzugefügt werden. Aber Leafs müssen trotzdem die Schnittstelle bieten. Wie löst man dieses Problem?
Component enthält Defaultimplementierungen, die einfach nichts Zweckmäßiges tun. Ein Leaf würde diese Implementierung belassen und nur die für ihn sinnvollen Methoden (operation()) überschreiben bzw. implementieren. Eine Composite hingegen überschreibt alle Methoden.
Wie könnte so eine Defaultimplementierung geartet sein?
- Zurückliefern von false/null (return-Methoden) oder nichts tun (void-Methoden).
- Gegebenenfalls zurückliefern einer leeren Collection (getChildren()).
- Werfen einer Exception: UnsupportedOperationException. Dies zwingt den Client allerdings zur Fallunterscheidung oder Ausnahmebehandlung mit try-catch.
Konsequenzen:
- Der Client profitiert von der gewonnenen Transparenz. Er hat den vollen Funktionsumfang aller möglichen Componenten zur Verfügung und muss keine Fallunterscheidung für Leafs und Composites machen. Er kann die Components wirklich einheitlich behandeln.
- Allerdings kann er sinnlose Methoden auf Leafs aufrufen (add() etc.) und muss damit rechnen, dass ein Aufruf nichts bewirken kann (oder gar Exceptions geworfen werden).
Die Gang of Four präferiert die Variante der Transparenz ([GoF], Seite 245).
Sicherheit
Die Componentenschnittstelle wird schmal definiert und beschreibt den kleinsten gemeinsamen Nenner aus den Leaf- und Compositemethoden.
Konsequenzen:
- Der Client gewinnt enorm an Sicherheit. Er kann sich darauf verlassen, dass alle Methoden, die er auf Components aufrufen kann, auch sinnvoll implementiert sind. Bei allen gemeinsamen Methoden kann der Clientcode allgemeingültig geschrieben werden.
- Allerdings wird damit Funktionsumfang eingebüßt. Um an die compositespezifischen Methoden zu gelangen, sind Fallunterscheidungen und Casts nötig. Er muss herausfinden, ob es sich bei einer Component um eine Composite handelt (instanceof-Test) und kann erst dann sicher einen Downcast durchführen, um die Compositemethoden (Verwaltungsoperationen) zu nutzen. Ist kein instanceof-Test möglich, so kann die Componentschnittstelle, um eine abstrakte isComposite()-Methode erweitert werden.
Zwischen diesen beiden Varianten gibt es keinen klaren Sieger. Es muss immer im konkreten Fall nach der Zweckmäßigkeit entschieden werden. Unter Anwendung in der Java Standardbibliothek werde ich auf diese Vorgehensweisen wieder Bezug nehmen und Beispiele bringen.
Referenz auf Elternkomponente
In einigen Fällen ist es hilfreich, wenn Kinder ihre Eltern kennen - fast so, wie in der wirklichen Welt. ;-) Das bedeutet, dass jede Component zusätzlich eine Referenz auf seine Elternkomponente hält.
Dies ermöglicht ein vereinfachtes und beliebiges Traversieren und Verwalten der Struktur. Weiterhin können Components leichter gelöscht werden, da einfach in der Hierarchie nach oben gegangen werden kann. Anschließend muss das zu löschende Kind aus der Kinderliste des Elterncomposite entfernt werden.
Allerdings besteht hierbei die Gefahr von Inkonsistenz. Es muss immer sichergestellt werden, dass die Elternreferenz mit der Kindreferenz der Elternkomponente korrespondiert. [GoF] schlägt vor, das Erstellen und Löschen von Referenzen an einer zentralen Stelle zu verlagern, beispielsweise in einer Composite-Superklasse, die den generischen Verwaltungscode enthält. Die Referenzen werden nur dann geändert, wenn eine Komponente gelöscht oder hinzugefügt wird - und nur dann. Diese Superklasse wird von allen Composites erweitert. Damit muss dieser kritische Code nur einmal geschrieben werden und wird von alle Composites vererbt.
Wahl der Datenstruktur für die Aggregation der Kinder im Composite
Auch soll darauf hingewiesen werden, dass die Datenstruktur, in der ein Composite seine Kindkomponenten vorhält, nicht zwangläufig eine einfache Liste sein muss. Je nach Anforderung des Systems (schnelle Iteration, schnelles Auffinden, Löschen oder Hinzufügen; Sortierung) kann die Datenstruktur der Wahl eine Liste, eine verlinkte Liste, ein Set, ein Baum, eine HashMap, eine sortierte Map/Set oder eine ganz andere sein.
Auch kann eine Composite seine Kinder in einer anderen Collection halten, als eine andere Composite.
Ort des Verwaltungscodes
In diesem Zusammenhang sei auch darauf aufmerksam gemacht, dass der Code zur Verwaltung der Kinder (Instanzvariable zur Datenstruktur, Verwaltungsmethoden add(), remove(), get() etc.) nicht zwangläufig in jedem Compositetyp erneut implementiert werden muss. Er könnte in einer neuen abstrakten Compositeklasse eingefügt werden, die jede Composite erweitert und sofort die Verwaltungslogik erbt.
Performancegewinn durch Caching
In manchen Fällen kann das Traversieren durch die Baumstruktur oder die rekursive Berechnungen durch diese sehr teuer und aufwändig sein. Dann ist es sinnvoll, die Ergebnisse solcher Berechnungen zwischen zu speichern. Bei einer späteren Anfrage wird einfach das gespeicherte Ergebnis geliefert, statt eine erneute Berechnung durchzuführen.
Doch was, wenn sich etwas an den enthaltenen Komponenten geändert hat? Dann muss der Speicher als ungültig markiert werden. Dies wird am einfachsten realisiert, in dem Kindkomponenten eine Referenz auf ihre Eltern besitzen und ihnen über eine definierte Methode (parentComponent.setCacheInvalid()) mitteilen, dass sie sich geändert haben und somit der Zwischenspeicher ungültig geworden ist. Dieser Aufruf wird dann rekursiv die Hierarchie aufwärts durchgereicht. Alle betroffenen Caches wurden damit entsprechend markiert.
Anwendungsfälle
-
Hierarchien von Objekten abbilden.
- Bestandteile, Inhalte, Mitglieder etc.
- Client soll in die Lage versetzt werden, zusammengesetzte und einzelne Objekte einheitlich zu behandeln.
Konkrete Anwendungsfälle sind:
-
Dateisystem. Dateien und Ordner können mit dem Composite Pattern modelliert werden.
Dabei repräsentieren die Dateien die Leafs und die Ordner den Composites, da sie ja wiederum weitere Dateien und Ordnern enthalten können. Die gemeinsame Schnittstelle von Ordnern und Dateien ("File", unser Componentinterface) kann Methoden wie rename(), add() oder delete() bereitstellen ([GruntzC], Folie 2). -
Menüs. Menüs bestehen ja bekanntlich aus einem Wurzeleintrag (Composite!) in der Menüleiste (Datei, Bearbeiten, Ansicht etc.). Darin enthalten sind weitere Menüpunkte. Manche von ihnen sind einfache Schaltflächen (Leaf!) und andere enthalten wiederum weitere Menüpunkte (Composite!).
Auch diese Struktur entspricht einer typischen Baumstruktur, die mit dem Composite Pattern elegant abgebildet werden kann. Denkbar sind gemeinsame Befehle, wie setName(), setEnabled() oder setTooltip() ([GruntzC], Folie 3). -
GUIs. Das Composite Pattern findet ebenfalls bei grafischen Benutzeroberflächenanwendung. Es gibt atomare GUI-Komponenten (Das passende Swing-Wording "Component" für GUI-Elemente kommt nicht ohne Grund! ;-)), wie Buttons, Textfelder, Labels oder Checkboxen, aber auch aggregierende Container, wie Frames, Panels oder ScrollPanes, die wiederum weitere GUI-Komponenten aufnehmen können.
Wird nun durch den Client auf dem (obersten) Frame die draw()-Methode aufgerufen, so ruft der Frame draw() auf all seinen Kindern auf (hier: Panel, Button1 und Button2) und das Panel wiederum auf dessen Kinder (Label, ScrollPane) und so weiter. Auf diese Weise wird die Oberfläche gezeichnet und der Client musste nur eine Methode aufrufen. Auch verfügen alle Container über Methoden, wie add() und remove(). Siehe Anwendung in der Java Standardbibliothek.
Vorteile
- Repräsentation von verschachtelten Strukturen. Es können Strukturen mit primitiven und zusammengesetzten Objekten implementiert werden. Zusammengesetzte Objekte können aus primitiven oder wiederum aus aggregierenden Objekten bestehen. Eine beliebige Verschachtelungstiefe und -breite ist somit möglich. Wann immer beim Client oder bei zusammengesetzten Objekten ein primitives Objekt erwartet wird, ebenso ein zusammengesetztes zurückgegeben werden.
- Vereinfachter Clientcode. Der Client ist von der Last befreit, Fallunterscheidungen und separate Funktionen für jedes mögliche Element zu schreiben. Sein Code wird schlank. Dank der Componentschnittstelle ist der Client von einer konkreten Implementierung entkoppelt und stützt sich allein auf Abstraktion. Die Unterschiede zwischen einzelnen Objekt und zusammengesetzten Objekten können ignoriert werden, da eine oder mehrere Operationen für die gesamte Struktur gelten.
- Elegantes Arbeiten mit der Baumstruktur. Der Client kann auf elegante Weise durch die Hierarchie traversieren, Operationen aufrufen (einmalig operate() auf Wurzelelement) und diese verwalten (neue Elemente hinzufügen und bestehende löschen).
- Flexibilität und Erweiterbarkeit. Um neue Elemente (Leafs oder Composites) in das System zu integrieren, bedarf es keiner Codeänderungen - weder beim Client noch bei bestehenden Componenten. Die neuen Elemente müssen lediglich die allgemeine Componentschnittstelle implementieren und können fortan in die Baumstruktur eingearbeitet werden. Bestehender Code behält seine Gültigkeit.
Nachteile
- Schwierige Definition der allgemeinen Componentschnittstelle. Zu entscheiden, welche Operationen in der allgemeinen Schnittstelle und welche nur in der Compositeklasse definiert werden, ist äußert problematisch. Hier müssen die Anforderungen des konkreten Anwendungsfalls untersucht werden und genau zwischen Transparenz und Sicherheit abgewogen werden.
- Spätere Einschränkungen der erlaubten Compositeelemente ist diffizil. Möchte man zu einem späterem Zeitpunkt die Kindkomponenten, die eine Composite haben darf, einschränken, so ist dies nur schwer zu realisieren und läuft auf aufwendige Typprüfungen zur Laufzeit hinaus. Diese Gefahr resultiert aus der ursprünglich angestrebten Allgemeingültigkeit des Entwurfs.
Anwendung in der Java Standardbibliothek
In der Java Core-API wird das Composite Design Pattern an zahlreichen Stellen angewandt.
Die folgenden Beispiele sind [GruntzC] (Folien 13-15) entlehnt, wurden jedoch angepasst und erweitert.
AWT: java.awt.Component
Wie der Klassenname von java.awt.Component erahnen lässt, handelt es sich hierbei um eine Realisierung des Composite Design Pattern.
Wir sehen, dass AWT die Strategie der Sicherheit verfolgt hat. Die Componentschnittstelle enthält nur jene Methoden, die für alle Componenten sinnvoll sind (getParent(), getWidth(), setVisible() etc.). Nur der Container (unser Composite) enthält die compositetypischen Verwaltungsmethoden (add(), remove() etc.). Nichtsdestotrotz bietet Component eine sehr breite Schnittstelle mit unzählige Methoden und ermöglicht damit das einheitliche Arbeiten mit seinen Subklassen.
Swing: javax.swing.JComponent
Auf die eben vorgestellte AWT-API setzt Swing auf. Allerdings mit dem Unterschied, dass Swing die Strategie der Transparenz verfolgt. Die abstrakte Klasse JComponent repräsentiert die Schnittstelle zu allen Swing-Komponenten und enthält neben zahlreichen Methoden auch jene, die eigentlich nur für die Verwaltung von Componenten durch Composites gedacht sind. Werden diese Verwaltungsmethoden auf Leafs aufgerufen, geschieht einfach nichts.
Das Zusammenspiel der AWT- und Swinglibrary wird im folgendem Diagramm dargestellt: