DEV Community

Vlad Hilko
Vlad Hilko

Posted on • Updated on

How to implement Decorator pattern in Ruby on Rails?

Decorator design pattern allows us to add new functionality to an object without affecting the behavior of the original class.

Why do we need it and what problems can this pattern solve?

Sometimes we have complex logic that is used directly in the business logic. For example, the following code can be used many times in different controllers and service objects:

def show
  user = User.first
  full_name = "#{user.name} #{user.surname}"
  full_address = "#{user.country} #{user.city} #{user.street}"

  render json: { name: full_name, address: full_address }
end
Enter fullscreen mode Exit fullscreen mode

Problems:

  • It's impossible to test separately from the controller.
  • It's not DRY
  • It's violates single-responsibility principle
  • It's not clear, hard to read and understand what's going on.

So what can we do to fix it?

  • We can move this logic to the model level.
  • We can create a Decorator class and move the logic there.

Let's look at these options one by one.


Model methods

Adding new model methods is the easiest option, we just need to extend our model in the following way:

# frozen_string_literal: true

class User < ApplicationRecord

  def full_name
    "#{name} #{surname}"
  end

  def full_address
    "#{country} #{city} #{street}"
  end

end

user = User.new
user.full_name
user.full_address
Enter fullscreen mode Exit fullscreen mode

Why do we need other options if the model methods solve our problems?

The problem here is that we violate the Single responsibility and the open-closed SOLID principles ('a class should basically serve one purpose' and 'a class/object should be open for extension, but closed for modification'). The model becomes too FAT and responsible for too many things. In real projects, we'll end up with a hundred different methods and lines of codes. You can probably imagine how difficult it would be to read, modify and maintain this class. That's why it would be nice to have the same interface, but move all the methods into a separate class.

How can we do that?

Meet Decorator Pattern.

In this article we'll consider 3 ways to create Decorator from the simplest to the most complex, here they are:

  • Adding Decorator via Plain Ruby
  • Adding Decorator via SimpleDelegator
  • Adding Decorator via Draper gem

Let's take a look at the examples with each options.


Plain Ruby Decorator

Plain Ruby Decorator is just a class that accepts a model object and returns a new object with all desired methods. For example:

# app/units/decorators/user.rb

# frozen_string_literal: true

module Decorators
  class User

    attr_reader :user

    def initialize(user)
      @user = user
    end

    def full_name
      "#{user.name} #{user.surname}"
    end

    def full_address
      "#{user.country} #{user.city} #{user.street}"
    end

  end
end

decorated_user = Decorators::User.new(User.last)
decorated_user.full_name
decorated_user.full_address
Enter fullscreen mode Exit fullscreen mode

The interface looks good, but we have to repeat user every time inside the decorator. Let's try to change this with our second option - SimpleDelegator Decorator


SimpleDelegator Decorator

SimpleDelegator Decorator is the same as a Plain Ruby Decorator, but with only one difference - we delegate all supported method calls to the object passed into the constructor. Let's take a look at the following example:

# app/units/decorators/user.rb

# frozen_string_literal: true

module Decorators
  class User < SimpleDelegator

    def full_name
      "#{name} #{surname}"
    end

    def full_address
      "#{country} #{city} #{street}"
    end

  end
end

decorated_user = Decorators::User.new(User.last)
decorated_user.full_name
decorated_user.full_address
Enter fullscreen mode Exit fullscreen mode

With SimpleDelagator our solution looks much clearer and more elegant. This approach solves 95% of the common cases and I really like it because of its simplicity, but if you need something more complex, here is our last option, draper decorator.


Draper Decorator

Draper is the most popular gem to implement Decorator pattern for Rails. With this gem our example would look like this:

# app/units/decorators/user.rb

# frozen_string_literal: true

module Decorators
  class User < Draper::Decorator

    delegate_all

    def full_name
      "#{name} #{surname}"
    end

    def full_address
      "#{country} #{city} #{street}"
    end

  end
end

decorated_user = Decorators::User.new(User.last)
decorated_user.full_name
decorated_user.full_address
Enter fullscreen mode Exit fullscreen mode

You can read more about draper here and decide for yourself whether it makes sense to add it or not.

So the final solution may look as follows:

def show
  user = Decorators::User.new(User.first)

  render json: { name: user.full_name, address: user.full_address }
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

  • Decorator can be easily tested separately from the controller
  • Decorator is DRY and reusable
  • Decorator allows us to avoid FAT model and stick to SOLID principles
  • Decorator reduces chances of breaking existing logic and increases product stability
  • Decorator reduces maintenance cost

Top comments (6)

Collapse
 
abhishek87654321 profile image
Abhishek Tanwar

Nice articulated 👍

Collapse
 
kgilpin profile image
Kevin Gilpin

Another option would be to make the decorator a module. It could be added to the User class, or to an individual user instance. Right?

Collapse
 
vladhilko profile image
Vlad Hilko

As an option - yes, but it won't be a decorator pattern anymore, because we affect the behavior of the original class, and there's still a risk of breaking something, so I'm not sure it will give us the benefits we want.

Collapse
 
naucode profile image
Al - Naucode

Hey! Thank you for this, I liked it ;) keep writing, you got my follow!

Collapse
 
vladhilko profile image
Vlad Hilko

Thank you :)

Collapse
 
ssd profile image
server side digest

Thanks, Really helpful.

👉 Kindly check my blog too and suggest any improvements dev.to/lovepreetsingh/what-is-dock...