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 models - business logic.
Moving your logic into a plain old ruby object (P.O.R.O.) is an easy way to start cleaning up your controller. You might also hear them referred to as a "service object" or "form object".
Let's consider the following example. 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(channel: :hiring_managers, @job_application.in_slack_format) 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( channel: :hiring_managers, job_application.in_message_format ) 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