06 September 2006
Decorator Pattern with Ruby in 8 lines
The Decorator Pattern is a design pattern that enables you to dynamically wrap behaviour around an existing object at runtime. It is especially useful when an object can have many variables that can be combined in different ways, which in turn affect it’s behaviour.
This small, simple implementation of the decorator pattern in Ruby sums up what I love about the hottest dynamic language on the planet.
I’ll borrow a simple example from the excellent Head First Design Patterns book by Eric Freeman, Elisabeth Freeman, Kathy Sierra, and Bert Bates.
Let’s say you want to calculate the cost of a cup of coffee. You have Coffee class, which implements a cost() method. For the purposes of this example, let’s just hardcode a value:
class Coffee
def cost
2
end
end
Great. But what if we want the cost of a coffee with milk? We could have a new class:
class WhiteCoffee
def cost
2.4
end
end
OK. But now we want cream. And sprinkles. Clearly, creating new classes is going to lead to a huge number of classes in our application. It just isn’t realistic to create classes for different combinations of coffee and extras. It could get worse – what if we have different types of coffee? We would then have to have combinations of extras with each different type of coffee. It just isn’t going to work. Enter the decorator pattern. Here’s the the 8 lines of Ruby mentioned in this entry’s title:
module Decorator
def initialize(decorated)
@decorated = decorated
end
def method_missing(method, *args)
args.empty? ? @decorated.send(method) : @decorated.send(method, args)
end
end
And thats all you need. You can include this into any class you want to act as a decorator. You can then use that decorator as if it was the object it is decorating; by default all messages sent to the decorator are forwarded on to the decorated object. You can then decorate the methods you need to extend:
class Milk
include Decorator
def cost
@decorated.cost + 0.4
end
end
So how does this solve our original problem? The real power of decorators lies in the fact that they can act like the objects they are decorating. By taking this one step further, you can wrap decorators with other decorators as long as they share the same interface. By creating decorators for our different “extras”, we can create coffees using a combination of decorators and get the total cost of the coffee.
class Whip
include Decorator
def cost
@decorated.cost + 0.2
end
end
class Sprinkles
include Decorator
def cost
@decorated.cost + 0.3
end
end
Whip.new(Coffee.new).cost
#=> 2.2
Sprinkles.new(Whip.new(Milk.new(Coffee.new))).cost
#=> 2.9
Of course, there’s nothing stopping us from making life easier with a few factory methods:
class CoffeeFactory
def self.latte
SteamedMilk.new(Espresso.new)
end
def self.cappuccino
Sprinkles.new(Cream.new(Milk.new(Coffee.new)))
end
end
order = Order.new
order.add(Coffee.new)
order.add(CoffeeFactory.cappuccino)
puts order.total
Due to Ruby’s highly dynamic nature, the Decorator pattern isn’t the only way of extending class functionality at runtime, however I just love how simple it is to implement the pattern in Ruby. For more information on implementing decorators in Ruby, including generic decorators and alternatives to the traditional decorator pattern, see the DecoratorPattern page at the RubyGarden.
Finally, it would be nice to maintain some kind of identity when using decorators. The RubyGarden page above has one approach. Whilst we aren’t using inheritance, it would be nice to retain one of its features:
CoffeeFactory.cappucino.kind_of? Coffee
#=> true
I can think of a few ways of extending my Decorator class to retain this functionality, but I’m gonna leave this one to the reader. Over to you.
Update: In typical Ruby-fashion, my 8 lines of code has been beaten by, well, zero lines of code by Trevor Squires. Using modules, super and extend, Trevor came up with this alternative solution, which maintains identity as well. Bow down to his Ruby-fu.
Update 2: Never one to step back from a challenge, I see Trevor’s Ruby-fu and raise him with a bit of my own. I still feel my decorator implementation carries some weight, with a nice bit of syntatic sugar:
class Milk
include Decorator
end
class Whip
include Decorator
end
class Sprinkles
include Decorator
end
# normal coffee
Coffee.new
# coffee with milk, whip and sprinkles
Coffee.with :milk, :whip, :sprinkles
The self.with method could do with being extracted into a Decoratable module but here’s a concrete implementation. Trevor’s method still has an advantage in that it maintains object identity, but I have some ideas to solve that. But alas, it is past 1am and bed beckons.
Update 3: Trevor fights back with his own self.with() implementation. Personally I prefer the Coffee.with :sym, :sym syntax over Coffee.with Module, Module syntax, but thats just personal preference. Building on his extend implementation, it eliminates the symbol to class trickery, and still maintains identity. His ‘fu is still strong.
However, there is one downside to Trevor’s method: you can only apply a decorator to an object using
Sprinkles.new(Sprinkles.new(Coffee.new))
# or with a bit of sugar (no pun intended)
Coffee.with :sprinkles, :sprinkles
Return to home page | Check out my tumblelog
12 Comments on this article
Return to home page | Check out my tumblelog
Commenting on this article is now closed
1. Comment by James on 06 Sep 2006 at 20:09
Very cool, but didn’t you forget the Mocha ;-)
2. Comment by Luke Redpath on 06 Sep 2006 at 20:09
James…very good. t-boom tish! ;)
3. Comment by beppu on 06 Sep 2006 at 23:09
That’s impressive. My Ruby-fu is still weak.
4. Comment by Aaron Blohowiak on 07 Sep 2006 at 00:09
=) Eeeeeexcellent.
5. Comment by Trevor Squires on 07 Sep 2006 at 03:09
Okay Luke, I’ll see your “double sprinkles” and raise you a “first sprinkle is free for iced coffees”.
http://pastie.caboo.se/12111
Trev
6. Comment by Luke Redpath on 07 Sep 2006 at 08:09
Heh, do you get the feeling we could go on for a while with this one?
I do feel you are straying outside the boundaries of the decorator pattern mind you. ;)
Implement the above as a DSL and your Ruby-fu will be forever powerful.
7. Comment by Golly on 07 Sep 2006 at 22:09
Hi. Nice article. Although you could simplify this
args.empty? ? @decorated.send(method) : @decorated.send(method, args)
into this
@decorated.send(method, *args)
8. Comment by trans on 08 Sep 2006 at 03:09
Trev, add Sprinkles multiple times you could just dup a module in #with if it’s already extending the object.
Alternatively, I’ve implemented Matz Module traits concepts with which one could do:
9. Comment by Pat Maddox on 15 Dec 2006 at 12:12
I just took a look at that book for the first time…pretty fun read :)
Anyway, I’m reading over the decorator, and I came up with a much simpler solution than anything.
class Coffee def initialize @ingredients = [] end def cost @ingredients.inject(2.5) { |total, i| total += i.cost } end def mix(ingredient) @ingredients << ingredient end end class Ingredient attr_reader :cost def initialize(cost, name = "secret") @cost = cost @name = name end endFeel free to make each ingredient its own subclass, if you want.
Perhaps I’m overlooking the fact that it’s just supposed to be an example…but I’m interested to know if this problem is actually something that would benefit from the Decorator pattern?
10. Comment by Luke Redpath on 15 Dec 2006 at 12:12
Well…you are right. It is just an example. If I was trying to solve this problem from scratch I’d probably come up with a solution similar to yours.
11. Comment by AlexK on 03 Jan 2007 at 13:01
Hi, Probably I’m lost somewhere, but why this way is not revealed at all:
class Window def draw puts “
-” puts “-” end endwnd=Window.new wnd.draw
class NewWindow < Window def draw puts “Yahoo!” super end end
wnd = NewWindow.new wnd.draw
class SuperNewWindow < NewWindow def draw puts “
=*=” super end endThanks, Alex
12. Comment by Luke Redpath on 03 Jan 2007 at 14:01
Hi there, you could use inheritance but if you have lots of different decorators and ways of combining them using inheritance only leads to a class explosion (one class for each combination). That is one of the problems the decorator pattern solves.