Assuming we need to post a message to Twitter, we usually do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">class</span> <span class="token class-name">TweetController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> create <span class="token function">send_tweet</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:message</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token function">send_tweet</span> <span class="token punctuation">(</span> tweet <span class="token punctuation">)</span> client <span class="token operator">=</span> <span class="token constant">Twitter</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">REST</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Client</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token class-name">do</span> <span class="token operator">|</span> config <span class="token operator">|</span> config <span class="token punctuation">.</span> consumer_key <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_CONSUMER_KEY'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> consumer_secret <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_CONSUMER_SECRET'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> access_token <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_ACCESS_TOKEN'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> access_token_secret <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_ACCESS_SECRET'</span> <span class="token punctuation">]</span> <span class="token keyword">end</span> client <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> tweet <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Looking at the code above, we have defined send_tweet to call twitter api. What if another controller calls twitter in the same way? Should it be on concern ?? But it doesn’t really belong to the controller, why don’t we try to make the Twitter API an object then call when needed
What is a service object?
The service object is designed to implement a specific logic where the object handling k belongs entirely to a model. The benefit of the Service object is that it helps us to focus all the functional logic on a separate object instead of breaking it down into controller or model. Whenever needed, call that object. Because the logical block is concentrated in one object, it will greatly reduce the controller and model, the code is clean and the maintenance process will be less difficult as well. See the example on the send_tweet method, which implements the only logic that makes a tweet. If this logic is encapsulated into a class, we can initialize and call it as follows:
1 2 3 | tweet_creator <span class="token operator">=</span> <span class="token constant">TweetCreator</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:message</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> tweet_creator <span class="token punctuation">.</span> send_tweet |
or
1 2 | <span class="token constant">TweetCreator</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:message</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> |
It’s convenient right k. We just define it once, can use it anywhere, easily maintance modification
Create Service object
We will create TweetCreator
in app/services
:
1 2 | $ mkdir app/services && touch app/services/tweet_creator.rb |
Add logic to the service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <span class="token comment"># app/services/tweet_creator.rb</span> <span class="token keyword">class</span> <span class="token class-name">TweetCreator</span> <span class="token keyword">def</span> <span class="token function">initialize</span> <span class="token punctuation">(</span> message <span class="token punctuation">)</span> <span class="token variable">@message</span> <span class="token operator">=</span> message <span class="token keyword">end</span> <span class="token keyword">def</span> send_tweet client <span class="token operator">=</span> <span class="token constant">Twitter</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">REST</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Client</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token class-name">do</span> <span class="token operator">|</span> config <span class="token operator">|</span> config <span class="token punctuation">.</span> consumer_key <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_CONSUMER_KEY'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> consumer_secret <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_CONSUMER_SECRET'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> access_token <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_ACCESS_TOKEN'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> access_token_secret <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_ACCESS_SECRET'</span> <span class="token punctuation">]</span> <span class="token keyword">end</span> client <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token variable">@message</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
You can then call by:
1 2 | <span class="token constant">TweetCreator</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:message</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> send_tweet |
TweetCreator class name is relatively short but when initialized and called, it looks quite long right. we can shorten the call by the following, If TweetCreator can be similar to proc in Ruby we can call it with TweetCreator.call(message)
Now we will turn the service object as a proc to facilitate calling service offline! Create 1 ApplicationService:
1 2 3 4 5 6 7 | <span class="token comment"># app/services/application_service.rb</span> <span class="token keyword">class</span> <span class="token class-name">ApplicationService</span> <span class="token keyword">def</span> <span class="token keyword">self</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> <span class="token operator">*</span> args <span class="token punctuation">,</span> <span class="token operator">&</span> block <span class="token punctuation">)</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> <span class="token operator">*</span> args <span class="token punctuation">,</span> <span class="token operator">&</span> block <span class="token punctuation">)</span> <span class="token punctuation">.</span> call <span class="token keyword">end</span> <span class="token keyword">end</span> |
Every time the call is called, it will create an instance of that class vs the variables passed to it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <span class="token comment"># app/services/tweet_creator.rb</span> <span class="token keyword">class</span> <span class="token class-name">TweetCreator</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> attr_reader <span class="token symbol">:message</span> <span class="token keyword">def</span> <span class="token function">initialize</span> <span class="token punctuation">(</span> message <span class="token punctuation">)</span> <span class="token variable">@message</span> <span class="token operator">=</span> message <span class="token keyword">end</span> <span class="token keyword">def</span> call client <span class="token operator">=</span> <span class="token constant">Twitter</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">REST</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Client</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token class-name">do</span> <span class="token operator">|</span> config <span class="token operator">|</span> config <span class="token punctuation">.</span> consumer_key <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_CONSUMER_KEY'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> consumer_secret <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_CONSUMER_SECRET'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> access_token <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_ACCESS_TOKEN'</span> <span class="token punctuation">]</span> config <span class="token punctuation">.</span> access_token_secret <span class="token operator">=</span> <span class="token constant">ENV</span> <span class="token punctuation">[</span> <span class="token string">'TWITTER_ACCESS_SECRET'</span> <span class="token punctuation">]</span> <span class="token keyword">end</span> client <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token variable">@message</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
At the controller we call
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">TweetController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> create <span class="token constant">TweetCreator</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> params <span class="token punctuation">[</span> <span class="token symbol">:message</span> <span class="token punctuation">]</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 way to make the code more optimal right )
Grouping Similar Service Objects
In the above example we only consider one service object, but in reality it may be more complicated than that. For example, we have hundreds of services that handle many different logic. We can’t put them into one file, it will be difficult to manage, right. We will use namespacing, we will group the service objects with the same characteristics into one module: For example:
1 2 3 4 5 6 | services ├── application_service.rb └── twitter_manager ├── profile_follower.rb └── tweet_creator.rb |
In service:
1 2 3 4 5 6 7 | <span class="token comment"># services/twitter_manager/tweet_creator.rb</span> <span class="token keyword">module</span> <span class="token constant">TwitterManager</span> <span class="token keyword">class</span> <span class="token class-name">TweetCreator</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 | <span class="token comment"># services/twitter_manager/profile_follower.rb</span> <span class="token keyword">module</span> <span class="token constant">TwitterManager</span> <span class="token keyword">class</span> <span class="token class-name">ProfileFollower</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
I called by
1 2 3 | <span class="token constant">TwitterManager</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">TweetCreator</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> arg <span class="token punctuation">)</span> <span class="token constant">TwitterManager</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">ProfileManager</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> arg <span class="token punctuation">)</span> |
Service Objects manipulate the database
In the above example we considered the api call, but the service object can also be used to call the database. It is really useful to update multiple DBs with complex logic such as:
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 | <span class="token keyword">module</span> <span class="token constant">MoneyManager</span> <span class="token comment"># exchange currency from one amount to another</span> <span class="token keyword">class</span> <span class="token class-name">CurrencyExchanger</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">def</span> call <span class="token constant">ActiveRecord</span> <span class="token punctuation">:</span> <span class="token punctuation">:</span> <span class="token constant">Base</span> <span class="token punctuation">.</span> transaction <span class="token keyword">do</span> <span class="token comment"># transfer the original currency to the exchange's account</span> outgoing_tx <span class="token operator">=</span> <span class="token constant">CurrencyTransferrer</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> from <span class="token punctuation">:</span> the_user_account <span class="token punctuation">,</span> to <span class="token punctuation">:</span> the_exchange_account <span class="token punctuation">,</span> amount <span class="token punctuation">:</span> the_amount <span class="token punctuation">,</span> currency <span class="token punctuation">:</span> original_currency <span class="token punctuation">)</span> <span class="token comment"># get the exchange rate</span> rate <span class="token operator">=</span> <span class="token constant">ExchangeRateGetter</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> from <span class="token punctuation">:</span> original_currency <span class="token punctuation">,</span> to <span class="token punctuation">:</span> new_currency <span class="token punctuation">)</span> <span class="token comment"># transfer the new currency back to the user's account</span> incoming_tx <span class="token operator">=</span> <span class="token constant">CurrencyTransferrer</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> from <span class="token punctuation">:</span> the_exchange_account <span class="token punctuation">,</span> to <span class="token punctuation">:</span> the_user_account <span class="token punctuation">,</span> amount <span class="token punctuation">:</span> the_amount <span class="token operator">*</span> rate <span class="token punctuation">,</span> currency <span class="token punctuation">:</span> new_currency <span class="token punctuation">)</span> <span class="token comment"># record the exchange happening</span> <span class="token constant">ExchangeRecorder</span> <span class="token punctuation">.</span> <span class="token function">call</span> <span class="token punctuation">(</span> outgoing_tx <span class="token punctuation">:</span> outgoing_tx <span class="token punctuation">,</span> incoming_tx <span class="token punctuation">:</span> incoming_tx <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token comment"># record the transfer of money from one account to another in money_accounts</span> <span class="token keyword">class</span> <span class="token class-name">CurrencyTransferrer</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token comment"># record an exchange event in the money_exchanges table</span> <span class="token keyword">class</span> <span class="token class-name">ExchangeRecorder</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token comment"># get the exchange rate from an API</span> <span class="token keyword">class</span> <span class="token class-name">ExchangeRateGetter</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
What should a service object return?
Recently we discussed how to construct a method call, so what should the method call return? There are 3 ways to return it
- Returns true / false
1 2 3 4 5 6 | <span class="token keyword">def</span> call <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">return</span> <span class="token keyword">true</span> <span class="token keyword">if</span> client <span class="token punctuation">.</span> <span class="token function">update</span> <span class="token punctuation">(</span> <span class="token variable">@message</span> <span class="token punctuation">)</span> <span class="token keyword">false</span> <span class="token keyword">end</span> |
- Returns 1 value
1 2 3 4 5 6 | <span class="token keyword">def</span> call <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token keyword">return</span> <span class="token keyword">false</span> <span class="token keyword">unless</span> exchange_rate exchange_rate <span class="token keyword">end</span> |
- Returns 1 enum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">class</span> <span class="token class-name">ExchangeRecorder</span> <span class="token operator"><</span> <span class="token constant">ApplicationService</span> <span class="token constant">RETURNS</span> <span class="token operator">=</span> <span class="token punctuation">[</span> <span class="token constant">SUCCESS</span> <span class="token operator">=</span> <span class="token symbol">:success</span> <span class="token punctuation">,</span> <span class="token constant">FAILURE</span> <span class="token operator">=</span> <span class="token symbol">:failure</span> <span class="token punctuation">,</span> <span class="token constant">PARTIAL_SUCCESS</span> <span class="token operator">=</span> <span class="token symbol">:partial_success</span> <span class="token punctuation">]</span> <span class="token keyword">def</span> call foo <span class="token operator">=</span> do_something <span class="token keyword">return</span> <span class="token constant">SUCCESS</span> <span class="token keyword">if</span> foo <span class="token punctuation">.</span> success <span class="token operator">?</span> <span class="token keyword">return</span> <span class="token constant">FAILURE</span> <span class="token keyword">if</span> foo <span class="token punctuation">.</span> failure <span class="token operator">?</span> <span class="token constant">PARTIAL_SUCCESS</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> do_something <span class="token keyword">end</span> <span class="token keyword">end</span> |
Some rules for writing good service objects
Each Service Object should have only 1 public method
Each service object only implements a specific bussiness, so there should only be one public method
Name the Service object according to its role
We should name the service object so that the code can understand its role as well
Do not perform multiple actions
Each service object only implements 1 bussiness
Handle Exceptions inside the service object
Hopefully the article helps you understand the service object and apply it in the project. Thanks for reading.
Reference at: https://www.toptal.com/ruby-on-rails/rails-service-objects-tutorial