The decorator pattern allows to enrich objects functionality dynamically. Thus excessive inheritance can be avoided in many cases.
Decorators also are often useful for adhering to the Single Responsibility Principle, as it allows functionality to be divided between classes with unique areas of concern.
Imagine a table, which can be decorated. If the table was set, it is a dining table. If a laptop was put on it, it is clearly a desk.
Products having a price is another simple example:
class Product
attr_accessor :price
def initialize price
@price = price
end
end
Each products total price is calculated different, depending on the context. It includes the base price, but also can grant a discount or can add taxes and so on.
Sure, the story also could be mapped with some logic in the Product class itself or handled by inheritance. But there is no doubt that both approaches will be awkward very soon.
It is more reasonable to leave the Product class and create decorators for the particular use cases:
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
The decorator interface has to meet the target object (the product) API for sustaining the entire functionality.
The products price can be changed dynamically, like:
shirt = Product.new 10
shirt.price # => 10
TaxDecorator.new(shirt).price # => 12.0
DiscountDecorator.new(shirt).price # => 9.0
Basically each decorator wraps the target object and adds new behavior.
Of course decorators also can be combined with each other:
tv = Product.new 200
TaxDecorator.new(DiscountDecorator.new tv).price # => 216.0
Additionally other classes can be decorated, if they sustain the expected interface:
class Employee
def initialize name, annual_salary
@name = name
@salary = annual_salary
end
def price
@salary / 1600 # ca. 1.600 man hours per year
end
end
Then Bobs man hour costs at least:
bob = Employee.new 'Bob', 50000
bob.price # => 31.25
TaxDecorator.new(bob).price # => 37.2