Serviceオブジェクト
エンティティに最重要ビジネスデータと最重要ビジネスロジックを持たせて、ユースケースを考えて、詳細は方針に依存して、、
あれ、ユースケースの小さく分解したロジックってどこに書けば良いんだろう?と思った。
サービスオブジェクトに書くことが多いとあったので、下記にまとめる。
基本的な概念
💡 A good service object is easy to test and follows the single responsibility principle.
テストを書いたり、読み進めることが簡単であるべき。ServiceObjectには一つのビジネスロジックしか書かれていないから。
Amin Shah Gilaniは、ServiceObjectに切り出すべきか迷った時のフローチャートを示している。
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.
このコードってServiceObjectに切り出すべき、、?
- routingやparams等のコントローラーっぽい処理をしている。
→no, it should be in the Controller
- 他のコントローラーと処理を共有している。
→no, it should be in the Concern
- 永続処理の不要なモデル処理をしている。
→no, it’s non-ActiveRecord model (FormObject or something)
- 詳しいビジネス処理(PDF作成、計算、ゴミ処理等)を行っている。
→yes, ServiceObject should help it!!!
コントローラーって、責務を知らないと肥大になりがち。
with rendering and redirecting—normal controller concerns.
コントローラーは、renderかredirectという、コントローラの関心ごとにfocusさせたい。
class UserController < ApplicationController
def create
user = User.new(user_params)
if user.save
send_welcome_email
notify_slack
if @user.admin?
log_new_admin
else
log_new_user
end
redirect_to new_user_welcome_path
else
render 'new'
end
end # private methods
end
かといってモデルにロジックを移していくと、今度はモデルが肥大化しがち。
コントローラーとモデルの間に受け皿という形で層を設けることができたら、それぞれの処理が何をやっているのかが明確になって後で使用の把握に役立つ。
その「層」の役割を担うのがいくつかある。
その一つにService層がある。
What are service objects?
Service objects are plain old Ruby objects (PORO’s) that do one thing.
単一責務のオブジェクト(PORO)
Serviceクラスにより詳細な機能を切り出すことで、
最終的にここまでコントローラーをスリムにすることができる。
class UserController < ApplicationController
def create
user = RegisterUser.new(User.new(user_params)).execute
if user
redirect_to new_user_welcome_path
else
render 'new'
end
end # private methods
end
ServiceObject.new(arguments).execute
の形が不恰好だと思ったら、下記のように独自にcallメソッドを定義しちゃってもいい。
class RegisterUser
def self.call(*args, &block)
new(*args, &block).execute
end def initialize(user)
@user = user
end def execute
# old code
end # private methods
end
コントローラーはこうなる
class UserController < ApplicationController
def create
result = RegisterUser.call(User.new(user_params))
if result.success?
redirect_to new_user_welcome_path
else
render 'new', error: result.errors
end
end # private methods
end
Serivice Classを理解する上で大事なことだから再掲
基本的な概念
💡 A good service object is easy to test and follows the single responsibility principle.
テストを書いたり、読み進めることが簡単であるべき。ServiceObjectには一つのビジネスロジックしか書かれていないから。
Amin Shah Gilaniは、ServiceObjectに切り出すべきか迷った時のフローチャートを示している。
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.
このコードってServiceObjectに切り出すべき、、?
- routingやparams等のコントローラーっぽい処理をしている。
→no, it should be in the Controller
- 他のコントローラーと処理を共有している。
→no, it should be in the Concern
- 永続処理の不要なモデル処理をしている。
→no, it’s non-ActiveRecord model (FormObject or something)
- 詳しいビジネス処理(PDF作成、計算、ゴミ処理等)を行っている。
→yes, ServiceObject should help it!!!
Service Objectがアンチパターンである理由とよりよい代替手段(翻訳)|TechRacho by BPS株式会社