Сущность

Паттерны проектирования / Паттерны чистой архитектуры
image

<?php

//entity
class OrderLine {
    function __construct(
        public readonly string $sku,
        public readonly int $price,
        public readonly int $count
    ) {
        if ($count <= 0) {
            throw new InvalidArgumentException("bad count");
        }
    }

    function cost(): int {
        return $this->price * $this->count;
    }
}

//entity
class Order {
    private array $lines = [];
    private bool $paid = false;

    function __construct(public readonly int $id) {}

    function add(string $sku, int $price,
                 int $count): void {
        $this->lines[] = new OrderLine($sku, $price,
                                       $count);
    }

    function total(): int {
        $sum = 0;
        foreach ($this->lines as $line) {
            $sum += $line->cost();
        }
        return $sum;
    }

    //business rule: 10% off from 200
    function discount(): int {
        $total = $this->total();
        return $total >= 200 ? intdiv($total, 10) : 0;
    }

    function payable(): int {
        return $this->total() - $this->discount();
    }

    //business rule: an empty order is never paid
    function pay(): void {
        if ($this->lines === []) {
            throw new LogicException("empty order");
        }
        $this->paid = true;
    }

    function paid(): bool {
        return $this->paid;
    }

    function size(): int {
        return count($this->lines);
    }
}

//client code
//no database and no screen anywhere above
$order = new Order(1001);
$order->add("pen", 30, 2);
$order->add("lamp", 190, 1);
echo "order ", $order->id, "\n";
echo "lines ", $order->size(), "\n";
echo "total ", $order->total(), "\n";
echo "discount ", $order->discount(), "\n";
echo "payable ", $order->payable(), "\n";
$order->pay();

//echo shows a bool as 1 and as nothing at all, so a
//bool goes through var_export
echo "paid ", var_export($order->paid(), true), "\n";

try {
    (new Order(1002))->pay();
} catch (LogicException $error) {
    echo "refused: ", $error->getMessage(), "\n";
}

try {
    $order->add("chair", 15, 0);
} catch (InvalidArgumentException $error) {
    echo "refused: ", $error->getMessage(), "\n";
}
//printed: order 1001
//         lines 2
//         total 250
//         discount 25
//         payable 225
//         paid true
//         refused: empty order
//         refused: bad count


Сущность держит правило предметной области и ничего не знает ни о базе, ни об экране, ни о сети, поэтому её можно проверить без них.
Read more

Задача. Магазин даёт скидку 10% на любой заказ от 200. Если это правило живёт на веб-странице и ещё раз в запросе к базе данных, две копии скоро разойдутся. Чтобы проверить правило, приходится запускать страницу или базу. Самое важное знание магазина разбросано, и проверить его трудно.

Идея. Сущность - это данные вместе с бизнес-правилами, которые к ним относятся: из чего состоит заказ, как считается его итог, когда его можно оплатить. О базе данных, экране или сети она ничего не знает. В чистой архитектуре есть одно главное правило - правило зависимостей: внешний код может знать внутренний, но внутренний никогда не знает внешнего. Сущности - самый внутренний код, поэтому они ни от чего не зависят.

Как читать пример. Сущностей две: позиция заказа и сам заказ. Заказ складывает свои позиции, даёт скидку 10% при сумме от 200 и считает сумму к оплате. Пока заказ пуст, он отказывается принимать оплату. Клиентский код собирает заказ 1001 из двух позиций и печатает его числа: итог 250, скидка 25, к оплате 225. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же.

Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры.

Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно.