Rails Service Objects: A Comprehensive Guide
2018-05-07
4157 words
20 mins read
This post was written by Amin Shah Gilani, Ruby Developer for Toptal.
Ruby on Rails ships with everything you need to prototype your application quickly, but when your codebase starts growing, you’ll run into scenarios where the conventional Fat Model, Skinny Controller mantra breaks. When your business logic can’t fit into either a model or a controller, that’s when service objects come in and let us separate every business action into its own Ruby object.
In this article, I’ll explain when a service object is required; how to go about writing clean service objects and grouping them together for contributor sanity; the strict rules I impose on my service objects to tie them directly to my business logic; and how not to turn your service objects into a dumping ground for all the code you don’t know what to do with.
Why Do I Need Service Objects?
Try this: What do you do when your application needs to tweet the text from
1
|
?
If you’ve been using vanilla Rails so far, then you’ve probably done something like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TweetController</span> < ApplicationController</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span>
send_tweet(params[<span class="hljs-symbol">:message</span>])
<span class="hljs-keyword">end</span>
private
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_tweet</span><span class="hljs-params">(tweet)</span></span>
client = Twitter::REST::Client.new <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
config.consumer_key = ENV[<span class="hljs-string">'TWITTER_CONSUMER_KEY'</span>]
config.consumer_secret = ENV[<span class="hljs-string">'TWITTER_CONSUMER_SECRET'</span>]
config.access_token = ENV[<span class="hljs-string">'TWITTER_ACCESS_TOKEN'</span>]
config.access_token_secret = ENV[<span class="hljs-string">'TWITTER_ACCESS_SECRET'</span>]
<span class="hljs-keyword">end</span>
client.update(tweet)
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
The problem here is that you’ve added at least ten lines to your controller, but they don’t really belong there. Also, what if you wanted to use the same functionality in another controller? Do you move this to a concern? Wait, but this code doesn’t really belong in controllers at all. Why can’t the Twitter API just come with a single prepared object for me to call?
The first time I did this, I felt like I’d done something dirty. My, previously, beautifully lean Rails controllers had started getting fat and I didn’t know what to do. Eventually, I fixed my controller with a service object.
Before you start reading this article, let’s pretend:
- This application handles a Twitter account.
- The Rails Way means “the conventional Ruby on Rails way of doing things” and the book doesn’t exist.
- I’m a Rails expert… which I’m told every day that I am, but I have trouble believing it, so let’s just pretend that I really am one.
What Are Service Objects?
Service objects are Plain Old Ruby Objects (PORO) that are designed to execute one single action in your domain logic and do it well. Consider the example above: Our method already has the logic to do one single thing, and that is to create a tweet. What if this logic was encapsulated within a single Ruby class that we can instantiate and call a method to? Something like:
1
2
3
4
5
6
7
tweet_creator = TweetCreator.new(params[<span class="hljs-symbol">:message</span>])
tweet_creator.send_tweet
<span class="hljs-comment"># Later on in the article, we'll add syntactic sugar and shorten the above to:</span>
TweetCreator.call(params[<span class="hljs-symbol">:message</span>])
This is pretty much it; our
1
|
service object, once created, can be called from anywhere, and it would do this one thing very well.
Creating a Service Object
First let’s create a new
1
|
in a new folder called
1
|
:
1
$ mkdir app/services && touch app/services/tweet_creator.rb
And let’s just dump all our logic inside a new Ruby class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<span class="hljs-comment"># app/services/tweet_creator.rb</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TweetCreator</span></span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(message)</span></span>
@message = message
<span class="hljs-keyword">end</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_tweet</span></span>
client = Twitter::REST::Client.new <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
config.consumer_key = ENV[<span class="hljs-string">'TWITTER_CONSUMER_KEY'</span>]
config.consumer_secret = ENV[<span class="hljs-string">'TWITTER_CONSUMER_SECRET'</span>]
config.access_token = ENV[<span class="hljs-string">'TWITTER_ACCESS_TOKEN'</span>]
config.access_token_secret = ENV[<span class="hljs-string">'TWITTER_ACCESS_SECRET'</span>]
<span class="hljs-keyword">end</span>
client.update(@message)
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
Then you can call
1
|
anywhere in your app, and it will work. Rails will load this object magically because it autoloads everything under
1
|
. Verify this by running:
1
2
3
4
5
6
$ rails c
Running via Spring preloader <span class="hljs-keyword">in</span> process <span class="hljs-number">12417</span>
Loading development environment (Rails <span class="hljs-number">5.1</span>.<span class="hljs-number">5</span>)
> puts ActiveSupport::Dependencies.autoload_paths
...
/Users/gilani/Sandbox/nazdeeq/app/services
Want to know more about how
1
|
works? Read the Autoloading and Reloading Constants Guide.
Adding Syntactic Sugar to Make Rails Service Objects Suck Less
Look, this feels great in theory, but
1
|
is just a mouthful. It’s far too verbose with redundant words… much like HTML (ba-dum tiss!). In all seriousness, though, why do people use HTML when HAML is around? Or even Slim. I guess that’s another article for another time. Back to the task at hand:
1
|
is a nice short class name, but the extra cruft around instantiating the object and calling the method is just too long! If only there were precedence in Ruby for calling something and having it execute itself immediately with the given parameters… oh wait, there is! It’s
1
|
.
**
1
<td> <div class="text codecolorer"> Proc*call </div> </td> </tr>
invokes the block, setting the block’s parameters to the values in params using something close to method calling semantics. It returns the value of the last expression evaluated in the block.
1
2
3
4
5
<td> <div class="text codecolorer"> a_proc = Proc.new {<span class="hljs-params">|scalar, *values|</span> values.map {<span class="hljs-params">|value|</span> value*scalar } }<br /> a_proc.call(<span class="hljs-number">9</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>) <span class="hljs-comment">#=> [9, 18, 27]</span><br /> a_proc[<span class="hljs-number">9</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>] <span class="hljs-comment">#=> [9, 18, 27]</span><br /> a_proc.(<span class="hljs-number">9</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>) <span class="hljs-comment">#=> [9, 18, 27]</span><br /> a_proc.<span class="hljs-keyword">yield</span>(<span class="hljs-number">9</span>, <span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>) <span class="hljs-comment">#=> [9, 18, 27]</span> </div> </td> </tr>
If this confuses you, let me explain. A
1
<td> <div class="text codecolorer"> proc </div> </td> </tr>
can be
1
<td> <div class="text codecolorer"> call </div> </td> </tr>
-ed to execute itself with the given parameters. Which means, that if
1
<td> <div class="text codecolorer"> TweetCreator </div> </td> </tr>
were a
1
<td> <div class="text codecolorer"> proc </div> </td> </tr>
, we could call it with
1
<td> <div class="text codecolorer"> TweetCreator.call(message) </div> </td> </tr>
and the result would be equivalent to
1
<td> <div class="text codecolorer"> TweetCreator.new(params[:message]).call </div> </td> </tr>
, which looks quite similar to our unwieldy old
1
<td> <div class="text codecolorer"> TweetCreator.new(params[:message]).send_tweet </div> </td> </tr>
.
So let’s make our service object behave more like a
1
<td> <div class="text codecolorer"> proc </div> </td> </tr>
!
First, because we probably want to reuse this behavior across all our service objects, let’s borrow from the Rails Way and create a class called
1
<td> <div class="text codecolorer"> ApplicationService </div> </td> </tr>
:
1
2
3
4
5
6
<td> <div class="text codecolorer"> <span class="hljs-comment"># app/services/application_service.rb</span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ApplicationService</span></span><br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">self</span>.<span class="hljs-title">call</span><span class="hljs-params">(*args, &block)</span></span><br /> new(*args, &block).call<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
Did you see what I did there? I added a class method called
1
<td> <div class="text codecolorer"> call </div> </td> </tr>
that creates a new instance of the class with the arguments or block you pass to it, and calls
1
<td> <div class="text codecolorer"> call </div> </td> </tr>
on the instance. Exactly what we we wanted! The last thing to do is to rename the method from our
1
<td> <div class="text codecolorer"> TweetCreator </div> </td> </tr>
class to
1
<td> <div class="text codecolorer"> call </div> </td> </tr>
, and have the class inherit from
1
<td> <div class="text codecolorer"> ApplicationService </div> </td> </tr>
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<td> <div class="text codecolorer"> <span class="hljs-comment"># app/services/tweet_creator.rb</span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TweetCreator</span> < ApplicationService</span><br /> <span class="hljs-keyword">attr_reader</span> <span class="hljs-symbol">:message</span><br /> <br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">initialize</span><span class="hljs-params">(message)</span></span><br /> @message = message<br /> <span class="hljs-keyword">end</span><br /> <br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span><br /> client = Twitter::REST::Client.new <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span><br /> config.consumer_key = ENV[<span class="hljs-string">'TWITTER_CONSUMER_KEY'</span>]<br /> config.consumer_secret = ENV[<span class="hljs-string">'TWITTER_CONSUMER_SECRET'</span>]<br /> config.access_token = ENV[<span class="hljs-string">'TWITTER_ACCESS_TOKEN'</span>]<br /> config.access_token_secret = ENV[<span class="hljs-string">'TWITTER_ACCESS_SECRET'</span>]<br /> <span class="hljs-keyword">end</span><br /> client.update(@message)<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
And finally, let’s wrap this up by calling our service object in the controller:
1
2
3
4
5
<td> <div class="text codecolorer"> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TweetController</span> < ApplicationController</span><br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span></span><br /> TweetCreator.call(params[<span class="hljs-symbol">:message</span>])<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
Grouping Similar Service Objects for Sanity
The example above has only one service object, but in the real world, things can get more complicated. For example, what if you had hundreds of services, and half of them were related business actions, e.g., having a
1
<td> <div class="text codecolorer"> Follower </div> </td> </tr>
service that followed another Twitter account? Honestly, I’d go insane if a folder contained 200 unique-looking files, so good thing there’s another pattern from the Rails Way that we can copy—I mean, use as inspiration: namespacing.
Let’s pretend we’ve been tasked to create a service object that follows other Twitter profiles.
Let’s look at the name of our previous service object:
1
<td> <div class="text codecolorer"> TweetCreator </div> </td> </tr>
. It sounds like a person, or at the very least, a role in an organization. Someone that creates Tweets. I like to name my service objects as if they were just that: roles in an organization. Following this convention, I’ll call my new object:
1
<td> <div class="text codecolorer"> ProfileFollower </div> </td> </tr>
.
Now, since I’m the supreme overlord of this app, I’m going to create a managerial position in my service hierarchy and delegate responsibility for both these services to that position. I’ll call this new managerial position
1
<td> <div class="text codecolorer"> TwitterManager </div> </td> </tr>
.
Since this manager does nothing but manage, let’s make it a module and nest our service objects under this module. Our folder structure will now look like:
1
2
3
4
5
<td> <div class="text codecolorer"> services<br /> ??? application_service.rb<br /> ??? twitter_manager<br /> ??? profile_follower.rb<br /> ??? tweet_creator.rb </div> </td> </tr>
And our service objects:
1
2
3
4
5
6
<td> <div class="text codecolorer"> <span class="hljs-comment"># services/twitter_manager/tweet_creator.rb</span><br /> <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">TwitterManager</span></span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TweetCreator</span> < ApplicationService</span><br /> ...<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
1
2
3
4
5
6
<td> <div class="text codecolorer"> <span class="hljs-comment"># services/twitter_manager/profile_follower.rb</span><br /> <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">TwitterManager</span></span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ProfileFollower</span> < ApplicationService</span><br /> ...<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
And our calls will now become
1
<td> <div class="text codecolorer"> TwitterManager::TweetCreator.call(arg) </div> </td> </tr>
, and
1
<td> <div class="text codecolorer"> TwitterManager::ProfileManager.call(arg) </div> </td> </tr>
.
Service Objects to Handle Database Operations
The example above made API calls, but service objects can also be used when all the calls are to your database instead of an API. This is especially helpful if some business actions require multiple database updates wrapped in a transaction. For example, this sample code would use services to record a currency exchange taking place.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<td> <div class="text codecolorer"> <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">MoneyManager</span></span><br /> <span class="hljs-comment"># exchange currency from one amount to another</span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CurrencyExchanger</span> < ApplicationService</span><br /> ...<br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span><br /> ActiveRecord::Base.transaction <span class="hljs-keyword">do</span><br /> <span class="hljs-comment"># transfer the original currency to the exchange's account</span><br /> outgoing_tx = CurrencyTransferrer.call(<br /> <span class="hljs-symbol">from:</span> the_user_account,<br /> <span class="hljs-symbol">to:</span> the_exchange_account,<br /> <span class="hljs-symbol">amount:</span> the_amount,<br /> <span class="hljs-symbol">currency:</span> original_currency<br /> )<br /> <br /> <span class="hljs-comment"># get the exchange rate</span><br /> rate = ExchangeRateGetter.call(<br /> <span class="hljs-symbol">from:</span> original_currency,<br /> <span class="hljs-symbol">to:</span> new_currency<br /> )<br /> <br /> <span class="hljs-comment"># transfer the new currency back to the user's account</span><br /> incoming_tx = CurrencyTransferrer.call(<br /> <span class="hljs-symbol">from:</span> the_exchange_account,<br /> <span class="hljs-symbol">to:</span> the_user_account,<br /> <span class="hljs-symbol">amount:</span> the_amount * rate,<br /> <span class="hljs-symbol">currency:</span> new_currency<br /> )<br /> <br /> <span class="hljs-comment"># record the exchange happening</span><br /> ExchangeRecorder.call(<br /> <span class="hljs-symbol">outgoing_tx:</span> outgoing_tx,<br /> <span class="hljs-symbol">incoming_tx:</span> incoming_tx<br /> )<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span><br /> <br /> <span class="hljs-comment"># record the transfer of money from one account to another in money_accounts</span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CurrencyTransferrer</span> < ApplicationService</span><br /> ...<br /> <span class="hljs-keyword">end</span><br /> <br /> <span class="hljs-comment"># record an exchange event in the money_exchanges table</span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExchangeRecorder</span> < ApplicationService</span><br /> ...<br /> <span class="hljs-keyword">end</span><br /> <br /> <span class="hljs-comment"># get the exchange rate from an API</span><br /> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExchangeRateGetter</span> < ApplicationService</span><br /> ...<br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
What Do I Return from My Service Object?
We’ve discussed how to
1
<td> <div class="text codecolorer"> call </div> </td> </tr>
our service object, but what should the object return? There are three ways to approach this:
Return
1
<td> <div class="text codecolorer"> true </div> </td> </tr>
or
1
<td> <div class="text codecolorer"> false </div> </td> </tr>
Return a value
Return an Enum
Return
1
<td> <div class="text codecolorer"> true </div> </td> </tr>
or
{#return-true-or-false}
1
<td> <div class="text codecolorer"> false </div> </td> </tr>
This one is simple: If an action works as intended, return
1
<td> <div class="text codecolorer"> true </div> </td> </tr>
; otherwise, return
1
<td> <div class="text codecolorer"> false </div> </td> </tr>
:
1
2
3
4
5
<td> <div class="text codecolorer"> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span><br /> ...<br /> <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span> <span class="hljs-keyword">if</span> client.update(@message)<br /> <span class="hljs-literal">false</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
Return a Value
If your service object fetches data from somewhere, you probably want to return that value:
1
2
3
4
5
<td> <div class="text codecolorer"> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span><br /> ...<br /> <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">unless</span> exchange_rate<br /> exchange_rate<br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
Respond with an Enum
If your service object is a bit more complex, and you want to handle different scenarios, you could just add enums to control the flow of your services:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<td> <div class="text codecolorer"> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExchangeRecorder</span> < ApplicationService</span><br /> RETURNS = [<br /> SUCCESS = <span class="hljs-symbol">:success</span>,<br /> FAILURE = <span class="hljs-symbol">:failure</span>,<br /> PARTIAL_SUCCESS = <span class="hljs-symbol">:partial_success</span><br /> ]<br /> <br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">call</span></span><br /> foo = do_something<br /> <span class="hljs-keyword">return</span> SUCCESS <span class="hljs-keyword">if</span> foo.success?<br /> <span class="hljs-keyword">return</span> FAILURE <span class="hljs-keyword">if</span> foo.failure?<br /> PARTIAL_SUCCESS<br /> <span class="hljs-keyword">end</span><br /> <br /> private<br /> <br /> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">do_something</span></span><br /> <span class="hljs-keyword">end</span><br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
And then in your app, you can use:
1
2
3
4
5
6
7
8
<td> <div class="text codecolorer"> <span class="hljs-keyword">case</span> ExchangeRecorder.call<br /> <span class="hljs-keyword">when</span> ExchangeRecorder::SUCCESS<br /> foo<br /> <span class="hljs-keyword">when</span> ExchangeRecorder::FAILURE<br /> bar<br /> <span class="hljs-keyword">when</span> ExchangeRecorder::PARTIAL_SUCCESS<br /> baz<br /> <span class="hljs-keyword">end</span> </div> </td> </tr>
Shouldn’t I Put Service Objects in
1
<td> <div class="text codecolorer"> lib/services </div> </td> </tr>
Instead of
1
<td> <div class="text codecolorer"> app/services </div> </td> </tr>
?
This is subjective. People’s opinions differ on where to put their service objects. Some people put them in
1
<td> <div class="text codecolorer"> lib/services </div> </td> </tr>
, while some create
1
<td> <div class="text codecolorer"> app/services </div> </td> </tr>
. I fall in the latter camp. Rails’ Getting Started Guide describes the
1
<td> <div class="text codecolorer"> lib/ </div> </td> </tr>
folder as the place to put “extended modules for your application.”
In my humble opinion, “extended modules” means modules that don’t encapsulate core domain logic and can generally be used across projects. In the wise words of a random Stack Overflow answer, put code in there that “can potentially become its own gem.”
Are Service Objects a Good Idea?
It depends on your use case. Look—the fact that you’re reading this article right now suggests you’re trying to write code that doesn’t exactly belong in a model or controller. I recently read this article about how service objects are an anti-pattern. The author has his opinions, but I respectfully disagree.
Just because some other person overused service objects doesn’t mean they’re inherently bad. At my startup, Nazdeeq, we use service objects as well as non-ActiveRecord models. But the difference between what goes where has always been apparent to me: I keep all business actions in service objects while keeping resources that don’t really need persistence in non-ActiveRecord models. At the end of the day, it’s for you to decide what pattern is good for you.
However, do I think service objects in general are a good idea? Absolutely! They keep my code neatly organized, and what makes me confident in my use of POROs is that Ruby loves objects. No, seriously, Ruby loves objects. It’s insane, totally bonkers, but I love it! Case in point:
1
2
3
4
5
6
7
8
9
10
11
<td> <div class="text codecolorer"> > <span class="hljs-number">5</span>.is_a? Object <span class="hljs-comment"># => true</span><br /> > <span class="hljs-number">5</span>.<span class="hljs-keyword">class</span> <span class="hljs-comment"># => Integer</span><br /> <br /> <br /> > <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Integer</span></span><br /> <span class="hljs-meta">?></span> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">woot</span></span><br /> <span class="hljs-meta">?></span> <span class="hljs-string">'woot woot'</span><br /> <span class="hljs-meta">?></span> end<br /> <span class="hljs-meta">?></span> end <span class="hljs-comment"># => :woot</span><br /> <br /> > <span class="hljs-number">5</span>.woot <span class="hljs-comment"># => "woot woot"</span> </div> </td> </tr>
See?
1
<td> <div class="text codecolorer"> 5 </div> </td> </tr>
is literally an object.
In many languages, numbers and other primitive types are not objects. Ruby follows the influence of the Smalltalk language by giving methods and instance variables to all of its types. This eases one’s use of Ruby, since rules applying to objects apply to all of Ruby.
Ruby-lang.orgWhen Should I Not Use a Service Object?
This one’s easy. I have these rules:
- Does your code handle routing, params or do other controller-y things?
If so, don’t use a service object—your code belongs in the controller.- Are you trying to share your code in different controllers?
In this case, don’t use a service object—use a concern.- Is your code like a model that doesn’t need persistence?
If so, don’t use a service object. Use a non-ActiveRecord model instead.- Is your code a specific business action? (e.g., “Take out the trash,” “Generate a PDF using this text,” or “Calculate the customs duty using these complicated rules”)
In this case, use a service object. That code probably doesn’t logically fit in either your controller or your model.Of course, these are my rules, so you’re welcome to adapt them to your own use cases. These have worked very well for me, but your mileage may vary.
Rules for Writing Good Service Objects
I have a four rules for creating service objects. These aren’t written in stone, and if you _really_want to break them, you can, but I will probably ask you to change it in code reviews unless your reasoning is sound.
Rule 1: Only One Public Method per Service Object
Service objects are single business actions. You can change the name of your public method if you like. I prefer using
1
<td> <div class="text codecolorer"> call </div> </td> </tr>
, but Gitlab CE’s codebase calls it
1
<td> <div class="text codecolorer"> execute </div> </td> </tr>
and other people may use
1
<td> <div class="text codecolorer"> perform </div> </td> </tr>
. Use whatever you want—you could call it
1
<td> <div class="text codecolorer"> nermin </div> </td> </tr>
for all I care. Just don’t create two public methods for a single service object. Break it into two objects if you need to.
Rule 2: Name Service Objects Like Dumb Roles at a Company
Service objects are single business actions. Imagine if you hired one person at the company to do that one job, what would you call them? If their job is to create tweets, call them
1
<td> <div class="text codecolorer"> TweetCreator </div> </td> </tr>
. If their job is to read specific tweets, call them
1
<td> <div class="text codecolorer"> TweetReader </div> </td> </tr>
.
Rule 3: Don’t Create Generic Objects to Perform Multiple Actions
Service objects are single business actions. I broke the functionality into two pieces:
1
<td> <div class="text codecolorer"> TweetReader </div> </td> </tr>
, and
1
<td> <div class="text codecolorer"> ProfileFollower </div> </td> </tr>
. What I didn’t do is create a single generic object called
1
<td> <div class="text codecolorer"> TwitterHandler </div> </td> </tr>
and dump all of the API functionality in there. Please don’t do this. This goes against the “business action” mindset and makes the service object look like the Twitter Fairy. If you want to share code among the business objects, just create a
1
<td> <div class="text codecolorer"> BaseTwitterManager </div> </td> </tr>
object or module and mix that into your service objects.
Rule 4: Handle Exceptions Inside the Service Object
For the umpteenth time: Service objects are single business actions. I can’t say this enough. If you’ve got a person that reads tweets, they’ll either give you the tweet, or say, “This tweet doesn’t exist.” Similarly, don’t let your service object panic, jump on your controller’s desk, and tell it to halt all work because “Error!” Just return
1
<td> <div class="text codecolorer"> false </div> </td> </tr>
and let the controller move on from there.
Credits and Next Steps
This article wouldn’t have been possible without the amazing community of Ruby developers at Toptal. If I ever run into a problem, the community is the most helpful group of talented engineers I’ve ever met.
If you’re using service objects, you may find yourself wondering how to force certain answers while testing. I recommend reading this article on how to create mock service objects in Rspec that will always return the result you want, without actually hitting the service object!
If you want to learn more about Ruby tricks, I recommend Creating a Ruby DSL: A Guide to Advanced Metaprogramming by fellow Toptaler Máté Solymosi. He breaks down how the
1
<td> <div class="text codecolorer"> routes.rb </div> </td> </tr>
file doesn’t feel like Ruby and helps you build your own DSL.
Related Articles:
- 2014/02/24 get the contents of whole site like some wiki or wikia
- 2012/02/21 glances – Get a glimpse of the whole system
- 2011/11/05 g flag in :s useless in vim
Authored By Amit Agarwal
Amit Agarwal, Linux and Photography are my hobbies.Creative Commons Attribution 4.0 International License.