
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
An entity holds a business rule of the shop and knows nothing about a database, a screen or the network, so it can be tested without them.Read more问题。商店对金额达到 200 的每笔订单减免 10%。如果这条规则写在网页里,又在数据库查询里写了一遍,两份副本很快就会不一致。要检查这条规则,就得启动网页或数据库。商店最重要的知识分散各处,难以测试。 思路。实体是一段数据,连同与它相关的业务规则:订单包含什么,总额如何计算,何时可以付款。它对数据库、界面或网络一无所知。整洁架构有一条主要规则,即依赖规则:外层代码可以了解内层代码,但内层代码从不了解外层代码。实体是最内层的代码,因此不依赖任何东西。 如何阅读示例。示例中有两个实体:订单行和订单本身。订单汇总各行,对 200 及以上的金额减免 10%,并计算应付金额。订单为空时,它拒绝付款。客户端代码构建含两个订单行的订单 1001,并打印其中的数字:总额 250,折扣 25,应付 225。它还尝试为一个空订单付款,实体予以拒绝。在某些语言中,实体是带有函数的普通值,而不是类;它的角色不变。 何时使用。业务中每个拥有自己规则的概念,例如订单或账户,都应做成一个实体。没有规则的数据不需要实体:简单的结构就够了。 常见错误。实体自己把自己保存到数据库,或者为界面格式化自己。这样它就依赖于外层代码,它的规则也无法再单独测试。 |