Command Objects - a.k.a Service Objects in Ruby on Rails - The Ergonomic Way

Overview

Ruby on Rails is a great framework, and we, at ErgoServ, really love it. It follows inherently strong principles like ‘Optimize for Programmer Happiness’ and our favorite ‘Convention Over Configuration’ amongst other significant foundational pillars.

Ruby on Rails has a lot of built-in tools that help build functional and feature-rich projects very quickly. Remember that famous tutorials blog in 15 minutes? (Yeah, 15 minutes! Installing and configuring WordPress may take more time ;) )

Rails provides an excellent and easily manageable structure for the codebase of unconventional projects based on the Model-View-Controller or the MVC pattern. The RoR framework works pretty well for small-size projects. But when a project grows, you need a proper place to store your domain or business logic.

Technically controllers were initially supposed to hold the business logic of the application. But modern web applications are getting complex and have many places to run a business process like background jobs, schedulers, rake tasks, etc. Plus, you want your code to be reusable.

Models are also not the best place to store business logic as they get bloated as the application grows. Flexibility is paramount as the app's logic may be needed to work with several models simultaneously or, in some cases, not work with models at all.

Concerns is another place where David Heinemeier Hansson, the creator of Ruby on Rails, recommends putting all the business logic. While Concerns are quite functional and can make things much shorter, we don't think they always make things clearer. So, following another item from the Rails doctrine "No one paradigm", we use concerns only for tiny things, but for more complex logic, we use other structures.

In the modern Ruby on Rails world, the approach of so-called Service Objects is quite popular nowadays. While Service Objects have many variations, at ErgoServ, we prefer to follow its more classic version that implements Command Pattern, calling this new application layer Command Objects.

Command Objects (a.k.a. Services Objects)

Introduction

Command Objects provide considerable benefits when we introduce them to encapsulate business logic, including the following:

  • Skinny Controllers, Skinny Models
  • Isolated testing of business processes
  • Reusable code
  • and many more

A Command should encapsulate a single user task such as registering for a new account or placing an order. You, of course, don’t need to put all code for this task within the Command. You can (and should) create other classes that your Command uses to perform its work.

Concept and Conventions

  • One public method (`perform`) - describes all steps of a process in a straightforward and unambiguous way.
  • Simple and descriptive `perform` method - keep it as short, simple, and clear as possible, yet it should describe what a command does.
  • Command steps - put all business logic steps into private methods with definite, self-explanatory, and meaningful names.
  • Name Command Objects accordingly - the name should be clear and meaningful, and since it performs some process or action, it should contain a verb that distinctly describes the action the command performs.
  • Directory `app/commands` - the most convenient palace for commands as another layer of the app.
  • Suffix `Command` for the class name - the command name should have the suffix `Command`. It helps define the object type explicitly.

Solution

You may have your own implementation of Command Objects (or Service Objects) by starting with a simple PORO (Plain Old Ruby Objects). You can find many tutorials and examples for PORO, or use one of the plenty gems that provide their vision of this approach.

We have also, in the past, tried many of them but finally came up with our own implementation of Command Objects which we wrapped into a gem called AuxiliaryRails.

Command Objects by AuxiliaryRails

Benefits

  • Is clear and simple, yet advanced and flexible definition of arguments (using `param` and `option`).
  • Integrates with `ActiveModel::Validations` - which helps to validate arguments in a familiar manner.
  • Integrates with `ActiveModel::Errors` - which allows providing an extensible and friendly response in case of errors.
  • Supports responding with multiple results if necessary, encapsulates into the instance of command, instead of wired kinds of “Result Objects”.
  • An instance of the Command Object has status (failure or success).
  • Uses commonly used and universal names for primary method “perform”, utilized magic method “call”, and provides some syntax-sugar.

Usage

# app/commands/application_command.rb
class ApplicationCommand < AuxiliaryRails::Application::Command
end

# app/commands/register_user_command.rb
class RegisterUserCommand < ApplicationCommand
  # Define command arguments
  # using `param` or `option` methods provided by dry-initializer
  # https://dry-rb.org/gems/dry-initializer/3.0/
  param :email
  param :password

  # Define the results of the command using `attr_reader`
  # and set it as a regular instance var inside the command
  attr_reader :user

  # Regular Active Model Validations can be used to validate params
  # https://api.rubyonrails.org/classes/ActiveModel/Validations.html
  # Use #valid?, #invalid?, #validate! methods to engage validations
  validates :password, length: { in: 8..32 }

  # Define the only public method `#perform` where command's flow is defined
  def perform
    # Use `return failure!` and `return success!` inside `#perform` method
    # to control exits from the command with appropriate status.

    # Use `return failure!` to exit from the command with failure
    return failure! if registration_disabled?

    # Method `#transaction` is a shortcut for `ActiveRecord::Base.transaction`
    transaction do
      # Keep the `#perform` method short and clean, put all the steps, actions
      # and business logic into meaningful and self-explanatory methods
      @user = create_user

      # Use `error!` method to interrupt the flow raising an error
      error! unless user.persistent?

      send_notifications
      # ...
    end

    # Always end the `#perform` method with `success!`.
    # It will set the proper command's execution status.
    success!
  end

  private

  def create_user
    User.create(email: email, password: password)
  end

  def send_notifications
    # ...
  end
end

### usage ###

class RegistrationsController
  def register
    cmd = RegisterUserCommand.call(params[:email], params[:password])

    if cmd.success?
      redirect_to user_path(cmd.user) and return
    else
      @errors = cmd.errors
    end

    ### OR ###

    RegisterUserCommand.call(params[:email], params[:password])
      .on(:success) do
        redirect_to dashboard_path and return
      end
      .on(:failure) do |cmd|
        @errors = cmd.errors
      end
  end
end

Conclusion

The Command Objects pattern can immensely improve your application's overall design. Our approach and gem can provide you with a simple yet effective way to manage your commands.

Gem AuxiliaryRails can be found on GitHub

We would be happy to respond to your questions or requests there as well.

Relevant tags:
#tech-advice #ruby #ruby-on-rails #auxiliary-rails #modular-rails #development
Dmitry Babenko
Dmitry Babenko
Feb 16, 2022
Relevant tags:
#tech-advice #ruby #ruby-on-rails #auxiliary-rails #modular-rails #development
Share post:

Related Articles

ErgoServ — software development services for every business

  • Timely updates
  • Workflow transparency
  • Personalized service
Contact us today