Patterns in der Softwareentwicklung: Das Decorator-Muster
Die Aufgabe des Decorator-Musters ist es, ein Objekt dynamisch mit Verantwortlichkeiten zu erweitern.
- 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. Das Decorator-Muster ist das dritte Strukturmuster aus dem Buch. Es erweitert ein Objekt dynamisch mit Verantwortlichkeiten.
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.
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
Struktur
Component
- Definiert die gemeinsame Schnittstelle fĂĽr den
Decorator
und dieConcreteComponent
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ätzlicherConcreteDecorator
oder eineConcreteComponent
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.
Beispiel
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.
Vor- und Nachteile
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.
Wie geht's weiter?
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)