この記事はWHITEPLUS Advent Calendar 2016 17日目になります。
こんにちは。株式会社ホワイトプラス、エンジニアの @ngmy です。
ホワイトプラスでは主に、
という4つのサービスのサーバサイドを担当しており、 PHP + Laravelを使用して、DDDで開発しています。
これらのサービスは、宅配クリーニングというサービスの性質上、ネットだけでなく、工場や物流といったリアルな事業ドメインも扱うことになるため、DDDのやりがいがあるサービスだと思います。
TL;DR
この記事は前回の続きです。
前回は、架空の宅配クリーニングのモデルを例にとって、『エリック・エヴァンスのドメイン駆動設計』で紹介されている仕様パターンについて書きました。
今回は、引き続き架空の宅配クリーニングのモデルを例にとって、複数の仕様を組み合わせる方法について書いてみます。
合成仕様
仕様を使っていると、それらを組み合わせたいと思う状況はよくあります。
『エリック・エヴァンスのドメイン駆動設計』では、And仕様・Or仕様・Not仕様といった合成仕様(Composite Specification)を実装して仕様を組み合わせるパターンが紹介されています。
まず、仕様を抽象化して組み合わせ可能にするために、仕様インタフェースを作成します。
/**
* 仕様インタフェース
*/
interface SpecificationInterface
{
/**
* 自分自身とのAnd仕様を生成する
*
* @param SpecificationInterface $spec
* @return AndSpecification
*/
public function andSpecification(SpecificationInterface $spec);
/**
* 自分自身とのOr仕様を生成する
*
* @param SpecificationInterface $spec
* @return OrSpecification
*/
public function orSpecification(SpecificationInterface $spec);
/**
* 自分自身のNot仕様を生成する
*
* @return NotSpecification
*/
public function notSpecification();
/**
* 仕様を満たすかを判定する
*
* @param object $candidate 候補
* @return boolean 仕様を満たす場合はtrue
*/
public function isSatisfiedBy($candidate);
}
次に、仕様インタフェースを実装する抽象仕様クラスを作成します。
/**
* 抽象仕様
*/
abstract class AbstractSpecification implements SpecificationInterface
{
/**
* 自分自身とのAnd仕様を生成する
*
* @param SpecificationInterface $spec
* @return AndSpecification
*/
public function andSpecification(SpecificationInterface $spec)
{
return new AndSpecification($this, $spec);
}
/**
* 自分自身とのOr仕様を生成する
*
* @param SpecificationInterface $spec
* @return OrSpecification
*/
public function orSpecification(SpecificationInterface $spec)
{
return new OrSpecification($this, $spec);
}
/**
* 自分自身のNot仕様を生成する
*
* @return NotSpecification
*/
public function notSpecification()
{
return new NotSpecification($this);
}
}
最後に、抽象仕様クラスを継承したAnd仕様クラス、Or仕様クラス、Not仕様クラスを作成します。
/**
* And仕様
*/
class AndSpecification extends AbstractSpecification
{
protected $one;
protected $other;
public function __construct(SpecificationInterface $one, SpecificationInterface $other)
{
$this->one = $one;
$this->other = $other;
}
/**
* 仕様を満たすかを判定する
*
* @param object $candidate 候補
* @return boolean 仕様を満たす場合はtrue
*/
public function isSatisfiedBy($candidate)
{
return $this->one->isSatisfiedBy($candidate)
&& $this->other->isSatisfiedBy($candidate);
}
}
/**
* Or仕様
*/
class OrSpecification extends AbstractSpecification
{
protected $one;
protected $other;
public function __construct(SpecificationInterface $one, SpecificationInterface $other)
{
$this->one = $one;
$this->other = $other;
}
/**
* 仕様を満たすかを判定する
*
* @param object $candidate 候補
* @return boolean 仕様を満たす場合はtrue
*/
public function isSatisfiedBy($candidate)
{
return $this->one->isSatisfiedBy($candidate)
|| $this->other->isSatisfiedBy($candidate);
}
}
/**
* Not仕様
*/
class NotSpecification extends AbstractSpecification
{
protected $one;
public function __construct(SpecificationInterface $one)
{
$this->one = $one;
}
/**
* 仕様を満たすかを判定する
*
* @param object $candidate 候補
* @return boolean 仕様を満たす場合はtrue
*/
public function isSatisfiedBy($candidate)
{
return !($this->one->isSatisfiedBy($candidate));
}
}
最終的に、クラス設計は次のようになりました。
続・架空の宅配クリーニングの例
例によって、モデルはあくまで架空であり、実在するサービス・企業とは一切関係ありません。
今回仕様インタフェースと抽象仕様クラスを追加したため、前回の記事で作成したキャンセル可能クリーニング品仕様クラスを次のように変更します。
/**
* キャンセル可能クリーニング品仕様
*/
Class CancelableItemSpecification extends AbstractSpecification
{
protected $orderRepository;
protected $currentDate;
public function __construct(OrderRepositoryInterface $orderRepository, DateTime $currentDate)
{
$this->orderRepository = $orderRepository;
$this->currentDate = $currentDate;
}
/**
* 仕様を満たすかを判定する
*
* @param object $candidate 候補
* @return boolean 仕様を満たす場合はtrue
*/
isSatisfiedBy($candidate)
{
$order = $this->orderRepository->orderOfId($candidate->order_id); // get_parent_class($order) => Illuminate\Database\Eloquent\Model
// 注文や注文のクリーニング処理工程、現在日時を使用し、クリーニング品がキャンセル可能かを判定する
}
}
さて、クリーニング品の中には特殊なクリーニングが必要なもの(特殊クリーニング品)もあるでしょう。
例えば、皮革・毛皮などがそうかもしれません。
ここで、特殊クリーニング品のうちキャンセル不可能なものを判定したいという要件が出てきたとします。
このときに、「特殊かつキャンセル不可能クリーニング品」などといったひとつの仕様クラスを作成してしまうと、あまりに用途が限定された特殊な仕様となり再利用できそうにありません。
そこで今回作成した合成仕様を使います。
特殊クリーニング品仕様(SpecialItemSpecification)という汎用的な仕様クラスを作成し、すでにあるキャンセル可能クリーニング品仕様クラスと組み合わせて、「特殊かつキャンセル不可能クリーニング品」という仕様を表現してみます。
$items = $this->itemRepository->allItemsOfOrder($order); // get_class($items) => Illuminate\Database\Eloquent\Collection
$specialAndNotCancelableItemSpec = app()->make('SpecialItemSpecification')
->andSpecification(app()->make('CancelableItemSpecification')->notSpecification()); // 特殊かつキャンセル不可能クリーニング品仕様
$specialAndNotCancelableItems = $items->filter(function ($item) use ($specialAndNotCancelableItemSpec) {
return $specialAndNotCancelableItemSpec->isSatisfiedBy($item);
});
まとめ
合成仕様を使って複数の仕様を組み合わせてみました。
- 仕様を組み合わせるためにAnd仕様、Or仕様 、Not仕様を作成しました
- 汎用的な仕様を組み合わせて特殊な仕様を表現できました
明日は弊社広報 @Osaminami の「小劇場俳優が企業広報やってみた」です。
ホワイトプラスではエンジニアを募集しています
ホワイトプラスでは、「ネット」×「リアル」なサービスをDDDで実現するぜ!という技術で事業に貢献したいエンジニアを募集しております。