Decorator pattern and usage of SimpleDelegator

Subscribe to receive new articles. No spam. Quality content.

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.

Subscribe to receive new articles. No spam. Quality content.

Comments