Ruby
Leveling Up Rails Controllers - Organizing Logic with Plain Ruby Objects
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