Open13

「ドメイン駆動設計入門」メモ

haseyuyhaseyuy

依存関係逆転の原則(Dependency Inversion Principle)

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

抽象型を利用するようになると、具象型にに向いていた依存の矢印が抽象型を向くようになる
この抽象型を用いた依存関係の制御は「依存関係逆転の原則」と呼ぶ

haseyuyhaseyuy

低レベルとえいば機械に近い具体的な処理を指す
データストアを操作するUserRepositoryの処理は、下位レベルの処理

改善前

以下の依存関係の図は、「上位レベルのモジュールは下位レベルのモジュールに依存してはならない」という原則に反している。


改善後

  • UserApplicationServiceが抽象型であるIUserRepositoryを参照すると以下になる
  • もはや上位レベルのモジュール(UserApplicationService)が下位レベルのモジュール(UserRepository)に依存しなくなり、「どちらのモジュールも抽象に依存すべきである」という原則に合致する
  • もともと具体的な実装に依存していたものが抽象に依存するようになり、依存関係は逆転したのである
haseyuyhaseyuy

主導権を抽象に

  • 抽象が詳細に依存するようになると、低レベルのモジュールのおける方針の変更が高レベルのモジュールに波及する
  • 低レベルなモジュールの変更を理由にして、重要な高レベルのモジュールを変更する(例えばデータストアの変更を理由にビジネスロジックを変更する)などということは起きてほしくない事態である
haseyuyhaseyuy

https://github.com/inversify/InversifyJS
チュートリアルをやってみる

DIは、あるクラス(A)の中で別のクラス(B)のインスタンスを利用する場合、BクラスをAクラス内でインスタンス化するのではなく、コンストラクタ、プロパティ、メソッド経由でAクラスにインスタンスに渡すこと

依存性とは、クラスAが利用する(依存する)オブジェクト
それを内部で生成するのではなく、外部から渡す(注入)するので、依存性の注入と呼ぶ

DIパターンのメリット:

  • クラス間の結合を低くできること
  • それによって手スタビリティを高くできること
haseyuyhaseyuy

Step 1: Declare your interfaces and types

interface定義

src/interfaces.ts
export interface Warrior {
    fight(): string;
    sneak(): string;
}

export interface Weapon {
    hit(): string;
}

export interface ThrowableWeapon {
    throw(): string;
}

IoCコンテナで管理されるクラスの識別子

src/types.ts
const TYPES = {
    Warrior: Symbol.for("Warrior"),
    Weapon: Symbol.for("Weapon"),
    ThrowableWeapon: Symbol.for("ThrowableWeapon")
}

export { TYPES };

Note: It is recommended to use Symbols but InversifyJS also support the usage of Classes and string literals (please refer to the features section to learn more).

haseyuyhaseyuy

Step 2: Declare dependencies using the @injectable & @inject decorators

src/entities
import { injectable, inject } from "inversify";
import "reflect-metadata";

import { ThrowableWeapon, Warrior, Weapon } from "./interface";
import { TYPES } from "./types";

// Iocコンテナで管理されるクラスに対して、@injectableデコレーターを付与
@injectable()
class Katana implements Weapon {
    public hit() {
        return "cut!";
    }
}

@injectable()
class Shuriken implements ThrowableWeapon {
    public throw() {
        return "hit!";
    }
}

@injectable()
class Ninja implements Warrior {
    private weapon: Weapon;
    private throwableWeapon: ThrowableWeapon;

    // コンストラクタ引数で依存性を注入している
    public constructor(
        // types.tsで定義したTYPESを使い、IoCコンテナに対して取得したいクラスを伝える
        @inject(TYPES.Weapon) weapon: Weapon,
        @inject(TYPES.ThrowableWeapon) throwableWeapon: ThrowableWeapon
    ) {
        this.weapon = weapon;
        this.throwableWeapon = throwableWeapon;
    }

    public fight() {
        return this.weapon.hit();
    }

    public sneak() {
        return this.throwableWeapon.throw();
    }
}

export { Katana, Ninja, Shuriken };
haseyuyhaseyuy

Step 3: Create and configure a Container

src/inversify.config.ts
import { Container } from "inversify";
import { TYPES } from "./types";
import { Warrior, Weapon, ThrowableWeapon } from "./interface";
import { Ninja, Katana, Shuriken } from "./entities";

const container = new Container();
// container.bind<"取得する時の型">("識別子").to("登録対象クラス")
container.bind<Warrior>(TYPES.Warrior).to(Ninja);
container.bind<Weapon>(TYPES.Weapon).to(Katana);
container.bind<ThrowableWeapon>(TYPES.ThrowableWeapon).to(Shuriken);

export { container };
haseyuyhaseyuy

Step 4: Resolve dependencies

src/index.ts
import { container } from "./inversify.config";
import { TYPES } from "./types";
import { Warrior } from "./interface";


function main() {
    const ninja = container.get<Warrior>(TYPES.Warrior);
    console.log(ninja.fight()); // -> cut!
    console.log(ninja.sneak()); // -> hit!
}

main();
haseyuyhaseyuy

値オブジェクト

1. ドメイン内の何かを計測したり定量化したり説明したりする

2. 状態を不変に保つことができる

private readonlyで宣言しているため、状態を不変に保つことができる

private readonly amount: number
private readonly currency: string

3. 関連する属性を不可欠な単位として組み合わせることで、概念的な統一体を形成する

  • 一つの属性値だけでは意味を持たず、それぞれが組み合わさることで適切な説明をできることを「概念的な統一体」と呼ぶ
  • 通貨の場合、「100」や「円」だけでは意味を持ちませんが、「100円」であれば、完結した意味を持つ値となる

4. 計測値や説明が変わったときには、全体を完全に置き換えられる

  • 値オブジェクトは「状態を不変に保つことができる」という特徴を持つため、途中で
    値の変更を行うことができない
  • 変更したい場合は、変更後の値を設定した新しいオブジェクトを生成して置き換える
    const myMoney = new Balance(1000, "JPY")
    const deposit = new Balance(1500, "JPY")
    const add_result = myMoney.add(deposit)
    console.log(add_result.Amount) // -> 2500

5. 値が等しいかどうかを、他と比較できる

  • エンティティは一意な識別子で判定できるが、値オブジェクトは各属性が持つすべての値を判定する
equals(money: Balance): boolean {
   return this.amount === money.Amount && this.currency === money.Currency
}

6. 協力関係にあるその他の概念に「副作用のない振る舞い」を提供する

  • 値オブジェクトを扱うメソッドは、全く副作用のない関数でなければいけない
  • add()もsub()も、自身のプロパティを変更せずに新しいインスタンスを返す
add(money: Balance) {
  return new Balance(this.amount + money.amount, this.currency)
}

class Balance {
    private readonly amount: number
    private readonly currency: string

    get Amount() {
        return this.amount
    }

    get Currency() {
        return this.currency
    }

    constructor(amount: number, currency: string) {
        if (amount === null || amount === undefined || !currency) throw new Error("未入力の項目があります")
        this.amount = amount
        this.currency = currency
    }

    equals(money: Balance): boolean {
        return this.amount === money.Amount && this.currency === money.Currency
    }

    add(money: Balance): Balance {
        if (this.currency !== money.Currency) throw new Error("通貨が異なります")
        return new Balance(this.amount + money.Amount, this.currency)
    }

    sub(money: Balance): Balance {
        if (this.amount < money.amount) throw new Error("残高より多く引き出すことはできません")
        if (this.currency !== money.Currency) throw new Error("通貨が異なります")
        return new Balance(this.amount - money.Amount, this.currency)
    }
}

function main() {
    const myMoney = new Balance(1000, "JPY")
    const deposit = new Balance(1500, "JPY")
    const add_result = myMoney.add(deposit)
    console.log(add_result.Amount) // -> 2500

    const withdraw = new Balance(3500, "JPY")
    const sub_result = myMoney.sub(withdraw)
    console.log(sub_result.Amount) // Error: 残高より多く引き出すことはできません
}

main();
haseyuyhaseyuy

エンティティ

1. 可変である

  • 例えばユーザ名は変更するケースがありうる

2. 同じ属性であっても区別される

  • エンティティ同士を区別するためには識別子が利用される
  • 例えば全く同じ名前のユーザがいても、それが同一のユーザかそれとも別ユーザかどうかはこの識別子によって区別される

3. 同一性により区別される

  • 例えばユーザ名を変更する前と変更した後のユーザは同一判定されるべき
haseyuyhaseyuy

エンティティの判断基準としてのライフサイクルと連続性

  • 何をエンティティにするかという判断の基準は、ライフサイクルが存在し、そこに連続性が存在するか
  • 例えばユーザは作成され、ユーザ名は変更され、最終的には削除される可能性もあるのでエンティティが適当
haseyuyhaseyuy

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

  • コードのドキュメント性が高まる
  • ドメインにおける変更をコードに伝えやすくする
haseyuyhaseyuy

ドメインサービス

  • ドメインサービスは値オブジェクトやエンティティと異なり、自身のふるまいを変更するようなインスタンス特有の状態をもたないオブジェクト
  • 値オブジェクトやエンティティなどのドメインオブジェクトには振る舞いされるが、システムには値オブジェクトやエンティティに記述すると不自然になってしまうふるまいが存在する
  • そのようなものをドメインサービで解決する

不自然な振る舞い

  • 例えばユーザの重複を確認する振る舞いを、ユーザクラス自身に重複の有無を自身に対して問い合わせるのは不自然
var userService = new UserService()

var userId = new UserId("id");
var userName = new UserName("naruse");
var user = new User(userId, userName);

var duplicateCheckResult = userService.Exists(user);

ドメインサービスの濫用が行き着き先

  • ドメインサービスにすべてのふるまいを記述するとエンティティにはゲッターとセッターだけが残る
  • このクラスの定義をみただけでは、どのようなふるまいやルールが存在するのか読み取ることは不可能
  • 語るべきことを何も語らないドメインオブジェクトの状態をドメインモデル貧血症と呼ぶ
  • 迷いが生じたらまずはエンティティや値オブジェクトに定義する