ソフトウェアのアーキテクチャについて(その2)~ ドメイン駆動設計から得られる知見

以下の記事の続きです。

threecourse.hatenablog.com

参考文献

以下の本でドメイン駆動設計について学んだところ、小〜中規模のプログラムの設計・記述についてもなかなかの知見・示唆が得られたので、まとめてみます。

以下も良い本らしいですが、まだ読んでないです

ドメイン駆動設計とは何か?

そもそもドメイン駆動設計とは何か?ということなのですが、私の理解では以下です。

  • ソフトウェアを開発する対象となる領域の知識に焦点を当てた設計方法
    ドメインの概念や事象を理解し、その中から問題解決に役立つものを抽出して得られた知識をソフトウェアに反映する。
  • ドメインとは、ソフトウェアの対象とする知識、影響、または活動の領域
  • 例えば会計システムであれば、金銭や帳票といった概念、その関係性や操作を適切にモデリングして設計する

ソフトウェアの対象とする領域を理解してモデリングしなきゃいけないのは当たり前だと思うので、これだけであればそれはそう、という感じ。

以下、「ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 」をもとに具体的なパターンをまとめてみます。筆者の勝手な解釈が入っているため、元の書籍と記述が同じとは限りません。

概要

ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 」では、ドメイン駆動設計のパターンが以下に分けられています。

知識を表現するパターン

  • 値オブジェクト
  • エンティティ
  • ドメインサービス

アプリケーションを実現するためのパターン

知識を表現する、より発展的なパターン

  • 集約
  • 仕様

1. 知識を表現するパターン

1.1 値オブジェクト

「値」をクラスや構造体とするなどしてオブジェクトとして定義したもの

  • 氏名を表すクラス(姓、名をフィールドとして持つ)
  • 通貨を表すクラス(量、種類をフィールドとして持つ)
性質
  • 不変である・交換が可能である
    値を変えたい場合には代入によって変更する、それ自身の値が変わることはない
  • 等価性によって比較される
    姓と名が同じであれば同じものであるとする、通貨の量と種類が同じであれば同じものとする
メリット
  • 表現力を増す
    値オブジェクトのクラスの定義のコード自体によって、氏名は姓と名から構成されるということが示される。
  • 不正な値を存在させない
    コンストラクタで判定するなどして不正な値を除外できる
  • 誤った代入を防ぐ
    ユーザIDに商品IDを代入するようなことをできなくする
  • ロジックの散在を防ぐ
    値オブジェクトに関連する処理をそのクラスにまとめて記述することができる
感想

上手く使うと便利。つい文字列で素朴に持ちがちな値も、構造体やクラスにまとめると見通しが良くなることがある。

1.2 エンティティ

同一性によって識別されるオブジェクト

  • ユーザ(姓、名、ユーザID、ユーザ種別などのフィールドを持つ)
性質
  • 可変である
    再代入ではなく自身のフィールドの値を変えることで、ユーザ名などを変更できる
  • 同じ属性であっても区別される
    姓や名といった属性が同じだけでは同一であるとは言えない。ユーザIDのような識別子で同一性を表す
  • 同一性を持つ
    ユーザ名などを変更したとしても同一のユーザとして認識される
メリット
  • コードのドキュメント性が高まる・ドメインにおける変更をコードに伝えやすくなる
    エンティティに関連するルールはそのクラスにまとめて記述することができる
感想
  • エンティティは値オブジェクトとの対比で理解するのがよさそう。
  • 値オブジェクトはクラス化するのは自然ではない(氏名は文字列として持ってしまうことはありそう)なのだが、エンティティはクラス化するのは自然なので、これらのメリットはオブジェクト指向のメリットといえる
  • エンティティはライフサイクルを持つというのは大きな特徴といえる

1.3 ドメインサービス

値オブジェクトやエンティティに関連するが、値オブジェクトやエンティティに記述すると不自然なふるまいを定義するクラス

ユーザの重複を確認するメソッドはユーザクラス自体に定義するとちょっと変。 UserServiceクラスを定義し、そこにExistsメソッドで確認するようにすると自然になる

感想

値オブジェクトやエンティティに記述すると不自然なメソッドをどう書こう・・と悩んだことがあるのでなるほどという感じ。名前もManagerとかUtilとかで悩んでいたが、Serviceを使えば良さそう。

2. アプリケーションを実現するためのパターン

2.1 リポジトリ

データの永続化と再構築の処理を抽象的に扱うためのオブジェクト

例およびメリット

アプリケーションを作る上では、ユーザの生成・変更といった処理をデータベースなどに反映し、「永続化」する必要がある。また、永続化されたデータから、ユーザーIDで検索するなどしたユーザをオブジェクトとして「再構築」する必要がある。

ここで、直接ユーザからデータベースを叩くコードを書いてしまうと、具体的なデータベースの操作を記述することになり、分かりづらく密結合なコードとなってしまう。 そこで、ユーザはIUserRepositoryインターフェイスに対して操作を行い、データベースとの接続等の処理はIUserRepositoryを実装したUserRepositoryクラスで行うことにする こうすることで、テストも行いやすくなる。リポジトリの実装をテスト用のものに差し替えることで、いちいちデータベースをセットアップする必要が無くなる。

感想

これもなるほど。知らないと直接データベースを叩いてしまうことがありそう。リポジトリだけでなく、一歩抽象化する思考を身に着けておくと応用が効きそう

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

ユースケースを実現するオブジェクト

ユーザの登録処理、ユーザ情報取得処理、ユーザ情報更新処理、退会処理といった処理を記述するクラス

論点
  • アプリケーションサービスはあくまでドメインオブジェクトのタスク調整に徹するべきであり、ドメインのルールを記述すべきでない
  • データ転送用オブジェクト
    ドメインオブジェクトを公開するかどうかという論点がある。

    • ドメインオブジェクトを公開する場合、ユーザ情報取得処理でユーザを直接返してしまう方法をとる。これは楽であるが、アプリケーションサービス以外がドメインオブジェクトを自由に操作できてしまうリスクがある。
    • 別の方法として、DTO(データ転送用オブジェクト)にデータを移し変えるという方法がある。つまり、IDと名前というフィールドを持つUserDataオブジェクトを返すようにする。
  • コマンドオブジェクト
    更新処理を定義するときに、ユーザ名だけを変更したいときもあれば、メールアドレスだけを変更したいこともある。このとき、引数で制御する方法もあるが、追加のたびにメソッドの引数が変わってしまう。この対処法として、コマンドオブジェクトを定義し、更新処理の引数としてコマンドオブジェクトを渡すという方法がある。

2.3 ファクトリ

生成を責務とするオブジェクトのこと

採番処理機能を実装したユーザ生成を行うため、IUserFactoryインターフェイスを実装したUserFactoryクラスを用いる

3. 知識を表現する、より発展的なパターン

3.1 集約

データを変更するための単位として扱われるオブジェクトの集まり

自動車とその要素である車輪・位置・タイヤがある場合に、自動車オブジェクトおよびそのフィールドとなる車輪・位置・タイヤのオブジェクトを定義する。 外部からは直接に車輪・位置・タイヤを参照せず、自動車オブジェクトに定義したメソッド経由で扱うようにする。このときの自動車オブジェクトを集約ルートという。

3.2 仕様

オブジェクトの評価を行うオブジェクト

オブジェクトの評価は単純なものであればメソッドとして定義すればよい。しかし、複雑な評価を仕様オブジェクトとすることで、評価のルールを切り出すことができる。 例えば、CircleFullSpecificationというクラスにサークルが満員かどうかという判定を定義し、それをアプリケーションサービスで呼び出すような方法がある また、仕様オブジェクトに対するAND/OR演算を定義するようなこともできる。

感想
  • Strategyパターンに似ているように思えた。評価・戦略といった抽象的な概念をオブジェクトとすることで見通しが良くなることがある。

その他

CQS/CQRS(コマンドクエリ分離原則、コマンドクエリ責務分離原則)

状態を変更するメソッドとそうでない単に問い合わせをするメソッドを区別すると良い。