实体必须有唯一不可变ID以保障同一性,值对象须不可变且无标识;需用uuid.uuid4()生成临时ID避免None问题,__eq__和__hash__仅基于ID,禁止在领域实体中混入ORM特性。

实体必须有唯一标识且生命周期可追踪
实体的核心不是数据,而是身份。哪怕所有属性都一样,只要 id 不同,就是两个不同实体。Python 中不靠语言特性强制约束,得靠设计自觉——__eq__ 和 __hash__ 必须只基于 id(或业务主键),不能包含可变字段。
常见错误是把数据库自增ID当唯一标识,结果在新建实体未持久化前,id 为 None,导致多个新对象 == 判定失效,集合操作出错。
- 新建实体用 UUID4 生成临时
id:uuid.uuid4(),避免None引发的逻辑断裂 - 禁止在
__eq__中比较非标识字段(如name、created_at) - 若用 ORM(如 SQLAlchemy),确保映射层不覆盖领域层的
id行为;实体类不要直接继承Base,用组合而非继承衔接持久化
值对象必须不可变且无标识
值对象代表概念性整体,比如 Money、Address、PhoneNumber。它没有独立生命周期,也不需要 ID。Python 没有原生不可变类,得手动封禁修改:用 __slots__ + 私有属性 + 只读 property + 禁用 __setattr__。
典型踩坑是把 list 或 dict 直接塞进值对象——表面不可变,实则引用可变。例如 self._tags = tags.copy() 不够,得深拷贝或转成 tuple。
- 构造后禁止任何字段修改:
def __setattr__(self, key, value): raise AttributeError(f"{type(self).__name__} is immutable") - 用
typing.NamedTuple或dataclasses.frozen=True是捷径,但注意frozen=True不阻止内部可变对象被改 - 相等性由所有字段共同决定:
__eq__要递归比对嵌套值对象,不能只比内存地址
实体与值对象的协作边界要清晰
实体可以持有值对象,但值对象绝不能持有实体——否则破坏无标识原则,还可能引发循环引用或持久化歧义。例如订单(实体)可含收货地址(值对象),但地址不能反向引用订单。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
另一个易错点是把本该是值对象的东西当成实体建模:比如“订单项数量”写成 Quantity 类却给了 ID 字段,这就混淆了语义。
- 判断标准很简单:删掉这个对象,业务逻辑是否还成立?如果只是“少了个数值”,那就是值对象
- 实体间关联用 ID(
order_id: UUID),而不是直接持引用(order: Order),避免聚合根边界模糊 - 值对象建议放在单独模块(如
domain.values),和实体分目录,从结构上强化认知
ORM 映射时别让框架污染领域模型
SQLAlchemy 的 @declared_attr、Django ORM 的 models.Model 子类,都会往类里注入魔法属性和方法,直接侵入领域层。一旦用了这些,Order 就不再是纯领域实体,而是“带 ORM 血统的混合体”。
最稳妥的做法是两套类:领域层定义纯净 Order 和 Address,持久化层用 OrderORM 和 AddressORM 做映射,通过显式转换函数(如 to_orm() / from_orm())桥接。
- 禁止在领域实体中出现
__tablename__、relationship、ForeignKey等 ORM 特定字段 - 避免用
sqlalchemy.orm.Mapped注解领域类属性,那是基础设施层的事 - 如果坚持用单类方案,至少把 ORM 元数据抽到
__table_args__之类静态配置里,不混入业务逻辑
值对象序列化到数据库时,通常转成 JSON 字符串或拆成多列,但反序列化必须还原为领域值对象实例——这步常被跳过,导致后续代码拿到的是 dict 而非 Address,类型安全彻底失效。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










