When thinking about more advanced OOP structure, one can sometimes ask – how to incorporate it into a Rails application? I will try to answer that question in this blog note, introducing some advanced OOP concept into a Rails application (without any architecture gems). The result? Skinny controller, fat business-logic service.

Note: If you haven’t read it, please start with the 101: Advanced OOP structure blog note.

Let’s start with a basic Rails application – I will use a newly created app (I will try to keep my setup minimal so that we can focus on OOP stuff here – therefore no models, etc., only one interesting controller here).

Let’s consider for now that a user can reserve an item – to purchase it later. Think of it as places in theatre or on an airplane. After choosing the item, the user will have 15 minutes to make the payment; otherwise their item will return to the pool of available ones. You can think about it as a simple lock with some expiration time. If the payment has been completed, an item will be marked as sold and will not be placed in the available pool by some other code – outside of the scope of this exercise.

So, let’s create a controller for this:

# app/controllers/reservations_controller.rb
class ReservationsController < ApplicationController
  def create
    # create new lock if there isn't one for that item
  end
end

and add the following to routes.rb:

# config/routes.rb
# (...)
resources :reservations, only: :create
# (...)

We need a way to store locks and release them after precisely 15 minutes. One can choose to create a new AR model, store the date of creation and select only newer than 15 minutes, while periodically clearing locks older than, e.g., one hour. With a smart construction of indexes and usage of UPSERT or ON CONFLICT UPDATE that would be a good approach. Other may use Redis as the lock provider; Redis is a NoSQL database natively supporting locks (even distributed ones).

However, I decided to mock my lock provider. That way I can keep the scope of this blog note as close to OOP as possible, without drifting too much into the (otherwise fascinating!) world of locking algorithms and techniques.

So, my imagination-lock (let’s think about it as a gateway to some service on same server providing locks) will have the following public interface:

* command=(command_string) # send connection query to a server
                           # currently it can be only `LOCK`
* argument_1=(argument_value) # sets first argument to command
                              # for lock it's user_id
* argument_2=(argument_value) # sets first argument to command
                              # for lock it's item_id
* execute_command # execute the command with provided arguments
                  # returns 0 if locking failed, and 1 if it succeeded

Please take note, that this API is complicated on purpose – to better illustrate the difference object-oriented refactoring will make here.

I will be using this mock code for this:

# lib/lock.rb
class Lock
  attr_accessor :command, :argument_1, :argument_2

  def execute_command
    if !argument_1 || !argument_2 || command != 'LOCK'
      raise ArgumentError, 'Bad arguments provided'
    end
    [0, 1].sample # hey, external API can always fail!
  end
end

Using our library, let’s write a basic implementation in our controller:

# app/controllers/reservations_controller.rb
require Rails.root.join('lib/lock')

class ReservationsController < ApplicationController
  skip_before_action :verify_authenticity_token
  # we're trying to make the example as simple as possible
  rescue_from ArgumentError, with: :argument_error
  # but some error handling would be good ;)

  def create
    lock = Lock.new
    lock.command = 'LOCK'
    lock.argument_1 = params[:user_id]
    # in production it should be some kind of current_user
    lock.argument_2 = params[:item_id]
    if lock.execute_command == 1
      render json: {}, status: :created
    else
      render json: {}, status: :conflict
    end
  end

  private

  def argument_error
    render json: { error: 'Bad arguments' }, status: :bad_request
  end
end

Every experienced developer should now get that itch in the back of their head – there is something wrong with the code. The create method has too many lines for a controller. It contains a lot of logic, especially for a controller. A controller’s responsibility should be to extract params, call another layer and return the result. This code could use more abstraction. Testing it also will be painful – we probably will have to mock some Lock calls. But what if we can make it easier? What if we can introduce some abstraction that will make testing this stack a breeze?

Let’s try it!

First – we will be adding new layers to our Rails app. I think services would be sufficient here. Start by creating our first service and moving most of the controller code there:

# app/services/lock_service.rb
require Rails.root.join('lib/lock')

class LockService
  def initialize(user_id:, item_id:)
    @user_id = user_id
    @item_id = item_id
  end

  def call
    lock.command = 'LOCK'
    lock.argument_1 = user_id
    lock.argument_2 = item_id
    lock.execute_command == 1
  end

  private

  attr_reader :user_id, :item_id

  def lock
    @lock ||= Lock.new
  end
end

and our controller will now looks a lot better:

# app/controllers/reservations_controller.rb
class ReservationsController < ApplicationController
  skip_before_action :verify_authenticity_token
  rescue_from ArgumentError, with: :argument_error

  def create
    if lock_service.call
      render json: {}, status: :created
    else
      render json: {}, status: :conflict
    end
  end

  private

  def argument_error
    render json: { error: 'Bad arguments' }, status: :bad_request
  end

  def lock_service
    @lock_service ||= LockService.new(user_id: user_id, item_id: item_id)
  end

  def user_id
    @user_id ||= params[:user_id]
  end

  def item_id
    @item_id ||= params[:item_id]
  end
end

Now the controller finally looks like something that would pass code review! How about testing it? Unfortunately doing DI for controllers is particularly hard in Rails, probably because DHH doesn’t believe in DI. But we will find a way!

First of all, let’s create a boilerplate class that will work as a singleton (i.e., there will be only one Registry object in our system) registry for our services, register our LockService and freeze it for all changes for every env (excluding test of course):

# config/initializers/service_provider.rb
class ServiceProvider
  @services = {}

  def self.register(key, klass)
    return false if @services.key?(key) && !Rails.env.test?
    @services[key] = klass
  end

  def self.get(key)
    @services[key]
  end

  def self.[](key)
    get(key)
  end

  def self.finished_loading
    @services.freeze unless Rails.env.test?
  end
end

ServiceProvider.register :lock_service, LockService
ServiceProvider.finished_loading

and of course, use it in the controller:

# app/controllers/reservations_controller.rb
class ReservationsController < ApplicationController
  skip_before_action :verify_authenticity_token
  rescue_from ArgumentError, with: :argument_error

  def create
    if lock_service.call
      render json: {}, status: :created
    else
      render json: {}, status: :conflict
    end
  end

  private

  def argument_error
    render json: { error: 'Bad arguments' }, status: :bad_request
  end

  def lock_service_class
    @lock_service_class ||= ServiceProvider.get(:lock_service)
  end

  def lock_service
    @lock_service ||= lock_service_class.new(user_id: user_id, item_id: item_id)
  end

  def user_id
    @user_id ||= params[:user_id]
  end

  def item_id
    @item_id ||= params[:item_id]
  end
end

Please take note that in a typical Rails project, you would probably instead write the lock_class method as simply returning a LockService and mock it in the test instead. I, however, wanted to explicitly have all the class relationships specified and avoid using monkey-mocking (my term similar to monkey patching, means reopening some class or object to change the behavior of some methods, may be done using an external library that is hiding it from us). Also – this is an article about object-oriented programming, so we should use dependency injection instead of monkey-mocking.

Ok, so we got a way to inject a mock for the LockService class in our controller – time to test it:

# test/controller/reservations_controller_test.rb
require 'test_helper'

class ReservationsControllerControllerTest < ActionDispatch::IntegrationTest
  SuccesfullLockService = Struct.new(:user_id, :item_id, keyword_init: true) do
    def call
      true
    end
  end

  FailedLockService = Struct.new(:user_id, :item_id, keyword_init: true) do
    def call
      false
    end
  end

  test 'returns HTTP bad_request when missing user_id' do
    ServiceProvider.register(:lock_service, LockService)
    post reservations_url, params: { item_id: 2 }
    assert_response :bad_request
  end

  test 'returns a JSON error when missing user_id' do
    ServiceProvider.register(:lock_service, LockService)
    post reservations_url, params: { item_id: 2 }
    assert_equal({ 'error' => 'Bad arguments' }, response.parsed_body)
  end

  test 'returns HTTP bad_request when missing item_id' do
    ServiceProvider.register(:lock_service, LockService)
    post reservations_url, params: { user_id: 2 }
    assert_response :bad_request
  end

  test 'returns a JSON error when missing item_id' do
    ServiceProvider.register(:lock_service, LockService)
    post reservations_url, params: { user_id: 1 }
    assert_equal({ 'error' => 'Bad arguments' }, response.parsed_body)
  end

  test 'returns emtpy JSON when service returns true' do
    ServiceProvider.register(:lock_service, SuccesfullLockService)
    post reservations_url, params: { user_id: 1, item_id: 2 }
    assert_equal({}, response.parsed_body)
  end

  test 'returns HTTP created when service returns true' do
    ServiceProvider.register(:lock_service, SuccesfullLockService)
    post reservations_url, params: { user_id: 1, item_id: 2 }
    assert_response :created
  end

  test 'returns empty JSON when service returns false' do
    ServiceProvider.register(:lock_service, FailedLockService)
    post reservations_url, params: { user_id: 1, item_id: 2 }
    assert_equal({}, response.parsed_body)
  end

  test 'returns HTTP conflict when service returns false' do
    ServiceProvider.register(:lock_service, FailedLockService)
    post reservations_url, params: { user_id: 1, item_id: 2 }
    assert_response :conflict
  end

  teardown do
    ServiceProvider.register(:lock_service, LockService)
  end
end

We provide two mocked lock services – one always returning false and one always returning true. Then we test the HTTP response codes. Also, we test whether the controller is handling the case of missing params correctly. Please take a note that since we’re injecting mocks into the controller, we need to re-inject the original dependency after each test – that is what teardown does.

Please take note that while this approach should be working correctly, it may fail when running the tests in parallel – manipulating a class level instance variable is not thread-safe. In normal operation, it will be all preloaded in an initializer and frozen, so there will be no manipulation of services at runtime; hence it will work correctly in, for example, multithreaded Puma application server.

Ok, so we’ve got the controller part quite right, time to move onto LockService tests. Let’s add a sparkle of DI there:

# app/services/lock_service.rb
require Rails.root.join('lib/lock')

class LockService
  def initialize(user_id:, item_id:, lock: Lock.new)
    @user_id = user_id
    @item_id = item_id
    @lock = lock
    # on a side note - that would fail miserably in Python
    # do you know why?
  end

  def call
    lock.command = 'LOCK'
    lock.argument_1 = user_id
    lock.argument_2 = item_id
    lock.execute_command == 1
  end

  private

  attr_reader :user_id, :item_id, :lock
end

while providing it with some struct as a mock, we can quickly test this PORO (plain old Ruby object):

require 'test_helper'

class LockServiceTest < ActiveSupport::TestCase
  MockedSuccessfulLock = Struct.new(:command, :argument_1, :argument_2) do
    def execute_command
      1
    end

    def inspect_fields
      [command, argument_1, argument_2]
    end
  end

  MockedFailedLock = Struct.new(:command, :argument_1, :argument_2) do
    def execute_command
      0
    end

    def inspect_fields
      [command, argument_1, argument_2]
    end
  end

  test 'sets fields correctly when calling LockService' do
    lock_object = MockedSuccessfulLock.new
    service = LockService.new(user_id: 1, item_id: 2, lock: lock_object)
    service.call
    assert_equal ['LOCK', 1, 2], lock_object.inspect_fields
  end

  test 'returns false when Lock returns 0' do
    lock_object = MockedFailedLock.new
    service = LockService.new(user_id: 1, item_id: 2, lock: lock_object)
    assert !service.call
  end

  test 'returns true when Lock returns 1' do
    lock_object = MockedSuccessfulLock.new
    service = LockService.new(user_id: 1, item_id: 2, lock: lock_object)
    assert service.call
  end
end

In conclusion – we’ve started with the controller that was doing all the work. We’ve created an additional layer of abstraction inside our Rails application, which in turn resulted in much more readable and testable code – all that while doing a real-life task inside the Rails application. Nifty!

As always, you can find the example application at our GitHub.