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.