Re: ドメインサービスでrepositoryの実行は必要か?

株式会社カミナシで VPoE を務めている pospome です。 (´・ω・`)

以下の記事に対する自分の考えを書こうと思います。

zenn.dev

結論

個人的にはドメインサービス内でリポジトリを実行することは避けるべきだと思っています。 ただ、結局はどの程度の実害があるか次第だと思います。無視できないくらいであれば、避けたほうが良いですし、そうでないなら気にしなくていいと思っています。

ドメインサービスでリポジトリを実行してはいけない理由

ネタ元の記事には「なぜ実行してはいけないのか」が明記されていない気がしますが、 当然ながらここが最も重要なポイントになります。

ドメインサービスでリポジトリの実行を避ける理由は、"アプリケーションレイヤ(ユースケースレイヤ)との責務が曖昧になるから" です。

  • アプリケーションレイヤは "処理の流れ" を実装します。
  • ドメインサービスは "ビジネスルール" を実装します。

ドメインサービス内でリポジトリを実行してしまうと、多くのロジックをそこに実装することができてしまうので、 "処理の流れ" と "ビジネスルール" が混ざってしまいます。 ドメインサービスがどんどんファットになっていき、 トランザクション境界も不明確になっていき、 謎に薄いアプリケーションレイヤができあがる・・・そんな実装になってしまう可能性があります。 ドメインサービスもどんどん増えていきそうですね。

一方で、ドメインサービスでリポジトリを実行しない場合、ドメインサービスに実装できるロジックはかなり限定されます。 具体的に言うと、以下の条件を満たす実装のみドメインサービスとして実装されるはずです。

  1. 複数の集約にまたがる操作であること。
  2. その操作にぴったりな名前が付けられること。
補足
ドメインサービスは "集約やエンティティに実装すると不自然なもの" を実装するものなので、
正確にいうと "以下の条件を満たす実装のみ" と言い切ることはできません。
ただ、自分の経験上「大体この条件なんじゃないの?(´・ω・`)」という感覚です。


元記事で紹介されている "銀行口座の送金" はまさに上記の条件を満たします。

  1. 送金元口座と送金先口座に対する操作であること。
  2. "送金" というぴったりな名前が付けられること。

上記1,2の条件を満たす実装は意外と少ないものです。 DDDを学ぶ過程で「ドメインサービスを使う機会は意外と少ない」という話を聞いたことがあるかもしれないですが、 こういった背景があったりします(ドメインモデル貧血症の文脈で説明されることの方が多いけど)。

実害あるかどうかが重要

「ドメインサービスでリポジトリを実行しない方がいいよ」という話をしましたが、 まあ正直 "どういった実害があるか次第" だと思います。

仮にドメインサービスでリポジトリを実行したとして、どの程度困るのでしょうか?

もしそれほど困らないのであれば、そのままでいいと思います。 困ったらリファクタリングしましょう。

ドメインサービスでリポジトリを実行したいときの回避策

一応ドメインサービスでリポジトリを実行したいときの回避策にも言及しておきます。

パターン1. 妥協してドメインサービスでリポジトリを実行する

これは前述した通り、実害ないならOKというパターンです(回避策ではない)。

パターン2. 1つの集約にまとめる

たまーにあるのが、集約の境界をミスっているパターンです。

雰囲気だけの擬似コードになりますが、例えば以下のように2つの集約をドメインサービスで操作しているとして・・・

func XxxDomainService() {
    aRepo.Save(a)
    bRepo.Save(b)
}

実は以下のように Third という別の集約を見逃していたり・・・・

type Third struct {
    f FirstAggregation
    s SecondAggregation
}

以下のようにFirst, Secondを入れ子にできたり・・・

type FirstAggregation {
    s SecondAggregation
}

このように集約の境界を見直して、上手く1つにまとめることができれば、ドメインサービスで複数の集約の一貫性を保証する必要がなくなります。

ただ、元記事で取り上げられている "Taskが作成されたらActivityReportが作成される" というケースに限っては、 おそらくこのパターンには該当しないと思います。

というのも、 "xxxされたら" とか "xxxのあとで" という表現がある場合、それはドメインイベントである可能性が高いからです(レポート作成は結果整合性でよいはずなので、この点からもドメインイベントである可能性が高いですね)。 そうなると、TaskとActivityReportは別の集約になると思います。

パターン3. ドメインイベントとして扱う

元記事で取り上げられている "Taskが作成されたらActivityReportが作成される" というケースであれば、 ドメインイベントが本来あるべき姿だと思います。

擬似コードですが、以下のようなイメージです。

func XxxUsecase() {
    //ドメインイベントを戻り値として返す。
    task, taskCreatedEvent := NewTask()

    taskRepo.Save(task)

    //イベントを発行する(イベントを受信する側がレポートを作成する)。
    event.Publish(taskCreatedEvent)
}

taskCreatedEventというドメインイベントが発行されているので、 これを明示的に無視するコードを書かない限り、ActivityReportが生成されることは保証されます。

イベントソーシングの場合は以下のように集約自体がイベントを保持する形になると思います。

func XxxUsecase() {

    //Taskが内部でドメインイベントの配列を保持しており、
    //NerTask() が実行されると、taskCreatedEvent が生成され、
    //Taskが持つドメインイベントの配列に追加される。
    task := NewTask()

    taskRepo.Save(task)

    //イベントを発行する(イベントを受信する側がレポートを作成する)。
    //Taskが内部で保持しているドメインイベントを渡すだけ。
    event.Publish(task.GetEvents())
}

ただ、こーゆーのを実装するの面倒ですよね(データ書き込みとイベント送信の一貫性を保証するためにアウトボックスパターンとか採用しないといけないので)。

パターン4. アプリケーションレイヤでリポジトリを実行する

アプリケーションレイヤでリポジトリを実行してしまえば、ドメインサービスを使わずに済みます(それはそう)。 アプリケーションレイヤはトランザクションの制御をするので、細かいことを気にしなければ、一番楽に自然に実装できる気がします。 「ドメインサービスを乱用するくらいなら、こちらに倒した方がいい」と考えることもできそうですね。

まとめ

実害があるかどうか次第ですね。

もし自分が "ドメインサービス内でリポジトリを実行しているコード" に出会ったら、 しばらくは実害あるか様子を見て、実害あったらリファクタリングする・・・程度の温度感で対処すると思います(つまり、そんな気にしない)。

宣伝

株式会社カミナシでは以下のポジションをすごく募集しています。 興味のある方は応募してみてください。