Wenn es um eine konkrete Implementierung des Decorator Entwurfsmusters geht, gibt es in Ruby entsprechende Gems dafür. Meistens wird Draper verwendet, wobei es sich dabei genau genommen um eine Presenter Implementierung handelt.
Allerdings ist dessen gesamter Komfort meistens nicht notwendig. Stattdessen bietet Ruby mit der Klasse SimpleDelegator ein adäquates Werkzeug für eine einfache Decorator Implementierung.
Der Einfachheit halber, basiert das folgende Beispiel auf ein Ruby on Rails Projekt. Es geht dabei um Produkte:
rail$ rails g model Product serial:string sold_at:datetime && rake db:migrate
Jedes Produkt hat eine Seriennummer und ein Verkaufsdatum. An dem Verkaufsdatum hängt die zu gewährleistende Garantie von 6 Monaten.
class Product < ActiveRecord::Base
WARRANTY_MONTHS = 6.months
validates :serial, presence: true
def warranty?
return true if sold_at.nil?
sold_at < WARRANTY_MONTHS.ago
end
end
Natürlich steigt im Laufe der Entwicklung der Umfang der Geschäftslogik im Modell. Daher lohnt es sich, rechtzeitig entsprechende Logik auszulagern, erst Recht, wenn sie nur in einem bestimmten Kontext gebraucht wird.
Das Decorator Entwurfsmuster hilft, Klassen schlank zu halten.
Die Implementierung des ProduktDecorators und das schlanke Product Modell:
class ProductDecorator < SimpleDelegator
WARRANTY_MONTHS = 6.months
def warranty?
return true if sold_at.nil?
sold_at < WARRANTY_MONTHS.ago
end
end
class Product < ActiveRecord::Base
validates :serial, presence: true
end
und deren Verwendung:
product = Product.create serial: '123'
product_decorator = ProductDecorator.new product
product_decorator.serial # => "123"
product_decorator.warranty? # => true
product_decorator.sold_at = 7.month.ago
product_decorator.warranty? # => false
product_decorator.save
Eine klare Aufteilung: Product ist verantwortlich für die Persistierung und ProductDecorator enthält Geschäftslogik.
Lediglich der Zugriff auf Klassenmethoden des dekorierten Objektes sieht etwas ungünstig aus:
product_decorator = ProductDecorator.new Product.first
product_decorator.__getobj__ # => #<Product id: 1, serial: "123", ...>
product_decorator.__getobj__.class.name # => "Product"
Zwar ist der Zugriff auf das an den Decorator übergebene Objekt über SimpleDelegator#getobj möglich, aber das kann komfortabler gestaltet werden. Damit alle Dekorateure davon profitieren, bietet es sich an, entsprechende Methoden in eine SimpleDecorator Klasse rauszuziehen, zumal dann bei Ableitung der Zweck eindeutig wird:
class SimpleDecorator < SimpleDelegator
alias_method :model, :__getobj__
delegate :class, to: :model, prefix: true
end
class ProductDecorator < SimpleDecorator
WARRANTY_MONTHS = 6.months
def warranty?
return true if sold_at.nil?
sold_at < WARRANTY_MONTHS.ago
end
end
Allein der Alias model auf __getobj__ und das Delegieren von class an das Objekt (model) erhöht die Lesbarkeit enorm:
product_decorator = ProductDecorator.new Product.first
product_decorator.model # => #<Product id: 1, serial: "123", ...>
product_decorator.model_class.name # => "Product"