Go to main contentGo to footer
Ruby
|
15 May 14

Slimming down Rails models and controllers with command objects

In the Ruby and Rails world, people often talk about best practices, testing methodologies and development tools. It's less common to hear about design patterns for structuring code in Rails. In this article, we introduce commands and explain how and why to use them.

Andrea PavoniDeveloper

For several years the prevailing mantra was "skinny controller, fat model": move application logic out of the controller and into the model, to make it easier to reuse. It's still a valid practice, even if it can sometimes feel a bit restrictive.

Let's take a concrete example: our application needs to run a series of actions whenever a new user signs up:

  • generating a key that grants access to the API;
  • sending a welcome email.

Using ActiveRecord hooks, this could be one possible implementation:

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :things

  validates :email,
    presence: true,
    uniqueness: { case_sensitive: false }

  before_validation :generate_api_token, on: :create
  after_save :send_welcome_email, on: :create

  private

  def send_welcome_email
    # ...
  end

  def generate_api_token
    # ...
  end
end


# app/controllers/users_controller.rb
class UsersController < ApplicationController
  # ...

  def create
    @user = User.new(params[:email])

    if @user.save
      redirect_to users_path, notice: "Welcome!"
    else
      render :new
    end
  end

  # ...
end

At first glance there's nothing wrong with it; we've all read and written code like this dozens of times without batting an eye. There are, however, a couple of observations we can make:

  • Because we're forced to implement all the application logic through hooks that run before, during or after data is saved, the flow tends to "jump around". If we came back to the code in a few weeks, we'd probably struggle to piece together the full set of "automatic" side actions;
  • Imagine a batch user import task: in that case we might not need to send a welcome email! Of course Rails lets us disable a particular hook under certain conditions, but since the behavior is "hidden", it's easy to forget about it;
  • At least in theory, a model's only responsibilities should be data persistence and consistency: a model shouldn't know anything about what happens at the business-logic level;
  • By putting all the application logic inside models, you risk quickly ending up with God objects: files thousands of lines long that are hard to manage and control the entire application.

So let's imagine an ActiveRecord model whose only responsibilities are declaring any associations with other models and validating data. Where could we put the application logic without breaking the rule of keeping controllers clean?

Introducing Commands

A command, or service object, is a class dedicated to performing a specific action, whether simple or complex. Going back to the example above, we could implement a command that creates the user, generates the API key and sends a welcome email:

# /app/commands/create_user.rb
class RegisterUser < Struct.new(:email)
  def execute
    user.email = email

    if user.valid?
      user.api_token = generate_api_token
      user.save!
      send_welcome_email
    end
    user
  end

  private

  def user
    @user ||= User.new
  end

  def send_welcome_email
    # ...
  end

  def generate_api_token
    # ...
  end
end


# /app/controllers/users_controller.rb
class UsersController < ApplicationController
  # ...

  def create
    @user = RegisterUser.new(params[:email]).execute

    if @user.persisted?
      redirect_to users_path, notice: "Welcome!"
    else
      render :new
    end
  end
  # ...
end

Even though we deliberately chose a very simple example, using a command has let us isolate the application logic, freeing the model and the controller from responsibilities that didn't belong to them.

It's worth noting a few characteristics of the command:

  • a single method, execute, that performs the action: while nothing stops you from exposing other public methods, having just one way to use it makes our code much simpler;
  • it's a plain Ruby object (also known as a PORO, Plain Old Ruby Object): it keeps us independent of other libraries and, among other things, makes it easy to make changes, even radical ones, painlessly;
  • easy to test: with a single method and a plain Ruby object, writing unit tests or running integration tests becomes much simpler;
  • easy to reuse: if we need to build an API, we can reuse exactly the same command, with no code duplication. The same goes for rake tasks, or even console operations.

Conclusions

This article aims to show, in simple terms, the potential of using commands. Although at first glance it may look like wasted code or, worse, unnecessary complexity, you start to appreciate the benefits of this approach over time, when you need to add new features or change existing ones, and you notice that every piece fits in without difficulty or upheaval. Organizing application logic with commands also gives you an overview of what the application does and its main use cases just by opening a single folder, app/commands, which makes onboarding new team members easier.

footer