Build a minimal decorator with Ruby in 30 minutes

Jun 12, 2025 - 08:45
 0  0
Build a minimal decorator with Ruby in 30 minutes

Build a minimal decorator with Ruby in 30 minutes

← Return to the Ruby blog

A few weeks ago, I needed to add some view-related methods to an object. Decorators are my go-to pattern to handle this kind of logic.

Normally, I’d use the draper gem to build decorators. But the app I’m working on used an older and incompatible version of Rails.

So I built a minimal decorator from scratch, added a bunch of extra behaviors, only to end up abstracting all of these away. Follow along!

What I’m working with

My Teacher class has a handful of methods:

  • A one-to-many relationship with the Student class.
  • Two public methods: one that exposes the maximum number of students a teacher can teach to, and one exposing the available teaching places.
  class Teacher < ApplicationRecord
    has_many :students

    def maximum_number_of_students = 30

    def available_places
      (maximum_number_of_students <=> students.size).clamp(0..)
    end
  end

In my views, I want to display a table of teachers where the number of available places for each teacher is backed by a background colour.

# teachers/index.html.erb

<table class="table table-striped">
  <thead>
    <tr>
      <th>Name of the teacher</th>
      Available placesth>
    </tr>
  thead>
  <tbody>
    <% teachers.each do |teacher| %>
      
        <td><%= teacher.full_name %>
        "<%= teacher.colour_coded_availability %>">
            <%= teacher.available_places %>
        
      
    <% end %>
  

I could write the Teacher#colour_coded_availability method in my model like so:

  class Teacher < ApplicationRecord
    has_many :students

    def maximum_number_of_students = 30

    def available_places
      (maximum_number_of_students <=> students.size).clamp(0..)
    end

    def colour_coded_availability
      case available_places
      when 0 then "bg-colour-red"
      else "bg-colour-green"
      end
    end
  end

However, models are not the place for methods generating CSS classes. Decorators are!

Drafting a decorator

My decorator should accept an instance of Teacher and expose the colour_coded_availability public method.

# app/decorators/teacher_decorator.rb

  class TeacherDecorator
    attr_reader :teacher

    def initialize(teacher:)
      @teacher = teacher
    end

    def colour_coded_availability
      case teacher.available_places
      when 0 then "bg-colour-red"
      else "bg-colour-green"
      end
    end
  end

Now, I can instantiate my decorator and use it in my views:

# app/controllers/teachers_controller.rb
  class TeachersController < ApplicationController
    def index
      @teachers = Teacher.all.map { TeacherDecorator.new(teacher: _1) }
    end
  end

# teachers/index.html.erb
  <table class="table table-striped">
    <thead>
      <tr>
        <th>Name of the teacher</th>
        Available placesth>
      </tr>
    thead>
    <tbody>
      <% @teachers.each do |teacher| %>
        
          <td><%= teacher.full_name %>
          "<%= teacher.colour_coded_availability %>">
              <%= teacher.available_places %>
          
        
      <% end %>
    
  

When I can call teacher.colour_coded_availability in my views, the method retrieves a CSS class and adds it to the HTML tag.

But if I were to run this code as is, I’d get a beautiful NoMethodError. Why?

My views do not handle instances of Teacher anymore. They handle instances of TeacherDecorator. So, when I’m calling the public methods defined on Teacher, the decorator doesn’t know what to do with them.

My decorator needs to be able to handle both its own public methods and the public methods defined on the underlying record (Teacher, in this case).

And we do that by using Ruby’s method_missing.

Ruby’s method_missing to the rescue

method_missing is how Ruby handles method calls made on objects where said methods are not defined. Ruby passes the method call along the ancestry chain until it can either resolves it or raises a NoMethodError.

When I call @teacher.full_name, I want my decorator to rescue the NoMethodError, and forward #full_name to the underlying instance of Teacher.

To do that, I need to re-open Ruby’s method_missing, add a custom behavior, then allow method_missing to run its normal course.

class TeacherDecorator
  attr_reader :teacher

  def initialize(teacher)
    @teacher = teacher
  end

  def availability_as_background
    case teacher.max_number_of_students <=> teacher.available_places
    when -1 then "background-danger"
    when 0 then "background-warning"
    when 1 then "background-success"
    end
  end

  private

  def method_missing(method, *args, &)
    return teacher.public_send(method, *args, &) if teacher.respond_to?(method)

    super
  end

  def respond_to_missing?(name, include_private = false)
    teacher.respond_to?(name) || super
  end
end

In this example, I keep the original signature of Ruby’s method_missing.

The only thing I tweak is forwarding the method call to the underlying teacher. I only forward it if the teacher responds to the method. Then, I let Ruby resume its original behavior 1.

Now, @teacher.full_name is properly forwarded to the underlying instance of Teacher.

What would be cool now, is to allow other decorators to share this behavior.

Normalizing the behavior to create other decorators

One way to gather default behavior shared across various decorators is to rely on inheritance. I can create an ApplicationDecorator whose job is to handle instantiation, and forwarding method calls to the underlying record.

Then, I can have my TeacherDecorator inherit from the ApplicationDecorator.

class ApplicationDecorator
  def initialize(record)
    @record = record
  end

  private

  attr_reader :record

  def method_missing(method, *args, &block)
    if record.respond_to?(method)
      record.public_send(method, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    record.respond_to?(name) || super
  end
end

class TeacherDecorator < ApplicationDecorator
  attr_reader :teacher

  def availability_as_background
    case teacher.max_number_of_students <=> teacher.available_places
    when -1 then "background-danger"
    when 0 then "background-warning"
    when 1 then "background-success"
    end
  end

  private

  alias_method :teacher, :record
end

My TeacherDecorator doesn’t need to bother about its initialization since it’s handled by the parent ApplicationDecorator. The only thing I added, is the ability to reference the record as teacher so it’s clearer what kind of record we’re working with.

Ensure Rails default behavior works well

Some Rails native helpers will have a hard time handling my decorator.

Consider this code:

  `edit_teacher_path(@teacher)` # => Should generate teachers/1/edit

But if @teacher references an instance of my TeacherDecorator, the generated path is teachers/#TeacherDecorator/edit.

How do I make my decorator integrate with Rails default behavior?

I can re-open the to_param method which is responsible for turning (among other things) a record into its id, and delegating its behavior to the record.

class ApplicationDecorator
  def initialize(record)
    @record = record
  end

  delegate :to_param, to: :record

  private

  attr_reader :record

  def method_missing(method, *args, &block)
    if record.respond_to?(method)
      record.public_send(method, *args, &block)
    else
      super
    end
  end

  def respond_to_missing?(name, include_private = false)
    record.respond_to?(name) || super
  end
end

Of course, forwarding every Rails default behaviors to the underlying record is not a great strategy (too much complexity). So, how should I do it?

Use Ruby standard SimpleDelegator

SimpleDelegator provides the means to delegate all supported method calls to the object passed into the constructor.

This means that by using SimpleDelegator, I can remove the initialization and the delegation logics from my ApplicationDelegator.

require "delegate"

class ApplicationDecorator < SimpleDelegator ; end

Everything is abstracted away. And it just works™. @record is not available anymore for my TeacherDecorator to reference, but SimpleDelegator exposes a __getobj__ that works exactly as my previous @record ivar.

Final implementation

Here’s what I ended up with:

require "delegate"

class ApplicationDecorator < SimpleDelegator ; end

class TeacherDecorator < ApplicationDecorator
  def availability_as_background
    case teacher.max_number_of_students <=> teacher.available_places
    when -1 then "background-danger"
    when 0 then "background-warning"
    when 1 then "background-success"
    end
  end

  alias_method :teacher, :__getobj__
end

That’s it! A 30-minute minimal decorator in plain Ruby.

Cheers,

Rémi - @[email protected]

PS: I'm available for hire.

  1. respond_to_missing? only ensures that the responds_to? does not return false positives by allowing the decorator to respond to methods even if they are not statically defined on it. 

What's Your Reaction?

Like Like 0
Dislike Dislike 0
Love Love 0
Funny Funny 0
Angry Angry 0
Sad Sad 0
Wow Wow 0