Decorator Pattern

Decorator Pattern

vg

Das Decorator Pattern ist ein Strukturmuster, das es ermöglicht, einem Objekt zur Laufzeit zusätzliche Funktionalitäten hinzuzufügen. Es stellt sicher, dass die ursprüngliche Klasse nicht verändert wird. Stattdessen wird die Funktionalität durch die Verwendung von Dekoratoren erweitert. Dies ist besonders nützlich, wenn eine Vielzahl von optionalen Features benötigt wird, die je nach Bedarf hinzugefügt werden können.

Was ist das Decorator Pattern?

Das Decorator Pattern bietet eine flexible Möglichkeit, Objekte zu erweitern, ohne sie direkt zu ändern. Stattdessen wird ein Wrapper um das Objekt gelegt, der die zusätzlichen Funktionalitäten hinzufügt. Dieser Wrapper, der als „Decorator“ bezeichnet wird, folgt der gleichen Schnittstelle wie das ursprüngliche Objekt. Dadurch können Dekoratoren miteinander kombiniert werden, um ein objektorientiertes Design zu ermöglichen.

Es gibt vier Hauptkomponenten im Decorator Pattern:

  1. Component: Die gemeinsame Schnittstelle, die das dekorierte Objekt und die Dekoratoren implementieren.
  2. ConcreteComponent: Das konkrete Objekt, das dekoriert werden soll. Es implementiert die Component-Schnittstelle.
  3. Decorator: Ein Wrapper, der ebenfalls die Component-Schnittstelle implementiert. Der Dekorator hält eine Referenz auf ein Component-Objekt und delegiert Aufrufe an dieses.
  4. ConcreteDecorator: Eine konkrete Implementierung des Dekorators, der zusätzliche Funktionalität hinzufügt.

Beispiel des Decorator Patterns in C++

Angenommen, wir entwickeln ein System für das Erstellen von Textnachrichten. Wir wollen verschiedene Features wie „Fett“, „Kursiv“ und „Unterstrichen“ hinzufügen. Anstatt eine Vielzahl von Klassen zu erstellen, um jede Kombination dieser Eigenschaften abzubilden, können wir das Decorator Pattern verwenden.

#include <iostream>
#include <memory>
#include <string>

// Component: Gemeinsame Schnittstelle
class Message {
public:
    virtual void send() const = 0;
    virtual ~Message() = default;
};

// ConcreteComponent: Das ursprüngliche Objekt
class SimpleMessage : public Message {
private:
    std::string text;

public:
    SimpleMessage(const std::string& text) : text(text) {}

    void send() const override {
        std::cout << text << std::endl;
    }
};

// Decorator: Der abstrakte Dekorator, der die Component-Schnittstelle implementiert
class MessageDecorator : public Message {
protected:
    std::shared_ptr<Message> wrappedMessage;

public:
    MessageDecorator(std::shared_ptr<Message> message) : wrappedMessage(message) {}

    void send() const override {
        wrappedMessage->send();
    }
};

// ConcreteDecorator: Ein konkreter Dekorator, der zusätzliche Funktionalität hinzufügt
class BoldDecorator : public MessageDecorator {
public:
    BoldDecorator(std::shared_ptr<Message> message) : MessageDecorator(message) {}

    void send() const override {
        std::cout << "<b>";
        MessageDecorator::send();
        std::cout << "</b>";
    }
};

class ItalicDecorator : public MessageDecorator {
public:
    ItalicDecorator(std::shared_ptr<Message> message) : MessageDecorator(message) {}

    void send() const override {
        std::cout << "<i>";
        MessageDecorator::send();
        std::cout << "</i>";
    }
};

// Client-Code
int main() {
    std::shared_ptr<Message> message = std::make_shared<SimpleMessage>("Hallo, Welt!");

    // Dekorieren mit Fettdruck
    std::shared_ptr<Message> boldMessage = std::make_shared<BoldDecorator>(message);
    boldMessage->send();  // <b>Hallo, Welt!</b>

    // Dekorieren mit Kursiv
    std::shared_ptr<Message> italicMessage = std::make_shared<ItalicDecorator>(boldMessage);
    italicMessage->send();  // <i><b>Hallo, Welt!</b></i>

    return 0;
}

Erklärung des C++-Beispiels

  1. Component: Die Message-Klasse definiert die gemeinsame Schnittstelle mit der Methode send(), die die Nachricht sendet. Sowohl die ursprüngliche Nachricht als auch die Dekoratoren müssen diese Methode implementieren.
  2. ConcreteComponent: Die Klasse SimpleMessage ist das grundlegende, nicht dekorierte Objekt. Sie speichert den Text und implementiert die send()-Methode.
  3. Decorator: Die MessageDecorator-Klasse ist der abstrakte Dekorator. Sie nimmt ein Message-Objekt als Parameter und ruft dessen send()-Methode auf. Diese Klasse ermöglicht das Hinzufügen von Funktionalität durch Vererbung.
  4. ConcreteDecorator: Die BoldDecorator und ItalicDecorator erweitern den Dekorator, um zusätzliche Funktionalität hinzuzufügen. BoldDecorator fügt <b>-Tags hinzu, während ItalicDecorator <i>-Tags hinzufügt.

Im Client-Code wird zuerst eine einfache Nachricht erstellt. Anschließend wird diese Nachricht mit verschiedenen Dekoratoren versehen. Jede zusätzliche Dekoration wird durch Erstellen eines neuen Dekorators erreicht, der das bestehende Message-Objekt erweitert.

Beispiel des Decorator Patterns in Python

Das Decorator Pattern ist ein Strukturmuster, das es ermöglicht, einem Objekt zusätzliche Funktionalitäten hinzuzufügen, ohne seine Struktur zu ändern. Es wird häufig verwendet, um Funktionen oder Methoden zur Laufzeit dynamisch zu erweitern. In Python wird das Dekorator-Muster oft durch Funktionen und Wrapper erreicht.

Hier ist ein einfaches Beispiel des Decorator Patterns in Python:

Beispiel: Logger Decorator

Angenommen, wir haben eine Funktion, die eine Nachricht druckt, und wir möchten diese Funktion so erweitern, dass sie beim Aufruf einen Logeintrag erstellt.

# Der Dekorator
def logger_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Logging: Die Funktion {func.__name__} wurde aufgerufen.")
        result = func(*args, **kwargs)
        print(f"Logging: Die Funktion {func.__name__} hat das Ergebnis {result} zurückgegeben.")
        return result
    return wrapper

# Die Funktion, die wir dekorieren möchten
@logger_decorator
def addiere(a, b):
    return a + b

# Die Funktion aufrufen
addiere(3, 4)

Erklärung:

  1. logger_decorator(func): Dies ist der Dekorator, der eine Funktion (func) als Argument entgegennimmt und eine neue Funktion (wrapper) zurückgibt, die die Originalfunktion erweitert.
  2. wrapper(*args, **kwargs): Dies ist der „Wrapper“, der die ursprüngliche Funktion umhüllt und vor und nach ihrem Aufruf zusätzliche Logik ausführt.
  3. @logger_decorator: Dies ist der Python-Decorator-Syntax, der eine Funktion umhüllt, sodass die Funktion addiere automatisch durch den Dekorator logger_decorator erweitert wird.

Ausgabe:

Logging: Die Funktion addiere wurde aufgerufen.
Logging: Die Funktion addiere hat das Ergebnis 7 zurückgegeben.

In diesem Beispiel wird der Aufruf der addiere-Funktion mit Logging umhüllt, ohne die ursprüngliche Funktion selbst zu ändern.

Vorteile des Decorator Patterns

  1. Flexibilität: Das Decorator Pattern erlaubt es, einem Objekt dynamisch zusätzliche Funktionalitäten hinzuzufügen. Dekoratoren können auch miteinander kombiniert werden, um unterschiedliche Effekte zu erzielen.
  2. Vermeidung von Subklassierung: Statt viele Subklassen zu erstellen, die verschiedene Kombinationen von Eigenschaften abbilden, können Dekoratoren verwendet werden. Das reduziert die Anzahl der Klassen und erhöht die Wartbarkeit.
  3. Unabhängigkeit der Dekorationen: Jeder Dekorator ist unabhängig von anderen Dekoratoren. Das bedeutet, dass neue Dekoratoren problemlos hinzugefügt werden können, ohne den bestehenden Code zu verändern.
  4. Erweiterbarkeit: Wenn neue Funktionalitäten benötigt werden, können neue Dekoratoren hinzugefügt werden, ohne dass Änderungen an der ursprünglichen Klasse vorgenommen werden müssen.

Nachteile des Decorator Patterns

  1. Komplexität: Wenn zu viele Dekoratoren verwendet werden, kann das System sehr komplex und schwer verständlich werden. Zu viele dekorierte Objekte können die Lesbarkeit des Codes beeinträchtigen.
  2. Kombination von Dekoratoren: Bei der Kombination mehrerer Dekoratoren muss darauf geachtet werden, dass sie miteinander kompatibel sind. Andernfalls können unvorhersehbare Ergebnisse auftreten.
  3. Zusätzlicher Speicherverbrauch: Jeder Dekorator führt zu einem zusätzlichen Wrapper-Objekt. Dies kann zu einem höheren Speicherverbrauch führen, wenn viele Dekoratoren verwendet werden.

Wann sollte das Decorator Pattern eingesetzt werden und wann nicht?

Das Decorator Pattern sollte in folgenden Szenarien eingesetzt werden:

1. Erweiterung der Funktionalität ohne Änderung des Codes:

Wenn du die Funktionalität einer Klasse oder Funktion erweitern möchtest, aber deren Code nicht direkt ändern möchtest (z. B. weil er Teil einer Bibliothek oder einer externen Quelle ist), dann ist das Decorator Pattern eine gute Wahl. Es ermöglicht dir, Verhalten hinzuzufügen, ohne die ursprüngliche Implementierung zu beeinflussen.

Beispiel: Du möchtest einer Klasse zur Laufzeit zusätzliche Methoden oder Eigenschaften hinzufügen, ohne die bestehende Klasse zu verändern.

2. Dynamische Anpassung der Funktionalität:

Wenn du die Funktionalität eines Objekts zur Laufzeit dynamisch ändern möchtest (z. B. das Hinzufügen von zusätzlichem Verhalten basierend auf bestimmten Bedingungen), ist der Decorator eine elegante Lösung. Du kannst verschiedene Dekoratoren anwenden oder entfernen, ohne die ursprüngliche Logik zu stören.

Beispiel: Ein System, das unterschiedliche Sicherheitsprüfungen für Benutzer durchführt, basierend auf den Benutzerrollen. Du kannst einfach unterschiedliche Sicherheits-Decorator-Klassen zur Laufzeit anwenden.

3. Wiederverwendbarkeit von Logik:

Wenn du ein häufig wiederkehrendes Verhalten in verschiedenen Funktionen oder Klassen anwenden möchtest (z. B. Logging, Caching, Transaktionsmanagement), kannst du einen Dekorator verwenden, um dieses Verhalten an mehreren Stellen im Code zu integrieren, ohne Duplikate zu erzeugen.

Beispiel: Ein Logging-Dekorator, der sicherstellt, dass alle wichtigen Funktionen protokolliert werden, ohne dass der Logging-Code in jeder Funktion wiederholt werden muss.

4. Vermeidung von Vererbung:

Wenn du die Funktionsweise einer Klasse erweitern möchtest, aber keine Vererbung verwenden willst (z. B. weil du keine neue Unterklasse erstellen möchtest), dann ist der Decorator eine gute Möglichkeit. In vielen Fällen kann das Decorator Pattern eine Alternative zur Vererbung bieten und eine flexiblere und weniger starre Struktur ermöglichen.

Beispiel: Du hast eine Basisklasse, die verschiedene Aufgaben erledigt. Statt mehrere spezialisierte Unterklassen zu erstellen, kannst du Dekoratoren verwenden, um diese Aufgaben dynamisch zu kombinieren.

5. Schichtung von Verhalten:

In einigen Fällen möchtest du mehrere kleine Änderungen oder Erweiterungen an einem Objekt vornehmen. Anstatt eine riesige Klasse zu erstellen, die all diese Änderungen enthält, kannst du mehrere kleine Dekoratoren verwenden, um verschiedene Funktionen zu schichten. Jeder Dekorator kann einen bestimmten Aspekt der Funktionalität modifizieren, ohne dass sie sich gegenseitig beeinflussen.

Beispiel: Ein Zahlensystem, bei dem du mehrere verschiedene Formate und Regeln für die Ausgabe anwenden möchtest (z. B. Dezimal, Hexadezimal, binär), könnte mehrere Dekoratoren verwenden, um das gewünschte Format zu bestimmen.

6. Trennung von Bedenken:

Das Decorator Pattern fördert die Trennung von Bedenken (Separation of Concerns). Jede Erweiterung, die du hinzufügst, wird in einem eigenen Dekorator behandelt, sodass du nicht eine große, monolithische Klasse hast, die für alle Funktionalitäten zuständig ist. Jeder Dekorator kümmert sich nur um einen Aspekt des Verhaltens.

Beispiele für konkrete Anwendungsfälle:

  • Logging: Jede Funktion oder Methode könnte um ein Logging-Verhalten ergänzt werden, ohne dass du das Logging in jede Funktion direkt einbaust.
  • Caching: Du kannst einen Dekorator verwenden, der Ergebnisse zwischenspeichert, um wiederholte Berechnungen zu vermeiden.
  • Sicherheitsprüfungen: Ein Dekorator könnte vor dem Ausführen einer Funktion sicherstellen, dass der Benutzer über die entsprechenden Berechtigungen verfügt.
  • Performance-Optimierung: Du kannst einen Dekorator verwenden, der die Zeit misst, die eine Funktion benötigt, und diese Information ausgibt oder speichert.

Wann nicht das Decorator Pattern verwenden:

  • Zu einfache Anforderungen: Wenn die Funktionalität, die du hinzufügen möchtest, sehr einfach ist, und du keine Dynamik oder Erweiterbarkeit benötigst, könnte das Dekorator-Muster unnötig komplex und übertrieben sein.
  • Übermäßiger Einsatz von Dekoratoren: Wenn du zu viele Dekoratoren schichtest, kann dies den Code schwer verständlich machen, besonders wenn die Logik hinter den Dekoratoren komplex ist. Es könnte dann schwierig sein, die Reihenfolge oder den Kontext der Dekoratoren nachzuvollziehen.

Insgesamt ist das Decorator Pattern ideal, wenn du modulare, flexible und wieder verwendbare Erweiterungen benötigst, ohne die bestehende Struktur zu verändern.

Fazit

Das Decorator Pattern ist ein nützliches Muster, das es ermöglicht, die Funktionalität eines Objekts dynamisch zu erweitern. Durch die Verwendung von Dekoratoren können zusätzliche Features hinzugefügt werden, ohne dass die ursprüngliche Klasse verändert werden muss. In C++ wird das Muster durch die Verwendung von abstrakten Dekoratoren und konkreten Implementierungen leicht verständlich.

Obwohl das Muster zu zusätzlicher Komplexität führen kann, insbesondere bei der Verwendung vieler Dekoratoren, bietet es eine hohe Flexibilität und Erweiterbarkeit. Das Decorator Pattern ist besonders vorteilhaft, wenn es darum geht, Objekten nachträglich neue Funktionalitäten hinzuzufügen, ohne den Code stark zu verändern.

decorator pattern

Gibt es sinnvolle Alternativen für das Decorator-Pattern im Embedded-Bereich?

Ja, z. B. statische Komposition über Präprozessor-Makros, Compile-Time-Konfigurationen, bedingte Kompilierung oder Templates (in C++). Diese Ansätze vermeiden Laufzeit-Overhead, sind aber weniger flexibel.

Ist das Decorator Pattern für echtzeitkritische Anwendungen geeignet?

Nur bedingt. Jede zusätzliche Dekorator-Schicht kann die Latenz erhöhen. In sicherheits- oder zeitkritischen Systemen sollte der Einsatz gut abgewogen werden. In manchen Fällen ist eine statische Lösung vorzuziehen.

Wann sollte man das Decorator Pattern einem Strategy Pattern vorziehen?

Wenn es darum geht, Verhalten zusätzlich zu einem bestehenden Verhalten zu schichten, ist das Decorator Pattern sinnvoll. Beim Strategy Pattern geht es um Austauschbarkeit eines bestimmten Verhaltens.

Was ist das Decorator Pattern und wozu dient es?

Das Decorator Pattern erlaubt es, einem Objekt zur Laufzeit zusätzliche Funktionalität hinzuzufügen, ohne dessen Klasse zu ändern. Es folgt dem Prinzip der Komposition über Vererbung und ermöglicht flexible Erweiterungen durch das Einwickeln (Wrapping) von Objekten.

Was sind typische Anwendungsfälle des Decorator-Pattern in Embedded-Systemen?

Beispiele sind Datenströme (z. B. UART → Logging → Verschlüsselung → CRC), Protokoll-Stacks oder flexible Treiberarchitekturen. Das Pattern hilft, Wiederverwendbarkeit und Modularität zu fördern, ohne viele spezialisierte Klassen schreiben zu müssen.

Wie funktioniert das Decorator-Pattern in C im Vergleich zu C++?

In C++ wird das Pattern meist mit Vererbung und Interfaces umgesetzt. In C, das keine Klassen kennt, wird das Pattern oft durch Strukturen mit Funktionszeigern implementiert. Dabei wird ein „Basis-Interface“ als Struct mit Zeigern auf Funktionen modelliert.

Wie geht man mit Dekoratoren um, wenn Polymorphie durch RTTI nicht erlaubt ist?

Statt dynamischer RTTI (Run-Time Type Information) nutzt man in C++ oft manuell gepflegte Interfaces oder rein virtuelle Klassen. In C verzichtet man ganz darauf und nutzt explizite Strukturfelder oder Tags zur Typdifferenzierung.

Wie geht man mit mehreren Schichten von Decorators um?

Die Decorator-Kette wird von außen nach innen aufgebaut. Jeder Decorator muss die gleiche Schnittstelle implementieren wie das ursprüngliche Objekt. Es empfiehlt sich, den Aufbau der Kette zur Initialisierungszeit klar zu strukturieren, z. B. per Factory-Funktion.

Wie implementiert man das Decorator Pattern in C?

Man definiert eine Struktur mit Funktionszeigern als Interface. Jeder „Decorator“ ist eine eigene Struktur, die das Interface implementiert und intern eine Referenz auf das „eingewickelte“ Objekt hält. Funktionen delegieren teilweise oder vollständig an das innere Objekt.

Wie kann das Decorator Pattern helfen, Code-Wiederverwendung in Treiber-Stacks zu verbessern?

Beispielsweise kann ein UART-Datenstrom durch einen Logging-Dekorator oder einen CRC-Dekorator erweitert werden, ohne den UART-Code selbst zu verändern. So bleibt jede Funktionalität separat testbar und wiederverwendbar.

Wie können Dekoratoren bei der Speicherverwaltung problematisch sein?

Jeder zusätzliche Dekorator erzeugt ein weiteres Objekt im Speicher. Bei mehrfacher Verschachtelung kann dies zu unerwartetem Speicherverbrauch führen. Besonders bei dynamischer Speicherallokation (Heap) muss auf Speicherlecks und Fragmentierung geachtet werden.

Wie lässt sich das Decorator-Pattern in einer Bare-Metal-Umgebung ohne OS verwenden?

Das Pattern lässt sich auch ohne Betriebssystem einsetzen, solange es keine dynamischen Speicherzugriffe oder Threads benötigt. Die Initialisierung erfolgt meist zur Compile- oder Startup-Zeit, und Funktionsaufrufe bleiben synchron und deterministisch.

Wie lässt sich ein Decorator effizient gestalten, um Overhead zu vermeiden?

Dekoratoren sollten möglichst leichtgewichtig sein und unnötige Speicher- oder Zeitkomplexität vermeiden. In Embedded-Systemen wird häufig statische Komposition bevorzugt (z. B. durch Makros oder Build-Optionen), um Laufzeit-Overhead zu eliminieren.

Wie sieht eine saubere Schnittstelle aus, damit verschiedene Dekoratoren kombinierbar bleiben?

Alle Komponenten sollten ein gemeinsames Interface implementieren, das einfache Operationen wie send(), receive() etc. bereitstellt. Jeder Decorator sollte nur dort eingreifen, wo er Funktionalität hinzufügen will, und den Rest delegieren. Ziel: Ein DataStream-Interface mit verschiedenen Dekoratoren: BaseStream: schreibt Daten in ein Dummy-Register LoggingDecorator: loggt alle gesendeten Daten CRCDecorator: fügt am Ende der Nachricht eine einfache […]

Wie testet man dekorierte Komponenten separat und integriert?

Einzeln testet man Dekoratoren, indem man sie mit einem einfachen Mock-Objekt versieht. In Integrationstests prüft man das Verhalten der gesamten Kette. Besonders wichtig ist es, Seiteneffekte und Datenweitergabe zwischen Dekoratoren zu validieren.

Wie unterscheidet sich ein Decorator von der Vererbung?

Vererbung fügt Funktionalität zur Compile-Zeit durch das Erweitern einer Klasse hinzu. Das Decorator Pattern hingegen erlaubt zur Laufzeit eine dynamische Kombination von Funktionalitäten durch das Zusammensetzen von Objekten.

Wie wirkt sich das Pattern auf RAM/ROM-Verbrauch aus?

RAM-Verbrauch steigt mit jeder Dekorator-Schicht, da zusätzliche Instanzen angelegt werden. ROM-Verbrauch kann durch zusätzliche Funktionszeiger und komplexeren Code steigen. Der Nutzen sollte gegen diese Kosten abgewogen werden.

Zurück zur Liste der Pattern: Liste der Design-Pattern

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)