Decorator Pattern in Ruby

24 Sep 2014

Decorators allow us to add behavior to objects without affecting other objects of the same class. The decorator pattern is a useful alternative to creating sub-classes. We will look at an example where we use subclassing to solve a problem, and then look at how decorator objects provide a better solution.

Imagine we have a Burger class with a cost method that returns 50.

classBurgerdefcost50endend

Now we need to represent burgers with an added layer of cheese, and the cost goes up by 10. The simplest approach is to create a BurgerWithCheese subclass that returns 60 in the cost method.

classBurgerWithCheese<Burgerdefcost60endend

Next, we need to represent a large burger that adds 15 to the cost of a normal burger. We can represent this using a LargeBurger subclass of Burger.

classLargeBurger<Burgerdefcost65endend

We could also have an ExtraLargeBurger which adds a further cost of 15 to our LargeBurger. If we were to consider that these burger types could be served with cheese, we would need to add LargeBurgerWithChese and ExtraLargeBurgerWithCheese subclasses.

With this approach, we end up with a total of 6 classes. Double that number if you want to represent these combinations with fries on the side.

Extending dynamically with modules

To simplify our code, we could use modules to dynamically add behavior to our Burger class. Let’s write CheeseBurger and LargeBurger modules for this.

This is quite an improvement over our inheritance based implementation. Instead of having 6 classes, we just have one class and 3 modules. If we needed to add fries to the equation, we need just four modules instead of 12 classes.

Applying the decorator pattern

The modules based solution has simplified our code a great deal, but we could still improve upon it by using the decorator pattern. We could consider an ExtraLargeBurger as being formed by twice adding 15 to the cost of a Burger.

Our module based implementation doesn’t allow this. It would be tempting to call burger.extend(LargeBurger) twice to get an extra large burger. But when a module has already been used to extend an object, the second invokation of #extend has no effect.

If we were to continue using the same implementation, we would need to have an ExtraLargeBurger module that returns super + 30 as the cost. Instead, we could use decorator objects, that can be composed to build more complex objects. We start with a decorator called LargeBurger that is a wrapper around a Burger object.

We can similarly represent cheese burgers using a BurgerWithCheese decorator. Using just three classes, we are now able to represent 6 types of burgers.

SimpleDelegator

Our decorator implementation has one disadvantage: if Burger has a #calories method, it will no longer be exposed after a decorator has been applied on it. To solve this problem, we will use Ruby’s SimpleDelegator class. First of all, we will implement a BurgerDecorator base class that all our decorator classes will inherit from.

When we call super in the #initialize method above, SimpleDelegator ensures that all the methods of the burger object are available on the decorated burger objects that we create.

When we create new decorator classes, we only need to inherit from BurgerDecorator and implement those methods that are new or different in those decorated objects.

Wrapping up

Decorators are a useful approach in cases where the objects have different types of behavior that can be combined in many ways. If we use inheritance in these cases, the number of subclasses can increase rapidly.

Since our decorated objects implement all the behavior of the original object, we can compose them to generate objects of any combination of behaviors.

This pattern can also be used to extract logic out of a complex class into other smaller classes. One common example is decorator classes that contain presentation logic in them.

Hi, I’m Nithin Bekal.
I work at Shopify in Ottawa, Canada.
Previously, co-founder of
CrowdStudio.in and
WowMakers.
Ruby is my preferred programming language,
and the topic of most of my articles here,
but I'm also a big fan of Elixir.
Tweet to me at @nithinbekal.