Your Minimum Viable Rails Service Pattern
·Service objects seem to be coming into fashion every year or so. I would love to share an approach that I use for service objects (which are actually service modules). It works very nicely, promised!
Update: this article calls for the same approach more or less
A service fits best in a standalone Module
which is is as close as you can get to a namespaced, yet freestanding function. Services are doers by definition (or Commands, if you will). So don’t make them objects. Don’t do this:
payment_processing = PaymentProcessing.new(user, purchase)
payment_processing.process
Note how even the name (a noun called “Processing” - what is this? an abstract processing what exactly?) feels wrong.
You are entering the execution in the kingdom of the nouns with this pattern, but moreover - you are opening doors for yourself to perform destructive actions in the PaymentProcessing#initialize
. For instance, I once had to deal with a Service object that was actually a Command, but it managed to perform both an SQL UPDATE (destructively touch the database!) and execute an HTTP call to an external service, all of the above in the constructor. In addition to unpredictability, a service like this is hard to test. Instead of having to force yourself to make a service that has a constructor only to fill it up with values, use a module that is a container of functions:
PaymentProcessing.process(user, purchase)
Creating these is very cheap, even if you are using more methods within the module. Module methods are very easy to attach using the extend self
idiom:
module PaymentProcessing # "DoingThings" is an acceptable naming convention for a module, better than for an object
extend self # now each method you define can be called on the module itself
def process(user, purchase)
…
debit(user)
credit(gateway)
prepare_invoice(purchase)
...
end
private
def debit(user)
...
end
def prepare_invoice(purchase)
...
end
end
If you really really want inheritance in this scenario, you can of course always use a class instead. Do not use class variables - limit yourself to method-local variables only - most of threading issues and pretty much all Rails reloading issues go out the window once you do.
When dealing with heavy models, flow control with Exceptions is totally fine
For controllng what happens to your command during execution, follow a very simple branching strategy:
Anything that does not raise an exception is a happy path, anything that does is a deviation.
Situations where there are multiple happy paths are exceptionally rare in my experience, and maybe you need to branch inside the service if you encounter them. But in the basic sense, here is how your controller action will look:
def create
user = User.find(params.require(:user_id))
payment = Payment.create!(params.require(:payment))
PaymentProcessing.process(user, payment)
render :nothing, status: 201
rescue ActiveRecord::RecordInvalid # when Payment.create! fails...
# edge case
rescue PaymentProcessing::InsufficientFunds
# another edge case
rescue PaymentProcessing::UserBlocked
# and another
rescue PaymentProcessing::UserDataMustBeStoredInFreedomostan
# and another...
rescue PaymentProcessing::PaymentGatewayError
# and another...
end
This way you get the benefit of pattern matching on return values that functional languages brag about. Do not be misled that Ruby does not have bona-fide matching on the return value - but it doesn’t unfortunately present us with a viable failure on uncaptured result. There is no syntax-level option for a case
statement without the default case. Exceptions, however, do give you opportunities for having an explicit failure scenario when you do not take care of the default (catch-all) outcome failure case.
Do not use rescue_from
in this pattern, because it separates your branching on the result of the command from the place where the command gets executed. And the Ruby “rescue without begin” idiom is just shorter, and more readable, and less Rails-specific.
.()
is a neat trick
There are multiple ways to make an arbitrary object in Ruby callable. Using .()
is fine these days, just like any other. Ruby has a whopping three idioms to help you - you can use callable.()
, you can use callable#[]
, and you can use callable#call
. Anything goes. And your callable can be used as a block passed to an iterator if you implement to_proc
:
module ResetPassword
extend self
def call(user)
…
end
def to_proc
method(:call).to_proc
end
end
# batch_reset_controller.rb
def reset
users = User.find(1, 2, 3, 4)
users.map(&ResetPassword) # Seriously, this works!
rescue SpecialCase
# with the caveat that you don't know on which User that happened
...
end
Having long method signatures in a Service is A-OK
Do not use “multi-parameter constructors” just to be able to do this:
service = PaymentProcessor.new(user, payment, gateway, mutex, logger, …)
service.perform
If you need 5 arguments to perform a certain operation, just make them the arguments of your callable command. Here is why: for executing the operation you need the caller to conform to a contract. This contract implies the availability of all of the given collaborators (the User, the Mutex, the Logger and whatnot). When executing the operation, you need to check for the presence of all of these parameters. If you move the parameter checks into the constructor of the service, and the execution to the perform
method (or whatever method that handles the actual operation you are performing), you are divorcing the contract for the operation from it’s execution - and it is very likely you will make a mistake because they no longer share a unit of scope (and a unit of execution). As a bonus, use keyword arguments while you are at it to exclude mistakes with positional arguments at the outset:
PaymentProcessor.process(user: current_user, gateway: PaymentGateway.default, …)
If you omit a keyword argument it will make Ruby give you clear exceptions on a contract violation. Additionally, if you introduce extra parameters you can plug them with default values in the operation itself:
module PaymentProcessor
extend self
def process(user:, gateway:, logger: Rails.logger)
# gives us a logger, both injectable and default for when we don't care that much
…
end
end
Do not be afraid of long keyword argument lists. See them as an extended contract of your Command. If you find them very unwieldy - replace the parameter list with an object that will contain all the keyword arguments. You can use anything that responds to to_hash
as a package for keyword arguments:
connection_config = EnterpriseDatabaseConnectionConfig.new
# will call `connection_config.to_hash` to extract a Hash keyed by Symbols. AND you can test
# `MyCompany.....#to_hash` in a separate unit test, as a bonus...
pg_conn = pg.connect(**connection_config)
Interesting read on exceptions versus ActiveRecord here.
When exceptions are actually expensive
Bear in mind that exceptions are expensive due to stack unwinding. So, using exceptions for flow control, like many things, is appropriate where it is appropriate and not appropriate in some other situations - for exampe, in tight loops. Where this really matters, you can use branching on the return value like you would do in “pedestrian” languages with “full-manual” error handling, like C and Go. For instance recent implementations of non-blocking writes in IO
etc. use that idiom:
case socket.write_nonblock(bytes)
when :iowait # The socket is clogged, do a pass
Thread.pass
when Fixnum # the number of bytes written
…
else # nil, the send has completed or there was nothing to send
end
Bear in mind however, that this is useful for tight loops or IO-intensive scenarios only, since it degrades your control flow to explicit branching on return values. This is indeed (at least in my opinion), not any better than
f, err := file.Open(path)
that you might have in some other languages claiming “explicit error handling” as an extreme fom of virtue.