The Decorator pattern allows you to attach new behavior to individual objects dynamically without affecting their classes or other objects of the same class.
Here's a video version of this article if you prefer to watch instead:
It's similar to the Adapter pattern which, I've talked about in a separate video that I'm going to link to in the description, but the difference is that an Adapter changes the interface of the object it wraps, while the decorator does not.
And because the interface remains unchanged, you can stack multiple decorators together to change behavior at runtime.
You might think you could achieve the same result by using inheritance instead, but the problem with inheritance is that you have less flexibility and a lot more complexity.
You need to create a class for ever possible combination you may use, and that gets very messy very quickly.
But let's look at an example of a decorator.
# lib/01_poros.rb
class Person
def feeling_at(outside_temp)
if outside_temp > 20
"Warm"
else
"Cold"
end
end
end
class Shirt
def initialize(person)
@person = person
end
def feeling_at(outside_temp)
if outside_temp >= 30
"Going for a swim"
else
@person.feeling_at(outside_temp)
end
end
end
class Coat
def initialize(person)
@person = person
end
def feeling_at(outside_temp)
if outside_temp >= 35
"Crazy hot"
else
@person.feeling_at(outside_temp)
end
end
end
# You need to take care of the object's entire interface.
# It doesn't address the "transparent interface" requirement.
outside_temp = 30
joe = Person.new
puts joe.feeling_at(outside_temp)
joe_shirt = Shirt.new(joe)
puts joe_shirt.feeling_at(outside_temp)
joe_coat = Coat.new(joe_shirt)
puts joe_coat.feeling_at(outside_temp)
puts "Class: #{joe_coat.class}"
In this first example, we're using the Shirt
object and the Coat
object to decorate the Person
object.
Each decorator object can wrap either the original Person
object or an already decorated one and change the behavior of the feeling_at
method.
You could also wrap the object with the same decorator multiple times. It doesn't make much sense to do that in this particular example, but you could if you wanted to.
One problem with this implementation is, if the original object has other methods as well, we need to delegate all those the other methods we don't care about to the wrapped object in order to comply with the transparent interface requirement in the Gang of Four book.
And another problem is that the decorated object's class is not the original one (i.e., the Person
class). It's the decorator's class. In this case, it's the Coat
class.
So let's look at a different approach.
# lib/02_modules.rb
class Pizza
def cost = 2.0
def foo = "foo"
end
module Onions
def cost = super + 1.0
end
module Cheese
def cost = super + 2.2
end
# We can only extend the object once.
pizza = Pizza.new
pizza.extend(Onions)
pizza.extend(Cheese)
puts "Your pizza costs: #{pizza.cost}"
puts "Class: #{pizza.class}"
We can use modules to decorate an object as well.
The only problem with this approach is we can't extend an object more than once with the same module. So we can't have a double cheese pizza, for example.
If we were to use the previous approach, with the POROs, we could wrap the pizza object in a Cheese
decorator twice to get the double cheese pizza.
But with the modules approach, Ruby won't allow that.
But otherwise, if you don't need that feature, the modules approach gives you the correct class name back, and it also looks pretty clean.
Ok, so we looked at how to decorate an object using both POROs and modules. But what else can we do?
Well...
# lib/03_method_missing.rb
module Decorator
def initialize(component)
@component = component
end
def method_missing(meth, *args)
if @component.respond_to?(meth)
@component.send(meth, *args)
else
super
end
end
def respond_to?(meth)
@component.respond_to?(meth)
end
end
class Coffee
def cost
2
end
def origin
"Colombia"
end
end
class Milk
include Decorator
def cost
@component.cost + 0.4
end
end
coffee = Coffee.new
puts "Americano (#{coffee.origin}): $#{coffee.cost}" # => Americano (Colombia): $2
latte = Milk.new(coffee)
puts "Latte (#{latte.origin}): $#{latte.cost}" # => Latte (Colombia): $2.4
puts "Class: #{latte.class}" # => Coffee
We could use method_missing
in combination with the POROs version to get past that inconvenience where the interface was not transparent.
Meaning if you had more methods on the original object, you needed to somehow forward those methods from the decorated object to the wrapped one.
There are two downsides to this approach.
First, using method_missing
is slow.
And second, the class of the decorated object is the decorator class, namely Milk
, instead of the Coffee
class.
Lastly, there is the option of using SimpleDelegator
instead of method_missing
to achieve the same result.
# lib/04_simple_delegator.rb
require "delegate"
class Coffee
def cost = 2
def origin = "Colombia"
end
class Milk < SimpleDelegator
def initialize(coffee)
@coffee = coffee
super
end
def class = __getobj__.class
def cost = @coffee.cost + 0.4
end
coffee = Coffee.new
puts "Americano (#{coffee.origin}): $#{coffee.cost}"
latte = Milk.new(coffee)
puts "Latte (#{latte.origin}): $#{latte.cost}"
puts "Class: #{latte.class}"
If I were to sort these approaches by personal preference, I'd pick this one second. After the modules version.
It's got almost everything right.
You get to stack how many decorators you want; you can stack the same decorator multiple times and get the original class name back. It's got a lot of good things going for it.
But there's just one thing I'm not necessarily a fan of. And that is redefining the class method. I'm not 100% sure that's a good idea.
I like the decorator pattern because it allows you to create multiple single-responsibility decorators that you can plug and play as you see fit.