Help us understand the problem. What is going on with this article?

Specification パターン

この記事について

PHPで学ぶデザインパターン Advent Calendar 2018 - Qiita の 2日目の記事です。

はじめに

Specification パターンとは

https://www.martinfowler.com/apsupp/spec.pdf

A valuable approach to these problems is to separate the statement of what kind of objects can be
selected from the object that does the selection. A cargo has a separate storage specification to
describe what kind of container can contain it. The specification object has a clear and limited
responsibility, which can be separated and decoupled from the domain object that uses it.

これらの問題(訳者注: 船舶関連のシステムで積み荷が適切なコンテナに確実に積まれるようにする、トレーディングシステムで銀行との契約のリスクを判定する、などの問題)に対する有用なアプローチは、どのオブジェクトが選択されうるか、という記述を、選択を行うオブジェクトに分離することである。積荷は分離された積み込みのための仕様を持っており、仕様にはどの種類のコンテナがその積み荷を格納できるか、が記載されている。仕様オブジェクトはすっきりしていて限られた責務しか持たず、それを使うドメインオブジェクトから切り離すことができる。

解決したい問題

  • コレクションの中から条件に一致するサブセットを抜き出したい
  • 特定のロール向けに適切なオブジェクトだけが使われることをチェックしたい
  • あるオブジェクトが条件を満たす場合と満たさない場合で処理を分けたい

ビジネスルールが複数になったり、細かくなったりして、if 文の中の条件が増えたり、if 文自体が増えたりすると、可読性が下がったり、ポータビリティが下がったりするので、モジュール化して名前で表現するようにするといいんじゃないか、ということですね。

実装例

注)上にリンクを張った PDF では、メソッド名は isSatisfiedBy となっていますが、本記事の例では簡略化して satisfied にしてあります(振る舞いは同じです)。

コレクションから条件に一致するサブセットを抜き出す

昇進に関するルールがあるとき、従業員一覧から昇進対象となる従業員のみを抜き出す処理です。

<?php

$employees = Employee::all();
// $params は昇進条件を判定するのに必要なパラメータ
$spec = new PromotionSpecification($params);
$promoted = $employees->filter(function (Employee $employee) use ($spec) {
    return $spec->satisfied($employee);
});

特定のロール向けに適切なオブジェクトだけが使われることをチェックする

Laravel の Gate を使った例です。

<?php
// AuthServiceProvider
Gate::define('update-post', function ($user, $post) {
    return (new UpdatePostSpecification($user))->satisfied($post);
});

あるオブジェクトが条件を満たす場合と満たさない場合で処理を分けたい

こちらの記事で使用した、 DiscountSpecification を使います。

https://qiita.com/nunulk/items/ea92393db04b5b89049b

仕様オブジェクトはコンストラクタにて渡されるとします。

<?php
// DiscountCalculator
public function run(PeriodOfUse $period, int $baseCharge): int
{
    if (!$this->discountSpecification->satisfied($period)) {
        return 0;
    }
    return (int)floor($baseCharge / 10);
}

このパターンを使わない場合は、

<?php
// DiscountCalculator
public function run(PeriodOfUse $period, int $baseCharge): int
{
    if ($period->value() < 7) {
        return 0;
    }
    return (int)floor($baseCharge / 10);
}

という形になります。

いまは条件が単純なんで単に if 文の中身が入れ替わっただけですが、値引きのルールが複雑になったり、他の条件でルールセット全体が入れ替わったりすると、途端にごちゃごちゃしてしまうので、こうしてクラスに閉じ込めてしまうのはぜんぜんありだなぁ、と思います。

おわりに

条件が単純な場合は if 文に直接条件を書くのと対して変わらないですが、あるていど複雑になってくるとクラス化することの恩恵が得られると思います。

ぜひお手元のコードでお試しください。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away