Heading image for post: Leveling Up Rails Controllers - Organizing Logic with Plain Ruby Objects

Ruby

Leveling Up Rails Controllers - Organizing Logic with Plain Ruby Objects

Profile picture of Andrew Vogel

For as long as I've been a part of the Rails community, I can always remember hearing "fat models, skinny controllers". Afterall, it is a good goal to reduce the size of your controller and scope of business logic in your actions. This trope is rooted in the "single responsiblity pricinciple" - your controllers should be responsible for handling request/response and your models, the business logic.

In this post, I'll show you a simplified approach to cleaning up a controller. We'll start by moving our logic into a plain old ruby object. This is a great way to isolate responsibility, and a step in the "form object" direction.

Let's say we have a Job Posting application, and in that application, this is the create action for when an applicant submits their resume.

class JobApplicationController < ApplicationController
  after_action :track_application_submit, only: :create

  def create
    @job_application = JobApplication.new(job_application_params)

    if @job_application.save
      JobApplicationMailer.notify_applicant(@job_application).deliver
      JobApplicationMailer.notify_original_poster(@job_application).deliver

      if @job_application.experience_preferential?
        MessagingService.send_message(
          job_application.in_message_format,
          channel: :hiring_managers,
        )
      end

      CRM.add_new_applicant(@job_application, high_priority: true)

      flash[:success] = "Application submitted! Thanks!"
      redirect_to root_path
    else
      render :new
    end
  end

  private

  def track_application_submit
    Events.track!(:application_submit)
  end

  # ...
  # ...
end

This controller action is trivial for now. But it's easy to see that this has the potential to grow in complexity as we add more features to it.

Let's review the featues of this create action: * We create a job application record * We send a confirmation email to the applicant * We send an email to the person that posted the application * We call a messaging service if the candidate had some "pre-screening" qualifications * After the action, we also track the application submission for analytics

Now, let's try moving our logic into a ruby class. Let's not worry too much about the exact design pattern, as this is a hybrid approach. It might look something like this:

class JobApplicationForm
  attr_reader :job_application

  def initialize(job_application_params)
    @job_application = JobApplication.new(job_application_params)
  end

  def valid?
    job_application.valid?
  end

  def submit
    return false unless valid?

    job_application.save

    JobApplicationMailer.notify_applicant(job_application).deliver
    JobApplicationMailer.notify_original_poster(job_application).deliver

    notify_hiring_managers_via_message if experience_preferential?

    track_application_submission

    true
  end

  private

  def track_application_submission
    Events.track!(:application_submit)
  end

  def experience_preferential?
    job_application.experience_preferential?
  end  

  def notify_hiring_managers_via_message
    MessagingService.send_message(
      job_application.in_message_format,
      channel: :hiring_managers,
    )
  end
end

This is really great because we've now encapsulated our logic in this class and we can unit test this in isolation - away from the request response cycle. In addition, we can accomplish our afformentioned goal of cleaning up our controller action.

Now, it just looks like this:

class JobApplicationController < ApplicationController
  def create
    @form = JobApplicationForm.new(job_application_params)

    if @form.submit
      flash[:success] = "Application submitted! Thanks!"
      redirect_to root_path
    else
      render :new
    end
  end

  # ...
  # ...
end

I really like this way of organizing our Rails code. We're able to encapsulate logic in appropriate spots and make things very easy to test. We've moved our logic out of our controller and into a form object. We now know that any logic for a job application submission should go into this class. New devs won't have to spend time digging through controllers or models to figure out where these things happen.

If you're interested in learning more about design patterns, check out some of our past blog posts! Here's a really cool one about using SimpleDelegator to create Decorators, which can be used to clean up view/presentation logic. https://hashrocket.com/blog/posts/using-simpledelegator-for-your-decorators

Unsplash - Image by Thanos Pal

More posts about Ruby controllers rails refactoring

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project