サービスレイヤ(ServiceLayer)パターン(PofEAA)

ドメインレイヤの前に置かれるFacadeとしてのレイヤ。

アプリケーションサービスとドメインサービスの2つがあり、コントローラーからアプリケーションロジックを分離する目的で作成されるのがアプリケーションサービス、アプリケーションサービスから複雑なドメインロジックをカプセル化するFacadeとして作成されるのがドメインサービスである。

ブラウザから実行する処理と同じことをバッチ処理やコンソールから実行できるようにしようとした時、同じアプリケーションサービスを呼び出すだけで実現できるようにできていれば、適切にアプリケーションロジックを分離できていると言える。

アプリケーションロジックを実装する上で必要なデータの収集はアプリケーションサービスから行われる。つまり、データソースレイヤ(インフラストラクチャ層、Gateways)に対するメソッド呼び出しはアプリケーションサービスから行われる(依存性の注入を用いるかどうかはともかくとして)。

ただ、コントローラーからデータソースレイヤへのアクセスを禁止すべきとは私は考えていない。ログイン中のユーザーのような情報はコントローラーのコールバックから参照できた方が良い場合もある。

RubyにはInteractorというアプリケーションサービスを構築するためのGemがある。このGemの場合だと、クラスとアプリケーションロジックが1対1になり、callメソッドがアプリケーションロジックのエントリポイントになる。

個人的にはこのinteractor gemを使うことには反対の立場である。

  • このGem固有の問題として、更新が停滞している点がある。2014年以降、二回しかリリースがなされていないし、その間にRubyファイルが変更されたのは6コミットだけである。v4の開発も2017年3月から放置されている。
  • 1クラス1アプリケーションロジックになるため、無闇にクラス名が長くなりやすい。関連するアプリケーションロジック(例えばCRUD操作など)を1つのクラスにまとめることができない。
  • 結果のステータスや戻り値はreturn { result: true, users: users }のようにHashで返せば良く、ライブラリに依存してまで複雑な仕組みを導入するメリットが薄い。理解も記述も容易なPOCOで十分である。

個人的な考えではあるが、アプリケーションサービスはユースケースに対応付けるのが良いと考えている。サブジェクトとアクターごとにモジュールを切り、ユースケース名をクラス名、各ステップをメソッドとして定義する。詳細は要求分析駆動設計のユースケース、スイムレーンに記述しているのでそちらを参照してもらいたい。

# 取引サブジェクト
module CommerceSubject
  # 一般ユーザーアクター
  module UserActor
    # 商品注文ユースケース
    class OrderGoodsUseCase
      attr_accessor :actor
      # 実行するアクターのインスタンスをコンストラクタで受け取る
      def initialize(actor)
        self.actor = actor
      end

      # 「欲しい商品を見つけたらカートに追加する」に対応するメソッド
      def add_goods_to_cart(goods_id)
        # 結果とカートを返す
        { result: result, data: cart }
      end

      # 他に「カートから取り除く」や「注文を確定する」などに対応するメソッドがある...
    end
  end
end

すでに書いている通り、ドメインサービスは複雑なドメインロジックをカプセル化するFacadeとして作成される。ドメインモデル(DomainModel)パターンを実装した複数のクラスを使う処理がアトミックな時にカプセル化したり、同じドメインロジックが複数のアプリケーションロジックに現れる時に共通化のために作成する。

動機があるならドメインサービスを作るべきだが、アプリケーションサービスからドメインモデルを呼び出す形で問題がないなら作る必要はないクラスである。

また、ドメインサービスはドメインレイヤのクラスのため、ドメインモデルと同様にデータソースレイヤの呼び出しはしてはならない。

2022/3/21追記:同僚に指摘されてわかったことなのだが、ドメインサービスからリポジトリを使うことはありらしい。(後述の追記を参照)
オニオンアーキテクチャではドメインレイヤにリポジトリのインターフェースをおいているし、IDDDではドメインレイヤからレジストリ(サービスロケータDI)経由でリポジトリを呼んでいる(第7章サービス 7.3ドメインにおけるサービスのモデリング)。ただ、同僚曰くreadのみでwriteはアプリケーションサービスでやるらしい。
個人的な所感を述べると依然としてドメインサービスであろうとドメインレイヤからDBなどの外部リソースにアクセスするのは避けるべきだという考えは変わらない。私はドメインレイヤのクラスは参照透過性を持つべき、つまり、同じ呼び出し方をされた場合は同じ結果を返すべきだと考えている。「べきだ」というよりも、そのようにした方が安全だと考えていると言った方が良い(細かいことをいえば、ドメインモデルによってはインスタンス変数を書き換えたりするので、参照透過性という言葉を使うのは間違っていると思うが)。

2024/9/15追記:オニオンアーキテクチャについて改めて読むと、これは単に依存関係の問題で実際に利用するコントローラやアプリケーションサービスの内側で定義しているにすぎないように思える。
セッションのインターフェースについても書かれているが、これはアプリケーションサービスのレイヤで定義されていて、コントローラでDIされることが示されている。通常、セッション情報をそのままアプリケーションサービスで利用することは考えにくいし、実際に利用するレイヤの一つ下のレイヤでインターフェースを定義するという方針をとっているのではないかと思う。
したがって、オニオンアーキテクチャについてはドメインサービスでリポジトリを呼ぶ(DIしようとしまいと)というのは想定していないように見える。

また、断っておくと上記で書いている同僚(今となっては元同僚だが)は、アーキテクチャやDDDに明るいわけでもない人間である。私ほどアーキテクチャやDDDについて学び思考をめぐらせているわけでもないし、おそらく何かしらの書籍にそういうふうなことが書かれていたというだけだろうと予測している。
これは実際のところは知らないただの予想でしかないのだが、IDDDを読むような胆力を彼が持っていたとは思えないし、IDDDを参考にしているドメイン駆動設計 モデリング/実装ガイド - little-hands - BOOTHにそのようなことが書かれていて、それを鵜呑みにしただけではないかと予想している。この書籍の中身は改めていないから実際に書かれているかどうかも含めて予測でしかないのだが。

いずれにせよ、私としてはドメインサービスもドメインレイヤのクラスであり、ドメインレイヤがDIをしようとしまいとデータベースなどに依存することには反対である。そもそも、私としてはドメインレイヤにDIするという発想はない。