Das Decorator-Muster

Definition

Das Decorator-Muster (Dekorierer) ist eines der GoFEntwurfsmuster (Design Pattern) und gehört zu der Kategorie der Strukturmuster (Structural Design Pattern). Das Muster „fügt einem Objekt dynamisch zusätzliche Verantwortlichkeiten hinzu. Dekorierer bieten eine flexible Alternative zur Ableitung von Unterklassen zum Zweck der Erweiterung der Funktionalität“ (EvKbF S. 91).

Beschreibung

Die Vererbung ist eines der grundlegenden Konzepte in der objektorientierten Programmierung. Jedoch sind auch diesem Konzept Grenzen gesetzt und seine Verwendung führt „nicht immer zu den flexibelsten oder wartbarsten Entwürfen“ (EvKbF S. 85). Eine überstrapazierte Verwendung der Vererbung kann zur Entstehung vieler Klassen führen und den Entwurf schließlich unübersichtlich werden lassen. Um diese Probleme zu vermeiden, verwendet das Decorator-Muster, ähnlich dem Strategy-Muster, die Komposition und Delegation um Verhalten „zu vererben“. Dabei setzt es auf eines der OO-Entwurfsprinzipien:

„Klassen sollten für Erweiterung offen, aber für Veränderungen geschlossen sein.“
Das Offen/Geschlossen-Prinzip

Das Decorator-Muster ermöglicht es das Verhalten einer Klasse dynamisch zu ändern. Dabei wird eine Klasse (Komponente), dessen Verhalten man erweitern will, von einer anderen Klasse (Dekorierer) dekoriert oder „umhüllt“. (Im englischen bedeutet „umhüllen“ „to wrap“. Daher auch der bekannte Begriff „Wraper-Klassen“.) Der Dekorierer verwendet die selbe Schnittstelle wie das zu dekorierende Objekt, womit er ebenso anstelle des eigentlichen Objekts verwendet werden kann. Dabei werden die Methodenaufrufe an die Komponente weiter delegiert und das eigene Verhalten davor oder danach ausgeführt.

Diese Methodik ermöglicht es bestehende Objekte um neue Funktionalitäten zu erweitern, indem man neuen Code schreibt, anstatt den vorhanden Code zu ändern. Dadurch sinkt die Gefahr erheblich unbeabsichtigte Fehler oder Nebeneffekte zu verursachen. Ebenso erreicht man auch einen Entwurf der flexibel genug ist neue Funktionalitäten aufzunehmen und den geänderten Anforderungen gerecht zu werden.

Beispiel

Der Vorteil des Decorator-Musters gegenüber der Vererbung wird deutlich, wenn der Entwurf die Möglichkeit bieten soll, viele Objekte miteinander zu kombinieren. Als Beispiel dafür schauen wir uns den Entwurf eines Coffee-Shops an. Ein Coffee-Shop bietet den Kunden mehrere Kaffeesorten an, die man mit weiteren Zutaten (Milch, Zucker, Schoko) kombinieren kann. Würde man bei dem Entwurf nur auf die Vererbung setzen, so müsste man für jede Getränk-Kombination eine eigene Klasse erstellen. Sicherlich ist es nicht schwer vorstellbar, dass dies nicht gerade flexibel ist und zu einem Wartungsalbtraum führen würde. Das Decorator-Muster schafft an dieser Stelle Abhilfe.

Als erstes erstellen wir eine Schnittstelle (abstrakte Klasse), die dann jede Kaffee-Klasse verwendet. Die Schnittstelle beinhaltet die Beschreibung der Sorte sowie eine Methode zur Preisberechnung.

public abstract class Beverage
{
	public string Description { get; private set; }

	public Beverage(string description)
	{
		this.Description = description;
	}

	public abstract double Cost();

	public override string ToString()
	{
		return this.Description;
	}
}

public class DarkRoast : Beverage
{
	public DarkRoast()
		: base("Dark Roast")
	{
	}

	public override double Cost()
	{
		return 0.99;
	}
}

public class Espresso : Beverage
{
	public Espresso()
		: base("Espresso")
	{
	}

	public override double Cost()
	{
		return 1.99;
	}
}

Als nächstes erstellen wir für die Zutaten einen Dekorierer, damit wir die Möglichkeit erhalten eine Kaffeesorte mit verschiedenen Zutaten zu kombinieren. Der Dekorierer verwendet die selbe Schnittstelle wie die Kaffee-Klassen und erhält durch den Konstruktor eine Referenz der konkreten Kaffee-Klasse. Alle weiteren Zutaten-Klassen leiten sich von dem Dekorierer ab und implementieren die Methode zur Preisberechnung. Dabei wird der Preis der Zutat mit dem Preis des Referenzobjektes zusammen gerechnet.

public abstract class CondimentDecorator : Beverage
{
	protected Beverage beverage;

	public CondimentDecorator(string description, Beverage beverage)
		: base(description)
	{
		this.beverage = beverage;
	}

	public override string ToString()
	{
		return this.beverage.ToString() + ", " + base.ToString();
	}
}

public class Mocha : CondimentDecorator
{
	public Mocha(Beverage beverage)
		: base("Mocha", beverage)
	{
	}

	public override double Cost()
	{
		return 0.20 + this.beverage.Cost();
	}
}

public class SteamedMilk : CondimentDecorator
{
	public SteamedMilk(Beverage beverage)
		: base("Steamed Milk", beverage)
	{
	}

	public override double Cost()
	{
		return 0.10 + this.beverage.Cost();
	}
}

Schließlich kombinieren wir Kaffee-Klassen mit weiteren Zutaten und berechnen den Preis.

static void Main(string[] args)
{
	Beverage beverage1 = new Espresso();
	Console.WriteLine(beverage1.ToString() + " $" + beverage1.Cost());

	Beverage beverage2 = new DarkRoast();
	beverage2 = new Mocha(beverage2);
	beverage2 = new Mocha(beverage2);
	beverage2 = new Whip(beverage2);
	Console.WriteLine(beverage2.ToString() + " $" + beverage2.Cost());

	Beverage beverage3 = new HouseBlend();
	beverage3 = new Soy(beverage3);
	beverage3 = new Mocha(beverage3);
	beverage3 = new Whip(beverage3);
	Console.WriteLine(beverage3.ToString() + " $" + beverage3.Cost());

	Console.ReadLine();
}

Fazit

Das Beispiel zeigt, wie das Decorator-Muster helfen kann einen Entwurf flexibel und robust zu gestallten. Neue Anforderungen können in Form neuer Klassen hinzugefügt werden. Der bestehende Code bleibt unverändert, wodurch Fehler vermieden werden. Statt für jede Kombination eine eigene Klasse zu erzeugen, können die Objekte mittels Komposition miteinander verwendet werden. Der Wartungsaufwand wird reduziert und der Entwurf bleibt übersichtlich.

UML

Decorator Design Pattern UML Diagram

Akteure

  • Component (Komponente)
    Definiert eine Schnittstelle für die zu dekorierenden Objekte.
  • ConcreteComponent (KonkreteKomponente)
    Implementiert die Schnittstelle der Komponente. Definiert ein Objekt, welches dekoriert werden kann.
  • Decorator (Dekorierer)
    Definiert eine Schnittstelle, die der Schnittstelle der Komponente entspricht. Enthält eine Referenz auf die dekorierte Komponente.
  • ConcreteDecorator (KonkreterDekorierer)
    Implementiert die Schnittstelle des Dekorierers. Erweitert die Funktionalität der Komponente durch ändern des Verhaltens.

Quellen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.