ドメイン駆動設計(Domain-Driven Design)- ①

はじめに

 ドメイン駆動設計(DDD: Domain-Driven Design)は、ソフトウェア開発における設計手法の一つです。DDDは、ソフトウェアの設計をビジネスのドメイン(業務領域)に強く結びつけることを目的としており、特に複雑なビジネスロジックを持つシステムの開発に有効です。
 今回は、DDDの基本的な考え方と、その主要な要素の一つであるバリューオブジェクトとエンティティを紹介します。
 ※サンプルコードは全てPHPで記載しています。
 

ドメイン駆動設計の基本的な考え方

ドメインとは

 DDDにおいて、ドメインとは、ソフトウェアが解決しようとする問題領域を指します。例えば、銀行業務や在庫管理など、特定のビジネスや業務領域がドメインとなります。DDDでは、ドメインのエキスパートと協力して、ドメインを深く理解し、その知識をソフトウェアに反映させることが重要です。

ドメインモデル

 ドメインモデルは、ビジネスの概念やルールをソフトウェアで表現するための抽象的なモデルです。ドメインモデルの構築には、次の点に注意が必要です。
・ビジネスの理解
 ドメインエキスパートと開発者が協力して、ビジネスの深い理解を共有します。開発者はドメインエキスパートの知識を引き出し、それをモデルに反映させます。
・継続的な改善
 ドメインモデルは固定的なものでなく、ビジネスの変化や理解の深化に伴って継続的に改善されます。
  

ユビキタス言語

 DDDの重要な概念の一つにユビキタス言語(Ubiquitous Language)があります。ユビキタス言語は、開発者とドメインエキスパートが共通の用語や概念を使ってコミュニケーションするための言語です。ユビキタス言語を使うことで、誤解や曖昧さを減らし、チーム全体の理解を統一できます。

境界づけられたコンテキスト

 大規模なシステムでは、ドメインを更に小さなサブドメインに分割し、それぞれを境界づけられたコンテキストとして扱います。各コンテキスト内では独立したモデルが使用され、コンテキスト間の依存関係を明確に管理します。

集約

 集約(Aggregate)は、関連するバリューオブジェクトとエンティティをグループ化し、一貫性を保つための設計パターンです。集約には、ルートとなるエンティティがあり、外部からのアクセスはこのルートエンティティを通じて行われます。
 バリューオブジェクト、エンティティについては後述します。
   

リポジトリ

 リポジトリは、エンティティの取得や保存を抽象化するための設計パターンです。これによりデータベースの操作をドメインロジックから分離でき、クリーンな設計を維持することができます。

 

バリューオブジェクトとエンティティ

 バリューオブジェクトとエンティティは、どちらもドメインモデルを表現するためのオブジェクトです。バリューオブジェクトとエンティティの使い分けは、DDDにおいて非常に重要です。バリューオブジェクトは、識別よりも値の組み合わせや不変性が重要な場合に使用されます。この区別により、ドメインモデルの設計がより明確になり、ビジネスロジックを効果的に表現することができます。
 一方、エンティティは一意の識別が必要な、時間と共に変化するオブジェクトをモデル化するのに適しています。

バリューオブジェクト(Value Object)

 バリューオブジェクト(Value Object)は、その属性によってのみ定義され、識別子を持たないオブジェクトです。
 バリューオブジェクトは不変であり、その値が変更される場合は新しいインスタンスが作られます。

・バリューオブジェクトの特徴
1.不変性
 バリューオブジェクトは作成後にその状態が変更されません。変更が必要な場合は新しいインスタンスを作成します。
2.属性による等価性
 バリューオブジェクトは、その属性が同じであれば同一とみなされます。同じ属性を持つ2つのバリューオブジェクトは同じものとして扱われます。
3.副作用のない操作
 バリューオブジェクトに対する操作は、新しいバリューオブジェクトを返すことにより、副作用を避けることができます。

・バリューオブジェクトの具体例
 バリューオブジェクトの例として、お金を表すクラスを考えてみましょう。お金オブジェクトは金額と通貨を表すプロパティをそれぞれ持つものとします。メソッドはプロパティの値を取得するものの他に、等価性を比較する`equals()`、金額を加算する`add()`を作成します。

class Money
{
    private float $amount;
    private string $currency;

    public function __construct(float $amount, string $currency)
    {
        if ($amount < 0) {
            throw new InvalidArgumentException('金額は0以上である必要があります');
        }

        if (empty($currency)) {
            throw new InvalidArgumentException('通貨が空です');
        }

        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function getAmount(): float
    {
        return $this->amount;
    }

    public function getCurrency(): string
    {
        return $this->currency;
    }

    public function equals(Money $money): bool
    {
        return $this->amount === $money->getAmount() && $this->currency === $money->getCurrency();
    }

    public function add(Money $money): Money
    {
        if ($this->currency !== $money->getCurrency()) {
            throw new InvalidArgumentException('通貨が異なります');
        }

        return new Money($this->amount + $money->getAmount(), $this->currency);
    }
}

 コンストラクタでは、金額と通貨を受け取り、それぞれの値が正しいかどうかをチェックしてからプロパティにセットしています。`equals()`では、金額と通貨が等しいかどうかで等価性の判定を行っています。`add()`では、加算する金額の通貨が自身の通貨と一致しているかをチェックしてから、金額を加算した新しい`Money`オブジェクトを返しています。
 これらの実装により、作成される`Money`オブジェクトは、金額と通貨が正しい値であることが保証され、比較や加算などの操作も副作用なく行うことができます。また、外部からプロパティを変更するメソッドを持たない為、作成されたインスタンスの値が変更されないことも保証されます。
 どこまでをバリューオブジェクトにするかは、ビジネスの要件や設計によって異なりますが、プロパティを更に細かく分割して、他のバリューオブジェクトをプロパティとして持つことも可能です。例えば、以下のように通貨を表す`Currency`クラスを別途定義し、`Money`クラスのプロパティとして持つことが可能です。

class Currency
{
    private string $code;

    public function __construct(string $code)
    {
        if (empty($code)) {
            throw new InvalidArgumentException('通貨コードが空です');
        }

        // 通貨コードのバリデーションなどが必要な場合はここで行うことができる

        $this->code = $code;
    }

    public function getCode(): string
    {
        return $this->code;
    }

    public function equals(Currency $currency): bool
    {
        return $this->code === $currency->getCode();
    }
}

class Money
{
    private float $amount;
    private Currency $currency;

    public function __construct(float $amount, Currency $currency)
    {
        if ($amount < 0) {
            throw new InvalidArgumentException('金額は0以上である必要があります');
        }

        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function getAmount(): float
    {
        return $this->amount;
    }

    public function getCurrency(): Currency
    {
        return $this->currency;
    }

    public function equals(Money $money): bool
    {
        return $this->amount === $money->getAmount() && $this->currency->equals($money->getCurrency());
    }

    public function add(Money $money): Money
    {
        if (!$this->currency->equals($money->getCurrency()) {
            throw new InvalidArgumentException('通貨が異なります');
        }

        return new Money($this->amount + $money->getAmount(), $this->currency);
    }
}

 Moneyクラスは以下のように使用できます。

// インスタンスの作成
$money1 = new Money(100, new Currency('JPY'));
$money2 = new Money(200, new Currency('JPY'));

// 等価性のチェック
if ($money1->equals($money2)) {
    echo 'money1とmoney2は等しいです';
} else {
    echo 'money1とmoney2は等しくありません';
}

// 通貨の加算
try {
    $money3 = $money1->add($money2);
    echo 'money1とmoney2の合計は' . $money3->getAmount() . $money3->getCurrency()->getCode() . 'です';
} catch (InvalidArgumentException $e) {
    echo $e->getMessage();
}

エンティティ(Entity)

 DDDにおいて、エンティティ(Entity)は、識別子によって一意に識別されるオブジェクトを指します。この識別子は、エンティティのライフサイクルの全体を通じて一貫した同一性を保持します。一方でエンティティの他の属性や状態は時間と共に変化することがあります。

・エンティティの特徴
1.一意性
 エンティティは、一意の識別子を持ちます。識別子はエンティティのライフサイクル全体を通じて一貫して保持され、識別子が同じであれば同一とみなされます。
2.状態の変更
 エンティティは状態を持ち、その状態は変更可能です。
3.ライフサイクルの管理
 エンティティはそのライフサイクルにおいて、生成から廃棄まで一貫した同一性を持ちます。これにより、エンティティの状態や振る舞いが一貫して管理されます。

・エンティティの具体例
 一般的にユーザーと呼ばれるものはエンティティになります。名前とメールアドレスをプロパティに持つクラスを考えてみましょう。ユーザークラスは識別子としてIDを持ちます。メソッドは、プロパティの値を取得するものと、等価性を判定する`equals()`、メールアドレスを変更する`changeEmail()`を作成します。

class User
{
    private int $id;
    private UserName $name;
    private Email $email;

    public function __construct(int $id, UserName $name, Email $email)
    {
        if ($id < 0) {
            throw new InvalidArgumentException('IDは0以上の値を指定してください。');
        }

        $this->id = $id;
        $this->name = $name;
        $this->email = $email;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): UserName
    {
        return $this->name;
    }

    public function getEmail(): Email
    {
        return $this->email;
    }

    public function equals(User $user): bool
    {
        return $this->id === $user->getId();
    }

    public function changeEmail(Email $email): void
    {
        $this->email = $email;
    }
}

 名前とメールアドレスは前項で説明したバリューオブジェクトで定義したクラスを使用します(各クラスの詳細は割愛します)。コンストラクタでは、ID、名前、メールアドレスを受け取り、それぞれの値が正しいかどうかをチェックしてからプロパティにセットしています。`equals()`では、IDが等しいかどうかで等価性の判定を行っています。名前やメールアドレスが違ってもIDが同じであれば同じユーザーとみなされます。`changeEmail()`では、メールアドレスを新しい値に変更しています。
 Userクラスは以下のように使用できます。

// インスタンスの作成
$user = new User(1, new UserName('山田 太郎'), new Email('taro_yamada@sample.com'));
$user2 = new User(2, new UserName('佐藤 次郎'), new Email('jiro_sato@sample.com'));

// 状態の変更
$user->changeEmail(new Email('taro_yamada@test.co.jp'));

// 等価性のチェック
if ($user->equals($user2)) {
    echo '同じユーザーです。';
} else {
    echo '違うユーザーです。';
}

 IDによって区別することにより、名前やメールアドレスなどの状態が変更されても同一のユーザーを判断することができます。また、`changeEmail()`のようにユーザーに関連するビジネスロジックをUserクラスに集中させることにより、コードの可読性と保守性が向上します。

 

まとめ

 DDDの基本的な考え方と、その中の重要な要素であるバリューオブジェクトとエンティティを紹介しました。バリューオブジェクトとエンティティはどちらもドメインモデルを表現するためのオブジェクトであり、適切に設計することによりシステム全体の品質を高め、メンテナンスを容易にすることができます。バリューオブジェクトで紹介したMoneyクラスは金額計算を行うためのドメインモデルを想定していました。
 もしビジネスにおいて、お金の媒体(紙、硬貨)などが重要な場合はそれらもプロパティとして持たせる必要があります。バリューオブジェクトやエンティティを作成する前に、適切なドメインモデルを設計することが重要になります。

 
 

お問い合わせはこちら