Programming ≈ Fun

Written by Krešimir Bojčić

Decorators in Ruby

The decorator pattern is used to extend the functionality of a certain object in a runtime. In a statically typed language you need to define decorator interface, then subclass from it and initialize the component (object) that you are decorating.

In Ruby you can pledge: “Yes I am a grownup” and skip the formal interface definition part.

In either case the payoff comes with the fact that you can combine “extensions” without writing out complex inheritance tree that accounts for all possible combinations. Therefore you end up using extension that you need when you need them.

Let’s check it out on Wikipedia coffee example.

class Coffee
  def cost
    1
  end
  def ingredients
    %w(coffee water sugar)
  end
end

If we want some latte, we can create a new decorator for the coffee:

class Latte
  def initialize(component)
    @component = component
  end
  def cost
    @component.cost * 1.2
  end
  def ingredients
    @component.ingredients << "milk" << "whipped cream" << "vanilla"
  end
end

We can now have some latte:

latte_coffee = Latte.new(Coffee.new)
pp latte_coffee.ingredients ==> ["coffee", "water", "sugar", "milk", "whipped cream", "vanilla"]
pp latte_coffee.cost ==> 1.2

You only need to implement methods that are extending the base methods. For double coffee we need to implement cost, but the ingredients method is just delegating to the decorated class:

class DoubleCoffee
  def initialize(component)
    @component = component
  end
  def cost
    @component.cost * 2
  end
  def ingredients
    @component.ingredients
  end
end

Instead of accounting for all cases with inheritance (upfront aka tedious):

You can plug it in decorator pattern that looks something like this:

This is small example but I hope that the full scale of combination explosions problem that would occur in bigger scenarios is visible.

Ruby has a cool solution for delegation blues (writing boilerplate code to wrap up unchanged delegated methods). On the example below you are arguably at lost for using forwardable, but the more methods you need to delegate better of you are with forwardable.

class DoubleCoffee
  extend Forwardable
  def_delegator :@component, :ingredients
  def initialize(component)
    @component = component
  end
  def cost
    @component.cost * 2
  end
end

If you have really big number of methods and you don’t need to be too selective about what you are delegating then you can pull out the big guns and implement some method_missing magic.

To digress a bit I thought that method_missing is the best thing since sliced bread and also really original idea. That was until I’ve met doesNotUnderstand message in Smalltalk. Then I felt humbled for a while… Man they had it back than and still it was new to me even today.

To get back on the decorator. You can be explicit about what’s going on:

class CoffeeDecorator
  extend Forwardable
  def initialize(component)
    @component = component
  end
end

class DoubleCoffee < CoffeeDecorator
  def_delegator :@component, :ingredients
  def cost
    @component.cost * 2
  end
end

class Latte < CoffeeDecorator
  def cost
    @component.cost * 1.2
  end
  def ingredients
    @component.ingredients << "milk" << "whipped cream" << "vanilla"
  end
end

Punchline is that decorators enable you to chain the functionality that you need the way you need it without spelling it out in inheritance tree:

latte = Latte.new(Coffee.new)
double_coffee_with_latte = DoubleCoffee.new(latte)

Ruby modules

You can create the same effect by using modules. It looks rather sexy:

module DoubleCoffee
  def cost
    cost * 2
  end
end

module Latte
  def cost
   super * 1.2
  end
  def ingredients
    super << "milk" << "whipped cream" << "vanilla"
  end
end

c = Coffee.new
c.extend Latte
c.extend DoubleCoffee

In practice you would probably build some kind of factory to ease the pain of all this flexibility for the client:

class CoffeeFactory
  def self.double_coffee_with_latte
   return Coffee.new.extend(DoubleCoffee).extend(Latte)
  end
end

One gotcha is that you need to watch out for you methods to be truly chainable. For example if we had:

module Latte
  def cost
    super + 0.5
  end
  def ingredients
    super << "milk" << "whipped cream" << "vanilla"
  end
end

We would not get the same cost for:

   Coffee.new.extend(DoubleCoffee).extend(Latte).cost ==> 2.5
   Coffee.new.extend(Latte).extend(DoubleCoffee).cost ==> 3

Methods wrapping

One more alternative is to wrap up methods in runtime. For example you can implement DoubleCoffee decoration like this:

c = Coffee.new
class << c
  alias old_cost cost
  def cost
    old_cost * 2
  end
end

Rails has a real nice variation of this tehnique with their alias_method_chain method.

Conclusion

I think this is one of those patterns that looks really good in theory. And as they say, “in theory, theory and practice are the same”. I would say that in practice you need to carefully judge wether the flexibility gained is worthed increased complexity for the client code. Nevertheless it is nice to know that Ruby is offering some refreshing ways to do it if you really need to.

« Favorite Vim Tools & Tricks Faking Multiple Blocks in Ruby »

Comments

Copyright © 2019 - Kresimir Bojcic (LinkedIn Profile)