Patterns in der Softwareentwicklung: Das Kompositum-Muster
Das Kompositum-Muster ermöglicht es, Objekte in Baumstrukturen zusammenzusetzen und einzelne und zusammengesetzte Objekte einheitlich zu behandeln.
- Rainer Grimm
Patterns sind eine wichtige Abstraktion in der modernen Softwareentwicklung. Sie bieten eine klar definierte Terminologie, eine saubere Dokumentation und das Lernen von den Besten. Das klassische Buch "Design Patterns: Elements of Reusable Object-Oriented Software" (kurz Design Patterns), das ich in meiner Serie über Muster vorstelle, enthält 23 Muster. Dank des Kompositum-Musters lassen sich Objekte in Baumstrukturen zusammensetzen und einzelne und zusammengesetzten Objekte einheitlich zu behandeln.
Das Kompositum-Muster ist dem Decorator-Muster, das ich in meinem letzten Artikel vorgestellt habe, sehr ähnlich. Beide Muster sind struktureller Natur. Der Hauptunterschied besteht darin, dass das Kompositum-Muster Baumstrukturen zusammensetzt, das Decorator-Muster aber nur ein Objekt.
Hier sind weitere Details zum Kompositum-Muster.
Kompositum-Muster
Zweck
- Kombiniert Objekte zu Baumstrukturen, um sie einheitlich zu behandeln
Anwendungsfall
- Repräsentiert Teil/Ganzes-Hierarchien
- Clients können einzelne und zusammengesetzte Objekte gleich behandeln
Struktur
Component
- Definiert die Schnittstelle
- Definiert eine Schnittstelle, sodass der
Composite
(Elternteil) auf das Kind seiner Komponente zugreifen kann (optional)
Leaf
- Repräsentiert das einzelne Objekt
- Implementiert die Schnittstelle
Composite
- Stellt das zusammengesetzte Objekt dar
- Definiert Mitgliedsfunktionen, um seine Kinder zu manipulieren
Operation an der Baumstruktur kann man an einem Blattknoten oder einem zusammengesetzten Knoten durchfĂĽhren. Der Vorgang wird direkt ausgefĂĽhrt, wenn es sich um einen Blattknoten handelt. Wenn es sich um einen zusammengesetzten Knoten handelt, wird die Operation an alle untergeordneten Komponenten delegiert. Ein zusammengesetzter Knoten hat eine Liste von Kindern und Mitgliedsfunktionen, um diese hinzuzufĂĽgen oder zu entfernen. Folglich kann jede Komponente (Blattknoten oder zusammengesetzter Knoten) den Vorgang entsprechend bearbeiten.
Beispiel
// composite.cpp
#include <iostream>
#include <string>
#include <vector>
class Graphic {
public:
virtual void print() const = 0;
virtual ~Graphic() {}
};
class GraphicComposite : public Graphic {
std::vector<const Graphic*> children; // (1)
const std::string& name;
public:
explicit GraphicComposite(const std::string& n): name(n){}
void print() const override { // (5)
std::cout << name << " ";
for (auto c: children) c->print();
}
void add(const Graphic* component) { // (2)
children.push_back(component);
}
void remove(const Graphic* component) { // (3)
std::erase(children, component);
}
};
class Ellipse: public Graphic {
private:
const std::string& name;
public:
explicit Ellipse(const std::string& n): name (n) {}
void print() const override { // (4)
std::cout << name << " ";
}
};
int main(){
std::cout << '\n';
const std::string el1 = "ellipse1";
const std::string el2 = "ellipse2";
const std::string el3 = "ellipse3";
const std::string el4 = "ellipse4";
Ellipse ellipse1(el1);
Ellipse ellipse2(el2);
Ellipse ellipse3(el3);
Ellipse ellipse4(el4);
const std::string graph1 = "graphic1";
const std::string graph2 = "graphic2";
const std::string graph3 = "graphic3";
GraphicComposite graphic1(graph1);
GraphicComposite graphic2(graph2);
GraphicComposite graphic(graph3);
graphic1.add(&ellipse1);
graphic1.add(&ellipse2);
graphic1.add(&ellipse3);
graphic2.add(&ellipse4);
graphic.add(&graphic1);
graphic.add(&graphic2);
graphic1.print();
std::cout << '\n';
graphic2.print();
std::cout << '\n';
graphic.print(); // (6)
std::cout << '\n';
graphic.remove(&graphic1);
graphic.print(); // (7)
std::cout << "\n\n";
}
In diesem Beispiel definiert Graphic
die Schnittstelle fĂĽr alle konkreten Komponenten. GraphicComposite
steht fĂĽr den zusammengesetzten Knoten und Ellipse
steht fĂĽr den Blattknoten. GraphicComposite
speichert seine Kinder in einem std::vector<const Graphic*>
(1) und unterstĂĽtzt Operationen zum HinzufĂĽgen und Entfernen von Kindern (2 und 3).
Jede Komponente muss die rein virtuelle Funktion print
implementieren. Die Ellipse
zeigt ihren Namen an (4). GraphicComposite
zeigt ebenfalls seinen Namen an, delegiert aber zusätzlich den Aufruf von show
an seine Kinder (5).
Das Hauptprogramm erstellt eine Baumstruktur mit der Wurzel graphic
. Der Baum graphic
wird zunächst mit dem Teilbaum grahpic1
(6) und dann ohne ihn (7) angezeigt.
Der folgende Screenshot zeigt die Ausgabe des Programms:
Bekannte Verwendungen
Die Anwendung eines Algorithmus wie find
oder find_if
auf dem Container der Standard Template Library kann als eine vereinfachte Anwendung des Kompositum-Musters angesehen werden. Das gilt insbesondere, wenn der Container ein geordneter assoziativer Container wie std::map
ist.
Verwandte Muster
- Das Decorator-Muster ist dem Kompositum-Muster strukturell ähnlich. Der Hauptunterschied besteht darin, dass das Decorator-Muster nur ein Kind besitzt. Außerdem fügt das Decorator-Muster einem Objekt neue Verantwortlichkeiten hinzu, während das Kompositum-Muster die Ergebnisse seiner Kinder zusammenfasst.
- Das Iterator-Muster wird gerne verwendet, um die Komponenten des Baums zu durchlaufen.
- Das Visitor-Muster kapselt die Operationen in ein Objekt, die auf die Komponenten des Baums angewendet werden.
Wie sieht es mit den Vor- und Nachteilen des Kompositum-Musters aus?
Vor- und Nachteile
Beginnen möchte ich mit den Vorteilen.
Vorteile
- Dank der Polymorphie und Rekursion lassen sich komplexe Baumstrukturen einheitlich behandeln.
- Es ist ziemlich einfach, die Baumstruktur mit neuen Komponenten zu erweitern.
Nachteile
- Jede neue Operation auf der Komponente muss auf dem Blattknoten und dem zusammengesetzten Knoten implementiert werden.
- Die Delegation von Operationen verursacht zusätzliche Laufzeitkosten.
Wie geht's weiter?
Mit dem Facade-Muster enthält das Buch "Design Patterns: Elements of Reusable Object-Oriented Software" ein weiteres Strukturmuster. Das Facade-Muster bietet eine vereinfachte Schnittstelle zu einer komplexen Bibliothek oder einem Framework an. (rme)