- 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:
- https://blog.appsignal.com/2018/09/04/ruby-magic-closures-in-ruby-blocks-procs-and-lambdas.html
- https://www.rubyguides.com/2016/02/ruby-procs-and-lambdas/
- https://pragprog.com/book/btrubyclo/mastering-ruby-closures
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.