This article was originally published on Rails Designer's Build a SaaS.


There is no shortage of patterns in programming: Singleton-, Service- or Command patterns. Some are so similar, I don't even know the actual differences between them. As such the only patterns I use in my own apps and those I build for others are Service (or was it Command?) objects and Decorator Pattern. Though both don't get their own special directory in the app-folder. I know—crazy (maybe I dedicate a post how I structure my apps some time—it is quite vanilla).

The decorator pattern is what I want to focus on here. It allows you to add responsibilities to (Active Record) objects without modifying its structure, but “decorating” it or, put differently: wrapping it. 🎁

I don't reach for such patterns easily. Lots of my data handling happens within the Active Record model, often using associated objects (e.g. Resource::Publishable). But I don't have “presentation logic” in my Active Record objects, as I have ViewComponent for that. Let's look at an example from an app I recently worked on:

class Resource < ApplicationRecord
  include Event::History, Identifiable, Lockable, Sluggable
  include Filterable, Renderable, Publishable

  belongs_to :owner

  delegated_type :resourceable, types: [Page, Cover], dependent: :destroy

  def primary_field
    settings.find_by(key: "title").value.presence || "Untitled"
  end
end

A standard “parent” Resource model used by delegated (type) models. Above's primary_field is an anti-pattern in my book; it should be defined in a component.

class ResourceComponent < ApplicationComponent
  def initialize(resource)
    @resource = resource
  end

  def primary_field
    @resource.settings.find_by(key: "title").value.presence || "Untitled"
  end
end

And now that primary_field method can be removed from the Resource model.

class Resource < ApplicationRecord
  include Event::History, Identifiable, Lockable, Sluggable
  include Filterable, Renderable, Publishable

  belongs_to :owner

  delegated_type :resourceable, types: [Page, Chapter], dependent: :destroy
end

Keeping things tidy and clear. Much better! But now the app needed an API endpoint, listing the resources in some way. Should I put the primary_field back into the Resource model? Could do that. And I wouldn't yell at you if it was just the one field. But that is almost never the case. And that is when I reach for decorators.

Let's add it:

class Resource < ApplicationRecord
- include Event::History, Identifiable, Lockable, Sluggable
+ include Decoration, Event::History, Identifiable, Lockable, Sluggable
  include Filterable, Renderable, Publishable

  belongs_to :owner

  delegated_type :resourceable, types: [Page, Chapter], dependent: :destroy
end

Here Decoration is a concern that can simply be added to any Active Record model and is really simple:

# app/models/concerns/decoration.rb
module Decoration
  def decorate(with: nil)
    decorator_class = with || "#{self.class}::Decorator".constantize

    decorator_class.new(self)
  end
end

This will look for a class using the class name it is used in, e.g. Resource::Decorator. This thus lives in app/models/resource/decorator.rb. Lets create it now:

class Resource::Decorator < SimpleDelegator
  def primary_field
    settings.find_by(key: "title").value.presence || "Untitled"
  end
end

This keeps all classes related to Resource together. Clean and maintainable!

So how to use this? You can chain decorate to the Resource model, like so: Resource.find(1).decorate. Or when using a collection: Resource.all.map(&:decorate). The optional with attribute allows to set another if needed: Resource.find(1).decorate(with: AnotherDecorator).

What is SimpleDelegator?

See the SimpleDelegator in above code? That is what is doing all the “magic” for you. It is a Ruby standard library class that implements the Decorator pattern, allowing you to wrap objects and selectively override or extend their behavior while delegating other method calls to the original object. This means methods not present in the decorator, gets passed on the wrapped class. So when you write: Resource.find(1).decorate.id, it would still return 1.

Nice, little technique using standard Ruby tools.