… That you will probably end up creating yourself …
After over 12 years of working with Ruby on Rails on multiple different codebases, I’ve some common themes. This is my list of patterns or types of objects you will see in Rails applications.
1. Query Objects.
ActiveRecord provides the scope macro which allows us to define queries related to that model. However, for larger, more complex queries that span multiple models, scopes can often lead to a patchwork of distributed logic. To address this, I prefer to build stand alone a Query object. I usually place this in a separate folder: `app/queries’. The following is an example of query object:
A simple base query object:
module Kenglish
QueryResult = Struct.new(:data, :meta)
class Query
class << self
def result(*args)
new(*args).result
end
end
attr_reader :params, :relation
def initialize(params, relation)
@params = params
@relation = relation
end
def build_result(data, meta)
QueryResult.new(data, meta)
end
end
end
With an example ProductQuery:
module ProductQuery
class All < Kenglish::Query
def result
records = apply_filters(search(relation)).includes(:owner).references(:owner)
records = apply_sorting(records)
records = paginate(records)
build_result(records, pagination_meta(records))
end
end
end
In a controller, this could be invoked like this:
def index
query = ProductQuery::All.result(params, site.products)
render json: to_jsonapi(data: query.data, meta: query.meta)
end
Some rules of thumb here are:
- Make your query objects injectable with ActiveRecord models or scopes (what I have called
relation
), this make it play well with policy scopes. - Build common methods for searching, sorting paginating and make these customizable for different types of applications (apis, frontend, graphql, etc)
2. Form Objects
A form object, or in other parlance, a contract object, is an object that receives data from frontend forms or API calls. Beginners to Rails will embed form-like logic into their controller. As application code matures, extracting a form object will be a natural next step.
Here’s a quick and dirty base form object:
module Kenglish
class Form
include Virtus.model
include ActiveModel::Validations
# Returns true if an id field is present for the obj/model
# @return [Boolean] Has form been persisted?
def persisted?
id.present? && id.to_i.positive?
end
# Returns true if form is valid
# @return [Boolean] Is form valid?
def valid?(options = {})
before_validation
options = {} if options.blank?
context = options[:context]
validations = [super(context)]
validations.all?
end
# Opposite of valid?
# @return [Boolean] Is form valid?
def invalid?(options = {})
!valid?(options)
end
end
end
A signup form would look like this.
module SignupForm < Kenglish::Form
attribute :first_name, String
attribute :last_name, String
attribute :password, String
attribute :password_confirmation, String
attribute :country, String, default: 'US'
validates :first_name, :last_name, :country, presence: true
validates :password, presence: true, confirmation: { case_sensitive: true }
end
One of the benefits of using Virtus, which requires you to specify the attributes, is that you don’t need to mess with strong params.
Some rules of thumb I’ve found useful:
- Use Virtus and ActiveModel::Validations so Form objects feel like models
- Don’t save data in your Form object, only use it for validation. Treat it more like ValueObject that holds data than something that save the form.
- Write class level methods to translate incoming data from GraphQL, jsonapi or html forms.
3. Command / Operation objects
I dislike the name “service object” because it does not invokes anything specific other than “something that is not a Model, View or Controller”. As an example, the trailblazer library has Operations and the Rectify library has Commands. Either name is appropriate.
The goal is to have an object that has one method responsible for performing business logic (in many cases database transactions) on incoming data (your Form object). We can extract an example from Rectify to make something very abstract:
module Kenglish
class ApplicationCommand
class << self
def call(*args, &block)
new(*args, &block).call
end
def success?(resp)
resp.key?(:success)
end
def error?(resp)
resp.key?(:error)
end
end
attr_reader :form
def initialize(form)
@form = form
end
def success(result)
{ success: result }
end
def error(result)
{ error: result }
end
end
end
An example of using this command object:
class ReservationCommand < Kenglish::ApplicationCommand
def call
return error(form.errors) unless form.valid?
arline = form.arline
reserversation = nil
arline.transaction do
# Airline booking logic goes here
reservation = create_reservation(form)
end
success(reservation)
end
end
Our controller would then look like this:
class ReservationsController < ApplicationController
def create
form = ReservationForm.new(params)
resp = ReservationCommand.call(form)
return render 'confirmation', locals: { reservation: resp[:success] } if success?(resp)
render 'new', locals: { reservation_form: form }, status: :unprocessable_entity
end
end
Some rules of thumb I’ve found useful
- Create a uniform response object that makes it easy to handle error/success.
- Design a way that commands can be chained together, functional programing style.
4. Decorators
Decorators should be used to wrap display logic that we only use in our view layer, serializers or grahpql resolvers. My goto library when using a purely ActiveRecord based Rails app is draper. However, a simple decorator can be created to delegate to any type of object.
A simple decorator base class looks like this:
module Kenglish
class Decorator
attr_reader :record, :helpers
alias h helpers
# Build a new decorator
# @params [record] ActiveRecord obj
# @params [helpers] Controller's view_context/helpers
def initialize(record, helpers)
@helpers = helpers
@record = record
end
# For activemodel compatibility
# @return [record]
def to_model
record
end
# For activemodel compatibility
# @return [record]
def model
record
end
# For activemodel compatibility
# @return [Boolean]
def is_a?(klass)
record.class.object_id == klass.object_id
end
# Delegate methods to record
# @return []
def method_missing(meth, *args)
if record.respond_to?(meth)
record.send(meth, *args)
else
super
end
end
# Respond to missing methods
# @return [Boolean]
def respond_to_missing?(meth, _include_private = false)
record.respond_to?(meth)
end
# Check if respond_to question mark method if
# it does not respond to regular.
# Respond to missing methods
# @return [Boolean]
def respond_to?(meth)
return true if record.respond_to?(meth)
super
end
delegate :class, to: :record
end
end
To decorator a user:
class UserDecorator < Kenglish::Decorator
def full_name
[first_name, last_name].join(' ')
end
def profile_link
h.link_to full_name, profile_path(self)
end
end
5. Serializers
Many Rails applications in the wild are api-only or end up supporting apis. It is important to use a standardized method of serializing data. For jsonapi, fast_jsonapi is a great library. More and more, I’m finding people using GraphQL schemas in which case you can use graphql-ruby and build your own resolvers.
I always suggest staying away from libraries like rabl and jbuilder that are tightly coupled with Rails’s view layer. Libaraies like fast_jsonapi can be tested outside of Rails and shared with non-Rails Ruby application
6. Policy Objects
Large applications require authorization to determine who has access to what data. A great policy library is Pundit. It is important to keep you role system as independent as possible from your policy library. In an object-oriented fashion, pundit can accomdate this. Pundit’s greatest feature is Scope which can be fed into your query objects.