Giả sử ta cần đăng 1 đoạn message lên Twitter, chúng ta thường làm như sau:
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> |
Nhìn vào đoạn code trên ta đã định nghĩa send_tweet gọi api twitter.Nếu 1 controller khác cũng gọi twitter 1 cách tương tự thì sao? Có nên cho nó vào concern?? Nhưng nó k hẳn thuộc về controller, tại sao ta k tìm cách cho Twitter API thành 1 đối tượng sau đó call khi cần
Service object là gì?
Service object được thiết kế để thực hiện một logic cụ thể nào đó mà đối tượng xử lí k hoàn toàn thuộc về 1 model nào cả.
Lợi ích của Service object là nó giúp chúng ta tập chung toàn bộ logic chức năng vào 1 object riêng biệt thay vì chia nhỏ nó ở controller hay model. Lúc nào cần đến thì gọi object đó ra.
Chính vì khối logic được tập trung hết vào trong 1 object nên sẽ tối giản controller và model đi rất nhiều, code clean và quá trình maintain sau này cũng đỡ vất vả hơn.
Xem ví dụ trên method send_tweet thực hiện 1 logic duy nhất là tạo 1 tweet. Nếu logic này được gói gọn vào 1 class, chúng ta có thể khởi tạo và gọi theo kiểu:
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 |
hoặc
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> |
Thật tiện lợi đúng k. Ta chỉ việc định nghĩa nó 1 lần, có thể dùng nó ở bất cứ đâu, dễ dàng maintance sửa đổi
Tạo Service object
Chúng ta sẽ tạo TweetCreator
trong app/services
:
1 2 | $ mkdir app/services && touch app/services/tweet_creator.rb |
add logic vào trong 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> |
Sau đó bạn có thể gọi bằng cách:
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 tương đối ngắn nhưng khi khởi tạo và gọi thì trông khá dài đúng không. ta có thể rút gọn câu lệnh gọi bằng cách sau đây, Nếu TweetCreator có thể giống với proc trong Ruby ta có thể gọi nó bằng TweetCreator.call(message)
Giờ chúng ta sẽ biến service object như 1 proc để thuận tiện cho việc gọi service nhé!
Tạo 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> |
Mỗi khi call dc gọi nó sẽ tạo ra 1 instance của class đó vs các biến truyền vào
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> |
Tại controller ta gọi
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 cách làm cho code trở nên tối ưu hơn đúng không )
Grouping Similar Service Objects
Ở ví dụ trên ta chỉ xét 1 service object, nhưng trên thực tế có thể phức tạp hơn thế. Ví dụ ta có hàng trăm service xử lí nhiều logic khác nhau. Ta k thể nhét chúng vào 1 file sẽ rất khó quản lí đúng không. Chúng ta sẽ sử dụng namespacing, ta sẽ nhóm các service object có chung đặc điểm vào 1 module:
Ví dụ:
1 2 3 4 5 6 | services ├── application_service.rb └── twitter_manager ├── profile_follower.rb └── tweet_creator.rb |
Trong 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> |
Ta gọi bằng cách
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 thao tác với database
Ở ví dụ trên ta xét api call, nhưng service object cũng có thể sử dụng để gọi tới database. Nó thực sự hữu ích khi ta cập nhập nhiều DB với nhiều logic phức tạp ví dụ như:
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> |
Service object nên trả về gì?
Vừa rồi chúng ta thảo luận cách xây dựng gọi method call như thế nào, vậy method call nên trả về gì? Có 3 cách trả về
- Trả về 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> |
- Trả về 1 giá trị
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> |
- Trả về 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> |
Một số rule để viết service object tốt
Mỗi Service Object chỉ nên có 1 public method
Mỗi một service object chỉ thực hiện 1 bussiness cụ thể nào đó, do vậy chỉ nên có 1 public method
Đặt tên Service object theo vai trò của nó
Ta nên đặt tên service object để người code có thể hiểu được vai trò của nó luôn
Không thực hiện nhiều action
Mỗi service object chỉ thực hiên 1 bussiness
Handle Exceptions bên trong service object
Hi vọng bài viết giúp bạn hiểu về service object và vận dụng được trong dự án. Thanks for reading.
Tham khảo tại: https://www.toptal.com/ruby-on-rails/rails-service-objects-tutorial