LaravelでDDD その1

この記事は[WHITEPLUS Advent Calendar 2016](http://qiita.com/advent-calendar/2016/whiteplus "") 8日目になります。

こんにちは。株式会社ホワイトプラス、エンジニアの @ngmy です。

ホワイトプラスでは主に、

* 衣類の宅配クリーニングサービス「[リネット](https://www.lenet.jp/)」
* 布団の宅配クリーニングサービス「[ふとんリネット](http://www.futonlenet.jp/)」
* 靴の宅配クリーニングサービス「[くつリネット](http://www.kutsulenet.jp/)」
* クリーニングの保管サービス「[リネット保管](http://www.lenet-hokan.jp/)」

という4つのサービスのサーバサイドを担当しており、 PHP  + Laravelを使用して、DDDで開発しています。
これらのサービスは、宅配クリーニングというサービスの性質上、ネットだけでなく、工場や物流といったリアルな事業ドメインも扱うことになるため、DDDのやりがいがあるサービスだと思います。

# TL;DR

この記事では、『エリック・エヴァンスのドメイン駆動設計』で紹介されている仕様パターンについて、架空の宅配クリーニングのモデルを例にとって書いてみます。

# 仕様パターン

あるエンティティに対して、何らかのビジネスルールを満たすかを検証したいことはよくあります。
単純な実装としては、検証メソッドをエンティティに追加すればいいです。
しかし、検証が複雑なロジックになったり、他のエンティティに依存したりすると、エンティティ自体がごちゃごちゃしてしまいます。
かといって、検証をアプリケーション層で行ってしまうと、ビジネスルールがアプリケーション層に流出してしまいます。
こういった場合に、ビジネススールを仕様（Specification）クラスとして括り出す方法が仕様パターンです。

![advent_calendar_20161208_1.png](https://qiita-image-store.s3.amazonaws.com/0/9588/b7457c51-516c-ac96-4c06-ab7bdcbc2f86.png)

# 架空の宅配クリーニングの例
例として、下記のような架空の宅配クリーニングのモデルを考えてみます。モデルはあくまで架空であり、実在するサービス・企業とは一切関係ありません。

![advent_calendar_20161208_2.png](https://qiita-image-store.s3.amazonaws.com/0/9588/f78ef37d-10c2-7f74-61af-2ca84b9c0a62.png)

* 注文（Order）は複数のクリーニング品（Item）を持ちます
* 注文は複数のクリーニング処理工程（OrderProcess）を持ちます

ここで、あるクリーニング品がキャンセル可能かを判定したいとします。
単純な実装では、クリーニング品クラスに下記のような判定メソッドを用意すればよいです。

```php
/**
 * キャンセル可能かを判定する
 *
 * @return boolean キャンセル可能な場合はtrue
 */
isCancelable()
{
    // キャンセル可能かを判定する
}
```

しかし、きっとキャンセル可能かの判定には、クリーニング品の情報だけではなく、注文がクリーニング処理工程のどこにあるかや、そのクリーニング処理工程が完了してから何日以内か、といった情報が関係してくるでしょう。こういった判定をクリーニング品クラスにやらせると、クリーニング品クラスがごちゃごちゃしてしまいます。
かといって、判定をアプリケーション層で行うと、たしかにクリーニング品クラスはすっきり保てますが、アイテムがキャンセル可能かというビジネスルールがドメイン層から流出してしまいます。

そこで仕様パターンを使って、ビジネスルールを仕様クラスとして括り出してみます。

```php
/**
 * キャンセル可能クリーニング品仕様
 */
Class CancelableItemSpecification
{
    protected $orderRepository;

    protected $currentDate;

    public function __construct(OrderRepositoryInterface $orderRepository, DateTime $currentDate)
    {
        $this->orderRepository = $orderRepository;
        $this->currentDate = $currentDate;
    }

    /**
     * 仕様を満たすかを判定する
     *
     * @param object $obj クリーニング品
     * @return boolean 仕様を満たす場合はtrue
     */
    isSatisfiedBy($obj)
    {
        $order = $this->orderRepository->orderOfId($obj->order_id);  // get_parent_class($order) => Illuminate\Database\Eloquent\Model
        // 注文や注文のクリーニング処理工程、現在日時を使用し、クリーニング品がキャンセル可能かを判定する
    }
}
```

クライアントクラスでは、上記の仕様クラスを使って、クリーニング品がキャンセル可能かを判定します。

```php
$items = $this->itemRepository->allItemsOfOrder($order);  // get_class($items) => Illuminate\Database\Eloquent\Collection
$cancelableItemSpec = app()->make('CancelableItemSpecification');
$cancelableItems = $items->filter(function ($item) use ($cancelableItemSpec) {
    return $cancelableItemSpec->isSatisfiedBy($item);
}); 
```

モデルは下記のようになります。

![advent_calendar_20161208_3_2.png](https://qiita-image-store.s3.amazonaws.com/0/9588/e72c70c4-3b72-cf13-b470-f02cef778560.png)

# まとめ

仕様パターンを使ってビジネスルールを括り出してみました。

* エンティティの責務をすっきりと保つことができました
* ビジネスルールを明確に仕様クラスとして括り出すことができました

その2では、複数の仕様を組み合わせる方法について書いてみようと思います。

----

明日は弊社デザイナー @makkoruri の「フライパンでつくるサンマの炊き込みご飯」です。

# ホワイトプラスではエンジニアを募集しています
ホワイトプラスでは、[「ネット」×「リアル」なサービスをDDDで実現するぜ！という技術で事業に貢献したいエンジニアを募集](https://www.wantedly.com/projects/69257)しております。

# 参考
* [エリック・エヴァンスのドメイン駆動設計](https://www.amazon.co.jp/dp/4798121967)
* [Specifications](http://martinfowler.com/apsupp/spec.pdf)