When it comes to a concrete Decorator pattern implementation, there are relevant Gems in Ruby land. Usually Draper is the Gem of choice, although it is actually a Presenter implementation.
However its built in convenience is usually unnecessary. Instead Ruby provides the class SimpleDelegator as an appropriate tool for a simple Decorator implementation.
For simplicity reasons, the following example is based on a Ruby on Rails project, dealing with products:
rail$ rails g model Product serial:string sold_at:datetime && rake db:migrate
Each product consists of a serial number and a sales date. The provided 6 month guarantee is tied to that sales date.
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
By nature the business logic complexity increases during the development process. That is why it is worthwhile moving the logic accordingly. More than ever the logic tends to be useful just in a certain context.
The Decorator pattern helps to keep the classes slim.
The ProductDecorator implementation and the slim Product model:
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
and its usage:
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
An explicit separation: Product is responsible for its persistence and ProductDecorator provides the business logic.
Merely the decorated objects class methods access looks awkward:
product_decorator = ProductDecorator.new Product.first
product_decorator.__getobj__ # => #<Product id: 1, serial: "123", ...>
product_decorator.__getobj__.class.name # => "Product"
In fact the access to the decorated object via SimpleDelegator#getobj is at hand, but it could be more convenient. For the benefit of all decorators it makes sense to move the appropriate methods into a super class SimpleDecorator. Additionally the purpose of inheritance is more evident:
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
The alias model for __getobj__ and delegating class to the object (model) alone improves the readability.
product_decorator = ProductDecorator.new Product.first
product_decorator.model # => #<Product id: 1, serial: "123", ...>
product_decorator.model_class.name # => "Product"