Patterns in der Softwareentwicklung: Das Decorator-Muster

Die Aufgabe des Decorator-Musters ist es, ein Objekt dynamisch mit Verantwortlichkeiten zu erweitern.

Lesezeit: 4 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 9 Beiträge

(Bild: Kenneth Summers / 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. Das Decorator-Muster ist das dritte Strukturmuster aus dem Buch. Es erweitert ein Objekt dynamisch mit Verantwortlichkeiten.

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

Die ersten beiden Strukturmuster sind das Adapter-Muster und das Bridge-Muster. Man sollte das Decorator-Muster nicht mit dem Decorator-Idiom in Python verwechseln. Die beiden verfolgen unterschiedliche Ziele: Mit dem Decorator-Muster kann man Objekte dynamisch erweitern, mit dem Decorator-Idiom hingegen Funktionen dynamisch erweitern.

Hier sind die Fakten zum Decorator-Muster.

Zweck

Erweitert ein Objekt dynamisch mit Verantwortlichkeiten

Auch bekannt als

Wrapper

Anwendungsfall

  • Hinzufügen oder Entfernen neuer Verantwortlichkeiten von einzelnen Objekten zur Laufzeit
  • Die Erweiterung der Klassenhierarchie durch Subclassing (siehe Adapter-Muster) ist nicht anwendbar

Component

  • Definiert die gemeinsame Schnittstelle für den Decorator und die ConcreteComponent

ConcreteComponent

  • Das zu dekorierende Objekt
  • Definiert das grundlegende Verhalten

Decorator

  • Implementiert die Schnittstelle von Component
  • Hat einen Verweis auf die Component
  • Er delegiert alle Operationen an die Component; diese kann entweder ein zusätzlicher ConcreteDecorator oder eine ConcreteComponent sein

ConcreteDecorator

  • Erweitert das Verhalten der Component
  • Überschreibt die Mitgliedsfunktionen seiner Basiskomponente
  • Ruft typischerweise in seiner überschreibenden Mitgliedsfunktion die überschreibende Mitgliedsfunktion seiner Basiskomponente auf

Eine wichtige Beobachtung des Decorator-Musters ist, dass mehrere Dekoratoren übereinander gesteckt werden können, wobei jeder Dekorator neue Funktionen zu den überschriebenen Mitgliedsfunktionen hinzufügt.

Das folgende Beispiel basiert auf dem Beispiel auf der Wikipedia-Seite Decorator Pattern.

// decorator.cpp 
// (based on https://en.wikipedia.org/wiki/Decorator_pattern)

#include <iostream>
#include <string>

struct Shape {
  virtual ~Shape() = default;

  virtual std::string GetName() const = 0;
};

struct Circle : Shape {
  void Resize(float factor) { radius *= factor; }

  std::string GetName() const override {
    return std::string("A circle of radius ") + 
      std::to_string(radius);
  }

  float radius = 10.0f;
};

struct ColoredShape : Shape {
  ColoredShape(const std::string& color, Shape* shape)    // (1)
      : color(color), shape(shape) {}

  std::string GetName() const override {
    return shape->GetName() + " which is colored " + 
    color + ".";                                           // (2)
  }

  std::string color;
  Shape* shape;
};

int main() {

  std::cout << '\n';

  Circle circle;
  ColoredShape colored_shape("red", &circle);
  std::cout << colored_shape.GetName() << '\n';

  std::cout << '\n';

}

In diesem Beispiel ist Shape die Component. Circle steht für die ConcreteComponent und ColoredShape für den Decorator. ColoredShape erhöht sich Shape in seinem Konstruktor (1), ruft in (2) die Mitgliedsfunktion shape->GetName() auf und dekoriert es mit seiner Farbe.

Hier ist die Ausgabe des Programms:

Wenn man ein FramedShape als zusätzlichen Decorator ableitet, kannt man sie auf beliebige Weise zusammenstecken:

// decoratorFrame.cpp 
// (based on https://en.wikipedia.org/wiki/Decorator_pattern)

#include <iostream>
#include <string>

struct Shape{
  virtual std::string str() const = 0;
};

class Circle : public Shape{
  float radius = 10.0f;
  
public:
  std::string str() const override{
    return std::string("A circle of radius ") + 
      std::to_string(radius);
  }
};

class ColoredShape : public Shape{
  std::string color;
  Shape& shape;
public:
  ColoredShape(std::string c, Shape& s): color{c}, shape{s} {}
  std::string str() const override{
    return shape.str() + std::string(" which is coloured ") +
      color;
  }
};

class FramedShape : public Shape{
  Shape& shape;
public:
  FramedShape(Shape& s): shape{s} {}
  std::string str() const override{
    return shape.str() + std::string(" and has a frame");
  }
};

int main(){

  Circle circle;
  ColoredShape coloredShape("red", circle);    // (1)
  FramedShape framedShape1(circle);            // (2)
  FramedShape framedShape2(coloredShape);      // (3)
  
  std::cout << circle.str() << '\n';
  std::cout << coloredShape.str() << '\n';
  std::cout << framedShape1.str() << '\n';
  std::cout << framedShape2.str() << '\n';

}

Der ColoredShape nimmt einen Circle (1), der FramedShape einen Circle (2) oder einen ColoredShape (3) an. Die entsprechenden Mitgliedsfunktionen str zeigen die verschiedenen Kombinationen an.

Verwandte Muster

  • Das Composite-Muster ist ein Strukturmuster, das dem Dekorator ähnlich ist. Der Hauptunterschied besteht darin, dass das Decorator-Muster nur ein Kind hat. Außerdem fügt es einem Objekt neue Verantwortlichkeiten hinzu, während das Composite-Muster die Ergebnisse seiner Kinder zusammenfasst.
  • Das Adapter-Muster ändert die Schnittstelle eines Objekts, während ein Decorator die Verantwortlichkeiten des Objekts erweitert.
  • Das Bridge-Muster hat den Zweck, die Schnittstelle von der Implementierung zu trennen. Decorators sind steckbar, aber weder Brücken noch Adapter.
  • Das Strategy Pattern verwendet Objekte, um die Implementierung zu ändern, aber der Decorator verwendet Objekte, um die Verantwortlichkeiten des Objekts zu erweitern.

Vorteile

  • Die Dekoratoren können zur Laufzeit beliebig übereinander gestülpt werden.
  • Jeder Dekorator kann eine Variante des Verhaltens implementieren und folgt somit dem Prinzip dem Single Responsibility.

Nachteile

  • Aufgrund der delegierten Aufrufe von Mitgliedsfunktionen ist der Kontrollfluss schwer zu verfolgen.
  • Der Aufruf der delegierten Mitgliedsfunktionen kann die Performanz des Programms beeinträchtigen.
  • Es ist ziemlich kompliziert, einen Dekorator aus zusammengesteckten Dekoratoren zu entfernen.

Das Composite-Muster ist ein Strukturmuster und dem Decorator-Muster sehr ähnlich. Der Hauptunterschied besteht darin, dass das Letzteres nur ein Kind hat. Außerdem fügt es einem Objekt neue Verantwortung hinzu, während das Composite-Muster die Ergebnisse seiner Kinder zusammenfasst. (rme)