Сущность

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

//C has no vector: an order keeps its lines in an array
//of fixed size and counts them
constexpr size_t maxLines = 8;

//entity
typedef struct {
    char sku[16];
    int price;
    int count;
} OrderLine;

int lineCost(const OrderLine *line) {
    return line->price * line->count;
}

//entity: C has no private fields, so the struct is open
//for reading, and only the functions below change it
typedef struct {
    int id;
    OrderLine lines[maxLines];
    size_t size;
    bool paid;
} Order;

Order orderMake(int id) {
    return (Order){.id = id};
}

//C has no exceptions: a rule that refuses returns the
//reason, and nullptr means done
const char *orderAdd(Order *order, const char *sku,
                     int price, int count) {
    if (count <= 0) {
        return "bad count";
    }
    if (order->size == maxLines) {
        return "too many lines";
    }
    OrderLine *line = &order->lines[order->size++];
    snprintf(line->sku, sizeof line->sku, "%s", sku);
    line->price = price;
    line->count = count;
    return nullptr;
}

int orderTotal(const Order *order) {
    int sum = 0;
    for (size_t i = 0; i < order->size; i++) {
        sum += lineCost(&order->lines[i]);
    }
    return sum;
}

//business rule: 10% off from 200
int orderDiscount(const Order *order) {
    int total = orderTotal(order);
    return total >= 200 ? total / 10 : 0;
}

int orderPayable(const Order *order) {
    return orderTotal(order) - orderDiscount(order);
}

//business rule: an empty order is never paid
const char *orderPay(Order *order) {
    if (order->size == 0) {
        return "empty order";
    }
    order->paid = true;
    return nullptr;
}

//client code
//no database and no screen anywhere above, and no
//header beyond the standard library
Order order = orderMake(1001);
orderAdd(&order, "pen", 30, 2);
orderAdd(&order, "lamp", 190, 1);
printf("order %d\n", order.id);
printf("lines %zu\n", order.size);
printf("total %d\n", orderTotal(&order));
printf("discount %d\n", orderDiscount(&order));
printf("payable %d\n", orderPayable(&order));
orderPay(&order);
printf("paid %s\n", order.paid ? "true" : "false");

//a line that breaks the rule never gets into the order,
//so the order below stays empty and is refused too
Order empty = orderMake(1002);
const char *reason = orderAdd(&empty, "pen", 30, 0);
if (reason != nullptr) {
    printf("refused: %s\n", reason);
}
reason = orderPay(&empty);
if (reason != nullptr) {
    printf("refused: %s\n", reason);
}
//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. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же.

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

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