DDD中实体须有唯一不可变ID以保障同一性,聚合根作为一致性边界入口控制内部对象访问与状态变更,禁止绕过其直接操作子对象或暴露私有状态。

实体必须有唯一标识且不可变
DDD 中的实体不是普通数据容器,它的核心是通过 id(或业务主键)定义“同一性”。Python 没有原生的值对象或实体语义,所以得靠约束来保障——比如禁止外部直接赋值 id,并在初始化后锁定。
常见错误是把 id 设为可变属性,导致两个不同实例被误认为同一实体;或者用 __eq__ 仅比对字段值,忽略身份一致性。
- 用
@property+ 私有属性(如_id)封装id,不提供 setter - 重写
__eq__和__hash__,只基于id判断相等(注意:若id可能为None,需统一处理) - 避免在
__init__外修改_id,必要时可用__slots__ = ("_id", ...)封锁动态属性
class Order:
def __init__(self, order_id: str):
self._id = order_id # 初始化即定,不再变更
<pre class="brush:php;toolbar:false;">@property
def id(self) -> str:
return self._id
def __eq__(self, other):
if not isinstance(other, Order):
return False
return self._id == other._id
def __hash__(self):
return hash(self._id)
聚合根要控制边界与一致性
聚合根不是“最顶层的类”,而是**一致性边界入口**。所有对聚合内对象(如 OrderItem)的访问,必须经由聚合根(如 Order)的方法,不能绕过它直接 new 或修改子对象。
立即学习“Python免费学习笔记(深入)”;
典型问题包括:外部直接调用 order_item.update_price(...),破坏了订单总价的同步更新逻辑;或让多个聚合根互相引用,形成跨边界修改。
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
- 聚合内部对象(如
OrderItem)不暴露公共构造函数,只允许聚合根创建 - 聚合根提供明确行为方法(如
add_item()、cancel()),把校验和状态变更逻辑收束其中 - 聚合根负责维护自身不变量(如“已支付订单不能删商品”),抛出领域异常(如
DomainError)而非通用异常
用私有属性 + 显式方法替代 public 字段
Python 的鸭子类型容易让人直接读写属性,但这会破坏封装和领域规则。DDD 要求状态变更必须走明确定义的行为路径,否则无法插入校验、事件发布或审计逻辑。
例如把 order.status = "shipped" 改成 order.ship(),就能在方法体内检查是否满足发货前提(如已付款、库存充足)。
- 所有状态字段设为私有(前缀
_),不加@propertygetter 除非确需只读暴露 - 每个状态变更对应一个动词方法(
confirm()、reject()、archive()),方法名体现业务意图 - 避免在方法中返回可变对象(如
list或dict),防止外部绕过控制直接修改内部状态
聚合根应持有子对象引用但不暴露其身份
聚合内子对象(如 OrderItem)可以有自己的 id,但该 id 仅在聚合内有意义,对外不可见、不可用于跨聚合查询。它的生命周期完全由聚合根管理。
容易踩的坑是:把 OrderItem.id 当作全局唯一标识导出到 API 或数据库外键;或让仓储(Repository)直接加载单个 OrderItem,绕过 Order 聚合根。
- 子对象的
id类型建议用UUID或组合键(如f"{order_id}-{seq}"),强调其局部性 - 聚合根提供集合操作接口(如
items()返回只读视图或tuple),不返回可变列表 - 仓储层只定义
OrderRepository.find_by_id(),不定义OrderItemRepository
复杂点在于:当聚合变大(如含几十个子项),简单遍历校验可能影响性能;这时需要分层建模(如引入规格对象 ShippingPolicy)或延迟验证,但入口仍必须是聚合根方法。


















