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 - @remi@ruby.social
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)