💡

なぜ自分はDDDを勉強しているのか?

2021/09/22に公開
5

DDDと出会う前

自分は元々アーリーステージ(シード)のスタートアップでRailsを書いていました。人手の問題で拙いながらもReact Nativeでモバイルアプリを作ったりAWSでインフラを構築したりとよくいるエンジニアです。昨年末に今の会社への転職がきっかけでDDDでの開発に従事するようになり独学でキャッチアップしました。元々DDDという単語自体は聞いたことがありました。きっかけは確かこちらの記事だったと思います。

ドメイン駆動設計の比類なきパワーでRailsレガシーコードなど大爆殺したるわあああ!!!

自分自身Railsを書いてはいましたが、自分のコードに納得感を得られたことは一度もありませんでした。

このロジックはここに書いて良いのだろうか?
DB設計ってこれで合ってるのか?
う〜ん、テストコード書きにくいなあ

よくある悩みです。しかし、スタートアップでスピード開発を優先していたので、特にアーキテクチャに関するインプットをしないまま1年半ほどRailsを書いていました。しかし、転職して時間にゆとりも出来たのでじっくりDDDというものに向き合いました。

Rails出身のエンジニアが感じたDDDの難しさ

まず初めに、自分は恵まれていると思います。というのも既に先人が日本語で多くのアウトプットを無料で公開してくれているからです。また、中には質問箱でのDDDに関する質問の受付もやって下さっており、回答を無料で閲覧する事が出来ます。自分自身質問させて頂いた事は1度や2度ではありません。しかし、それだけのサポートがあってもDDDで以ってをソフトウェアを開発する事は大変難しいと感じました。なぜか?

決まったディレクトリ構成は存在しない

これ、RailsのようなフルスタックFW出身のエンジニアは結構面食らうのではないでしょうか?Railsであればrails newした段階で基本のフォルダ構成が生成されますし、モデルやコントローラの命名もRailsに沿ったものを強制されます。これは設定より規約を重視するRailsの思想であり、自分は歓迎しています。しかし、DDDには決まったディレクトリ・パッケージ構成は存在しません。そもそもDDDはフレームワークでもなければデザインパターンではないからです。もちろん、GitHub上には多くのDDDの思想に基づいて構築されたアプリケーションが存在しますが、それらが唯一解ということはありません。

stemmlerjs/ddd-forum
VaughnVernon/IDDD_Samples

DDDを勉強し始めた当初はDDDには決まったディレクトリ構成があるものだと思い散々探し回りました笑。DDDはDomain Driven Development(Design)の頭文字をとった略語であり、Domain Drivenであり続ける事が何より大事です。そのためにはドメイン以外の部分がどう実装されていようが大して重要ではないというのが現時点での自分の認識です。それに気がつくのに自分は多くの時間を費やしました。

Interface? DI?

Rubyはダックタイプを採用しており、Interfaceは存在しません。

https://twitter.com/yukihiro_matz/status/1066980158429552640

Rubyしか経験のない自分がInterfaceと言うものを理解するのにずいぶん苦労しました。ちなみにInterfaceを理解できたのはこちらの記事のサンプルコードを5回ほど写経した経験のおかげです。記事の著者の方には本当に感謝です。

関心の分離を意識してサーバーを作ってみる(TypeScript + Express)

Interfaceを利用しないとドメイン層がインフラ層に依存したレイヤードアーキテクチャと呼ばれるものになります。ドメインを外界の永続化に関する技術的関心事から隔離するためにはInterfaceによるDIが欠かせません。ぜひ勉強してみて下さい。ここら辺の話は松岡さんのこちらのブログを一読するとイメージしやすいのではないかと思います。

ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か[DDD]

Entity, Value Object, Aggregateの見分け方

正直これは今でも全く自信がないのですが、いったんまずEntity・Value Object・Aggregateがそれぞれどんな概念なのかを本当に簡単にソースコードを交えて説明したいと思います。

Entity

abstract class Entity<T> {
  abstract id: UniqueEntityIdentity;
  
  equal(that: T): boolean {
    return this.id === that.id
  }
}

こちらがEntityです。Entityは生まれた瞬間に識別子を持ち、これは誕生してから消滅するライフサイクルの中で不変です。他のEntityとはこの識別子を比較して判別します。Entityのドメインオブジェクトを実装するときは上記のような基底クラスを継承します。

ValueObject

ddd-forum/src/shared/domain/ValueObject.ts

interface ValueObjectProps {
  [index: string]: any;
}

/**
 * @desc ValueObjects are objects that we determine their
 * equality through their structrual property.
 */

export abstract class ValueObject<T extends ValueObjectProps> {
  public props: T;

  constructor (props: T) {
    let baseProps: any = {
      ...props, 
    }

    this.props = baseProps;
  }

  public equals (vo?: ValueObject<T>) : boolean {
    if (vo === null || vo === undefined) {
      return false;
    }
    if (vo.props === undefined) {
      return false;
    }
    return JSON.stringify(this.props) === JSON.stringify(vo.props);
  }
}

ちょっと分かりづらいのですが、ValueObjectは先程のEntityとは違い自身が持つフィールドの値を比較して全て等しければ等価とみなします。また、生成されてから値が不変であることもEntityとの大きな違いです。より詳しい効能や解説はこちらの記事を参照して下さい。自分も完全には理解出来ておらず・・・。

設計要件をギッチギチに詰めたValueObjectで低凝集クラスを爆殺する

Aggregate

DDDをキャッチアップし始めた当初、何なら今でもEntityとAggregateの違いをうまく説明する自信はありません。よく集約は整合性を保ちたい範囲で設計すると言われていますが、いざ設計してみると難しいです。

実践DDD本 第10章「集約」~トランザクション整合性を保つ境界~
集約の境界と整合性の維持の仕方に悩んで2ヶ月ぐらい結論を出せていない話

現時点での自分の理解している範囲内で書き出してみますと

  1. DDDでは集約単位での更新・永続化しか認められていない
  2. 1回の操作(ユースケース)では1つの集約のみ操作して良い、つまり複数の集約を1つのユースケースで操作してはならない。複数集約での更新が必要な時は集約の境界が間違っていないか一度見直した方が良い。
  3. 複数の集約にまたがる操作はドメインイベントを使うか、集約のマージ(再設計)などがある
  4. 集約(厳密には集約ルート)とエンティティは別概念(と自分は認識している)
  5. 集約間で関係性を持たせたい場合、つまりある集約が他の集約を参照したい場合は他集約の識別子のみを持たせる
  6. 集約は整合性を保ちたい単位で設計する
  7. 集約は識別子や、エンティティ、バリューオブジェクト(VO)、他集約の識別子(これもVO)などを持つオブジェクト
  8. エンティティは識別子を持つオブジェクト
  9. エンティティと集約の違いはドメインイベントを保持するかどうかで、集約は発行されたドメインイベントをコレクションで保持し、ユースケース層かインフラ層のどちらかでディスパッチする

という感じです。興味のある方はこちらの記事も読まれても良いかもしれません。(2度目の松岡さんのブログ)

DDDで複数集約間の整合性を確保する方法(サンプルコードあり)[ドメイン駆動設計]

ドメインモデリングの難しさ

最後の方に来てしまいましたが、このDDDのプロセスの一番最初であるドメインエキスパートとのドメインモデリングが本当に難しいのです。サービスがToDoアプリなどのシンプルなCRUD操作で完結するレベルのものであれば大丈夫でしょうが、そうは問屋がおろさないのが世の常です。ドメインモデリングで登場した概念をユビキタス言語として統一し、ブラッシュアップしてドメインオブジェクトに落とし込んでいきます。またユースケース分析もこのドメインモデリングで登場した単語を中心にして記述していくので、まさにドメインモデリングこそDDDのプロセスの中で最も心血注ぐべき作業であると言っても過言ではありません。しかし、最初から完璧なドメインモデリングを行うことは難しいです。なぜでしょうか?その答えのひとつが松岡さんの質問箱にあります。

DDDを実践する上で、まず一番最初に正しいドメインモデル図・ユースケースを洗い出せるようになるのが最重要だと思うので、エヴァンス本やIDDDより先にユースケース駆動開発実践ガイドを読むのが良いのでは、と思うのですが松岡さんはDDDをマスターする上で読むべき本のステップの目安はありますか?

最初から完璧にサービスの全体像を理解することは難しいのです。サービスの全体像を理解しないまま作ったドメインモデル図は完璧にサービスの概念を投影できておらず、不完全なままです。しかし、延々とドメインモデリングだけを行なっていたのではいつまで経ってもサービスはリリースされません。なのでまずはコードを実際に書いてみることをお勧めします。

  1. チームでドメインモデリングを行う
  2. チームでユースケース分析を行う
  3. エンジニアがコードに落とし込む
  4. 違和感やロジックの矛盾に気がつく
  5. 1に戻る

このサイクルを早く回せるかどうかがDomain Driven Designの鍵な気がしています。

(番外編)TypeScriptでDDDをやってみた時に難しかったこと

TypeScriptでValueObjectを作成すると

// ユーザ名のValue Object
class UserName extends ValueObject {
  public readonly name: string;
  
  constructor(name: string) {
      super();
    if (name.length > 1000 || name.length < 3) {
      throw new Error('長さが不正です');
    }
    this.name = name;
  }
}

// 商品名のValue Object
class ProductName extends ValueObject {
  public readonly name: string;
  
  constructor(name: string) {
    super();
    if (name.length > 50 || name.length < 2) {
      throw new Error('長さが不正です');
    }
    this.name = name;
  }
}

これだと同じプロパティしか持たないので

const userName = new UserName('Katsukiniwa');
User.create(id, userName);

とするところを

const userName = new ProductName('Katsukiniwa');
User.create(id, userName);

と書いても気付けません。
PHP/Javaならこの時点でエラーが出る(はず)。
もちろんTypeScriptでも公称型を実現する方法はありますが、一筋縄ではいかなさそうです。

公称型をTypeScriptで実現するための基礎

TSでDDDを実践している人いらっしゃいましたらぜひ教えて下さい!

あと細かい所ですと

  • switch文でinstanceofを使って判定すると型が消失するからユーザ定義タイプガードを作る必要がある
  • interfaceにstaticメソッドを定義できない

とかですかね。TS何もわからん。

取り組んでみて以前とどう変わったか?

DDDを勉強して良かったな、出会えて本当に良かったと思っています。ドメインエキスパートと話し合ってプロダクトに対する解像度を高めてから自分でクラス設計をしてコードに落とし込む一連の流れを経験出来たことでソフトウェアの構築スキルや経験が上がった気がします。これはRailsを非難しているのでは決してありません。Railsという巨人の肩から降りて自分の力で歩く経験をしたことで視野が広がり、Railsが自分に提供してくれていたものの多くを認識させてくれる形になったというだけの話です。ちょっと筆が進まなくなったので今回はここら辺で終わりとさせて頂きます。ご指摘等ございましたらコメントにて。では!

Discussion

j5ik2oj5ik2o

拝見しました。興味深かったです。

気になったところをコメントします。感想みたいなものだと思っていただければ。

  1. 1回の操作(ユースケース)では1つの集約のみ操作して良い、つまり複数の集約を1つのユースケースで操作してはならない。複数集約での更新が必要な時は集約の境界が間違っていないか一度見直した方が良い。

1ユースケースで複数集約を扱っても問題ありません。以下のようなものです。

class UserAccountUseCase {
  // ...
  createUserAccountWithGroup(...): void {
    let group1 = Group.create(...);
    let userAccount1 = UserAccount.create(id, group1.id, ...);

    groupRepository.save(group1);
    userAccountRepository.save(userAccount1);
  }
}
  1. 複数の集約にまたがる操作はドメインイベントを使うか、集約のマージ(再設計)などがある

複数の集約にまたがる振る舞いは上記のようなユースケース(アプリケーションサービス)もしくはドメインサービスで表現することも可能です。

サービスという言葉は無茶苦茶混乱している人がいるので、この記事も読んでもらうとよいと思います。

混乱しがちなサービスという概念について
https://blog.j5ik2o.me/entry/2016/03/07/034646

こういった一連のプロセスにはコレオグラフィー方式かオーケストレーション方式があります。

コレオグラフィーは、各オブジェクトが何をすべきかを指示する中央集権的なコーディネータがいません。ドメインイベントを使う方法はこちらに近いです。
オーケストレーションは、各オブジェクトが何をすべきかを指示することだけを目的とするオーケストレータが存在します。ユースケースやドメインサービスはこちらのパターンです。

  1. エンティティと集約の違いはドメインイベントを保持するかどうかで、集約は発行されたドメインイベントをコレクションで保持し、ユースケース層かインフラ層のどちらかでディスパッチする

これは実装例の一つで生じる差異という認識ですかね?

エンティティや集約を実装する際、ドメインイベントは必ず使うものでありません。

意味としては以下の認識でよい気がします。

・エンティティは識別が責務
・集約は、集約内部にあるオブジェクト(エンティティや値オブジェクト)をカプセル化し、整合性を維持する単位

集約がないと、個々のオブジェクトがお互いに関連を持ち合い、関連が爆発してしまいます。そのような状況で整合性を保つことも難しいという話ですね。
オブジェクトがオブジェクト自身で自己防衛しなければならないので、どこからどこまでの範囲が自身の境界なのかはっきりしなければいけませんという話になると思います。

集約を実装するとき、

エンティティ{ 値オブジェクト, ... } にした場合、外側のエンティティをルートエンティティといいます。
つまり、ライフサイクル面で見れば集約といい、ドメイン文脈で見ればエンティティといいます。
なので、

エンティティ{ 値オブジェクト, エンティティ(集約内でローカルな), ... }
ルートエンティティ{ 値オブジェクト, エンティティ(集約内でローカルな), ... }
集約 { 値オブジェクト, エンティティ(集約内でローカルな), ... }

どれも意味は変わりません。

クラスベースのオブジェクト指向言語だとだいたいこういう構造になっていることが多いです。

KatsukiniwaKatsukiniwa

コメントありがとうございます!

1ユースケースで複数集約を扱っても問題ありません

サンプルコードも掲載して頂きありがとうございます!こちらですとトランザクションを貼る場合アプリケーション層にORMのトランザクションの実装が漏れてしまうので1ユースケースで複数集約を扱うことはなるべく避けたほうが良いのかな?という認識でした。

複数の集約にまたがる振る舞いは上記のようなユースケース(アプリケーションサービス)もしくはドメインサービスで表現することも可能です。

参考の記事にはめちゃくちゃお世話になってます・・・!ただ個人的にドメインサービスかどうかを見極める自信がなく、チーム内でうまく共通認識をそろえることが難しいのでなるべくドメインオブジェクトに直接振る舞いを持たせる方針でいます。

エンティティと集約の違いはドメインイベントを保持するかどうかで、集約は発行されたドメインイベントをコレクションで保持し、ユースケース層かインフラ層のどちらかでディスパッチする

これは実装例の一つで生じる差異という認識ですかね?

はい、その認識で相違ありません!自分がエンティティ・集約でそれぞれの基底クラスを作る際は記事中のようなクラスを作成しているので自然とソースコードベースの理解に終始していました、IDDD本読み直します!

エンティティ{ 値オブジェクト, ... } にした場合、外側のエンティティをルートエンティティといいます。
つまり、ライフサイクル面で見れば集約といい、ドメイン文脈で見ればエンティティといいます。

なるほど、この説明が個人的にとてもしっくり来ました。Aggregateクラスを継承しているから集約、Entityクラスを継承しているからエンティティと機械的に判断するのではなく結果として集約・エンティティそれぞれの責務を担うといった認識でしょうか?

改めてコメントありがとうございます、大変勉強になりました。

j5ik2oj5ik2o

サンプルコードも掲載して頂きありがとうございます!こちらですとトランザクションを貼る場合アプリケーション層にORMのトランザクションの実装が漏れてしまうので1ユースケースで複数集約を扱うことはなるべく避けたほうが良いのかな?という認識でした。

ユースケースは集約より粒度が大きい操作を提供する役割なので、複数種の集約を操作することはよくありますね。

でトランザクション境界の考え方ですが、

集約の境界=トランザクション境界が基本的な考え方だと思います。

集約は強い整合性を発揮する境界なので、複数の集約を束ねるトラザクションを強制しません。
この考え方は、「実践ドメイン駆動設計」や「マイクロサービスパターン」という書籍でも推奨されている方法です。

こちらのブログにも書いているので読んでみてもらうとよいかもです。
https://blog.j5ik2o.me/entry/2021/03/22/102520

マイクロサービスのようにサービスが分散している場合に、集約が独立したトランザクションを持っていたほうがサービスの独立性を担保しやすいという考え方ですね。あと、システムが分散していなくても、RDB以外にRedis, Memcachedなどを使う場合でも同様です。集約AはRDB、集約BはMemcachedに保存する場合、そもそも同一トランザクションにできません。

もちろん、モノリスで一つのRDBだけの場合はユースケースをトランザクション境界にすればいいと思います。ただし、あとでマイクロサービスに切り出すときは、大きな修正コストを払うことになると思いますが(経験談

参考の記事にはめちゃくちゃお世話になってます・・・!ただ個人的にドメインサービスかどうかを見極める自信がなく、チーム内でうまく共通認識をそろえることが難しいのでなるべくドメインオブジェクトに直接振る舞いを持たせる方針でいます。

お、読んでいただいてたのですね。ありがとうございます。

オブジェクトに振る舞いを割り当ていくことを考えるとその方針で問題ないと思いますが、それでもユースケースのロジックが長くなったり、再利用したくなる場合はドメインサービスを利用することが多いです(もちろん、そのロジックから新たなドメインモデルが発見できた場合はこの限りではないですが…)。

ところで、ドメインサービスはそんなに難しいものじゃないです。ドメインオブジェクトの一種ですね。エンティティや値オブジェクトの仲間です。簡単にいうとドメイン層の関数(クラスに従属するメソッドではなく、ファーストクラスな関数)です。その関数内で引数に渡したドメインオブジェクトを使って複雑なロジックを実行します。戻り値はまたドメインオブジェクトで返す形になります(ドメインオブジェクトは永続化責務を持たないいので、ドメインサービスも持たないほうがよいと思います)。

Aggregateクラスを継承しているから集約、Entityクラスを継承しているからエンティティと機械的に判断するのではなく結果として集約・エンティティそれぞれの責務を担うといった認識でしょうか?

はい。そのとおりです。モデルは問題や課題を解く考え方のことで、それをソフトウェアに反映するときにクラスなどにします。たとえば、スタジアムの座席管理をするシステム(Evans本に同様の事例がでてきます)で、座席をエンティティにするか値オブジェクトにするかは、どんな問題を解くかに関係しますね。指定席は座席を特定・識別しなければならないのでエンティティ、自由席は数だけが重要なので値オブジェクトになると思います。ビジネスや要件を知らないとモデルを考えることができないですね。

まぁ難しいところはいろいろありますが、ドメインロジックの主役は値オブジェクトなのでそこにかける時間を増やせればいいですね。モデルを考えてソフトウェアに反映する能力は鍛えないとなかなか難しいですね。僕は以下の割り勘のモデリングワークショップやってます。レイヤー化アーキテクチャとか、リポジトリとか一旦忘れてモデリングして実装できるようになろうというやつですね。ご参考までに。

https://github.com/j5ik2o/warikan-domain-java

hatchineehatchinee

ご指摘の通り、構造的部分型を採用しているTypeScriptではJavaのようなValueObjectの実装はうまく働きません。
代替としてよく使われているのはBranded Typeと呼ばれるものです。これは他の言語ではPhantom Type(コンパイル時にのみ存在して、ランタイムには消滅する型なのでそれを幽霊に例えています)とも呼ばれるテクニックで、MSのコアチームも利用しています。

ググると沢山例が出てきますが、こちらのほうがTypeScriptの特性にもマッチしていて、何よりランタイムのオーバーヘッドもないのでおすすめです。
他にも、もう少し厳格さを緩めたFlavorという型をおすすめしている方もいます。こちらは外部APIからのJSONをシリアライズするときとかに便利ですが、安全性は多少落ちます。

https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/

switch文でinstanceofを使って判定すると型が消失するからユーザ定義タイプガードを作る必要がある

恐らく何らかの誤解から出た結論だとおもうのですが、TypeScriptでType Guardsが必要になるのは「型が消失するから」ではありません。

TypeScriptの型システム上にのみ存在する型と、JavaScriptの持つオブジェクト指向システムに存在する型の互換性を取る方法がないから、という言い方のほうが近い(実際には他にも多様なユースケースが存在します)です。
例えば例で定義してあるProductNameのインスタンスから型が消滅することはありません(プロトタイプベースをいじるなど、特殊な操作をすると壊れることはあります)。

class, instanceofはJavaScriptにも存在する仕組みで、正常に動作します。
しかしTypeScriptの型システムは、

const name = new ProductName('namae');
const nameLikeJson = {
  props: 'aaa',
  name: 'namae',
  equal: () => true,
};

のように nameLikeJsonProductName の定義を満たしている限り両者を区別しません。
ただし、 instanceof ProductName がtrueを返すのは前者のみです。
そもそもオブジェクト指向的な健全性から言ってもinstanceofが出てくるのは赤信号ですが、TypeScriptではなおさらinstanceofを使うべきではありません。

これは少し過激な意見になるので細目で読んでほしいんですが、そもそもTypeScriptにおいてclassは基本的に避けたほうがいい機能で、継承は完全にバッドプラクティスです。

どちらにせよ、Javaとは完全に似て非なる言語なのでDDD実装の際にはTypeScriptに詳しい方を読んで翻訳をかけてもらうのが良いかとおもいます。
少し読んでみたのですが、作られたのが古いのもあってddd-forumのアプローチはTypeScript的にベストとは言い難いようにみえます。

Branded Type、Tagged Union Typesやただの関数を使うことでより堅牢にDDDのパターンを満たせるケースも数多くあるとおもいます。

ちなみにinterfaceが実装を持てないのはJavaScriptではなく、TypeScriptの構文だからです。

長々と失礼しました。
「JavaScriptの上にかぶさっている」という特殊な言語なのでTypeScriptの学習は脳が混乱しますよね……慣れると面白い言語なので頑張ってください!

KatsukiniwaKatsukiniwa

コメントありがとうございます!
Branded Typeは初めて聞きました!自分が求めていたものに近そうなので調べてみます、ありがとうございます!!!

恐らく何らかの誤解から出た結論だとおもうのですが、TypeScriptでType Guardsが必要になるのは「型が消失するから」ではありません。

TypeScriptの型システム上にのみ存在する型と、JavaScriptの持つオブジェクト指向システムに存在する型の互換性を取る方法がないから、という言い方のほうが近い(実際には他にも多様なユースケースが存在します)です。

すいません、こちらは完全に自分のTypeScriptに対する理解不足でした。switch文でinstanceofを使って判定すると

const sampleName = '' as any;

switch (true) {
  case sampleName instanceof UserName: 
    console.log(sampleName);
  case sampleName instanceof ProductName: 
    console.log(sampleName);
  default:
    console.log('undefined instance');
}

case文の中のsampleNameはanyのままなのでてっきり型が消失したのかと勘違いしていました。

どちらにせよ、Javaとは完全に似て非なる言語なのでDDD実装の際にはTypeScriptに詳しい方を読んで翻訳をかけてもらうのが良いかとおもいます。

なるほど、確かにDDDはサンプルコードの多くがJava/C#で書かれているので無意識のうちにJava/C#で書かれたコードをいかにTypeScriptで表現するかに注力していました・・・。今後はDDDのエッセンスは取り入れつつよりTypeScriptっぽい書き方で実装出来るように頑張っていきます!

改めてありがとうございました!!!