Gourmet Service Objects
Is your Rails app’s business logic hidden in ugly controllers with 10+ lines long method and fat models powered by Linguini callbacks? Are your tests getting out of control and you spend most of your days looking at green dots? Do you want to impress your coworkers with Unicorn level code?
You need Gourmet Service Objects™!
I have been using service objects for the past three years and they reconciled my take on Rails (as much as automated testing reconciled my feelings for software programming!).
A service object does one thing
A service object (aka method object) performs one action. It holds the business logic to perform that action. Here is an example:
The three conventions I follow are:
- Services go under the
app/servicesdirectory. I encourage you to use subdirectories for business logic-heavy domains. For instance:
- The file
- The file
- Services start with a verb (and do not end with Service):
- Services respond to the
callmethod. I found using another verb makes it a bit redundant:
ApproveTransaction.approve()does not read well. Also, the
callmethod is the de facto method for lambda, procs, and method objects.
Service objects show what my application does
I can just glance over the
services directory to see what my application does:
A quick look into a service object and I know what business logic is involved. I don’t have to go through the controllers, ActiveRecord model callbacks and observers to understand what “approving a transaction” involves.
Clean-up models and controllers
Controllers turn the request (params, session, cookies) into arguments, pass them down to the service and redirect or render according to the service response.
Models only deal with associations, scopes, validations and persistence.
This makes models and controllers much easier to test and maintain!
DRY and Embrace change
I keep service objects as simple and small as I can. I compose service objects with other service objects, and I reuse them. My code is quite modular and I’m ready to Embrace Change™.
Clean up and speed up your test suite
Services are easy and fast to test since they are small ruby objects with one point of entry (the
call method). Complex services are composed with other services, so you can split up your tests easily.
I tend not to use any mocks or stub to test services that deal with ActiveRecord objects. rspec-set helps me keep the running time quite low while having simple and robusts test. Once again, service objects are small and do one thing, so they tend to have a limited amount of dependencies.
Call them from anywhere
Service objects are likely to be called from controllers as well as:
- Other service objects:
- DelayedJob / Rescue / Sidekiq Jobs:
- Rake tasks:
- The console:
- Even from test helpers to setup my integration tests!
Real world services
I like to use instances of service objects to take advantage of private methods. I add Virtus into the mix to handle parameters. For instance:
I extracted the
def self.call into a helper module
I sometimes inject dependencies to test services that orchestrate
complex operations. Since services respond to the
call method, a
proc does the job.
Values: The Return
The services I write have three flavours when it comes to communicating back to the caller.
Flavour #1: Fail loudly
Most services are not supposed to fail. They do not return anything (meaningful) but they raise an exception when something goes wrong. Those services are likely to use methods that fail loudly such as
Flavour #2: Return a persisted ActiveRecord model
The caller can check if an AR instance is persisted and then has access to its errors.
Flavour #3: Response object
Some services have several outcomes and complex error handling. They return a response object which responds to
That’s it for service objects for now. Experiment with them, as I believe they will make your codebase more expressive and easier to maintain!
I’m happy to respond to any question or concern you guys might have. Feel free to leave a comment below. <3 <3 <3