業務のRailsアプリのリファクタ対応の一環でService Objectを入れてみたところ、なかなか快適に使えている感触なので、RailsでのService Objectについて私なりに調べたことをまとめてみたい。アンチパターンとして批判されることも多いService Objectについて、どういった使い方をするとマズいのかも掘り下げてみる。
Service Objectが欲しくなるとき
Rails標準のMVCで業務アプリケーションを実装して行くと、ビジネスロジックが複雑になるにつれて、ControllerまたはModelの処理が肥大化してつらい状態になりやすい。具体的には、
- 可読性が悪い
- テストが書きづらい
- Modelにメソッドが乱立し、そのModelがビジネス上どういう振る舞いを持つ概念なのか読み取りづらい
これらの観点が挙げられるだろう。この問題を解決する手段の一つとして、 Service Objectを導入する設計手法がある。
Service Objectとは
Service Objectは、Patterns of Enterprise Application Architecture(PofEAA)やドメイン駆動設計(DDD)におけるサービスレイヤを実装に落とし込む設計手法と言える。
上のPofEAAの図のように、ユーザーインターフェースとドメインモデルの中間でビジネスプロセスを処理するのがサービスレイヤの役割だ。
Service objects are Plain Old Ruby Objects (POROs) which encapsulate a whole business process/user interaction.
サービスオブジェクトは、ビジネスプロセスやユーザーとの対話の全体をカプセル化するプレーンなRuby Object(PORO)。
Forem*1の開発者向けドキュメントの上の定義もシンプルで分かりやすい。
Service Objectの実装方針
オープンソースの実装例や、Webの記事で紹介されている手法を読み解くと、Service Objectには、ベストプラクティスと呼べるような実装方針が一定確立されていることが分かる。それは下記のようなものだ。
- Object名を見て処理の概要が理解できるように命名する
call
やexecute
のような、1つだけ定義されたpublicメソッドを呼び出して利用する- privateメソッドを細かく切って処理の概要を理解しやすくする
- 単一責任の原則を意識してService Objectを分割し、Service Objectの中で他のService Objectを呼び出す
- Rubyのプレーンなclass(PORO)で実装する
以下、詳細を補足したい。
Objectの命名
- 動詞 + (名詞) + Service
FollowService
RemoveStatusService
- namespaceを分けるパターン
Commits::CreateService
Object名がビジネスプロセスの実態を呼び出し先で表現するように命名することが大事だろう。namespaceは分けておいた方が予期せぬ要件の拡張があっても秩序を保ちやすい印象がある。
publicメソッド名の選択肢
call
, execute
, run
, perform
など、特定の文脈を想起させないメソッド名を採用する。個人的には、call
がカプセル化されたビジネスプロセスを呼び出すニュアンスを最も良く表している気がして好み。
publicメソッドの定義方法
publicメソッドの定義方法は上に挙げた3つのRailsアプリにおいてもバラバラで、特にどれかが決定的に優れているとも言えないので、方式を統一しようとすると悩ましい部分になるだろう。以下に代表的な例と、各パターンについての私見を書く。
publicメソッドをインスタンスメソッドとし、newに引数を渡す
これが最も素直な書き方という感じがする。
Rubyのプレーンなclass(PORO)で実装する
プレーンなclassを使うことで、開発チームメンバーのスキル感を問わず分かりやすく、ライブラリの実装に依存する想定外の動作も起きない。
ActiveModel::Model
をincludeしてvalidation周りの機能を活用する手法もあるようだけれど、業務でService Objectを運用してみた感触でも、POROで見通し良く実装するのがService Objectの持ち味を活かせる気がしている。
理想的に活用できた場合のService Objectのメリット
上のような方針に従ってService Objectを活用した場合には、以下のようなメリットがあると感じている。
- ビジネスプロセスに細かい粒度で名前が与えられ、コードの可読性が向上する
- 再利用性のある形で設計することも可能
- チームメンバーのスキル感によらず、設計方針を理解して従うことができる
- ControllerとModelの中間層として、RailsのMVCに後からでも導入しやすい
- 引数と返り値の仕様が定まったclassになるので、テストを書きやすい
簡単*4で便利に使えてRailsとの相性も良いということで、非常に優れた設計手法という感じだ。
その一方で、Service Objectは決して銀の弾丸ではなく、間違った使い方をすると悲惨な事態を招きかねない側面もある。以下、Service Objectの誤った運用とその改善方法を見ていきたい。
Service Objectアンチパターン説
Service Objectはアンチパターンとして批判されることも多い。しかしこれらの批判をよく読むと、Service Objectがアンチパターンというよりは、その他の設計上の観点を考慮することなく作られたService Objectがアンチパターンであるように読める。
まずドメインモデルを考慮してからService Objectを作る
多くのOOエキスパートたちが、処理を行うレイヤをドメインモデルの一番上に置いて、サービスレイヤを作るよう推奨していることが、混乱のそもそもの原因です。 ただしこれは、振る舞いのないドメインモデルを作るということではありません。 そうではなくて、サービスレイヤの支持者は、振る舞いをたくさん含んだドメインモデルと一緒に使っています。
サービスの中に振る舞いを見つければ見つけるほど、ドメインモデルのメリットを奪っていくでしょう。サービスの中にすべてのロジックを埋めてしまうと、何も見えなくなってしまいます。
PofEAAのFowlerさんが「ドメインモデル貧血症」と呼ぶような、ModelのメソッドとしてModelに対応するビジネス上の概念の振る舞いが十分に定義されていない状態で、Service Objectを活用してしまうと、それはただの手続き型設計になってしまい、オブジェクト指向設計のメリットをまったく活かせなくなってしまう。
以下のような観点で、まずはドメインモデル側の設計を良く検討してみることが大事だろう。
- DBのテーブル構成と各Modelはビジネスの実態に対応しているか
- DBのテーブルに1対1に対応しないModelを作って見通しを良くできないか
- Railsの場合、
ActiveModel::Model
をincludeする手法が有効
- Railsの場合、
- Modelの汎用的な振る舞いをModel側のメソッドとして定義できているか
おわりに
Service Objectを切り口に色々と考えて行くと、ドメインモデルを重視するPofEAAやDDDの設計思想のコアも見えてきた気がする。プロダクトもチームも各現場で多様な中、オブジェクト指向設計に正解はなく、プロダクトが扱う実世界の概念をどのようにソースコードに落とし込めば、見通しが良くメンテナンス性が高い状態になるかということを良く考えて設計する必要があるということになるだろう。