Wednesday, October 30, 2013

Quick Review: Decorator Pattern in Ruby


In the object-oriented world, simple applications usually require small classes with static behaviors. Adding, modifying, and sharing those behaviors can be achieved by mixing in modules or inheriting from other classes at compile time. However, more complex applications might require a particular instance of a class to gain additional functionality at runtime. To modify the behavior of an object dynamically, we can utilize the decorator design pattern.

When to Decorate

Decoration can be used to add behavior to any individual object without affecting the behavior of other objects of the same class. Essentially, the existing object is being "wrapped" with additional functionality. Some practical problems that can be solved by decoration are:
  • applying one or more UI elements to a specific UI widget at runtime
  • saving an ActiveRecord model in various ways based on conditionals in a Rails controller
  • adding additional information to data streams by pre/appending with additional stream data

To properly implement a decorator, it must adhere to the following guidelines:
  • The decorator must implement the original object's interface.
  • The decorator must delegate any methods to the decorated object.

The first guideline makes sure a decorator remains transparent to any clients of the original object. As far as the clients know, the decorated object hasn't changed in terms of its interface. The second guideline makes sure the decorator adds behavior before or after delegating the message to the wrapped object. This allows decorators to be stacked on top of one another, building on the original objects behavior.

Now, there are several ways to implement the decorator pattern in Ruby, but I'll only cover my two favorite methods. The first is a very basic implementation using only Ruby wrapper classes on objects. The second uses modules and extend to add functionality to objects.

Race Cars

Let's start by defining the base class. We'll use race cars as a starting point, then add some performance upgrades by using decorators. Here is our RaceCar class:
class RaceCar
  def initialize(make, model)
    @make  = make
    @model = model
  end

  def name
    "#{@make} #{@model}"
  end

  def horsepower
    200
  end
end
Now, we can create a race car:
car = RaceCar.new('Dodge', 'Charger')

car.name
# Dodge Charger

car.horsepower
# 200

TurboCharge Wrapper

Race car garages can increase horsepower by adding different parts to their vehicles. Using a simple wrapper, let's create a TurboCharge decorator class that takes a RaceCar object and increases its horsepower by 30:
class TurboCharge
  def initialize(race_car)
    @race_car = race_car
  end

  def horsepower
    @race_car.horsepower + 30
  end
end
Now, we can use this class to decorate our car:
turbo_car = TurboCharge.new(car)

turbo_car.horsepower
# 230
This is very straightforward and achieves the desired behavior, but breaks the first guideline of decorators. For instance, what happens when we try to treat it like a RaceCar?
turbo_car.name
# undefined method `name' for TurboCharge (NoMethodError) ...
Remember, we have to implement the original interface of the object we are decorating! Our decorator class, as it stands, is incomplete. We can remedy this by implementing name on it and delegating down to the original object:
class TurboCharge

  ...

  def name
    @race_car.name
  end
end
Now, it behaves correctly:
turbo_car.name
# Dodge Charger
What if our original object has a ton of methods? It would be quite painful to re-implement all of the methods and delegate them down. However, if the application does not require all methods in the original object to function, you can simply add the ones that are needed. This is when best judgement should be used. Sometimes, the simplicity of implementing wrapper classes outweighs the need to to provide absolute transparency.

TurboCharge Module

Let's say transparency is very important. Well, there's another way to add behaviors dynamically without losing the original interface of the decorated object We can create a TurboCharge module and extend any instance of RaceCar with it:
module TurboCharge
  def horsepower
    super + 30 # 'super' refers to the original object's horsepower method
  end
end
Modules are usually used to share similar functionality across multiple class definitions. In our case, we want to add functionality to an instance. We can "mix in" our TurboCharge module to any object by using extend. Using this method, we can decorate to our RaceCar instance without changing how it originally behaves:
car.extend(TurboCharge)

car.horsepower
# 230

car.name
# Dodge Charger
Voila!

Decorating Your Way

When I decide to decorate, I'll usually start with the wrapper approach first. This allows me to be explicit in designing the interface of my decorated object. However, if I find it is too painful to try and recreate the original interface as the application requires, I'll opt for extending the object with a decorator module. Adjust the design pattern to fit your needs!

Questions? Comments? Let me know below or send me a Tweet. Thanks for reading!

5 comments:

  1. This Wrapper approach contradicts the Decorator pattern (Gang of Four definition) as it creates new classes with the added functionality. If you are happy with creating a new class, you could as well subclass the RaceCar class into TurboCharge (and you won't have to worry about those "tons of method" as the come along :-)

    ReplyDelete
    Replies
    1. Yes, the Wrapper approach contradicts GOF's definition, but still providers the "decorator" functionality we're seeking. It also allows us to apply functionality at runtime (not achievable with subclassing) to each individual instance. Even if the original instance was not a RaceCar (maybe a Truck class), we could still wrap it in a TurboCharge object. I'm basically preferring to compose my objects to achieve functionality rather than inherit it. Hope that clarifies the approach!

      Delete
  2. Why not open the original RaceCar (or Truck) object and add the functionality directly? Beacuse of loosing `super` or loosing the ability to remove the functionality? Of course it wouldn't be anything related to a decorator :-)

    ReplyDelete
    Replies
    1. Do you mean opening the RaceCar class object? Or redefining the method on a RaceCar instance object? If you modify the RaceCar class object, you are permanently redefining the method for all instances of the class. This is not the desired behavior, as I only want specific instances to exhibit the behavior.

      If you are talking about redefining the RaceCar instance object, then you're approach would work just fine and dandy. You can do this by "extending" your existing instance. Sure, you can do that anywhere in the code, but like you said, it would not be a decorator, per se. Although, I would still say you are "decorating" the object.

      Delete
  3. Yes, I suggested to modify the instance (not the class).

    ReplyDelete