PHP Mentors — Practical DDD #1: Specificationパターンの例

1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

Practical DDD #1: Specificationパターンの例

あるエンティティに対して、何らかの条件を満たすものをグループとして扱いたいことがよくあります。安直な実装としては、条件を加味してエンティティを抽出するようなメソッドをリポジトリに追加する方法をとってしまうかもしれません。

このようにリポジトリにメソッドを持たせてしまうと、条件が集合操作の中に埋もれてしまい、再利用しづらくなります。そこでDDDではSpecification(仕様)としてこういった条件をくくり出すパターンが紹介されています。『エリック・エヴァンスのドメイン駆動設計』p.229「仕様の適用と実装」では、次のように書かれています。

仕様の価値の多くは、全く異なるように見えるアプリケーションの機能を統一することにある。以下に挙げる3つの目的のうち、1つでも当てはまれば、オブジェクトの状態を(筆者注:仕様として)定義する必要があるだろう。

  1. オブジェクトを検証して、何らかの要求を満たしているか、何らかの目的のための用意ができているかを調べる。
  2. コレクションからオブジェクトを選択する(期限が超過した請求書を問い合わせる場合など)。
  3. 何かの要求に適合する新しいオブジェクトの生成を定義する。

検証、選択、要求に応じた構築という、これら3つの用途は、概念レベルでは同じものである。仕様のようなパターンがなければ、同一のルールがさまざまな外見で、場合によっては矛盾した形式で出てきてしまうかもしれない。それにより、概念の統一性が失われかねない。仕様パターンを適用することで、実装が異なってしまう場合でも、一貫性のあるモデルを使用できるようになる。

『エリック・エヴァンスのドメイン駆動設計』第9章 暗黙的な概念を明示的にする p.229

具体例で見てみましょう。キャンペーンの開催情報を管理するシステムがあり、キャンペーンエンティティには開催開始日付が設定されています。この日付とシステムの現在日時を比較することで、キャンペーンが開催中かどうかを判定します。この条件を満たすキャンペーンエンティティを「開催中キャンペーン」というグループで扱いたいとします。「開催中かどうか」の条件は、キャンペーンエンティティが満たすべき仕様となります。この例では単なる日時の比較なので単純ですが、もっと複雑な条件の場合なども想像してください。また、開発・運用中にこのような仕様が変更される可能性もあるでしょう。そういった場合に、仕様そのものが1箇所で表現されているととてもメンテナンスしやすくなります。

DDD本では、仕様オブジェクトには次のような責務を持たせています。

  1. 単一のエンティティに対して、仕様を満たすかどうかを判定する
  2. リポジトリから仕様を満たすエンティティの集合を取得する

1.については、対象のエンティティを引数にとるisSatisfiedBy()メソッドを実装します。

2.については、対象エンティティのリポジトリオブジェクトを引数にとるsatisfyingElementsFrom()メソッドとして実装します。このメソッドでは、リポジトリから一旦汎用的な条件でエンティティの集合を取得し、集合の各エンティティに対してisSatisfiedBy()を使って仕様を満たすかどうかをチェックするような実装にします。

このように実装したサンプルをGitHubにあげてあります。

OpenCampaignSpecificationクラスは次のようになっています。

phpmentors-example-campaign / src / Example / CampaignBundle / Domain / Specification / OpenCampaignSpecification.php

class OpenCampaignSpecification
{
    /**
     * @var Clock
     */
    protected $clock;

    /**
     * @param Clock $clock
     */
    public function __construct(Clock $clock)
    {
        $this->clock = $clock;
    }

    /**
     * @param  Campaign $campaign
     * @return bool
     */
    public function isSatisfiedBy(Campaign $campaign)
    {
        if (
            ($campaign->getStartDate() <= clock->getCurrentDateTime()) &&
            ($campaign->getEndDate() > $this->clock->getCurrentDateTime())
        )
        {
            return true;
        }

        return false;
    }

    /**
     * @param  CampaignRepository $campaignRepository
     * @return ArrayCollection
     */
    public function satisfyingElementsFrom(CampaignRepository $campaignRepository)
    {
        $campaignList = $campaignRepository->findAll();
        $campaignList = $campaignList->filter(function($campaign) { return $this->isSatisfiedBy($campaign); });

        return $campaignList;
    }
}
image

仕様をくくり出すことで、リポジトリがさまざまな条件のファインダメソッドで膨れ上がることを避けられます。分離した後に、必要であれば最適なクエリーを仕様とリポジトリとで協調して生成する例などもDDD本には紹介されています。他に、ファクトリーオブジェクトと協調して、最初から仕様を満たすオブジェクトを生成するといった使い方もあります。

参考

『エリック・エヴァンスのドメイン駆動設計』第9章 暗黙的な概念を明示的にする - 「仕様(Specification)」(p. 226〜)

仕様オブジェクトとリポジトリとで協調動作する際の実装として、ダブルディスパッチが使われています。一般的には仕様はバリエーションを持ちますが、それに対するリポジトリは単一なので、2次元の可変性確保という目的とは合致しません。関心・責務の分離の観点でダブルディスパッチについて解説した記事も参照してください。

practical.ddd ddd

See more posts like this on Tumblr

#practical.ddd #ddd