Composite Pattern: Musterlösung für Teil-Ganzes-Hierarchien

Dynamische Datenstrukturen wie die Baumstruktur einer Dateiverwaltung oder eines Programm-Interfaces erfordern ein klares, möglichst eindeutiges Hierarchie-Gefüge. Die Umsetzung solcher Strukturen ist häufig jedoch gar nicht so einfach. So gilt es beispielsweise darauf zu achten, dass nicht jedes Mal der Typ eines Objekts vor der eigentlichen Datenverarbeitung abgefragt werden muss, da ein solches Szenario weder effizient noch performant wäre. Insbesondere, wenn viele primitive Objekte auf zusammengesetzte Objekte treffen, empfiehlt sich daher der Einsatz des sogenannten Composite Design Patterns (dt. Kompositum-Entwurfsmuster). Der Softwaredesign-Ansatz ermöglicht es Clients, individuelle und zusammengesetzte Objekte einheitlich zu behandeln, indem ihre Unterschiede vor dem Client verborgen werden.

Was ist das Composite Pattern (Composite-Muster)?

Das Composite Design Pattern (dt. Kompositum-Entwurfsmuster), kurz Composite Pattern, ist eines der 23 GoF-Design-Pattern für die Software-Entwicklung, die 1994 von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides (gemeinsam auch als „Gang of Four“ bekannt) veröffentlicht wurden. Wie beispielsweise auch das Facade Pattern und das Decorator Pattern zählt es dabei zu den Strukturmustern, deren grundlegende Funktion es ist, Objekte und Klassen zu größeren Strukturen zusammenzufassen.

Welche Probleme löst das Composite-Entwurfsmuster?

Der grundsätzliche Zweck des Composite Design Patterns ist – wie bei allen GoF-Mustern – der bestmögliche Umgang mit wiederkehrenden Design-Problemen in der objektorientierten Entwicklung. Das gewünschte Ergebnis ist eine möglichst flexible Software, die sich durch leicht implementierbare, testbare, austauschbare und wiederverwendbare Objekte auszeichnet. Das Composite Pattern beschreibt zu diesem Zweck einen Weg, wie sich einzelne Objekte und zusammengesetzte Objekte auf die gleiche Weise behandeln lassen. Auf diese Weise lässt sich eine Objektstruktur schaffen, die leicht verständlich ist und maximal effiziente Client-Zugriffe ermöglicht. Zudem wird auch die Fehleranfälligkeit des Codes minimiert.

Composite Design Pattern: Grafische Darstellung (UML)

Um die angesprochenen, effizienten Teil-Ganzes-Hierarchien umzusetzen, sieht das Composite-Muster die Implementierung eines einheitlichen Komponenten-Interfaces für simple Teil-Objekte, die auch als Leaf-Objekte (engl. für „Blatt“) bezeichnet werden, und zusammengesetzte Composite-Objekte (engl. für „zusammengesetzt“) vor. Einzelne Leaf-Objekte binden dieses Interface direkt ein, Composite-Objekte geben konkrete Client-Anforderungen an die Schnittstelle automatisch an ihre untergeordneten Komponenten weiter. Für den Client spielt es dabei keine Rolle, um welchen Typ von Objekt (Teil oder Ganzes) es sich handelt, da er lediglich das Interface ansprechen muss.

Das nachfolgende Klassendiagramm in der Modellierungssprache UML macht die Zusammenhänge und Hierarchien in einer Software auf Composite-Pattern-Basis deutlicher.

Stärken und Schwächen des Composite-Entwurfsmusters

Das Composite Pattern zählt in der Softwareentwicklung zu einer festen Größe. Insbesondere Projekte mit stark verschachtelten Strukturen profitieren von dem praktischen Ansatz für die Organisation von Objekten: Ob primitives Objekt oder zusammengesetztes Objekt, ob mit simplen oder mit komplexen Abhängigkeiten – die Tiefe und Breite der Verschachtelung spielt beim Composite Design Pattern grundsätzlich keine Rolle. Die Unterschiede zwischen den Objektarten können vom Client gänzlich ignoriert werden, sodass keine separaten Funktionen für den Zugriff erforderlich sind. Das bringt den Vorteil mit sich, dass der Client-Code einfach und schlank bleibt.

Eine weitere Stärke des Kompositum-Entwurfsmusters ist die Flexibilität und einfache Erweiterbarkeit, die das Pattern einer Software verleiht: Das universelle Komponenten-Interface ermöglicht es, neue Leaf- und Composite-Objekte ohne Code-Änderungen einzubinden – ob auf Client-Seiten oder bei bereits bestehenden Objektstrukturen.

Auch wenn das Composite Pattern und seine einheitliche Schnittstelle eine Reihe von Vorteilen bieten, ist diese Musterlösung nicht frei von Schwächen. Insbesondere das Interface kann Entwicklern eine Menge Kopfzerbrechen bereiten. Bereits die Implementierung stellt die Verantwortlichen vor große Herausforderungen, da beispielsweise zu entscheiden ist, welche Operationen konkret in der Schnittstelle und welche in den Composite-Klassen definiert werden sollen. Auch eine nachträgliche Anpassung der Composite-Eigenschaften (zum Beispiel die Einschränkung, welche Child-Elemente erlaubt sind) erweist sich meist als kompliziert und nur schwer zu realisieren.

Vorteile Nachteile
Liefert alles, um stark verschachtelte Objektstrukturen darzustellen Implementierung des Komponenten-Interfaces ist sehr herausfordernd
Schlanker, leicht verständlicher Programmcode Nachträgliche Anpassung der Composite-Eigenschaften sind kompliziert und zur schwer umzusetzen.
Gute Erweiterbarkeit  

Anwendungsszenarien für das Composite Pattern

Der Einsatz des Composite-Musters zahlt sich überall dort aus, wo Operationen auf dynamische Datenstrukturen ausgeführt werden sollen, deren Hierarchie von komplexer Breite und/oder Tiefe ist. Man spricht in diesem Fall auch von einer binären Baumstruktur, die für die verschiedensten Softwareszenarios interessant ist und häufig zur Verwendung kommt. Typische Beispiele sind:

Dateisysteme: Zu den wichtigsten Komponenten von Geräte-Software gehören Dateisysteme. Diese lassen sich optimal mit dem Composite Pattern abbilden: Einzelne Dateien als Leaf-Objekte und Ordner – die ihrerseits selbst Dateien oder weitere Ordner enthalten können – als Composite-Objekte.

Software-Menüs: Auch Programmmenüs stellen einen typischen Use Case für eine binäre Baumstruktur nach dem Composite Design Pattern dar. In der Menüleiste befinden sich ein oder mehrere Wurzeleinträge (Composite-Objekte) wie „Datei“. Diese gewähren Zugriff auf verschiedene Menüpunkte, die wahlweise direkt klickbar sind (Leaf) oder weitere untergliederte Menüs (Composite) enthalten.

Grafische Benutzeroberflächen (GUIs): Auch bei der Gestaltung von grafischen Benutzeroberflächen können Baumstrukturen und das Composite-Entwurfsmuster eine wichtige Rolle spielen. Abseits von simplen Leaf-Elementen wie Buttons, Textfeldern oder Checkboxen sorgen zusammenfassende Composite-Behälter wie Frames oder Panels für eine klare Struktur und mehr Übersichtlichkeit.

Code-Beispiel: Composite Pattern

In kaum einer Programmiersprache ist der Composite-Pattern-Ansatz so fest etabliert wie in Java. Unter anderem bildet das Muster beispielsweise auch die Basis des Abstract Window Toolkits (AWT), eines praktischen und beliebten API mit rund 50 einsatzfertigen Java-Klassen für die Entwicklung plattformübergreifender Java-Oberflächen. Auch im nachfolgenden Code-Beispiel, das sich am Beitrag „Composite Design Pattern in Java“ auf baeldung.com orientiert, haben wir uns daher für die populäre Programmiersprache entschieden.

Im Praxis-Beispiel soll die hierarchische Struktur von Abteilungen (departments) in einem Unternehmen (company) dargestellt werden. Zunächst wird hierfür das Komponenten-InterfaceDepartment“ definiert:

public interface Department {
	void printDepartmentName();
}

Anschließend werden die zwei einfachen Leaf-KlassenFinancialDepartment“ (für die Finanzabteilung) und „SalesDepartment“ (für die Verkaufsabteilung) definiert. Beide implementieren die Methode printDepartmentName() von der Komponenten-Schnittstelle, enthalten aber ansonsten keine weiteren Department-Objekte.

public class FinancialDepartment implements Department {
	private Integer id;
	private String name;
	public void printDepartmentName() {
		System.out.println(getClass().getSimpleName());
	}
	// standard constructor, getters, setters
}
public class SalesDepartment implements Department {
	private Integer id;
	private String name;
	public void printDepartmentName() {
		System.out.println(getClass().getSimpleName());
	}
	// standard constructor, getters, setters
}

Eine zur Hierarchie passende Composite-Klasse wird schließlich mit „HeadDepartment“ definiert. Diese setzt sich aus mehreren Department-Komponenten zusammen und enthält zusätzlich zu der printDepartmentName()-Methode auch Methoden, um weitere Elemente hinzuzufügen (addDepartment) oder vorhandene Elemente zu entfernen (removeDepartment):

public class HeadDepartment implements Department {
	private Integer id;
	private String name;
	private List<department> childDepartments;</department>
	public HeadDepartment(Integer id, String name) {
		this.id = id;
		this.name = name;
		this.childDepartments = new ArrayList<>();
	}
	public void printDepartmentName() {
		childDepartments.forEach(Department::printDepartmentName);
	}
	public void addDepartment(Department department) {
		childDepartments.add(department);
	}
	public void removeDepartment(Department department) {
		childDepartments.remove(department);
	}
}