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.

I nsight: 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 setup any additional overhead for the controller or API just to test the business logic.

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

The Bloated Controller Action

Here’s a simple example of a 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 Blocks 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 scenarios 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.

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: