ボトムアップドメイン駆動設計

  • 2018.08.17
  • DDD
ボトムアップドメイン駆動設計

こちらの記事は大幅にボリュームアップ(8万文字→30万文字)して書籍化されました!

Link: ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本

目次

はじめに

この記事は前後編に分かれています。
順序だてた解説になっているので最後までお付き合いいただけると幸いです。

後編記事: https://nrslib.com/bottomup-ddd-2/

順序立っての説明になっておりますので、前編からご覧になることを強くお勧めします。

セミナー情報

こちらの内容のセミナーを不定期で開催しています。

◆セミナーページ
第一回: https://ddd-community-jp.connpass.com/event/103428/
第二回: https://ddd-community-jp.connpass.com/event/107106/
第三回: https://nrs-seminar.connpass.com/event/117283/

◆あとがき
第一回ボトムアップドメイン駆動設計勉強会を開催しました

セミナースライド


まえがき

この章は飛ばしても構いません。

この記事を書くにあたって

ドメイン駆動設計について学習しているときに常々感じていたことがあります。

それは「壮大すぎる」ということです。

ドメイン駆動設計は大方の説明ではまずユビキタス言語やコンテキストマップの説明から入ります。
これらの概念はドメイン駆動設計の根底に関わるものですので非常に重要です。この概念の理解なくしてはドメイン駆動設計を習得したとは言い難いです。

しかし、初学者としてドメイン駆動設計の門戸を叩いた時、まずはユビキタス言語の説明をされ「ドメインエキスパート(業務に精通した人)と一緒にモデリングを行っていきましょう」と言われたらどのように感じるでしょうか。
初学者が皆都合よく、ドメインエキスパートが協力してくれる環境にいるのでしょうか。
多くはどうにも自分には実現不可能なようだ、と感じて踵を返してしまうのではないでしょうか。

そうして初学者が踵を返してしまう状況が続くようであれば、ドメイン駆動設計はいつまで経っても実践されない空論になってしまいます。
これは非常に勿体ないことだと思います。

ドメイン駆動設計は壮大ではありますが、その実オブジェクト指向を最大限に活用しただけの設計です。

ドメイン駆動設計が合う合わないは存在します。

合わないとする理由は所属している組織の環境であったり、個人の趣味嗜好であったりと様々な理由があるでしょう。

しかし、どのような知識であってもその可否を判断できるのは、ある程度全体を俯瞰できるようになったときこそ初めて合う合わないの判断が下せるようになるものだと思います。

この記事は理解や実践が難しいものについては後回しにし、理解しやすく実践しやすい小さい部分のパターンからボトムアップ的にシステムを構築して「動くものが作れるんだ」という実感をつかんでほしいという思いから作られました。

構成について

この記事はとにかくコードベースでドメイン駆動設計で利用されるパターンを利用してみて、ドメイン駆動設計の雰囲気を感じ取るためのものです。

そのため解説はなるべくソースコードをベースに説明します。
言語については C# をサンプルに利用しています。
極力 C# 特有の構文を使わないようにしているので、ほかのオブジェクト指向言語に置き換えることは容易かと思います。

始めは小さなモデリングのパターンから始まり、最終的には MVC フレームワークを用いた Web システムを構築します。
Web システムはサンプルコードも用意してあります。もしよければご参考ください。

モデリング

値オブジェクト

小さく始めるということで最も理解がしやすいものからはじめようと考えました。
最も理解がしやすいものは何か。一番に思い浮かんだのが、この値オブジェクトです。

まずはこの値オブジェクトを足がかりとしてドメイン駆動設計の門戸を開いてみましょう。

値オブジェクトとは

値といわれてすぐに思い浮かぶものは何でしょうか。
おそらく真っ先に「文字列」や「数字」などが思い浮かぶのではないでしょうか。

私たちはこれらの値を利用して、プログラミングを行い処理を実現しています。

例えば、文字列を使って氏名を表現することがあるでしょう。
さてこの fullname という変数ですが氏名を表現するのにはこの変数で十分といえるでしょうか。

実際に値を代入してみましょう。
問題なく動作していると言えます。

では少し趣向を変えて氏名から名字だけを取得してみましょう。
そうすると困った事態になってしまうのではないでしょうか。

“tanakataro” のうち名字である部分を取得するロジックが思いつきません。
名字を取得できるようにするにはデータに細工をする必要があるようです。

例えば名字と名前を空白で区切ったらどうでしょうか。
無事氏名から名字を取得することができるようになりました。

ところで次のような名前はどうでしょうか。
外国の方ですね。

先ほどの名字の取得方法を実践してみると……。
“john” が取得されてしまいます。
これは名字ではありません。

どうにもこのやり方ではデータによってロジックを切り替える必要が出てきてしまうようです。

こういった問題を解決するときに通常利用されるのがクラスです。
この FullName クラスを使って名字を取得してみましょう。
どちらのデータであっても問題なく名字を取得することができます。

ドメイン駆動設計ではこういったクラスのことを値オブジェクトとしています。

値オブジェクトのルール

値オブジェクトにはいくつかのルールが存在します。

  • 状態を不変に保つ
  • 同じ値オブジェクト同士で値が等しいかどうかの確認ができる
  • 完全に交換可能である

それぞれ詳しく見てみましょう。

状態を不変に保つ

クラスはミュータブル(mutable, 可変)なクラスとイミュータブル(Immutable, 不変)なクラスに分けることができます。

FullName クラスはミュータブルでしょうか。イミュータブルでしょうか。
クラスの状態を表す familyname フィールドや firstname フィールドが readonly になっており、変更させる手だてがありません。
この FullName クラスはイミュータブルなクラスです。

では FullName クラスをミュータブルなクラスにしてみます。
フィールドを変更するためにメソッドを公開し、クラスの状態である familyname や firstname が変更できるようになりました。
状態を変更出来るこのクラスはミュータブルです。

このようにクラスは「イミュータブルなクラス」と「ミュータブルなクラス」の二種類に分別できます。

さて、このイミュータブルなクラスとミュータブルなクラスでは、どちらが値オブジェクトの実装として相応しいのでしょうか。

「状態を不変に保てる」というルールに照らし合わせると、値オブジェクトに相応しい実装はイミュータブルなクラスです。

値オブジェクトはそれ自身が値として振舞います。

値は一貫して変化することがありません。
もし 0 という数値がプログラムの実行途中で 1 という数値に変更されてしまったら混乱を引き起こしてしまいます。

値としての役割を持たされる値オブジェクトもまた、不変であるべきです。

また、不変にすることで

  • フィールドが変更されることを考慮せずに済む
  • 並列や並行で実行した際に状態が変更される恐れがない
  • キャッシュしてメモリを節約することが可能になる

という明確なメリットが存在します。
これらは値オブジェクトに限らず適用されるメリットですので、通常のクラスであっても可能であればイミュータブルになるように意識するとよいでしょう。

同じ値オブジェクト同士で値が等しいかどうかの確認ができる

値は値同士で比較が出来ます。

値オブジェクトも同じように、値オブジェクト同士の比較手段を提供する必要があります。

それでは FullName クラスも値オブジェクトにするべく、比較の手段を提供してみましょう。
等値比較のメソッドを実装し、過不足なく値を表すフィールドを比較します。
※ C# の Equals を提供するときのルールのため GetHashCode などのメソッドも用意しています。

このメソッドを利用することで値オブジェクト同士が等しいかどうかの確認が可能になります。

完全に交換可能である

プログラムにおいて値と言われたら思い浮かぶのは数字や文字、文字列などのプリミティブな値が思い浮かぶと思います。

このプリミティブな値を変更するプログラムを記述してみましょう。
いずれの場合も代入を利用して変更を行っています。

これは値を変更したのでしょうか。
いいえ、 ‘0’ という値を ‘1’ という値に変更することは不可能です。
とは記述できないのです。

代入とはつまり値を「変更」しているのではなく、値を「交換」しているのです。

値オブジェクトもまた交換が可能です。
交換することにより変更を表現します。

値オブジェクトを作るモチベーション

値オブジェクトを作る動機としては、値に振る舞いを持たせたいというのが動機になります。
それ以外の動機として「存在しない値を存在させない」ということと「間違った代入を防ぐ」というのも動機になることがあります。

存在しない値を存在させない

例えばユーザ名を表す値オブジェクトを考えてみましょう。
ユーザ名はユーザが任意につけることができますが、ユーザ名の長さは無限というわけにはいきませんね。
そこでユーザ名は 50 文字以下とします。

このユーザ名を表す値オブジェクトは次のようになるでしょう。
UserName はコンストラクタで文字列の長さのチェックを行っており 50 文字を超える長さのユーザ名を受け付けません。

このように「本来存在してはいけない値」というものを存在させないようにすることができます。

誤った代入を防ぐ

次のようなクラスがあります。
名前を受け取って User オブジェクトを戻すメソッドを作ってみましょう。
このメソッドは正しいでしょうか。

正しいか間違っているかは正直わかりません。
名前を Id に使うパターンは存在するでしょうし、ユーザ名はまた別に設定できるかもしれません。
何よりコンパイルが通るので動作します。

それではこちらのクラスの場合はどうでしょうか。
プロパティは値オブジェクトになりました。
この User オブジェクトを戻すメソッドを作ってみます(比較しやすいように引数も値オブジェクトを受け取ります)。
Id は UserId という型ですので UserName を代入することができません。
値オブジェクトのおかげで、間違ってユーザ名が Id に代入されることがなくなったのです。

エンティティ

値オブジェクトと並ぶモデリング要素がエンティティです。
ドメイン駆動設計ではおよそデータというものは、このエンティティと値オブジェクトに分けられます。

値オブジェクトとの違い

ドメイン駆動設計におけるモデリングの最小要素は値オブジェクトとエンティティの二つです。
値オブジェクトでないものは全てエンティティであるといえます。

値オブジェクトとの違いを比べることはエンティティの理解を助けることになるでしょう。
エンティティは値オブジェクトの特徴とは対照的に次のような特徴を持っています。

  • 可変
  • 同じ属性でも区別される
  • 同一性を持つ

一つずつ解説をしていきます。

可変

値オブジェクトは不変なオブジェクトでしたが、エンティティは可変なオブジェクトです。

例えば氏名のデータを持った User クラスを考えてみましょう。
ただのデータの入れ物になっているクラスです。

ユーザは登録した氏名を変更することができます。
そのための振る舞いを追加してみましょう。
フィールドを変更するメソッドを追加しました。
このようにエンティティは振る舞いにより属性を変更することができるのです。

同じ属性でも区別される

値オブジェクトは属性(フィールド)が同じであれば同じ物として扱うことができました。これは値オブジェクト自体が属性の表現であるからです。
それに比べてエンティティは属性が全て同一であっても同じ物として扱うことができません。

具体的な例で説明をすると “tanakataro” という「文字列」と “tanakataro” という「文字列」は全く同じ物ですが “tanakataro” という「ユーザ」と “tanakataro” という「ユーザ」は必ずしも同じ人物ではありません。
同姓同名のユーザが存在することは往々にしてあります。

属性が全く同じオブジェクトを区別できるようにエンティティには識別子が必ず必要です。
識別子もまた値オブジェクトです。
もちろん Id オブジェクト以外にも、ユーザ登録にはメールアドレスが必要等のルールに基づいてメールアドレスが識別子となったり、他の値オブジェクトやプリミティブな値が識別子になることもあります。

同一性を持つ

エンティティには必ずライフサイクルが存在します。
そのライフサイクルの中で属性が変わったとしてもそれが同一であることがわかるようにする必要があります。

わかりやすい例として、ユーザを登録してから名前を変更してみます。
さて、「登録したユーザ」と「名前を変更した後のユーザ」は同一のユーザでしょうか。

多くの場合は同一と認識してほしいでしょう。
登録している名前を変更したら全く別のユーザーになってしまうようなシステムでは大変です。

よって、エンティティは属性が異なっても同一と見なす必要があります。
同一のエンティティかどうかを判定する際には識別子を用いて比較します。

同一性ではなく属性を比較する必要が出来た場合は属性比較メソッドを用意する必要があるでしょう。

ドメインサービス

例えばユーザを登録するときに「ユーザ名の重複を許してはいけない」というルールはよくあるでしょう。
そのルールを記載すべきはどこでしょうか。

ルールはなるべくなら関連するものの近くに記述しておきたいものです。

ユーザに関することはユーザに記載するということで User クラスに記述してみます。
IsDuplicated メソッドを定義しました。このメソッドを利用してみます。
すると少々おかしな状況になるのではないでしょうか。
このメソッドは true を返すのでしょうか。それとも false を返すのでしょうか。
単純に処理を読み解くと “ttaro” というユーザに “ttaro” ユーザは重複しているかと問い合わせている形になるので true を返してきそうです。

これでは混乱の元になってしまいそうですね。

ではチェック用のオブジェクトをインスタンス化してみるというのはどうでしょうか。
結果は false を返してきそうです。
しかし、”forcheck” というユーザは一体何者なのでしょうか。
仮とはいえ存在しないユーザを作るのが正しいプログラムなのでしょうか。

このようにエンティティに関係するロジックであるけれども、エンティティに実装すると違和感を産むロジックは必ず存在します。
そういったロジックを受け持つものとしてドメインサービスが存在します。
UserService は User エンティティに対するドメインサービスです。
User エンティティ専用の横断的な知識(User 間での重複チェック等)を実装することを許します。

ドメインサービスに記述するか迷ったとき

ドメインサービスとエンティティのどちらにロジックを記述するか迷ったときはエンティティに記述してください。
ドメインサービスと値オブジェクトのどちらにロジックを記述するか迷ったときは値オブジェクトに記述してください。

ドメインサービスはエンティティや値オブジェクトに関する処理ではありますが、エンティティや値オブジェクトに実装するのが自然でない場合のみに扱います。

結果としてドメインサービスを用意する必要がないことも往々にしてあり得ます。

各モデルを利用する

ここまででモデリングの要素はすべて出そろいました。
あとはこれまでの要素を利用するだけでビジネスロジックを表現することができるのです。

早速プログラミングをしてみましょう。

今回利用するモデルはいままでサンプルにしていた User モデルです。
User モデルは Id プロパティで UserId オブジェクトが、 Name プロパティ(メソッド)で FullName オブジェクトが取得できるようにしています。

ユーザ登録

まずは手始めにユーザ登録をするロジックを書いてみます。
登録したいユーザ名を引数で受け取り、重複しないようであれば登録をするメソッドを作ってみましょう。
User オブジェクトを作成するところまでは今までのオブジェクトを利用して記述できました。
しかしここで、モデルの保存処理という今までに出てきていない要素が出てくることに気づきます。

データを保存する媒体としてデータベースを選択した場合は次のような処理になるのでしょうか。
動作はしそうですが、意図が読みづらいコードですね。

このコードは柔軟性にも乏しいコードです。
もしデータストアが RDB でなく NoSQL や API になったときはどうすればよいでしょうか。
テストをしたいときはデータベースにデータを用意してテストをするのでしょうか。
どちらのケースもあまり考えたくない状況です。

視点を変えて、ドメインサービスの UserService.IsDuplicated というメソッドの実装についてはどうでしょう。
重複を確認するために、こちらのメソッドでも保存されたデータからデータを読み出すことになります。
結果として重複チェックをするだけのメソッドであっても、データの取得処理やそれに伴う準備と後始末が記述の大半を占めることになります。

データを取り扱う以上、データの保存処理等に関わる処理を記述することは避けられません。
しかし「ユーザを登録する」という処理において、データの保存処理はロジックの大半を占めるべき内容でしょうか。

「ユーザを登録する」というメソッドで表現すべきは

  1. 与えられたユーザ名でユーザを生成
  2. ユーザ名が重複しないかを確認する
  3. 重複した場合は例外を投げ、重複しなかった場合はユーザ登録をする

という内容です。

ドメイン駆動設計では勿論、ビジネスロジックがインフラストラクチャにまつわる準備や後始末に終始することをよしとしません。
しかしデータの保存処理はソフトウェアを作る上で無くてはならない機能です。

こうしたデータの保存処理をどのようにして取り扱えばよいのかを次の章の「データの永続化」で解説します。

データの永続化

データを生成したプログラムが終了しても、次にプログラムが起動したときにデータを利用できるようにソフトウェアは保存処理を行います。
こういった保存処理にまつわる事柄を「データの永続化」と表します。

データの永続化の対象は多くはエンティティが対象です。
ドメイン駆動設計においてどのようにエンティティを永続化させるのかをこの章で解説します。

リポジトリ

ドメイン駆動設計でデータの永続化の処理を行うのはリポジトリと呼ばれるオブジェクトです。

リポジトリはエンティティ毎(厳密には集約毎)に用意します。

SQL を用いたリポジトリ

早速ですが前章の SQL を利用した User オブジェクト用のリポジトリを作成してみましょう。
UserRepository にはユーザ名によるユーザの検索とユーザの保存のメソッドを実装しました。

このリポジトリを使って、ユーザ登録をする処理を記述してみます。

まずはドメインサービスでリポジトリを利用するように修正します。
ドメインサービスはコンストラクタでリポジトリを受け取るようになります。

重複を確認するメソッドの IsDuplicated はリポジトリを利用するようになったので SQL を利用したデータベースを操作する処理が消え、「ユーザ名をキーにして検索し、存在していたら重複とする」という処理の意図が読み取りやすくなっています。

続いてメインの処理です。
データベースを操作する処理がリポジトリに隠された結果、「重複確認をして重複しないようだったらユーザを保存する」という意図が見やすくなったのではないでしょうか。

リポジトリにデータの永続化に関わる処理を集中させることで、処理自体が何を目的にしているのかを際立てることができます。

テスト用リポジトリ

前章ではテストをするときにデータベースにデータを用意しなくてはいけないという欠点がありました。
これについては前項の SQL を用いたリポジトリでも同じことが起きています。

テスタビリティを確保するためにデータを用意しやすいリポジトリを作りましょう。
よくテスト用リポジトリでは連想配列が用いられます。
このリポジトリは連想配列で実装されているため、メモリにデータが格納されます。
リポジトリ名はメモリ上でのリポジトリを強調し InMemory をプレフィックスにしました。
保存先がデータベースからメモリに変更されているという違いはありますが、このクラスは User エンティティを検索したり、保存したりすることができます。

早速このリポジトリを組み込んでみましょう。
三行目で UserRepository をインスタンス化していたところで InMemoryUserRepository をインスタンス化するように組み込みました。
また UserService も InMemoryUserRepository を利用するように書き換えました。

ロジック自体は変更がなく問題なく動作しそうなものですが、InMemoryUserRepository にテストデータを準備することができない状態です。

テストデータを用意するには InMemoryUserRepository に初期データを用意する必要があります。
しかし User を使ったロジックは「ユーザ登録」以外にもあるでしょう。
ロジック毎のテストの度にプログラムを書き換えることは非常に手間です。
そもそも UserRepository を利用しているところ InMemoryUserRepository を使うように変更してテストすること自体が間違っています。

根底の問題は処理が特定の具象クラスに依存していることにあるようです。

依存

オブジェクト指向における依存とは、具象クラスへの依存を指します。

前項のプログラムの依存について見てみましょう。
CreateUser メソッドは以下のクラスに依存しています。

  • User
  • UserRepository
  • UserService
  • Exception

このプログラムは UserRepository に依存しているため、処理を書き換えないとテストができない状態にあります。
テストを行えるようにするには UserRepository に依存しないように処理を書き換える必要があります。

具体的に実装していきましょう。

まずインターフェースを用意します。
リポジトリの検索と保存機能を強制するインターフェースです。
インターフェースがない言語の場合は完全抽象クラスで用意するとよいでしょう。

リポジトリインターフェースの用意ができたら、このインターフェースを引数で受け取るようにします。
受け取るリポジトリは SQL を利用したプロダクション用リポジトリか、インメモリのテスト用リポジトリかはわかりません。
しかしロジックの意図は伝わるかと思います。

UserRepository という具象クラスがロジック上から消え Program クラスは UserRepository に依存しないようになりました。

次にリポジトリにも修正が必要です。
と言っても処理自体はすでに作成してあるので、インターフェースを実装するように変更するだけです。

出来上がったクラスを利用してみましょう。

まずはプロダクションモードの場合のスクリプトです。
プロダクション用リポジトリをインスタンス化して引数に渡すだけです。
この処理の場合データベースに “taro tanaka” さんが存在していたら失敗しますし、存在していなかったら登録に成功するでしょう。

続いてテストをするときの使い方です。
同じくリポジトリをインスタンス化して引数に渡すだけです。
処理を実行する前にテストデータで “taro tanaka” さんを登録しているので、必ず登録に失敗することになります。

このように、具象クラスに依存せず、抽象リポジトリを引数などで受け取ることで、主処理を変更することなくロジックのテストを行うことができるようになるのです。

アプリケーションサービス

エンティティ、値オブジェクト、ドメインサービス、リポジトリの四つでロジックを表現するための最低限のパーツは揃いました。
いよいよパーツを組み立てる時が来たのです。

この章ではロジックを表現するドメインモデルを「誰が」扱うかに焦点をあてます。
ドメイン駆動設計ではドメインモデルを直接扱う要素として「アプリケーションサービス」というものを利用します。

リポジトリの節で作った Program というクラスはドメインモデルを直接扱っているのでアプリケーションサービスに近しい物でした。

ユーザ関連処理

アプリケーションサービスは API です。
モデルはドメイン領域を表現しますが、アプリケーションサービスはドメイン領域で何ができるのかを表現します。

まずはユーザに関して「何ができるのか」を表現したアプリケーションサービスを作ってみましょう。

今回作るアプリケーションはユーザ管理ツールとします。
アクターは管理者とし、管理者はユーザに関して以下の処理を行えます。

  • ユーザの登録
  • ユーザ情報変更
  • ユーザの削除
  • ユーザ情報取得
  • ユーザ一覧取得

これらの項目をユースケースにすると以下のようになります。

ユーザの登録

早速定義していきます。
まずは「ユーザの登録」を記述します。
アプリケーションサービスではメソッドでリポジトリを受け取るのではなく、コンストラクタでリポジトリを受け取るようにしましょう。

ロジック自体は前回書いたものと同じです。

ユーザ情報変更

ユーザ情報を変更する際にはどのユーザか特定できなくてはいけません。
ユーザはエンティティですので同一性を識別するための手段として UserId という識別子がありました。
この値を引数としてもらうようにしてみます。
リポジトリに UserId で検索する手段が必要になるためインターフェースにメソッド追加しています。
リポジトリインターフェースを実装しているクラスはコンパイルエラーになりますが、一旦他のメソッドの実装を優先しましょう。

ユーザの削除

現行のリポジトリは検索と保存しか処理が存在していないためユーザの削除が行えません。
そのためユーザの削除を実装する場合、リポジトリに削除処理を追加する必要があります。
このリポジトリの削除処理を利用して、アプリケーションサービスに「ユーザの削除」処理を実装すると次のようになります。

ユーザ情報取得

「ユーザ情報取得」はこれまでの処理と決定的に異なる部分があります。

それは戻り値が存在するという点です。

この戻り値ですがどのオブジェクトを返すべきでしょうか。
ユーザの情報そのものである User オブジェクトでしょうか。

プロジェクトのポリシーによりますが、この例ではドメイン領域の知識が流出しないように DTO (Data Transfer Object)を用意する方針で記述します。
DTO を用いるメリットはドメイン領域の外部でメソッドを呼び出して名前を変更したりするといった操作ができなくなります。
反対にデメリットは同じデータ構造のオブジェクトを生成することになるため、メモリ効率や変換処理の CPU 効率が悪くなることでしょう。

DTO を利用してユーザ情報を取得できるようにすると次のようになります。

ユーザ一覧取得

管理者は任意のユーザを編集したり、削除処理を行います。
任意のユーザを操作するためには一覧でユーザを選択できるようにする必要があります。

ユーザ一覧も取得できるようにメソッドを用意しましょう。
ユーザ一覧ではすべての情報は不要です。
サマリとして ID とユーザ名だけをデータとして渡しています。

リポジトリに追加した動作を実装

リポジトリインターフェースに処理を追加したので、本番用とテスト用のリポジトリに実装を追加しましょう。

プロダクション用リポジトリ

SQL での検索処理や削除処理を追加しました。
保存処理(Save)はデータが存在するかどうかによって挿入か更新かいずれかの動作が選択されて実行されます。

テスト用リポジトリ

連想配列利用しているので、追加された処理は非常にシンプルです。

完成

ドメイン領域の表現(モデリング)とその API(アプリケーションサービス)が揃い、ドメイン駆動設計のパターンに倣った最小限のアプリケーションがこれで完成しました。

いよいよ次の章より実際にユーザが操作するアプリケーションと連携できるようにしていきます。

ユーザインターフェース

アプリケーションはユーザインターフェースと連携してユーザにその機能を提供します。
最小限のアプリケーションをユーザインターフェースに繋ぎこんで Web システムを完成させましょう。

サンプルアプリケーション

動作するサンプルアプリケーションを用意しています。
https://github.com/nrslib/BottomUpDDD
サンプルアプリケーションは ASP.NET Core で構成されています。
記事では触れないアプリケーションサービスのメソッドについても利用するように実装してあるので、もしよければご覧ください。

MVC フレームワーク

ユーザインターフェースとして MVC フレームワークを利用します。

コントローラ

アプリケーションサービスを利用するのはコントローラです。
コントローラのコンストラクタでアプリケーションサービスを受け取るようにします。
このように特定のオブジェクトをコントローラで受け取るためには Dependency Injection という手法を利用します。

Dependency Injection

Dependency Injection は単純に言えばコンストラクタでインスタンスを受け取る程度の意味です。
つまりコンストラクタでインスタンスを受け取っていれば Dependency Injection であると言えます。

焦点となるのは「どのオブジェクトがコンストラクタで渡されるのか」という点です。

MVC フレームワークではコントローラはフレームワーク部分で生成されます。
つまりプログラマは任意のコンストラクタを実行することができません。

コントローラのコンストラクタで特定のオブジェクトを受け取るためには予めどのオブジェクトを受け取るかの設定をする必要があります。
この設定で利用されるのが DIContainer と呼ばれるもので、多くの MVC フレームワークではこの仕組みが備わっています。

サンプルプロジェクトでは StartUp.cs で設定されています。
この設定の場合は

  • IUserRepository が要求される個所 → InMemoryUserRepository
  • UserApplicationService が要求される個所 → UserApplicationService

という関係でオブジェクトがインスタンス化されて引き渡されます。

また InMemoryUserRepository はシングルトンでの登録がされているので Web アプリケーション起動中は一つのインスタンスが使いまわされます。
反対に UserApplicationService はリクエストごとに毎回インスタンスが生成されます。

ユーザ一覧の取得処理

アプリケーションサービスをコントローラが受け取ることができたら後は利用するだけです。
Index ページでユーザの一覧を取得して View にデータを渡してみましょう。
コントローラからアプリケーションサービスのメソッドを呼び出し、データの取得を行うことができました。
これで繋ぎこみが完了です。

最小限の Web システムが完成しました。

単体テスト

ドメイン駆動設計のパターンに従って構築をするとテストも行いやすくなります。
早速テストを書いてみましょう。

テストを書く単位はアプリケーションサービスのメソッドを単位としてテストを記述すると、ちょうどビジネスロジック毎のテストになってよいかと思います。
今回は「ユーザ登録」についてのテストを記述してみます。
重複したときの失敗パターンと登録完了時のパターンのテストですね。

ロジックだけのテストであれば、このようにインメモリのリポジトリを用意して都合のよいデータを用意すればテストを行えます。

中間地点

最小限の Web システムが完成しました。
テストも用意することができました。
切りもよいのでここで一旦の区切りとします。

ここまでで出てきていないパターンやいくつかの課題が残っています。
またドメイン駆動設計の根底にある考え方については全く触れていません。

しかしドメイン駆動設計の片鱗に触れることはできたのではないでしょうか。

残りのパターンやいくつかの課題(トランザクションの扱いや ID を独自の採番の仕組みにしたいとき等)を解決するための方法についても引き続き記事を作っていこうと思いますが、今はここまでです。
長文にお付き合いいただきありがとうございました。

後編書きました。
https://nrslib.com/bottomup-ddd-2/

ドメイン駆動設計系の記事リンク

この記事を書く前に個別の項目で記事を書いていました。
しかしそれぞれ個別の記事だと繋がりがわかりづらいなーと考えてこの記事を作った次第です。
もし興味があればどうぞ。
◆ ValueObject
記事リンク: https://nrslib.com/valueobject/
◆ Entity
記事リンク: https://nrslib.com/entity/
◆ AggregateRoot
記事リンク: https://nrslib.com/aggregateroot/
◆ Repository
記事リンク: https://nrslib.com/repository/

DDDカテゴリの最新記事