Сущность

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

using System;
using System.Collections.Generic;
using System.Linq;

//client code
//no database and no screen anywhere below
var order = new Order(1001);
order.Add("pen", 30, 2);
order.Add("lamp", 190, 1);
Console.WriteLine($"order {order.Id}");
Console.WriteLine($"lines {order.Lines.Count}");
Console.WriteLine($"total {order.Total}");
Console.WriteLine($"discount {order.Discount}");
Console.WriteLine($"payable {order.Payable}");
order.Pay();
Console.WriteLine($"paid {order.Paid}");

//every rule below is refused by the entity itself
try
{
    order.Add("pen", 30, 1);
}
catch (InvalidOperationException error)
{
    Console.WriteLine($"refused: {error.Message}");
}

try
{
    new Order(1002).Pay();
}
catch (InvalidOperationException error)
{
    Console.WriteLine($"refused: {error.Message}");
}

try
{
    new OrderLine("chair", 30, 0);
}
catch (ArgumentException error)
{
    Console.WriteLine($"refused: {error.Message}");
}
//printed: order 1001
//         lines 2
//         total 250
//         discount 25
//         payable 225
//         paid True
//         refused: already paid
//         refused: empty order
//         refused: bad count

//entity: a record still guards its own rule, because a
//positional property may be re-declared with a check
record OrderLine(string Sku, int Price, int Count)
{
    public int Count { get; } = Count > 0
        ? Count
        : throw new ArgumentException("bad count");

    public int Cost => Price * Count;
}

//entity
sealed class Order
{
    private readonly List<OrderLine> lines = [];

    public Order(int id) => Id = id;

    public int Id { get; }

    public IReadOnlyList<OrderLine> Lines => lines;

    public bool Paid { get; private set; }

    //business rule: a paid order never changes
    public void Add(string sku, int price, int count)
    {
        if (Paid)
        {
            throw new InvalidOperationException(
                "already paid");
        }
        lines.Add(new OrderLine(sku, price, count));
    }

    public int Total => lines.Sum(line => line.Cost);

    //business rule: 10% off from 200
    public int Discount => Total >= 200 ? Total / 10 : 0;

    public int Payable => Total - Discount;

    //business rule: an empty order is never paid
    public void Pay()
    {
        if (lines.Count == 0)
        {
            throw new InvalidOperationException(
                "empty order");
        }
        Paid = true;
    }
}


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

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

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

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

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

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