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 astatus
enum column, - an
Author
model withfirst_name
andlast_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
# app/decorators/author_decorator.rb
class AuthorDecorator < SimpleDelegator
def full_name
name = [first_name, last_name].compact.join(' ')
name.presence || 'Guest'
end
end
Now we can use it this way:
@posts = Post.all.map { PostDecorator.new(it) }
<%= AuthorDecorator.new(post.author).full_name %>
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.
# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
+ include Decoratable
end
# 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
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 %>
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)