No More Seat Costs: Semaphore Plans Just Got Better!

    24 Aug 2016 · Software Engineering

    How to Reduce Controller Bloat with Interactors in Ruby

    13 min read
    Contents

    Introduction

    Let’s consider a hypothetical situation — You’ve been working on a Rails application for about a year. When the application was new and its functionality limited, you could add features relatively simply by spinning up a new controller, model and view or — worst case — add a few lines to an existing controller, model or view.

    However, as time went by, the application’s feature set kept increasing. Moreover, existing features had to be regularly changed to reflect the changes in business goals and priorities. Since there are only so many controllers, views and models that can pertain to a given application’s domain, you’re noticing that the number of lines in these files is slowly increasing as well. Combined with the frequent changes and updates to existing features ,which introduce quite a few edge cases, you’re realizing that your codebase is becoming harder to understand, test and ultimately change.

    In this tutorial, we’ll take a look at how we can prevent the situation described above from happening or fix it if it has already happened by using the “Interactor” pattern. We’ll start with a general discussion of code smells, in particular smells which indicate bloated code, and their prescriptive refactorings. Then, we’ll take a brief look at where these smells are typically found in a Rails application and the different techniques available to refactor them, the Interactor pattern being one of them. Finally, we’ll go through an example with code that will illustrate how an Interactor is used in practice.

    Code Smells

    The most important quality of any piece of software, apart from the fact that it works, is how easy it is to understand and change it. Code smells are warning signs that our software is getting hard to understand and change.

    Martin Fowler, in his classic book Refactoring, laid out 23 types of code smells, each with a prescriptive refactoring. As Sandi Metz mentioned in her RailsConf 2016 talk, there are two important “big picture” takeaways from this book:

    1. Each code smell has a descriptive name and definition, e.g. “Long Method”.
    2. Each code smell can be refactored with one or more prescriptive refactorings that are associated with it. Each refactoring also has a name and a clear definition, e.g. “Replace Method with Method Object”.

    Contrary to the view that a code smell refers vaguely to “ugly” code, Fowler’s precise definitions allow us to assess how much code smell there actually is in our codebase. That being said, a code smell ought to be treated as an indication that a problem might exist and not as definitive proof — this is where intuition and experience come into play.

    In general, being cognizant of code smells and the tools available to fix them will result in your code being easier to change in the future.

    Long Methods and Large Classes

    In their paper Subjective evaluation of software evolvability using code smells: An empirical study, Mika V. Mäntylä & Casper Lassenius categorized the 23 code smells into five distinct categories. One of these categories is known as the “Bloaters”, and as the name implies, contains the smells which most indicate bloat in our code. The “Bloaters” category contains the following code smells:

    • Long Method,
    • Large Class,
    • Primitive Obsession,
    • Long Parameter List, and
    • Data Clumps.

    In Rails applications which resemble the hypothetical one described in the introduction, growth of the feature set is tied directly to growth in the size of controllers and models. Growth in the size of controllers and models implies that the classes and the methods they contain are getting bigger. As one would expect, this most often corresponds to the Long Method and Large Class code smells.

    Why is This a Bad Thing?

    A key idea of “good” object oriented design is that a class or method should do the smallest possible useful thing (Metz). A class or method which restricts itself to one responsibility can be easily reused, understood and tested. A large class or method usually implies the opposite — that it is doing too much, and is thus harder to reuse, understand and test.

    Birth of a Fat Controller

    Let’s consider an example. We are building an application and have decided to roll our own authentication, like in the Hartl Rails tutorial. In our SessionsController's #create action, we have something like this:

      def create
        user = User.find_by(email: params[:session][:email].downcase)
        if user && user.authenticate(params[:session][:password])
          log_in user
          redirect_to user
        else
          render 'new'
        end
      end

    The test for this looks as follows:

    describe SessionsController do
      let(:user) { FactoryGirl.create(:user) }
    
      describe "POST #create" do
        let(:email) { user.email }
        let(:password) { user.password }
    
        def do_create
          put :create, session: { email: email, password: password }
        end
    
        before { do_create }
    
        context 'valid email and password' do
          it { is_expected.to redirect_to(user) }
        end
    
        context 'invalid email/password' do
          let(:password) { 'wrong password' }
          it { is_expected.to render_template(:new) }
        end
      end
    end

    As our application grows in popularity, we realize that if someone wanted to keep guessing a user’s password, there’s no way to prevent them. Therefore, we decide to implement a “lock-out” feature. A lock-out feature works by setting a hard limit on the number of failed attempts and preventing a user from logging in when this limit is reached.

    The way we’ll choose to implement this feature is by adding a new integer column failed_attempts with a default of 0 to our User table. Whenever a given authentication attempt fails, we’ll increment this column. When an authentication attempt succeeds, we’ll check that the value in the failed_attempts column doesn’t exceed a maximum value, and reset it to 0 if it doesn’t. Here’s how we could potentially code it:

      class SessionsController < ApplicationController
        def create
          user = User.find_by(email: params[:session][:email].downcase)
          if user && user.login_allowed? &&
    user.authenticate(params[:session][:password])
            log_in user
            user.update(failed_attempts: 0)
            redirect_to user
          else
            if user
              user.increment!(failed_attempts)
              flash.now[:error] = "You are locked!" unless user.login_allowed?
            end
            render 'new'
          end
        end
      end

    We’d implement #login_allowed? in the User class as follows:

      class User < ActiveRecord::Base
        MAX_LOGIN_ATTEMPTS = 3
    
        def login_allowed?
          failed_attempts < MAX_LOGIN_ATTEMPTS
        end
      end

    In RSpec, the feature test for this would look like:

    ...
    context 'a hacker is trying to brute force guess a password' do
      let(:user) { FactoryGirl.create(:user) }
      before do
        User::MAX_LOGIN_ATTEMPTS.times do
          fill_in "Email",    with: user.email
          fill_in "Password", with: ''
          click_button "Sign in"
        end
      end
    
      it { is_expected.to have_content('You are locked') }
    end
    ...

    The sessions controller test would also have to be updated to ensure that the failed_attempts column is incremented or reset, which will be left as an exercise.

    When we first wrote the spec for SessionsController, we saw that its primary responsibility was to authenticate an email/password combination, and either redirect to the user’s home page or render the sign-in form. SessionsController‘s knowledge of the User model was limited to .find_by and #authenticate.

    However, with our addition above, we see that SessionsController now has the additional task of managing the failed_attempts column on User. Namely, if authentication succeeds, it has to reset failed_attempts, and if authentication fails, it increments it. It also needs knowledge of User#login_allowed?. This additional responsibility will also be reflected in the SessionsController spec.

    For the sake of brevity, this feature has been implemented sparsely. If authentication requirements for your application were limited, leaving the code in your controller might suffice. That being said, here are a few examples of how we can add to this in a real world application:

    1. Notify the user that their account has been locked — this could possibly include the generation of a secure unlock or password reset token,
    2. On the last attempt, notify the user that their account will be locked if authentication fails, and
    3. Add an expiry time to the lockout — e.g. the account will be automatically unlocked after 24 hours.

    The more we add to the controller and model, the harder it will be to understand and eventually change them.

    Moving Responsibility Out of the Controller

    We can simplify SessionsController by leveraging the fact that we’re using an object oriented language. What if there was another object whose sole responsibility it was to perform user authentication?

    If this object named AuthenticateUser existed, we could say something like this in our controller:

      def create
        result = AuthenticateUser.call(params[:session])
        if result.success?
          log_in result.user
          redirect_to result.user
        else
          flash.now[:error] = result.error_message
          render 'new'
        end
      end

    This leaves our controller looking much like it did before we decided to add the lock-out feature. Our controller no longer cares about what ‘authentication’ entails — just that it was successful, has an associated user, and an error message. This means that if we decide to add capabilities to our authentication, the controller won’t have to be changed. Moreover, our controller spec also becomes simplified because we only have to ensure that the AuthenticateUser class receives the .call message with the right params.

    What would AuthenticateUser look like?

    class AuthenticateUser
      attr_reader :success, :error_message, :email, :password
    
      def self.call(*args)
        new(*args).call
      end
    
      def initialize(session_params)
        @email = session_params.fetch(:email)
        @password = session_params.fetch(:password)
      end
    
      def call
        if authentication_successful?
          user.update(failed_attempts: 0)
          @success = true
        else
          @success = false
          handle_authentication_failure
        end
        result
      end
    
      private
    
      def authentication_successful?
        return false unless user
        user.login_allowed? && user.authenticate(password)
      end
    
      def handle_authentication_failure
        return unless user
        user.increment!(:failed_attempts)
        @error_message = 'You are locked' unless user.login_allowed?
      end
    
      def result
        OpenStruct.new(success?: success, error_message: error_message, user: user)
      end
    
      def user
        @user ||= User.find_by(email: email.downcase)
      end
    end

    A few things to note from the AuthenticateUser class above:

    1. Since we want our SessionsController to be able to say AuthenticateUser.call(...), we have to define the self.call class method. This class method instantiates an object and then sends #call to this instance with new(*args).call,
    2. In our controller code, we assign the return value of AuthenticateUser.call(...) to a local variable called result. The return value of the #call method is an OpenStruct. Since the result is an instance of OpenStruct, it allows us to query it with messages such as success?, user & error_message.

    Our test for AuthenticateUser will be very similar to that of SessionsController when we implemented the lock-out feature. It will test authentication for valid and invalid cases, ensure that failed_attempts is set correctly and check the return value.

    Placing AuthenticateUser in the File Structure

    One of the benefits of having a set of interactors in your application is that they can tell you at a glance what your application ‘does’. A common place to put your interactors is in the app/interactors/ directory. They can also be placed in the app/services/ directory.

    Adding More Features

    Now, let’s say we’ve implemented this and a few months later we realize we’re spending too much time manually unlocking user accounts. So, we decide to add a feature through which a user whose account gets locked can unlock it through an external channel like email, in a similar way to how they reset their password.

    We have two options here. One is to add the requisite code to AuthenticateUser, and the other is to define a new object which will take over the responsibility for generating an unlock token,sending a notification email to the user, and call this object from AuthenticateUser. The second option could look something like this:

      class AuthenticateUser
        ...
        def call
          if authentication_successful?
            ...
          else
            ...
            SendUnlockInstructions.call(user)
          end
        end

    The guiding factor as to which one of these two options you should choose depends on the amount of code smell caused by adding this new feature directly in AuthenticateUser. It can be helpful to first add the needed code to AuthenticateUser while getting your feature specs to pass, and then assess whether extracting to a second class would make things clearer.

    Automatic Code Smell Detection

    You’ve now seen how to use an interactor to reduce code bloat in a Rails controller. The next step would be to identify where the bloat exists in your own codebase and whether you can potentially use the interactor pattern to clean it up.

    Rubocop and Reek are two great tools which you can use to identify bloat in your codebase.

    It can be useful to set up your local development environment with overcommit. It allows you to hook into git actions, e.g. git commit, and run a tool of your choice. To install overcommit and have it run Rubocop when you make any new commit, you need to:

    1. Add the overcommit gem to your Gemfile in the development group,
    2. Run overcommit --install, and
    3. Configure overcommit to run Rubocop before a commit is complete by modifying the .overcommit.yml file. PreCommit: # Ignore all Overcommit default options ALL: enabled: false on_warn: fail
           # Enable explicitly each desired pre commit check
           RuboCop:
             enabled: true
             description: 'Analyzing with Rubocop'
             required_executable: 'rubocop'
      

    You’ll notice that we’ve set up overcommit to fail the commit if any offenses are found on Rubocop.

    It can also be very helpful to run Rubocop as part of your Semaphore CI build setup, and configure it to stop the build if Rubocop reports any offenses. Since we’ve already set up overcommit, we can use it directly to achieve our goal:

    overcommit --run

    The above command, when included in your Semaphore CI build setup along with the configuration in .overcommit.yml, will ensure that Rubocop is run. However, the caveat is that the --run flag assumes that all files in the repo have changed.

    Conclusion

    In this tutorial, we started off with a brief discussion of code smells, with a focus on, code smells in the “bloaters” category. Then, we discussed how we’d apply the interactor pattern to address bloat in an authentication controller.

    In most situations where there is code bloat, there is likely an object that’s trying to make itself seen. The more you train yourself to find these objects, the more you can break down long methods and large classes into chunks which are easy to understand and test. This process is known as decomposition.

    In addition to interactors, the following patterns are commonly used in Rails application to reduce code bloat:

    • Form Objects,
    • Query Objects,
    • Value Objects,
    • View Objects, and
    • Decorators.

    If you have any questions and comments, feel free to leave them in the section below.

    P.S. Would you like to learn how to build sustainable Rails apps and ship more often? We’ve recently published an ebook covering just that — “Rails Testing Handbook”. Learn more and download a free copy.

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    Sid Krishnan is a Ruby and Rails consultant. He is passionate about reducing technical debt, and helping product teams accelerate feature development.