DEV Community

Cover image for Simple decorators with SimpleDelegator
Georgy Yuriev
Georgy Yuriev

Posted on

Simple decorators with SimpleDelegator

Decorator is a powerful pattern that helps us keep our models clean while adding presentation logic. Today, I want to share my approach to implementing decorators in Ruby on Rails.

I was inspired by a great article:
Build a minimal decorator with Ruby in 30 minutes
by Rémi - @[email protected]
Thanks to Ruby Weekly for sharing it!


What if we took a different approach? Let's solve this problem in reverse, like in the Tenet movie where time flows backwards 😈

Imagine we have

  • a Post model with a status enum column,
  • an Author model with first_name and last_name fields.

Let's create some decorators to enhance these models.

# app/decorators/post_decorator.rb

class PostDecorator < SimpleDelegator
  STATUS_COLORS = {
    published: :green,
    draft: :indigo
    archived: :gray,
    deleted: :red
  }.freeze

  def status_color = STATUS_COLORS[status.to_sym]
end
Enter fullscreen mode Exit fullscreen mode
# app/decorators/author_decorator.rb

class AuthorDecorator < SimpleDelegator
  def full_name
    name = [first_name, last_name].compact.join(' ')
    name.presence || 'Guest'
  end
end
Enter fullscreen mode Exit fullscreen mode

Now we can use it this way:

@posts = Post.all.map { PostDecorator.new(it) }
Enter fullscreen mode Exit fullscreen mode
<%= AuthorDecorator.new(post.author).full_name %>
Enter fullscreen mode Exit fullscreen mode

It adds so much boilerplate to the source code - I really dislike these wrappers. Let's clean this up adding method_missing method to all our models.

Image description

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

+  include Decoratable
end
Enter fullscreen mode Exit fullscreen mode
# app/models/concerns/decoratable.rb

module Decoratable
  extend ActiveSupport::Concern

  def respond_to_missing?(method_name, ...)
    return false unless decorator_class

    decorator_class.instance_methods.include?(method_name)
  end

  def method_missing(method_name, ...)
    return super unless respond_to_missing?(method_name)

    decorated_instance.public_send(method_name, ...)
  end

  private

  def decorator_class
    @decorator_class ||= "#{model_name}Decorator".safe_constantize
  end

  def decorated_instance
    @decorated_instance ||= decorator_class.new(self)
  end
end
Enter fullscreen mode Exit fullscreen mode

This will check if the method exists in the corresponding decorator before throwing an error, allowing us to use decorator methods as if they were defined in the models themselves!

- @posts = Post.all.map { PostDecorator.new(it) }
+ @posts = Post.all

- <%= AuthorDecorator.new(post.author).full_name %>
+ <%= post.author.full_name %>
Enter fullscreen mode Exit fullscreen mode

This approach gives us the best of both worlds: clean models and convenient access to decorator methods. The method_missing implementation acts as a bridge between our models and decorators, making the code more maintainable and easier to work with.

Much cleaner now, right?

Top comments (0)