🐕‍🦺

「ひとまとまりの処理をどうまとめるか」の話:Service編

に公開

本記事はCAMPFIRE AdventCalendarの1日目の記事です。

この記事では、Webアプリケーションのコード整理で頻出する「Service」パターンが中心的に扱う課題「ひとまとまりの処理をまとめる」というテーマについて議論を整理し、Ruby on Rails を使った開発では実際にどう解決されているのかを見ていきたいと思います。

はじめに

一定の規模の Web アプリケーションでは、コードの肥大化は避けて通れません。これに対処するため、さまざまな整理パターンが使われます。Ruby on Rails をはじめとする MVC フレームワークでは、Service はその定番パターンのひとつと言えるでしょう。

Railsにおいても、たとえば以下のような有名な記事があります
https://codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models

しかしこの Service は、広く利用されている一方で、議論を呼びやすいパターンでもあります。中にはアンチパターンとみなす意見すらあります。

私の勤務先である CAMPFIRE でも、最近 Service をテーマに議論する機会がありました。大いに盛り上がったものの、「では具体的にどうするべきか」という結論にはなかなか到達しませんでした。

その理由として考えられるのは、Service とその派生形を巡る議論が非常に複雑であることです。定番であるがゆえに、人によって「Service」のイメージが異なっていたり、Service と同じ役割を果たすものが別の名前で呼ばれていたりします。こうした認識の揺れが、議論の見通しを悪くしていると感じています。

Serviceって何をするものなの?

そもそも、Service は何をするものなのでしょうか。さまざまな意見があるとは思いますが、中心的な責務としては、おそらく「いくつかの操作を、ひとつの意味あるまとまりとしてまとめるもの」という点であれば、多くの人の合意が得られるのではないでしょうか。

Service をアンチパターンとする意見にも一理ありますが、「ひとまとまりの処理をどこに置くか」という問題自体は、アプリケーション開発では必ず向き合うテーマです。Service という名称にこだわらずとも、これを手がかりに議論を深めることには大きな意味があります。

そこで本記事では、この「ひとまとまりの処理をまとめる」という観点に焦点を当てつつ、Service の使われ方を掘り下げてみたいと思います。

なお、Service にまつわる派生語には、Service クラス、Service レイヤー、ServiceObject などさまざまな呼び方がありますが、本記事では混乱を避けるため、引用を除き「Service」に統一して表記します。

Service はどう定義されてきたか

「ひとまとまりの処理をまとめる」責務を考えるにあたって、Service に関連する概念がこれまでどのように定義されてきたのか、代表的な例を通じて概観してみたいと思います。ここでは PofEAA と DDD の 2 つを取り上げます。どちらもアプリケーションアーキテクチャにおける古典であり、現在の Service をめぐる議論でもしばしば参照されるためです。

PofEAA における Service Layer

マーティン・ファウラーの『エンタープライズアプリケーションアーキテクチャパターン(以降PofEAA)』では「サービスレイヤ」という形で次のように定義されています。

サービスレイヤは、アプリケーションの境界と利用できる操作セットをクライアントレイヤとのインタフェースという観点から定義している。サービスレイヤはアプリケーションのビジネスロジックをカプセル化し、トランザクションを制御し、操作の実装におけるレスポンスを調整します。

マーチン・ファウラー 『エンタープライズアプリケーションアーキテクチャパターン』 (p.143)

ここでは概ね 2 つの責務が示されています。

  1. ひとまとまりの操作のインターフェース提供
  2. ビジネスロジックをまとめてカプセル化すること

さらに、PofEAA ではしばしば混同されがちな「ビジネスロジック」を 2 つに分類している点も重要です。

私も含めて設計者の多くは、「ビジネスロジック」を2 種類に分類する傾向がある。1 つは、純粋に問題ドメイン(...)を扱う「ドメインロジック」で、もう1 つは、アプリケーションの責任(...)を扱う「アプリケーションロジック」である。

マーチン・ファウラー 『エンタープライズアプリケーションアーキテクチャパターン』 (p.143)

  • ドメインロジック:純粋にそのドメインに関連する問題領域そのものに関わるロジック(例:料金計算)
  • アプリケーションロジック:ドメインというよりは、そのアプリケーション固有のロジック(例:通知処理)

この区別を踏まえ、サービスレイヤの実装には 2 つの基本形があると説明しています。

2つの基本的な実装バリエーションには、ドメインファサード的手法と操作スクリプト手法がある。ドメインファサード手法では、サービスレイヤはドメインモデル上の薄いファサードのセットとして実装される。ファサードを実装するクラスにはビジネスロジックは一切実装されていない。ビジネスロジックはすべてドメインモデルによって実装される。(...)
操作スクリプト手法では、厚いクラス群としてサービスレイヤが実装され、アプリケーションロジックは直接実装されるが、ドメインロジックはカプセル化されたドメインクラスに委譲される。(...)

マーチン・ファウラー 『エンタープライズアプリケーションアーキテクチャパターン』 (p.143)

  1. ドメインファサード手法:サービスレイヤは薄いファサードのみを担い、ビジネスロジックはすべてドメインモデル側で実装される。
  2. 操作スクリプト手法:サービスレイヤにアプリケーションロジックを実装し、ドメインロジックはドメインクラスに委譲する。

さらに、マーティン・ファウラーのエッセイ「ドメインモデル貧血症」では、これらとは別のアンチパターンについても触れられています。

https://bliki-ja.github.io/AnemicDomainModel

ドメインモデル貧血症の基本的な症状は、一見、それが本物のドメインモデルに見えるという点です。(...)ただし、オブジェクトの振る舞いを見れば違いが分かります。それらのオブジェクトにはわずかな振る舞いしかない、ということに気づくと思います。 (...)その代わり、すべてのドメインロジックを含むサービスオブジェクト群が存在しているのです。 こういったサービスはドメインモデルの上位に居座り、データのためだけにドメインモデルを使うのです。(...)すべての振る舞いをサービスに押し込むと、どうしても トランザクションスクリプト になってしまいます。これでは、ドメインモデルのもたらすメリットを失います。

いわゆる「Service はアンチパターン」という主張は、多くの場合この状況を指しています。PofEAAにおいては、サービスレイヤではいずれの実装バリエーションでもドメインモデルが活用されています。ですが、ドメインモデル貧血症に陥った場合は、ドメインモデルはデータの入れ物としての責務しか担っていません。結果として、手続き型処理の羅列となってしまいます。

以上を踏まえると、PofEAA(+関連議論)でのサービスレイヤは次の 3 パターンに分類できるでしょう。

  1. ドメインファサード的手法
    • アプリケーションロジック/ドメインロジックともにドメイン側へ
    • サービスレイヤは薄いファサードに徹する
  2. 操作スクリプト手法
    • アプリケーションロジックはサービスレイヤ
    • ドメインロジックはドメインモデルへ
  3. トランザクションスクリプト的手法(アンチパターン)
    • モデルは単なるデータの入れ物
    • ビジネスロジックはサービスレイヤに手続き型として実装される

DDD本(エリック・エヴァンスのドメイン駆動設計)における Service

続いて、エリック・エヴァンスの『ドメイン駆動設計(以降DDD本)』における Service の定義を見てみましょう。

DDD本 では、「オブジェクトとして表現するより、操作として表す方が自然なもの」を扱う要素として、「サービス」が定義されます。

オブジェクトとしてよりもアクションや操作として表現した方が明確な側面は、サービスとして表現する。(...)サービスとは、要求に応じてクライアントのために行われる何かである。

エリック・エヴァンス『エリック・エヴァンスのドメイン駆動設計』(p.79)

さらに、DDD におけるサービスには 3 つの特徴があります。

  1. ドメインの概念に関係し、エンティティや値オブジェクトに自然に収まらない。
  2. ドメインモデルの他要素の観点からインターフェースが定義される。
  3. 状態を持たない。

エリック・エヴァンス『エリック・エヴァンスのドメイン駆動設計』(p.104)

ここから、サービスは以下のような役割を担うということが分かります。

  1. エンティティ・値オブジェクトに収まりきらない操作やアクションを扱う
  2. クライアントに対する操作の入口(インターフェース)を提供する

そして DDD では、サービスはレイヤに応じて 3 種類に分類されます。

  1. アプリケーションサービス
    • 一連の操作における手続きのまとめと調整
  2. ドメインサービス
    • ドメインに関連するが、他のドメインオブジェクトからは外れる操作
  3. インフラストラクチャサービス
    • インフラ固有の動作(メール送信・永続化など)

DDD本での表を以下に引用しておきます。

レイヤー 責務・例
アプリケーション 資金振替アプリケーションサービス
●入力(XMLリクエストなど)を理解する。
●処理を実行するよう、ドメインサービスにメッセージを送信する。
●確認を待つ。
●インフラストラクチャサービスを使用して、通知を送信することを決定する。
ドメイン 資金振替ドメインサービス
●必要な口座オブジェクトおよび元帳オブジェクトとやり取りし、適切な引き落としと振り込みを行う。
●結果(振替の可否など)の証跡を出力する。
インフラストラクチャ 通知送信サービス
●アプリケーションの指示に従って、電子メールや手紙、その他のメッセージを送信する。

エリック・エヴァンス『エリック・エヴァンスのドメイン駆動設計』(p.106)

おそらくアプリケーション実装においては、インフラストラクチャサービスは実装する対象というよりは呼び出しをすることが多く、開発者が設計・実装する場面が多くなりそうなのは、アプリケーションサービス、ドメインサービスということになりそうです。

PofEAAとDDD本におけるServiceのまとめ

以上、PofEAA と DDD 本で語られるサービス概念を見てきました。「ひとまとまりの処理を扱う」という観点から両者をおさらいしてみましょう。

両者の定義には、多くの共通点があります。
どちらの文脈でも、サービスは「ドメインモデルに収まりきらない一連の操作のインターフェースを提供する」という点で一致しており、サービス自体はできるだけ薄く保ち、可能な限りドメインモデル側に振る舞いを寄せることが望ましいとされています。したがって、両者ともサービスにおいては、ビジネスロジックをベタ書きするのではなく、ドメインモデルの振る舞いを呼び出すことで、「ひとまとまりの処理」とするのがよいでしょう。

一方で、明確な違いもあります。
PofEAA ではサービスを「ドメインサービス」「アプリケーションサービス」のように分類していません。その代わり、ビジネスロジックをどこに置くかによって、ドメインファサード手法と操作スクリプト手法という 2 つの実装バリエーションが提示されています。

これに対し、DDD本ではサービスはレイヤごとに分類されており、アプリケーションサービスとドメインサービスは別の役割を持つものとして定義されています。この分け方は、どちらかといえば PofEAA の操作スクリプト手法に近いと考えられます。

私見ではありますが、PofEAA と比べると、DDD におけるサービス定義はやや分かりにくいです。特に、ドメインサービスとアプリケーションサービスの区別の記述は、PofEAA が示すビジネスロジックの区別ほど明確ではなく、理解に少し苦労する印象があります。

Rails において「ひとまとまりの処理」をどう扱うか

前節では、PofEAA と DDD 本におけるサービスの定義から、「ひとまとまりの処理」をどのように整理するかを眺めてきました。この節では、視点を Rails に移し、Rails ではこの「ひとまとまりの処理」をどのように扱っているのか、その代表的なパターンを見ていきます。

Rails における Service は、DDD 本のようにアプリケーションサービスとドメインサービスを明確に分ける扱いはほとんど見られません。その理由は明確ではありませんが、Rails の「極力デフォルトのレイヤーに収める」という思想と、DDD の戦術的設計との間にギャップがあるからかもしれません。Rails ではカスタムのレイヤーを生やすこと自体は批判されませんが、積極的に推奨されるわけではなく、極力デフォルトのレイヤーを維持するほうが好まれます 。

こうした背景を踏まえつつ、Rails では「ひとまとまりの処理」がどのように整理されているのか、2つの代表的な例を取り上げます。

1. Service 層を追加する

ひとつ目は、 app/services のようにService用ディレクトリを追加し、新たなレイヤーとして Serviceクラス を定義する方法です。先述のようにRails では新たなレイヤーの追加が奨励されるわけではありませんが、一定規模以上のアプリケーションではよく採用されるアプローチです。
https://techracho.bpsinc.jp/hachi8833/2022_03_17/46482
実装のばらつきはあるものの、例えば上記の記事のように Service は概ね DDD の広義のサービスに近く、状態を持たず、ひとまとまりの処理をカプセル化する点でサービスの定義と一致しています。ただし、アプリケーションサービスとドメインサービスを明確に区別することはなく、PofEAA でいうアプリケーションロジックとドメインロジックが混在したものとなることが多く思われます。

Rails でこのようなService層を実装する場合には、どのような方針が望ましいのでしょうか。ここでも PofEAA や DDD 本の知見が役立ちます。すなわち、「サービス自体はできるだけ薄くし、可能な限りドメインモデル側に振る舞いを寄せる」という原則です。

2. Model 層に操作ドメインを定義する

もうひとつの方法は、サービス専用のレイヤーを生やすのではなく、app/models に操作を表すモデルを定義してしまうスタイルです。37signals の “Vanilla Rails is plenty” という記事で紹介されており、記事内ではBasecamp のコード例が紹介されています。

https://dev.37signals.com/vanilla-rails-is-plenty/

ここでは、ひとまとまりの処理をサービスとしてではなく、ドメインモデルの一種として表現しています。DDD的なお作法に則ったステートレスでもありません。

記事では以下のように説明されています。

We don’t use services as first-class architectural artifacts in the DDD sense (stateless, named after a verb), but we have many classes that exist to encapsulate operations. We don’t call those services and they don’t receive special treatment. We usually prefer to present them as domain models that expose the needed functionality instead of using a mere procedural syntax to invoke the operation.

また、Rails では「モデル=ActiveRecordを伴う永続化層」という固定観念が強くありますが、Basecamp の実装では、永続化を伴わない ActiveModel や PORO を積極的に使い、ドメインモデルとして扱っています。もちろんここでも、アプリケーションサービスとドメインサービスを分けることはせず、ひとつのクラスとして定義しています。

記事の筆者いわく、こうした設計を採用する理由としては、サービス層を分けることで生まれる以下の問題を避けたいからだと述べています。

  • 大量のボイラープレートコード(操作の多くは結局ドメインモデルに委譲するだけ)
  • ドメインモデル貧血症(ドメインモデルがデータを運ぶ入れ物になってしまう)
  • Tons of boilerplate code because many of these application-level elements simply delegate the operation to some domain entity. Remember that the application layer should not contain business rules. It just coordinates and delegates work to domain objects in the layer below.
  • An anemic domain model, where the application-level elements are the ones implementing the business rules, and domain models become empty shells carrying data around.

最近公開された once-campfire のコードは、このアプローチの実例として非常に参考になります。
https://github.com/basecamp/once-campfire

Rails における「ひとまとまりの処理」のまとめ

紹介してきた 2 つの例はいずれも、アプリケーション層とドメイン層のサービスを厳密に分けることなく、「ひとまとまりの処理」を扱っています。レイヤーが混在しているとも言えますが、そのかわりに複雑さや冗長さ、そして判断の難しさを避けることができています。個人的には、DDD のようにアプリケーションサービスとドメインサービスを厳密に分けることには、最初からそれほどこだわる必要はないように感じます。

2つの例では、「ひとまとまりの処理」をサービス層を追加するか、モデル層に操作を定義するかの違いがありました。個人的には両者には優劣はなく、おそらくどちらを選んでも構いません。重要なのは、処理をできる限りドメインモデル側に寄せるという原則であり、置き場所はその次の問題だからです。

一点、避けるべきなのは、「ひとまとまりの処理」の方針がひとつのアプリケーションの中で複数存在したままになることです。そうなると、実装する側がどこに処理を寄せればよいか判断できなくなり、コードが一気に混乱してしまいます。チームとして、明確にどのアプローチを取るか、合意をとっておくのが大事でしょう。

おわりに

「ひとまとまりの処理をまとめる」の観点から主にServiceの議論や実装を追ってきました。正直なところ、「ひとまとまりの処理」を扱う概念は、たとえばFormModelのような概念もあります。次の機会はこちらも掘り下げて整理していければと思います。
拙い形ですが、本記事での整理がみなさんの実装の議論の見通しをよくすることに寄与できればいいなと思います。

Discussion