3.9. Спецификация (Specification)

3.9.1. Назначение

Строит ясное описание бизнес-правил, на соответствие которым могут быть проверены объекты. Композитный класс спецификация имеет один метод, называемый isSatisfiedBy, который возвращает истину или ложь в зависимости от того, удовлетворяет ли данный объект спецификации.

3.9.2. Примеры

3.9.3. Диаграмма UML

Alt Specification UML Diagram

3.9.4. Код

Вы можете найти этот код на GitHub

Item.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification;
 6
 7class Item
 8{
 9    public function __construct(private float $price)
10    {
11    }
12
13    public function getPrice(): float
14    {
15        return $this->price;
16    }
17}

Specification.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification;
 6
 7interface Specification
 8{
 9    public function isSatisfiedBy(Item $item): bool;
10}

OrSpecification.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification;
 6
 7class OrSpecification implements Specification
 8{
 9    /**
10     * @var Specification[]
11     */
12    private array $specifications;
13
14    /**
15     * @param Specification[] $specifications
16     */
17    public function __construct(Specification ...$specifications)
18    {
19        $this->specifications = $specifications;
20    }
21
22    /*
23     * if at least one specification is true, return true, else return false
24     */
25    public function isSatisfiedBy(Item $item): bool
26    {
27        foreach ($this->specifications as $specification) {
28            if ($specification->isSatisfiedBy($item)) {
29                return true;
30            }
31        }
32
33        return false;
34    }
35}

PriceSpecification.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification;
 6
 7class PriceSpecification implements Specification
 8{
 9    public function __construct(private ?float $minPrice, private ?float $maxPrice)
10    {
11    }
12
13    public function isSatisfiedBy(Item $item): bool
14    {
15        if ($this->maxPrice !== null && $item->getPrice() > $this->maxPrice) {
16            return false;
17        }
18
19        if ($this->minPrice !== null && $item->getPrice() < $this->minPrice) {
20            return false;
21        }
22
23        return true;
24    }
25}

AndSpecification.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification;
 6
 7class AndSpecification implements Specification
 8{
 9    /**
10     * @var Specification[]
11     */
12    private array $specifications;
13
14    /**
15     * @param Specification[] $specifications
16     */
17    public function __construct(Specification ...$specifications)
18    {
19        $this->specifications = $specifications;
20    }
21
22    /**
23     * if at least one specification is false, return false, else return true.
24     */
25    public function isSatisfiedBy(Item $item): bool
26    {
27        foreach ($this->specifications as $specification) {
28            if (!$specification->isSatisfiedBy($item)) {
29                return false;
30            }
31        }
32
33        return true;
34    }
35}

NotSpecification.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification;
 6
 7class NotSpecification implements Specification
 8{
 9    public function __construct(private Specification $specification)
10    {
11    }
12
13    public function isSatisfiedBy(Item $item): bool
14    {
15        return !$this->specification->isSatisfiedBy($item);
16    }
17}

3.9.5. Тест

Tests/SpecificationTest.php

 1<?php
 2
 3declare(strict_types=1);
 4
 5namespace DesignPatterns\Behavioral\Specification\Tests;
 6
 7use DesignPatterns\Behavioral\Specification\Item;
 8use DesignPatterns\Behavioral\Specification\NotSpecification;
 9use DesignPatterns\Behavioral\Specification\OrSpecification;
10use DesignPatterns\Behavioral\Specification\AndSpecification;
11use DesignPatterns\Behavioral\Specification\PriceSpecification;
12use PHPUnit\Framework\TestCase;
13
14class SpecificationTest extends TestCase
15{
16    public function testCanOr()
17    {
18        $spec1 = new PriceSpecification(50, 99);
19        $spec2 = new PriceSpecification(101, 200);
20
21        $orSpec = new OrSpecification($spec1, $spec2);
22
23        $this->assertFalse($orSpec->isSatisfiedBy(new Item(100)));
24        $this->assertTrue($orSpec->isSatisfiedBy(new Item(51)));
25        $this->assertTrue($orSpec->isSatisfiedBy(new Item(150)));
26    }
27
28    public function testCanAnd()
29    {
30        $spec1 = new PriceSpecification(50, 100);
31        $spec2 = new PriceSpecification(80, 200);
32
33        $andSpec = new AndSpecification($spec1, $spec2);
34
35        $this->assertFalse($andSpec->isSatisfiedBy(new Item(150)));
36        $this->assertFalse($andSpec->isSatisfiedBy(new Item(1)));
37        $this->assertFalse($andSpec->isSatisfiedBy(new Item(51)));
38        $this->assertTrue($andSpec->isSatisfiedBy(new Item(100)));
39    }
40
41    public function testCanNot()
42    {
43        $spec1 = new PriceSpecification(50, 100);
44        $notSpec = new NotSpecification($spec1);
45
46        $this->assertTrue($notSpec->isSatisfiedBy(new Item(150)));
47        $this->assertFalse($notSpec->isSatisfiedBy(new Item(50)));
48    }
49}