Here's a quick and simple pattern that comes in handy when building simple service objects in Ruby applications. They can be used anywhere in the system to model a command but are commonly found at the boundary between the application domain and the UI layer. In the Rails world, this generally means the "in the controller". You may also know them as interactors.
The premise
The easiest way to explain the premise is with examples so let's have a quick look at a simple Rails controller example before and after extracting the service object.
Imagine we have a website that allows a user to sign in using a username and password. A simple sign_in
action for our authentication controller might look something like this
# Simplified controller. In the real world we'd want to do
# things like sanitize params and encrypt passwords
class AuthenticationController < ApplicationController
def sign_in
username = params[:username]
password = params[:password]
if User.exists?(username: username)
@user = User.find(username: username)
if @user.password == password
session[:current_user] = @user
flash[:success] = 'You are now signed in'
redirect_to :success
else
flash[:error] = 'Login failed'
redirect_to :login
end
else
flash[:error] = 'Login failed'
redirect_to :login
end
end
end
Now let's jump straight to the good stuff and have a look at what this might look like once we've implemented our service object
class AuthenticationController < ApplicationController
def sign_in
username = params[:username]
password = params[:password]
UserAuthenticator.new(username, password).authenticate do |response|
response.on(:success) do |user|
session[:current_user] = user
flash[:success] = 'You are now signed in'
redirect_to :success
end
response.on(:unknown_user, :invalid_password) do
flash[:error] = 'Login failed'
redirect_to :error
end
end
end
end
Why is this good?
So what makes the second example better than the first? There are a few reasons.
Separation of concerns
The most important reason that this is better in my opinion is that we have now separated the details of how we handle authentication from how we interact with the user. If we look at the controller code we can see that everything that remains is now specific to the controller. We fetch some values from params
. We feed them to the authenticator and based on the response we store the user in the session, set a flash and perform a redirect. This is all web layer stuff and is now separate from the nuts and bolts of how we actually perform the authentication. This in turn then yields (no pun intended) a number of other benefits
Isolated change
If we want to change the way we interact with the user during the authentication process, the changes are limited to the controller. If we want to change how we actually perform the authentication the changes are limited to the authenticator. These two things are likely to change for different reasons. For example our UX guys might decide that we should show different messages and redirect to different actions depending on the type of error. This would require only changing the controller code. Conversely we may decide that we want to move our application to a service-oriented architecture and authenticate our users against an external REST API. In which case it is just our authenticator that needs to change, the controller remains the same. We don't know but the point is that by isolating the different areas of concern we have prepared ourself better for changes in the future.
Easier to reuse
In the first example, the logic of how to perform an authentication is embedded in the controller. If we wanted to reuse this logic elsewhere it would be tricky to do so. By separating out the business logic (how to perform the authentication) from the web layer, not only is it easier to reuse this elsewhere in the web layer but if we wanted to reuse it in other ways, for example in a CLI or as part of a script, it is easy to do so.
Easier to test
Again by separating the concerns we have made it easier to test the authentication logic separately to the controller. This makes it easier to write simpler yet more thorough tests of each part. We can also avoid hitting the persistence layer for our controller tests and loading the full Rails stack for our authenticator tests which helps keep our tests running nice and quick.
How it works
Now that we've seen what we are trying to achieve let's look at the code that makes it happen. Here is an example of what the UserAuthenticator in our second example might look like
class UserAuthenticator
def initialize(username, password)
@username = username
@password = password
end
def authenticate(username, password)
yield Response.new(:unknown_user) and return if unknown_user?
yield Response.new(:invalid_password) and return if invalid_password?
yield Response.new(:success, user)
end
private
attr_reader :username, :password
def unknown_user?
!User.exists?(username: username)
end
def invalid_password?
user.password != password
end
def user
@user ||= User.find(username: username)
end
class Response
def initialize(result, *args)
@result = result
@args = args
end
def on *outcome
yield(*args) if outcome.include?(result)
end
private
attr_reader :result, :args
end
end
So actually we have two classes here. The UserAuthenticator itself and then an inner Response class that represents the response to be returned. Clearly it is more code than the first example, however both of these classes are tiny and simple. It is easy to see what criteria we are using for each response and because they are so simple, it is easy to see what is happening at each step. In practice, the response class is so generic you may wish to extract it and reuse it across several service objects.
I've pushed simple implementations of the service object and the response object up to Github. If the above example still seems complex to you, have a look at those as they are even simpler. You can also find examples of how to test them.