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!