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.

Lesezeit: 4 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen
World,Philosophy,Day,Education,Concept,With,Tree,Of,Knowledge,Planting

(Bild: Chinnapong / Shutterstock.com)

Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

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.

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.

// 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.

  • 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.

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)