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?
Top comments (8)
I used Service Objects in my last project with the same gem simple_command, that was a beautiful experience. And it helps me find bugs more quickly. In your approach, you split the code in a better way. I think it gives more control on the code base. As the code base grows, you will have less pain to maintain it.
Thanks for sharing.
I tend to use commands only for small tasks like creating/updating/filtering models, and not for full-fledged services because it leads to an incredible proliferation of classes. If you have 4 different models and you implement the basic CRUD commands for each of them, you end up with 16 different classes. Aggregating them in service modules and using those in your business code allows for more clarity and consistency (you know that all the user-related functionality lives inside
UserService
etc) while leveraging your 10s of small commands under the hood.Nice post! Since
UpdateTodo
is very much related toTodoService
, you could keep it under theTodoService
namespace, likeTodoService::UpdateTodo
.This is a really valid point, thanks for the idea!
It also helps to keep files more organized in the directory structure
How does this jive with where your head is at with all this @maestromac ?
Thanks for tagging me! SimpleCommand gem seems really useful.
I use to write my own service objects until I discovered the LightService gem. It provides a lot more functionality than the roll-your-own version.
Didn't know it, at first glance it seems like a more featureful command library than
simple_command
.Interesting, but we're not talking about the same kind of service objects here. LightService and simple_command give you a single operation (or command, or action) implemented in each class, while a service object in my context (and the canonical design pattern) is more of a swiss army knife aggregating all the needed functionalities related to a domain element.