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"