エンティティのライフサイクルに基いたクラス設計

エンティティとはその属性によってではなく識別子によって識別されるオブジェクトである。 属性が違っていても識別子が同一であればそのエンティティは同一のものと見なされる。 そのためエンティティは多くの場合、変更可能(Mutable)なオブジェクトとして表現される。 変更可能(Mutable)なオブジェクトは最初に生成されてから、それ以後に状態を変えることがある。 この状態遷移の流れがエンティティのライフサイクルである。

では、エンティティの状態はどのように変化するだろう。 それは、ドメインモデルを使って業務手順を記述したユースケースによって決まる。 ユースケースでは業務手順に基いてエンティティを作成し、状態を変更し、あるいは削除する。 これがエンティティのライフサイクルだ。

PHPにおけるエンティティの実装

エンティティはユースケースによってその状態を変えるが、ドメインモデルの一部であるエンティティをドメインのルールに反するような状態にしてはならない。 エンティティがドメインのルールに反する状態にあるというのは、バグに他ならないからだ。 実装レベルでエンティティがドメインのルールに反する状態にすることをできなくするのが望ましい。

基本的な方針としてエンティティの持つプロパティが、DBの列名と一致するように実装する。 プロパティは自由に参照・変更できないように private に隠蔽しておく。

class Person
{
    private $person_id;

    private $first_name;

    private $last_name;

    ...
}

コンストラクター

エンティティのコンストラクターprivate にして隠蔽しておく。 自由にインスタンスを作られては状態を適切に管理できないからだ。 インスタンスの生成は後述のFactory Methodによって行う。

private function __construct()
{
}

ゲッター

エンティティの属性値として、ただの文字列(String)や数値(Integer)ではドメインの関心を表現するオブジェクトして適切ではないことがある。 その場合はプロパティをそのまま返すのはでなく、ドメインモデル(値オブジェクト)を返すようにする。 そうでない場合はそのままプロパティを返却する。

public function getName()
{
    return new PersonName($this->first_name, $this->last_name);
}

セッター

単純なプロパティのセッターはすべて private にして隠蔽しておく。 自由に属性を設定できては状態を適切に管理できない。 外から状態の変更するメソッドはセッターではなくドメインに基いた振舞いとして別途適切に実装する。 セッターで設定される値を検証して値がエンティティの属性として適切ではなければ例外を投げるということもありうる。 この検証はあくまで属性レベルの検証なので、エンティティ全体の状態として正しいことを保証しているわけではない。

private function setFirstName($first_name)
{
    if ($first_name === null || $first_name === '') {
        throw new \InvalidArgumentException('名前は必ず設定する必要がある。');
    }
    $this->first_name = $first_name;
}

Factory Method

エンティティを生成する時は必ずFactory Methodを使用する。 コンストラターでいいと思うかもしれないが、PHPはメソッドのオーバーロードができないので別途Factory Methodを作った方が都合がいい。 このメソッドで作成されたエンティティの状態はドメインにおいて合法であることが保証される。

public static function create(Name $name)
{
    // first_name や last_name を持たない人間を作ることはできない
    $person = new Person();
    $person->setFirstName($name->firstName());
    $person->setLastName($name->lastName());
    return $person;
}

エンティティのライフライクルに基いた振舞い

これはドメインのルールに基いて、エンティティの状態を変更させるメソッドである。 これは対象とするドメインによって異なるので、その都度考える必要がある。 多くの場合ドメインの用語(ユビキタス言語)で動詞に対応するものが命令形で定義されることになる。

例:名前を変更する

public function changeName(Name $name)
{
    $this->setFirstName($name->firstName());
    $this->setLastName($name->lastName());
}

まとめ

業務手順を表すユースケースとは、エンティティのライフサイクルの記述に他ならない。 エンティティのライフサイクルはドメインに基いたものでなければならず、ユースケース実装に際してエンティティを不適切な状態にできるのは望ましくない。 これを回避するために、コンストラクターとセッターを隠蔽して、エンティティのライフサイクルに基いた振舞いだけを公開するといった手法を紹介した。 これによってエンティティを不適切な状態にすることができなくなり、ライフサイクルを適切に管理することができるようになった。