Decorator pattern and usage of SimpleDelegator
- 07 October 2016
- Patterns Ruby Design
- Decorator pattern and usage of SimpleDelegator
Recently I wrote about template pattern. Today I would like to talk about another useful pattern - decorator.
Let's see how Wikipedia describes that pattern:
In object-oriented programming, the decorator pattern — is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.
It's a relatively simple pattern. Let's go through simple example:
We have User
class with two attributes: first_name
and last_name
:
class User
attr_reader :first_name, :last_name
def initialize(first_name, last_name)
@first_name = first_name
@last_name = last_name
end
end
If you work on web-app and use MVC-framework, it's usual case that in view layer you want to show info about user. For example we might need to show full name which would contain of first_name
and last_name
.
First and obvious way is to add full_name
method straight into model. But the problem is that with time model is getting fat and contains too much code related to representation layer rather that model itself.
In this case we could use decorator pattern. Let's see example:
class DecoratedUser
attr_reader :user
def initialize(user)
@user = user
end
def full_name
"#{user.first_name} #{user.last_name}"
end
end
u = User.new("John", "Doe")
decorated_user = DecoratedUser.new(u)
decorated_user.full_name # => John Doe
So we wrote a class which accepts User
object on initialization. Also we've added full_name
method which works as expected - displays full name of user.
Everything works good, but definition of pattern says that decorator should have existing methods of object as well. In this case decorated_user
has full_name
, but doesn't have first_name
and last_name
which defined in User
class.
decorated_user.first_name # => NoMethodError
Properly implemented decorator pattern should give following methods to decorated_user
:
decorated_user.full_name # => John Doe
decorated_user.first_name # => John
decorated_user.last_name # => Doe
DecoratedUser
should keep behaviour of User
class plus add new methods.
To solve this issue we can use Ruby's module Forwardable. It allows us to pass method calls to particular object inside class. It sounds a bit complicated, but it's easy to understand if look at this example:
require 'forwardable'
class DecoratedUser
extend Forwardable
def_delegators :@user, :first_name, :last_name
def initialize(user)
@user = user
end
def full_name
"#{first_name} #{last_name}"
end
end
u = User.new("John", "Doe")
decorated_user = DecoratedUser.new(u)
decorated_user.full_name # => John Doe
decorated_user.first_name # => John
decorated_user.last_name # => Doe
We extended Forwardable
module, which allows us to define list of methods that will be passed to defined object automatically. In our case it's:
def_delegators :@user, :first_name, :last_name
By this code we delegate methods first_name
and last_name
to @user
object. So if we call decorated_user.first_name
we want to pass method first_name
to @user.first_name
.
If we look at code - it works fine. DecoratedUser
saves basic behaviour of User
class, plus extends it by new method. But there is one problem in our implementation with forwardable
. If we decide to add new method to User
class - we have to remember to add it to def_delegators
in DecoratedUser
class. Otherwise it will not be accessible for decorator.
Module Forwardable
is useful and there are many cases when it fits perfectly, but for this particular case SimpleDelegator works better.
Let's see how code will look like if we inherit DecoratedUser
from SimpleDelegator
:
require 'delegate'
class DecoratedUser < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
end
u = User.new("John", "Doe")
decorated_user = DecoratedUser.new(u)
decorated_user.full_name # => John Doe
decorated_user.first_name # => John
decorated_user.last_name # => Doe
Looks much better and works as expected! On each method call Ruby tries to find it in current class (DecoratedUser
) and if there is no such method - it tries to find that method in object we passed on initialization (User
).
Having such decorator, you could use it inside Rails controller this way:
def show
user = User.find(params[:id])
@decorated_user = DecoratedUser.new(user)
end
In view you can use all available methods of decorator, such as: full_name
, first_name
and last_name
.
If we need to add city
to user, you can easily use decorator to show nice welcome message to user:
require 'delegate'
class User
attr_reader :first_name, :last_name, :city
def initialize(first_name, last_name, city)
@first_name = first_name
@last_name = last_name
@city = city
end
end
class DecoratedUser < SimpleDelegator
def full_name
"#{first_name} #{last_name}"
end
def greeting
"Hi, I'm #{first_name}! I live in #{city}"
end
end
u = User.new("John", "Doe", "London")
decorated_user = DecoratedUser.new(u)
puts decorated_user.greeting
# => Hi, I'm John! I live in London
To sum-up: decorator will be useful if you want to extend behavior of basic class by new feature. Also it's good to have decorators to decouple object's logic from object's presentation.
Try to use that approach in your project and you'll feel the benefits of it. For rails-apps there is a popular gem called draper which implements decorator pattern.