How to work with decorators in ruby
Sometimes, we find ourselves wanting to add functionality to objects of a class without affecting all objects of the class (unlike inheritance or composition via mixin modules).
Assume we have a User
class like the one below:
We now want to generate the full name of some users (based on some condition that is not important in the context of this post). What are our options?
One option would be to create a full_name method directly for the User
class:
The disadvantage to this approach is that we add the functionality to all User
instances, even the ones we don’t want to have it or use it. Using inheritance (e.g. having a superclass implementing full_name and then User
extend this class) has the same disadvantage along with all the risk of creating a whole hierarchy of classes just to add functionality that will be used only by some of them.
Another option is composition through mixins, like below:
This approach would work and we can even add it to other classes or remove it from the User
class in the future, if need be. Nevertheless, the functionality is added once again to all objects of the User
class.
Here is where the decorator pattern comes in handy. Simply put, decorator is a class that extends the functionality of an object dynamically. Let’s see a simple implementation of a decorator:
Now, for the specific users we want to have this extra functionality, we can user the decorator in place of a User
instance: decorated_user = UserDecorator.new(user)
. By using some metaprogramming (method_missing), we forward all method calls to User
and extend the functionality in the definition of UserDecorator
.
There are numerous advantages to this approach:
- the
User
class is not changed at all, no new responsibilities are added to this - the
UserDecorator
class has clear responsibilities and we can easily see what is the functionality it adds to users - the decorator can be wrapped by other decorators infinitely (not a great idea in my opinion, both in terms of performance and of readability of the code)
Finally, an approach from the ruby standard library (see more in the ruby docs) is SimpleDelegator
:
SimpleDelegator
is similar to the method_missing approach we built ourselves. The extra benefit of this approach - apart from not having to build it ourselves :) - is that there is a method __setobj__
, where we can change the delegate object dynamically after instantiation (I cannot find any use to it currently, but in some contexts it might be needed).
Thanks all for now!