Das decorator pattern erlaubt es, ein Objekt dynamisch mit zusätzlicher Funktionalität anzureichern. So kann in vielen Fällen ausufernde Vererbung vermieden werden.
Dekorierer sind außerdem sinnvoll, wenn Klassen dem Single Responsibility Principle entsprechen sollen, weil es erlaubt, Funktionalität auf verschiedene Klassen aufzuteilen.
Man stelle sich zum Beispiel einen Tisch vor, der dekoriert werden kann. Wenn der Tisch gedeckt wird, wird daraus ein Esstisch. Wenn ein Laptop drauf gestellt wird, ist es eben eine Arbeitsstation.
Ein anderes einfaches Beispiel sind Produkte, die einen Preis haben:
class Product
attr_accessor :price
def initialize price
@price = price
end
end
Der Gesamtpreis des jeweiligen Produktes berechnet sich unterschiedlich, abhängig vom Kontext. Ausgehend vom Grundpreis, können Rabatte gewährt werden, müssen Steuern aufgeschlagen werden und so weiter.
Sicherlich könnte man das mit etwas Logik in der Product Klasse abbilden oder aber entsprechende Produktklassen von Product ableiten, allerdings wird es dann sehr schnell unübersichtlich.
Sinnvoller ist es, die Produkt Klasse so zu belassen und stattdessen decorators für den jeweiligen Kontext zu nutzen:
class TaxDecorator
TAX_FACTOR = 1.2
def initialize object
@object = object
end
def price
@object.price * TAX_FACTOR
end
end
class DiscountDecorator
attr_reader :discount_factor
DISCOUNT_FACTOR = 0.9
def initialize object
@object = object
end
def price
@object.price * discount_factor
end
private
def discount_factor
@discount_factor || DEFAULT_DISCOUNT_FACTOR
end
end
Wichtig ist es, dass das interface der decorators dem des Zielobjektes (das Produkt) entspricht, um die volle Funktionalität zu erhalten.
Nun kann der Preis eines Produktes dynamisch geändert werden:
shirt = Product.new 10
shirt.price # => 10
TaxDecorator.new(shirt).price # => 12.0
DiscountDecorator.new(shirt).price # => 9.0
Im Grunde umhüllt jeder decorator das Zielobjekt und fügt neues Verhalten hinzu.
Natürlich können decorators auch miteinander kombiniert werden:
tv = Product.new 200
TaxDecorator.new(DiscountDecorator.new tv).price # => 216.0
Auch andere Klassen können dekoriert werden, wenn sie dem erwarteten interface entsprechen:
class Employee
def initialize name, annual_salary
@name = name
@salary = annual_salary
end
def price
@salary / 1600 # ca. 1.600 Arbeitsstunden hat das Jahr
end
end
Die Arbeitsstunde von Bob kostet dann also mindestens:
bob = Employee.new 'Bob', 50000
bob.price # => 31.25
TaxDecorator.new(bob).price # => 37.2