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.