If you missed it, Uncle Bob gave a keynote at Ruby Midwest regarding Rails application structure and being able to intuit a project's functionality from its structure. It really struck a chord with me, and I wanted to share a pattern I've just used for the first time (but considered for quite a while) that really makes me happy.

Commands with ActiveModel

Model, briefly.

So we have a project that has general ledgers. In our model, a Student has a checking account and a savings account. We also have a class called CreditManager that has methods on it that represent each transfer we support within our system. So there are methods in there called transfer_funds_from_checking_to_savings(student, amount) and transfer_funds_from_savings_to_checking(student, amount). Each of these methods are well-tested in the CreditManager's unit tests with mocks ensuring that the ledger models get sent the appropriate messages to handle the transfers.

The old busted way

We have a form in the interface for transferring funds from checking to savings or vice versa. In many Rails apps, this form might submit to a controller action that loads up the CreditManager, checks validations, executes the transfer, and returns. This puts logic into the controller that really doesn't belong there.

In some 'next tier' Rails apps you might see a model built, persisted to the database (but not really used, ideally, for reporting) that was a StudentTransfer. This model might have an after_create that kicked off the credit manager calls. That's a decent step up, but it's really weird to have a database-backed model for this, and inevitably someone starts reporting on 'the ledger' by reading these models, which is....stupid.

Commands!

So the thing I'm doing now, and I'm extremely happy with the result, is to build out a Command class, backed by ActiveModel, to handle this use case.

Benefits
  • Validation of input can be handled, and tested, painlessly.
  • There is an obvious pattern for building a form to execute the command using typical form_for idioms.
  • We will be reusing a custom validator on all transfer commands in the system, so Rails' customer validator framework is very nice.
  • We could easily move to a message based architecture here without missing a beat.
  • Refactoring the bottom layers doesn't affect the controllers at all.
  • Testing a ton of different user interactions can be done entirely at the unit test level, with nary a database connection or session setup to be seen.
Code

So this is a long way to get in a blog post without code, but I figured I'd try to set the stage nicely. Without further ado, here's the relevant bits:

# config/routes.rb
resources :student_transfer_commands


# app/views/banks/_transfer_credits.html.haml
= form_for StudentTransferCommand.new do |f|
  = f.text_field :amount, placeholder: "Amount"
  = f.select :direction, options_for_select([["Checking to Savings", "checking_to_savings"], ["Savings to Checking", "savings_to_checking"]])
  = submit_tag "Transfer"


# app/controllers/student_transfer_commands_controller.rb
class StudentTransferCommandsController < LoggedInController
  def create
    transfer = StudentTransferCommand.new(params[:student_transfer_command])
    transfer.student_id = current_person.id
    if transfer.valid?
      transfer.execute!
      flash[:success] = "Transfer successful."
    else
      flash[:error] = "Invalid transfer."
    end
    redirect_to bank_path
  end
end


# app/models/student_transfer_command.rb
class StudentTransferCommand
  include ActiveModel::Validations
  include ActiveModel::Naming
  include ActiveModel::Conversion

  attr_accessor :amount, :direction, :student_id

  validates :direction, presence: true
  validates :student_id, presence: true, numericality: true
  validates_inclusion_of :direction, in: ["savings_to_checking", "checking_to_savings"]
  validates :amount, positive_decimal: true

  def initialize params={}
    @amount = BigDecimal(params[:amount]) if params[:amount]
    @direction = params[:direction]
    @student_id = params[:student_id]
  end

  # This is so that activemodel acts like we want in the form
  def persisted?
    false
  end

  # The transfer knows what to call on credit manager based on its direction
  def transfer_method
    case direction
    when "savings_to_checking"
      :transfer_credits_from_savings_to_checking
    when "checking_to_savings"
      :transfer_credits_from_checking_to_savings
    else
      raise "unknown direction"
    end
  end

  def student
    Student.find(student_id)
  end

  def credit_manager
    CreditManager.new
  end

  def execute!
    credit_manager.send(transfer_method, student, amount)
  end
end


# app/validators/positive_decimal_validator.rb
class PositiveDecimalValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value.present?
      record.errors[attribute] << "Must be present"
      return false
    end
    unless value.is_a?(BigDecimal)
      record.errors[attribute] << "Must be a BigDecimal"
      return false
    end
    record.errors[attribute] << "Must be positive and non-zero" unless value > BigDecimal('0')
  end
end


# test/unit/student_transfer_command_test.rb
require 'test_helper'

describe StudentTransferCommand do
  subject { StudentTransferCommand.new }

  it "requires valid amount" do
    subject.wont have_valid(:amount).when(nil)
    subject.wont have_valid(:amount).when(0)
    subject.wont have_valid(:amount).when(BigDecimal('-1'))
    subject.wont have_valid(:amount).when('asdf')
    subject.wont have_valid(:amount).when('123')

    subject.must have_valid(:amount).when(BigDecimal('1'))
  end

  it "requires valid direction" do
    subject.wont have_valid(:direction).when(nil)
    subject.wont have_valid(:direction).when("foo")

    subject.must have_valid(:direction).when("savings_to_checking")
    subject.must have_valid(:direction).when("checking_to_savings")
  end

  it "requires valid student_id" do
    subject.wont have_valid(:student_id).when(nil)
    subject.wont have_valid(:student_id).when("foo")

    subject.must have_valid(:student_id).when(1)
  end

  it "knows the type of credit manager transfer to execute based on its direction" do
    subject.direction = "checking_to_savings"
    subject.transfer_method.must_equal :transfer_credits_from_checking_to_savings

    subject.direction = "savings_to_checking"
    subject.transfer_method.must_equal :transfer_credits_from_savings_to_checking
  end

  it "executes the appropriate transfer when #execute! is called" do
    amount = BigDecimal('5')
    subject.amount = amount
    method = :meth
    student = mock "student"
    credit_manager = mock "credit manager"
    subject.expects(:student).returns(student)
    subject.expects(:credit_manager).returns(credit_manager)
    credit_manager.expects(method).with(student, amount).returns(true)
    subject.expects(:transfer_method).returns(method)

    subject.execute!
  end
end

What never shows up in the above? Anything related to ActiveRecord. This is unbelievably fast to test.

Thoughts?

I'd love to hear some feedback on this method. It feels so unbelievably good I don't know where to start.

Josh Adams is a developer and architect with over eleven years of professional experience building production-quality software and managing projects. Josh is isotope|eleven's lead architect, and is responsible for overseeing architectural decisions and translating customer requirements into working software. Josh graduated from the University of Alabama at Birmingham (UAB) with Bachelor of Science degrees in both Mathematics and Philosophy. He also occasionally provides Technical Review for Apress Publishing, specifically regarding Arduino microprocessors. When he's not working, Josh enjoys spending time with his family. <a href="http://www.erlang-factory.com/conference/show/conference-6/home/"><img src="http://www.erlang-factory.com/static/upload/media/1389191028314604speaker120x125gif" alt="speaker badge" /></a>