Сущность

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

import java.util.*;

//entity
class OrderLine {
    final String sku;
    final int price;
    final int count;

    OrderLine(String sku, int price, int count) {
        if (count <= 0) {
            throw new IllegalArgumentException("bad count");
        }
        this.sku = sku;
        this.price = price;
        this.count = count;
    }

    int cost() {
        return price * count;
    }
}

//entity
class Order {
    final int id;
    private final List<OrderLine> lines = new ArrayList<>();
    private boolean paid;

    Order(int id) {
        this.id = id;
    }

    void add(String sku, int price, int count) {
        lines.add(new OrderLine(sku, price, count));
    }

    int total() {
        int sum = 0;
        for (OrderLine line : lines) {
            sum += line.cost();
        }
        return sum;
    }

    //business rule: 10% off from 200
    int discount() {
        return total() >= 200 ? total() / 10 : 0;
    }

    int payable() {
        return total() - discount();
    }

    //business rule: an empty order is never paid
    void pay() {
        if (lines.isEmpty()) {
            throw new IllegalStateException("empty order");
        }
        paid = true;
    }

    boolean paid() {
        return paid;
    }

    int size() {
        return lines.size();
    }
}

//client code
//no database and no screen anywhere above
var order = new Order(1001);
order.add("pen", 30, 2);
order.add("lamp", 190, 1);
System.out.println("order " + order.id);
System.out.println("lines " + order.size());
System.out.println("total " + order.total());
System.out.println("discount " + order.discount());
System.out.println("payable " + order.payable());
order.pay();
System.out.println("paid " + order.paid());

//a line that breaks the rule never gets into the order,
//so the order below stays empty and is refused too
var empty = new Order(1002);
try {
    empty.add("pen", 30, 0);
} catch (IllegalArgumentException error) {
    System.out.println("refused: " + error.getMessage());
}
try {
    empty.pay();
} catch (IllegalStateException error) {
    System.out.println("refused: " + error.getMessage());
}
//printed: order 1001
//         lines 2
//         total 250
//         discount 25
//         payable 225
//         paid true
//         refused: bad count
//         refused: empty order


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

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

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

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

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

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