Matteo Joliveau
Posted on June 4, 2018
As a Java developer that recently transitioned to a Ruby on Rails company, I felt kinda lost when I discovered that the use of models directly inside of controllers was a common practice.
I have always followed the good practices of Domain Driven Design and encapsulated my business logic inside special classes called service objects, so in Java (with Spring) a controller would look like this:
@Controller
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping
public ResponseEntity<Iterable<User>> getAllUsers() {
return ResponseEntity.ok(userService.getUsers());
}
}
Verbosity aside, this is nice and clean with good separation of concerns. The actual business logic that retrieves the user list is delegated to the UserService
implementation and can be swapped out at any time.
However, in Rails we would write this controller as such:
class Api::UserController < ApplicationController
def index
@users = User.all
render json: @users
end
end
Yes, this is indeed shorter and even cleaner than the Java example, but it has a major flaw. User
is an ActiveRecord model, and by doing this we are tightly coupling our controller to our persistence layer, breaking one of the key aspects of DDD. Moreover, if we wanted to add authorization checks to our requests, maybe only returning a subgroup of users based on the current user's role, we would have to refactor our controller and putting it in charge of something that is not part of the presentation logic. By using a service object, we can add more logic to it while being transparent to the rest of the world.
Let's build a service object
In Java this is simple. It's a singleton class that is injected into other classes by our IoC container (Spring DI in our example).
In Ruby, and Rails especially, this is not quite the same, since we can't really inject anything in our controller constructor. What we can do, however, is taking inspiration by another programming language: Elixir.
In Elixir, a functional language, there are no classes nor objects, only functions and structs. Functions are grouped into modules and have no side effects, a great feature to ensure immutability and stability in our code.
Since Ruby too has modules, we can use them to implement our service object as stateless collections of methods.
Our UserService can look something like this:
module UserService
class << self
def all_users
User.all
end
end
end
And then will be used like this:
class Api::UserController < ApplicationController
def index
@users = UserService.all_users
render json: @users
end
end
This doesn't sound like a smart move, does it? We just moved the User.all
call in another class. And that's true, but now, as our application grow we can add more logic to it without breaking other code or refactoring, as long as we keep our API stable.
One small change I'll make before proceding. Since we may want to inject some data into our service on every call, we'll define our methods with a first parameter named ctx
, which will contain the current execution context. Stuff like the current user and such will be contained there.
module UserService
class << self
def all_users _ctx # we'll ignore it for now
User.all
end
end
end
class Api::UserController < ApplicationController
def index
@users = UserService.all_users { current_user: current_user }
render json: @users
end
end
Applying business logic
Now let's build a more complex case, and let's use a user story to describe it first. Let's imagine we're building a ToDo app (Wow, how revolutionary!).
The story would be:
As a normal user I want to be able to see all my todos for the next month.
The RESTful HTTP call will be something like:
GET /api/todos?from=${today}&to=${today + 1 month}
Our controller will be:
class Api::TodoController < ApplicationController
def index
@ctx = { current_user: current_user }
@todos = TodoService.all_todos_by_interval @ctx, permitted_params
render json: @todos
end
private
def permitted_params
params.require(:todo).permit(:from, :to)
end
end
And our service:
module TodoService
class << self
def all_todos_by_interval ctx, params
Todos.where(user: ctx[:current_user]).by_interval params
end
end
end
As you can see we are still delegating the heavy database lifting to the model (throught the scope by_interval
) but the service is actually in control of filtering only for the current user. Our controller stays skinny, our model is used only for persistence access, and our business logic doesn't leak in every corner of our source code. Yay!
Service Composition
Another very useful OOP pattern we can use to enhance our business layer is the composite pattern. With it, we can segregate common logic into dedicated, opaque services and call them from other services. For example we might want to send a notification to the user when a todo is updated (for instance because it expired). We can put the notification logic into another service and call it from the previous one.
module TodoService
class << self
def update_todo ctx, params
updated_todo = Todos.find ctx[:todo_id]
updated_todo.update! params # raise exception if unable to update
notify_expiration ctx[:current_user], updated_todo if todo.expired?
end
private
def notify_expiration user, todo # put in a private method for convenience
NotificationService.notify_of_expiration { current_user: user }, todo
end
end
end
Commands for repetitive tasks
As the Gang of Four gave us a huge amount of great OOP patterns, I'm going to borrow one last concepts from them and greatly increase our code segregation. You see, our services could act as coordinators instead of executors, delegating the actual work to other classes and only caring about calling the right ones. Those smaller, "worker-style" classes can be implemented as commands. This has the biggest advantage of enhancing composition by using smaller execution units (single commands instead of complex services) and separating concerns even more. Now services act as action coordinators, orchestrating how logic is executed, while the actual execution is run inside simple, testable and reusable components.
Side Note: I'm going to use the gem simple_command to implement the command pattern, but you are free to use anything you want
Let's refactor the update logic to use the command pattern:
class UpdateTodo
prepend SimpleCommand
def initialize todo_id, params
@todo_id = todo_id
@params = params
end
def call
todo = Todos.find @todo_id
# gather errors instead of throwing exception
errors.add_multiple_errors todo.errors unless todo.update @params
todo
end
end
module TodoService
class << self
def update_todo ctx, params
cmd = UpdateTodo.call ctx[:todo_id], params
if cmd.success?
todo = cmd.result
notify_expiration ctx[:current_user], todo if todo.expired?
end
# let's return the command result so that the controller can
# access the errors if any
cmd
end
private
def notify_expiration user, todo # put in a private method for convenience
NotificationService.notify_of_expiration { current_user: user }, todo if todo.expired?
end
end
end
Beautiful. Now every class has one job (Controllers receive requests and return responses, Commands execute small tasks and Services wire everything together), our business logic is easily testable without needing any supporting infrastructure (just mock everything. Mocks are nice.) and we have smaller and more reusable methods. We just have a slightly bigger codebase, but it's still nothing compared to a Java project and it's worth the effort on the long run.
Also, our services are no longer coupled to any Rails (or other frameworks) specific class. If for instance we wanted to change the persistence library, or migrate one business domain to an external microservice, we just have to refactor the related commands without having to touch our services.
Are you using service objects in your Ruby projects? How did you implement the pattern and what challenges did you solved that my approach does not?
Posted on June 4, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.