
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. Ещё он пытается оплатить пустой заказ, и сущность отказывает. В некоторых языках сущность - не класс, а обычное значение с функциями; роль у неё та же. Когда применять. Сущность стоит заводить для каждого понятия бизнеса, у которого есть свои правила, например для заказа или счёта. Данным без правил сущность не нужна: хватит простой структуры. Частая ошибка. Сущность, которая сама сохраняет себя в базу данных или оформляет себя для экрана. Тогда она зависит от внешнего кода, и её правила уже нельзя проверить отдельно. |