Feel free to reach out! I'm excited to connect. You can contact me here

  • Software Development
  • Ruby on Rails

How to refactor Ruby on Rails controllers using blocks and service objects

Using service objects and success or failure blocks to help you write maintainable ruby on rails controllers

ℹ️ Reposting an article I originally wrote on ITNEXT publication on medium. Just re-sharing it here on my own blog

What is a Service Object?

Simply said, it’s a plain ruby object that serves only a single purpose. Just like a chair. It only serves to let people sit on it. Period. There are many reasons for this. It makes it easier for anyone to understand what it does while also making it easier to write tests.

Insight: One thing I have learned over the years writing maintainable code is that objects in the software development world can be better understood and written by relating it to real world objects. Just like how you wouldn’t want your electric stove top to also serve as your work desk (fire hazard!), it only makes sense to create objects that serves only a single purpose.

Below is a simple example of a plain ruby service object class that only creates posts. We’ll elaborate on this further to demonstrate how we can make use of this simple class to refactor your rails controllers using success or failure blocks.

class CreatePost
  attr_reader :subject, :body
  def initialize(subject:,body:)
    @subject = subject
    @body    = body
  end
  
  def call
    Post.create!({ subject: subject, body: body })
  end
end

Tip: Try to always name your service objects as a verb (i.e. create, build, update, etc.) It forces you to focus on the action and purpose of the object. Also makes it mentally hard for anyone to add business logic that doesn’t belong there.

Why a service object?

Encapsulating the business logic while keeping it isolated from the rest of the Rails framework makes it a component that you can reuse elsewhere within your app. Let’s say you need to apply the same business logic in your controller and API endpoint, you can re-use the same service object with the freedom to decide how you want to respond back to requests (i.e. in your controller, you can redirect the user while in your API endpoint, you can send back a JSON response)

Furthermore, it makes it so much easier to test your business logic since you don’t have to set up any additional overhead for the controller or API just to test the business logic.

Without further ado, let’s jump right into success or failure blocks.

The bloated controller action

Here is a simple example of a typical controller action containing business logic that we can later refactor into a service object:

class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    if @post.save
      send_email
      track_activity
      redirect_to posts_path, notice: 'Successfully created post.'
    else
      render :new
    end
  end
end

Success or Failure block to the rescue

I first encountered this technique while peeking into the “inherited_resources” gem. It was the section on how we can overwrite the default inherited resources actions using success or failure blocks that piqued my interest. Here’s a snippet from the README:

class ProjectsController < InheritedResources::Base
  def update
    update! do |success, failure|
      failure.html { redirect_to project_url(@project) }
    end
  end
end

Here’s a high-level overview of what the code is doing. When an update is being made to a project resource, if the update fails, it will invoke the failure response by redirecting to the project resource’s show page. However, if the update succeeds, it will default to the normal flow of redirecting to the projects index page (that’s just how inherited resources does it by default). Allowing us to have control over what to do for each success and failure scenario while encapsulating the business logic helps simplifies our controller logic.

Let’s use our earlier service object example with a few tweaks to achieve the same outcome using a more simplified implementation logic (as compared to the one used in Inherited Resources)

Here’s the refactored version of our controller action along with the service object and other classes necessary to pull this off.

# app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def create
    @post = Post.new(post_params)
    CreatePost.call(@post) do |success, failure|
      success.call { redirect_to posts_path, notice: 'Successfully created post.' }
      failure.call { render :new }
    end
  end
end

# app/services/create_post.rb
class CreatePost
  attr_reader :post

  def self.call(post, &block)
    new(post).call(&block)
  end

  def initialize(post)
    @post = post
  end
  private_class_method :new

  def call(&block)
    if post.save
      send_email
      track_activity
      yield(Trigger, NoTrigger)
    else
      yield(NoTrigger, Trigger)
    end
  end

  def send_email
    # Send email to all followers
  end

  def track_activity
    # Track in activity feed
  end
end

# app/services/trigger.rb
class Trigger
  def self.call
    yield
  end
end

# app/services/no_trigger.rb
class NoTrigger
  def self.call
    # Do nothing
  end
end

The CreatePost#call instance method (line 25) essentially accepts a block of code with success and failure as arguments (Line 5)

We can pass a Trigger or NoTrigger class object as the success argument. If the success argument is given a Trigger class, the block given to success.call will be yielded which redirects the user request to the posts index page along with a success notice. However, if the success argument is given a NoTrigger class, the block given to it will not be called since NoTrigger.call class method does nothing. This entire logic applies to the failure argument as well.

Don’t you love blocks?

Additional Resources

Blocks, procs and lambdas can be somewhat confusing, but when you finally grasp the concept, they can be a very powerful and flexible tool to help you write simpler and better code. Here are a few useful resources if you want to learn more:

Hope you enjoyed reading my article. I love refactoring code and hope to share more refactoring techniques in the future. As always, feel free to leave a comment if you have any feedback or suggestions that can help improve my posts.

Till next time.