Using Service Objects In Code Design
The more code we add to our rails app, the more concern we start to feel that our code become messy. In fact, to write clean code does not only mean to write unduplicated, well-refactored code, but also with a clear, well-connected, easy to understand logics in mind. Refactoring code to its proper ...
The more code we add to our rails app, the more concern we start to feel that our code become messy. In fact, to write clean code does not only mean to write unduplicated, well-refactored code, but also with a clear, well-connected, easy to understand logics in mind. Refactoring code to its proper place, keeping each part do what it is designed to do and nothing else. Large-scaled rails app cannot survive without clean codes. Using service object means refactoring codes to their proper places that acts as a services to other component such as controllers and models, so that controllers just "control" and "use services", whereas models just concern with the business logic. service object is just plain old ruby object or in short, PORO.
Understanding Services
I think in order to get a good grasp of why we need to write code using services at all, we need to understand about domain driven design. I'm going to quote some comments about DDD from stackoverflow since it makes a pretty good explanation of DDD.
DDD is about trying to make your software a model of a real-world system or process. In using DDD, you are meant to work closely with a domain expert who can explain how the real-world system works. For example, if you're developing a system that handles the placing of bets on horse races, your domain expert might be an experienced bookmaker.
Between yourself and the domain expert, you build a ubiquitous language (UL), which is basically a conceptual description of the system. The idea is that you should be able to write down what the system does in a way that the domain expert can read it and verify that it is correct. In our betting example, the ubiquitous language would include the definition of words such as 'race', 'bet', 'odds' and so on.
This coding design says that each part of codes live in its own domain so that the expert of each domain can easily verify that it is correct. And these are several pattern that DDD recocommend:
- Repository, a pattern for persistence (saving and loading your data, typically to/from a database)
- Factory, a pattern for object creation
- Service, a pattern for creating objects that manipulate your main domain objects without being a part of the domain themselves
So services normally do not live in the main domain but acts upon the main domain objects.
The standard rails app comes with 6 folders in folder app/ that are:
app assets controllers helpers mailers models views
But this files structure should not limit our imagination in creating new folder and files to fullfil our goal in using service object model. We'll get into a few examples to make things clear:
Example with rails API
Suppose in we have an api about a blog with a post controller which goes something like this:
# app/controllers/posts_controller.rb class PostsController < ApplicationController before_action :set_post, only: [:show, :update, :destroy] def index @posts = Todo.all json_response(@posts) end def create @post = Post.create!(post_params) json_response(@post, :created) end def show json_response(@post) end def update @post.update(post_params) head :no_content end def destroy @post.destroy head :no_content end private def post_params params.permit(:title, :content, :created_by) end def set_post @post = Post.find(params[:id]) end end
You see that I am using methods json_response which I'll put in folder app/controllers/concerns in file response.rb.
# app/controllers/concerns/response.rb module Response def json_response(object, status = :ok) render json: object, status: status end end
Here, what json_response do is to respond with json and http status code. we somehow seperate the 'concerns' from the controller. Putting method json_response in the file app/controllers/concerns/response.rb seems just the right place. The logic become clearer.
One more thing you notice that in our PostsController, you don't see if statement such as:
if @post.save ... else ... end
This is because we have created an ExceptionHandler module to deal with it.
# app/controllers/concerns/exception_handler.rb module ExceptionHandler # provides the more graceful `included` method extend ActiveSupport::Concern included do rescue_from ActiveRecord::RecordNotFound do |e| json_response({ message: e.message }, :not_found) end rescue_from ActiveRecord::RecordInvalid do |e| json_response({ message: e.message }, :unprocessable_entity) end end end
In this way when set_post is called to find a post by id and the record does not exist, ActiveRecord will throw an exception ActiveRecord::RecordNotFound. We'll rescue from this exception and return a 404 message. In our create method in PostsController, note that we're using create! instead of create. This way, the model will raise an exception ActiveRecord::RecordInvalid.
Both modules Response and ExceptionHandler are included in file app/controllers/application_controller.rb
# app/controllers/application_controller.rb class ApplicationController < ActionController::API include Response include ExceptionHandler end
By creating services, codes become reuseable. This is the goal of most great code; we want some snippet of codes to be reuseable. Even if we move the code to other applications, we just need minimal change. I think we can think of Gemfile as an extreme form of services too. Let's continue further with other examples.
Examples with authentications
Suppose we use gem jwt for token authentication. Here is the link to the gem documentation https://github.com/jwt/ruby-jwt A clean way to implement this is to first create a singleton class JasonWebToken to encode and decode token based on expiration date and user_id. And the best place to put this file is app/lib since it is not domain-specific.
# app/lib/json_web_token.rb class JsonWebToken HMAC_SECRET = Rails.application.secrets.secret_key_base def self.encode(payload, exp = 24.hours.from_now) payload[:exp] = exp.to_i JWT.encode(payload, HMAC_SECRET) end def self.decode(token) body = JWT.decode(token, HMAC_SECRET)[0] HashWithIndifferentAccess.new body # rescue from expiry exception rescue JWT::ExpiredSignature, JWT::VerificationError => e # raise custom error to be handled by custom handler raise ExceptionHandler::ExpiredSignature, e.message end end
We updated the ExceptionHandler module to handle the error with JWT encoding and decoding. Remember its location? It's app/controllers/concerns/exception_handler.rb.
module ExceptionHandler extend ActiveSupport::Concern # Define custom error subclasses - rescue catches `StandardErrors` class AuthenticationError < StandardError; end class MissingToken < StandardError; end class InvalidToken < StandardError; end included do rescue_from ActiveRecord::RecordInvalid, with: :four_twenty_two rescue_from ExceptionHandler::AuthenticationError, with: :unauthorized_request rescue_from ExceptionHandler::MissingToken, with: :four_twenty_two rescue_from ExceptionHandler::InvalidToken, with: :four_twenty_two rescue_from ActiveRecord::RecordNotFound do |e| json_response({ message: e.message }, :not_found) end end private def four_twenty_two(e) json_response({ message: e.message }, :unprocessable_entity) end def unauthorized_request(e) json_response({ message: e.message }, :unauthorized) end end
Let's also create another service AuthenticationUser to handle the authentication procedure. What should we put this class? Since it does authentication service, let's put it in app/auth/authentication_user.rb
class AuthenticateUser def initialize(email, password) @email = email @password = password end def call JsonWebToken.encode(user_id: user.id) if user end private attr_reader :email, :password def user user = User.find_by(email: email) return user if user && user.authenticate(password)d raise(ExceptionHandler::AuthenticationError, "Invalid Credentials") end end
And now we can use this in AuthenticationController like this:
# app/controllers/authentication_controller.rb class AuthenticationController < ApplicationController def authenticate auth_token = AuthenticateUser.new(auth_params[:email], auth_params[:password]).call json_response(auth_token: auth_token) end private def auth_params params.permit(:email, :password) end end
Now we have a clean and slim AuthenticationController. Authentication controller just control by using Authentication service. Now we can see that by using service object, we can create a reuseable code with logic that is easy to follow. We can easily reuse these code with little modification.
Example with Registration
Here is another good example I have found on the web. Suppose that in registering a new user, We often do something else, such as sending a welcoming email to the user mailbox. Therefore it is usually wise to create a NewRegistrationService which can perform other services. This service lives in the file app/services/new_registration_service.rb.
class NewRegistrationService .... .... def call(param) ... send_welcome_email ... end private .... def send_welcome_email WelcomeEmailMailer.welcome_email(@user).deliver_later end ... end
So that we can use the NewRegistrationService like this:
result = NewRegistration.build.call({some_params}) if result redirect_to root_path(result.user) else redirect_to last_path(result.user), notice: 'Error saving record' end
Let make a good summary about service object: Service objects encapsulates single process of our business. They take all collaborators (database, logging, external adapters like Facebook, user parameters) and performs a given process. Services belongs to our domain - They shouldn’t know they’re within Rails or webapp!
We get a lot of benefits when we introduce services, including:
-
Ability to test controllers - controller becomes a really thin wrapper which provides collaborators to services - thus we can only check if certain methods within controller are called when certain action occurs,
-
Ability to test business process in isolation - when we separate process from its environment, we can easily stub all collaborators and only check if certain steps are performed within our service.
-
Lesser coupling between our application and a framework - in an ideal world, with service objects we can achieve an absolutely technology-independent domain world with very small Rails part which only supplies entry points, routing and all 'middleware’. In this case we can even copy our application code without Rails and put it into, for example, desktop application.
-
They make controllers slim - even in bigger applications actions using service objects usually don’t take more than 10 LoC.
-
It’s a solid border between domain and the framework - without services our framework works directly on domain objects to produce desired result to clients. When we introduce this new layer we obtain a very solid border between Rails and domain - controllers see only services and should only interact with domain using them.