Сущность

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

from dataclasses import dataclass


# entity: one line of the order; a line without goods
# in it simply does not exist. eq=False, because an
# entity is equal only to itself, never by its fields
@dataclass(frozen=True, eq=False)
class OrderLine:
    sku: str
    price: int
    count: int

    def __post_init__(self):
        if self.count <= 0:
            raise ValueError("bad count")

    @property
    def cost(self) -> int:
        return self.price * self.count


# entity: the order keeps its own rules and knows
# nothing about a database or a screen
class Order:
    def __init__(self, id: int):
        self.id = id
        self._lines: list[OrderLine] = []
        self._paid = False

    @property
    def lines(self) -> tuple[OrderLine, ...]:
        return tuple(self._lines)

    def add(self, sku: str, price: int, count: int):
        # business rule: a paid order is closed
        if self._paid:
            raise RuntimeError("order is paid")
        self._lines.append(OrderLine(sku, price, count))

    @property
    def total(self) -> int:
        return sum(line.cost for line in self._lines)

    # business rule: 10% off from 200
    @property
    def discount(self) -> int:
        if self.total < 200:
            return 0
        return self.total // 10

    @property
    def payable(self) -> int:
        return self.total - self.discount

    @property
    def paid(self) -> bool:
        return self._paid

    # business rule: an empty order is never paid
    def pay(self):
        if not self._lines:
            raise RuntimeError("empty order")
        self._paid = True


# client code
order = Order(1001)
order.add("pen", 30, 2)
order.add("lamp", 190, 1)
print("order", order.id)
print("lines", len(order.lines))
print("total", order.total)
print("discount", order.discount)
print("payable", order.payable)

order.pay()
print("paid", order.paid)

# every refusal comes from the rules themselves:
# no storage and no screen took part in them
try:
    order.add("pen", 30, 1)
except RuntimeError as error:
    print("refused:", error)

try:
    Order(1002).pay()
except RuntimeError as error:
    print("refused:", error)

try:
    OrderLine("pen", 30, 0)
except ValueError as error:
    print("refused:", error)
# printed: order 1001
#          lines 2
#          total 250
#          discount 25
#          payable 225
#          paid True
#          refused: order is paid
#          refused: empty order
#          refused: bad count


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

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

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

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

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

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