Open20

ドメインモデルについて本を読み返しながら頭の中を整理していく#2

うぃうぃ

「現場で役立つシステム設計の原則」編

うぃうぃ

ドメイン駆動設計とは何か
→ソフトウェアの利用者の問題を見極め、洞察や設計を繰り返していく中で、ソフトウェアの利用者を取り巻く世界と実装を結びつけること

うぃうぃ

値オブジェクト

→システム固有の値を表現するオブジェクト(ドメインオブジェクト)

値オブジェクトの性質

  • 不変である
var money = new Money(1000);
money.ChangeTotal(15000); // 値を変更している。これは値オブジェクトでするべきではない。
  • 変換が可能である
var money = new Money(1000);
money = new Money(15000); // 値の変換は可能なので、これはOK
  • 等価性によって比較される
var moneyA = new Money(1000);
var moneyB = new Money(1000);
moneyA.Equals(moneyB) // これはtrue

*属性を直接取り出して比較するべきではない。なぜなら”値”であるから。
*値オブジェクトとしてまとめることで、比較ロジックなどが散乱せず、変更箇所をまとめることができる

どこまで値オブジェクトとして扱うか

  • ルールがあるか
  • 単体で扱いたいか

独自の振る舞いを定義できる

Moneyクラスであれば、数値以外はコンストラクだで受け付けないなど。
また、第2引数が通貨単位だとすると、同通貨でないとaddやsubtractできないなど。

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

  • 表現力を増すことができる
    →3つの文字列が連なったIDをただの文字列型として引数に入れ扱うのではなく、1つ1つ分けて扱うことでそれぞれどんな意味があるものなのかを表現できる
void ID(string ...) // 文字列であるということしかわからない

void ID(string name, string birthday, string favorite color) ...
public override string ToString()
{
    return name + "_" + birthday + "_" + favorite color; // IDがどういうものなのかが表現できる
}
  • 不正な値を存在させない
    IDが8桁以上16桁以下のような制限をかけた場合に、オブジェクト内で値の判定をすることができる。
    異常な値をチェックするロジックが散らばらないし、オブジェクトを使用する場合に不正な値が存在する場合を考えなくて良くなる

  • 誤った代入を防ぐ
    値オブジェクトを作ることで型の恩恵を受けることができる。予測不能なエラーが潜む箇所を減らすことができる。

  • ロジックの散在を防ぐ
    IDが8桁以上16桁以下のような制限をかけた場合に、オブジェクト内で値の判定をすることができ、異常な値をチェックするロジックが散らばらない。

どくどく毒でんぱ・かがむー(じけいれつには【Commentsタブ】みてね! メンどくどく毒でんぱ・かがむー(じけいれつには【Commentsタブ】みてね! メン

あ、ごめんなさい、
マウント合戦したいわけではないのですが、
下記の個所はこのようにしたほうが良いかと思います。。。

var money_A = new Money(1000);
var money_B = new Money(15000); // 値の変換は可能なので、これはOK

→『不変(イミュータブル)』オブジェクトは、
一度設定した『変数の値』は変更しないほうが良いです。

『設定を変更するとき』は、
ほとんどすべてにおいて、もともとは異なるなんらかの『理由』が
あるはずなんです。。

ために、
基本的にはその目的に沿った『変数名』とすべきです。

けれども
現実には柔軟性が求められます。

例えば、繰り返し処理の
『ループカウンタ』は、
さすがにこれは『ミュータブル(変動的)』に
したほうが便利です(;^_^A

けれども、
『ループカウンタ』以外の用途に使わない方が良いです。

極端な例で言うと、
この正反対は
フィールドを
『グローバル変数』とか『パブリック変数』にすることです。。。

ご不快になられたかもしれませんけど、
よろしくお願いいたします。

うぃうぃ

コメントしていただきありがとうございます!

→『不変(イミュータブル)』オブジェクトは、
一度設定した『変数の値』は変更しないほうが良いです。
おっしゃる通りだと思いました。理解の助けになります。ありがとうございます!

うぃうぃ

エンティティ

→ドメインモデルを実装したオブジェクト(ドメインオブジェクト)

エンティティの性質

  • 可変である
    Userオブジェクトがあり、ユーザー名がフィールドに保存されているとする。
    ChangeNameメソッドで名前を変更することができる。Value Objectとは新しいオブジェクトに値を代入して、再度オブジェクトを作り直していた点が異なる。
    なお、全ての属性を可変にする必要はない。
  • 同じ属性であっても区別される
    Value Objectであれば同じものとして扱われたもの。UserAとUserBが同じ名前でも、UserAとUserBは同じものとして判断されないということ。
  • 同一性により区別される
    同一性(IDなどの識別子)を持つ。UserAが名前を変えても、UserAはUserXに変わったりせずUserAとして判断される。なので、エンティティは識別子を持たなければならない。
    また、同一性を判断するメソッド(Equalsなど)では、全ての値が同じであると判断する(Value Object)のではなく、識別子が同じかを判断する。

エンティティ例外処理

エンティティでは取りうる値の判定など例外処理を発生させる。しかし、Entity内の例外処理はあくまで最後の砦として考えておき、値を代入する前に値が異常値ではないか判定を入れるべきである。

Value ObjectとEntityの判断基準

ライフサイクルが存在し、そこに連続性があればEntityとすれば良い。
また、状況によってValue ObjectにもEntityにもなりうるものがある。
消耗品としてのタイヤはValue Objectであるが、工場や出荷元から見たらそれぞれIDが割り振られているEntityかもしれないということ。

うぃうぃ

ドメインオブジェクトを定義するメリット

  • コードのドキュメント性が高まる
    ドメイン知識のないエンジニアでも、コードを読めば業務知識を得ることができる
  • ドメインにおける変更をコードに伝えやすくなる
    ドメインオブジェクトが存在し、そこに業務ルールが書かれていることにより、修正範囲の散乱が免れる。
うぃうぃ

ドメインサービス

→不自然さを解決するオブジェクト
例)Userクラスが重複した名前の存在を許可しないとき、その重複をチェックするメソッドはどこに定義されるべきか

  • Userクラスに書く
    →これは不自然である。なぜかというと重複をチェックするのに自信に問い合わせる形になるから(user.Exists(user)みたいになってしまう)。
    ではどこに書くべきかと考えたときに、ドメインサービスに書く。

ドメインサービスは値オブジェクトやエンティティと異なり、自身の振る舞いを変更するようなインスタンス特有の状態を持たないオブジェクトである。
ドメインサービスを濫用しない。ドメインモデル貧血症(ドメインオブジェクトに何も書かれておらず、どのような振る舞いをするのかわからなくなってしまっている状態)になってしまうから。

うぃうぃ

リポジトリ

→ドメインオブジェクトではなく、ドメインオブジェクトの永続化や再構築を行うことが責務。
データベースとの疎通であったり、インスタンスの保存、読み込みの処理などを書く場所。

存在判定などをリポジトリに書きたくなることがあるかもしれないが、存在判定はあくまでドメインの仕事なので、リポジトリに書くべきではない。

ビジネスロジックに特定の技術基盤に依存した処理を記述したいとはならないが、リポジトリの実装クラスでは技術基盤に依存した処理を記述しても問題ない。

テスト用のリポジトリを作成する
連想配列やなどを使用したテスト用のリポジトリを用意することで、テストが容易になる。なぜならデータベースに接続する必要がなくなったから。

永続化を行う場合は永続化を行うオブジェクトを引数にとる。つまり、更新対象の項目を引き渡して更新させるようなメソッドは用意しない。

再構築に関しては、識別子によって検索されるメソッドを用意する。または、全てのオブジェクトを再構築するメソッドを定義します(リソースを食い潰す可能性があるので、注意する)。

うぃうぃ

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

→ドメインオブジェクトを組み合わせてユースケースを表現する
CRUDなどの基本的な操作はアプリケーションサービスで表現できる

READする場合にドメインオブジェクトをそのまま返すべきかどうか

  • そのままオブジェクトとして返すという選択肢
    →そのまま返すと、クライアント側でドメインオブジェクトが使用可能になってしまう(ドメインオブジェクトのふるまいを呼び出すのはアプリケーションサービスの役割である)
  • DTOにデータを移し替えて返す方法(better)
    →ドメインオブジェクトが返されるわけではないので、クライアントはドメインオブジェクトのメソッドを呼び出すことができない
    →デメリットはコード量が増えるので、開発チームのメンバーから理解を得られない可能性がある

複数項目をUPDATEする場合
→コマンドクラスを作成して、それを引数としてUPDATEメソッドに渡すことで、更新する項目が増えるたびに引数が増えるということを防ぐことができる

update(command)
// ↓の代わりに↑を使う。command.nameなどでフィールドにアクセスできる
update(name, email, ...)

*アプリケーションサービスにドメインルールを記述するべきではない
*アプリケーションサービスに状態を持たせない。つまり、自分の状態を変更させるフィールドを持つべきではない。しかし、リポジトリのような自分の状態を変化させないフィールドは持っても構わない。

うぃうぃ

依存関係

→依存とはオブジェクトAがオブジェクトBを参照しているなど
→実態クラスはインターフェースに依存しているなど

特定のデータストアに結びついたクラスに依存すると、気軽にコードの実行が不可能になる
→データベース、テーブルを必ず準備しなければならなくなるため
→インターフェースに依存するべき

依存関係逆転の原則
→上位モジュールは下位レベルのモジュールに依存してはならない。どちらのモジュールも抽象に依存すべきである。
→抽象は、実装の詳細に依存してはならない。実装の詳細が抽象に依存すべきである。

レベル
→入出力からの距離
→低レベル→機械に近い具体的な処理
→高レベル→人間に近い抽象的な処理

なぜ上位モジュールは下位レベルのモジュールに依存してはならないか
→低レベルなモジュールの変更を理由に、重要な高レベル(ビジネスロジックなど)を変更するなどは起きてほしくない

Service Locatorパターン
→ServiceLocatorに何を使うか登録(データストアであるならインメモリDB、RDBなど)
→ServiceLocatorを参照することにより、依存先を変える

public UserApplicationService()
{
    // ServiceLocator経由でインスタンスを取得する
    this.userRepository = ServiceLocator.Resolve<IUserRepository>();
}

// 以下のように参照を変える
ServiceLocator.Register<IUserRepository, InMemoryUserRepository>();
ServiceLocator.Register<IUserRepository, UserRepository>();

Service Locatorパターンがアンチパターンだと言われる理由

  • 依存関係が外部から見えず楽なる
    →上記のUserApplicationServiceを使用するときに、ServiceLocatorに何も登録されていなければエラーになってしまう
    →これはクラスのシグネチャを見ただけではわからず、実装を確認しなければならない(bad)
  • テストの維持が難しくなる
    →実装の変更が行われてテストコードが壊れた際に、テストコードを実行するまで、それが壊れていることに気付けない
    *ちょっとあんまり理解できんかった

IoC(DI) Containerパターン
*IoC(Inversion of Control:制御の反転)
DI(Dependency Injection)を使用すると、あちこちにインスタンスを生成するコードが生まれてしまう。
つまり、具象クラスを変えてインジェクションを行いたい場合は、インスタンス化されている部分を全て見つけて書き換えなければならない。
そこで、IoC Container(依存解決の設定を登録できる)を参照してインスタンス化することにより、その問題を解決する。設定はスタートアップスクリプトなどで行う。

スタートアップスクリプト、つまり、メインのプログラムを動かす前に動かすスクリプトでIoC Containerに依存関係の設定を登録する。
また、依存関係の設定スクリプトを用意して、テスト用、本番用など出し分ける。

うぃうぃ

ファクトリ

→複雑なオブジェクト生成処理をオブジェクトとして定義する
→ドメインを表現する手助けをするもの

  • シンプルにFactoryクラスで複雑な処理をまとめてオブジェクトを作成する
    →コンストラクタ内で他のオブジェクトを生成するかが一つの目安
  • メソッド内でオブジェクトを生成して返す
    →例)Userクラス、Circleクラスタがあり、Circle(user.Id, circle_name)としたい場合に、UserクラスにCircleFactory(circle_name)メソッドを作成し、return Circle(user.Id, circle_name)とする。user.Idを取得するgetterを作成しなくて良いことがメリット。
うぃうぃ

データの整合性

(直接ドメイン駆動に関わることのないパート)

  • ユニークキー制約を使用する
    →ドメインのルールが技術基盤(RDBのユニークキー制約)に依存してしまっている
    →ドメインのルールが本来記述されるべきところから漏れ出している
    なので、ドメインでの重複チェックとユニークキー制約の両方を利用し、ユニークキー制約はセーフティネットとして使う
  • トランザクション処理を使う
    →特定の技術に依存してトランザクション処理を書いてしまうと依存が大きくなってしまう
    →C#などではトランザクションスッコープを使用する(トランザクションの範囲を指定して、RDBなどを使用した場合は自動でトランザクションがはられる)
    →AOP(Aspect Oriented Programming)を利用する(Javaのアノテーションなど)
    →ユニットオブワークを利用する(あるオブジェクトの変更を記録するオブジェクト)
    *ユニットオブワークあんまり分からんかった

*トランザクション処理を使用するときは、ロックに注意すること

うぃうぃ

アプリケーションの組み立て手順
→ユースケースを洗い出す
→実現に必要なドメインオブジェクトを準備する
→アプリケーションサービスを準備する

うぃうぃ

集約

→関連するオブジェクト同士を線で囲う境界
境界:どのオブジェクトが含まれるのかを定義する
ルート:集約に含まれる特定のオブジェクト

集約の外部から境界内部のオブジェクトを操作してはいけない。
集約を操作するための直接のインターフェースとなるオブジェクトが集約ルート(AR:Aggregate Root)
→集約内部の不変条件を保つため
→デメテルの法則により達成される

フィールドをプライベートにしたい
→getterを使用しない(効率的だが、制限力が弱い)
→通知オブジェクトを使う

集約をどう区切るか
→変更の単位で区切る
→なるべく小さく保つ

リポジトリは集約の単位ごとに用意する

集約からプロパティを経由してメソッドを呼び出すことが可能であることが問題
→Entityであれば識別子をインスタンスの代わりに持つ

getterはなるべく使用しない方が良いが、Entityの識別子であれば許容できる。
→識別子に対してビジネスルールが記述されることが多くないため

うぃうぃ

仕様

→あるオブジェクトがある評価基準に達しているかを判定するオブジェクト
→エンティティや値オブジェクトにリポジトリを操作させないために取られる手段

オブジェクトの評価処理を安直にオブジェクト自身に実装してしまうと、オブジェクトの趣旨がぼやけてしまう。

仕様とリポジトリを組み合わせた使い方
→リポジトリに仕様を引き渡して、仕様に合致するオブジェクトを検出する
→重要なルールをリポジトリに漏れ出すことを防ぐ
→デメリットは全件検索するため件数が多いとパフォーマンスが悪い

この章はむずい

うぃうぃ

アーキテクチャ

→ドメインとアーキテクチャは切り分けて考えるべき。DDDはドメインが隔離されていることのみが重要。また、アーキテクチャに従ったからと言ってDDDになるわけではない。

利口なUI
→ドメインオブジェクトに記載されるべき重要なルールやふるまいが、UIに記述されてしまっている状態

レイヤードアーキテクチャ
→プレゼンテーション層、アプリケーション層、ドメイン層、インフラストラクチャ層で構成されており、依存が上から下にのみ許される

ヘキサゴナルアーキテクチャ
→アプリケーション以外のモジュールは差し替え可能にする。
→レイヤードアーキテクチャと異なる点はインターフェースを利用した依存関係の整理の言及をしているかどうか
→昨今はレイヤードアーキテクチャでインターフェースを利用しているので、違いはほとんどない

クリーンアーキテクチャ
→ヘキサゴナルアーキテクチャとほぼ同じ
→ビジネスルールをカプセル化したモジュールを依存の中心に添えるというコンセプト
→ヘキサゴナルアーキテクチャとの違いは実装方法が具体的に示されているかどうか

うぃうぃ

最終章

軽量DDD
→ドメイン駆動設計に登場するパターンだけを取り入れる手法

ドメインモデルをドメインエキスパートと深く話し合うことで作成する。
また、ユビキタス言語を使いましょい。

境界付けられたコンテキスト
→モデルに対しての捉え方が異なるところで境界を引く

コンテキストマップ
→コンテキスト同士の関係を定義し、ドメイン全体を俯瞰できるようなもの

うぃうぃ

Appendix D

フォルダ構造などが載っていてどうDDDを実践していけば良いかが書かれている。
より具体的に書かれており読み返したいパート