在 DDD 领域设计里,把业务里的概念变成代码对象一点都不难,但难就难在把这些概念背后真正的业务含义给精准表达出来。想要做好这件事,就得先把各类设计元素的作用掰扯清楚,再配合一套规范的设计思路,一步步打造出适配业务的专用对象。
本节重点讲解搭建领域模型的三大核心角色:实体、值对象、服务。
实体很好理解,就是那些有唯一身份、能一直存续的业务事物;值对象不单独做身份标识,只用来描述事物的状态和属性;服务则专门负责各类业务行为、操作动作。
除此之外,还少不了业务之间普遍存在的关联关系,以及模块这个重要概念。模块不是随便划分的,是吃透业务深层逻辑后做出的规划,能把领域里的概念规整归纳,让整个业务模型更清晰有条理。
关联关系是软件设计中最普遍的概念,其本质是把业务里“谁和谁有关、怎样有关,如何约束”,用对象模型精确、可执行的表达出来。如何设计和简化关联是关键。模型中每个可遍历的关联,软件中都要有同样的属性机制。
实际的关联有:一对一,一对多,多对多的关联,不明确的关联对软件实现意义不大。
尽可能的对关联进行约束是非常重要的。减少设计中的关联有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。
如何使关联变得可控,可约束?可尝试三种方式。
下面,从一对多的关联,通过添加限定符变为一对一关联的例子。
一个国家可以有多位总统,但是一个国家在一段时间内只有一位总统。通过加入时间的概念,限制了国家和总统一对一的关联。
再比如实际代码中:

图1,限制关联关系。
在最开始的业务中,一个账户 (Brokerage Account) 可以有多笔投资 (Investment),是典型的一对多的关系。后续通过设置限制关系 (stock):一个账户仅允许有一笔投资,那账户和投资之间就变成了一对一的关系。代码的实现也从 set 集合转变成了 map 映射。
同时账户和投资之间有明确的指向关系,账户类中持有投资类的引用,而投资类不能持有账户类的应用。不像账户和客户之间可以相互持有,并没有明确的指向关系。
关联是一个更广泛上层的概念,聚合和组合都是关联的两种特殊形式,是关联的加强版,有约束的版本。
具体的表述为:
小结: 有限制的关联,关系少,指向明确,代码好写,维护容易。关联是连接,组合是绑定,聚合是边界。
2,实体 ENTITY
实体:通过连续性和标识定义的对象。
满足两个条件
举例:员工实体
实体的连续性如何追踪,需要设计实体的标识。
标识设计最佳实践
值对象:用于描述领域的某个方面而本身没有概念表示的对象。
当只关心一个模型元素的属性时,就应该把它归为值对象。应该使这个模型元素能够表示出其属性的意义,并为他提供相关的功能。值对象应该是不可变的,不能为其分配任何的标识。
值对象被实例化后用来表示一些设计元素,对于这些设计元素,我们只关心他们是什么,而不关心他们是谁。
值对象所包含的属性应该形成一个概念整体。
举例:

图2,值对象的定义。
在客户的类中包含了用户ID,名称,街道等一些属性。显然街道,城市等作为人的属性不合适,可以将其包装成一个地址的总属性,进而形成一个地址的值对象。
设计值对象有多种的选择,包括复制,共享或保持值对象不变。
如何通过不变性和共享,优化程序设计和代码实现?
@dataclass(frozen=True)以下代码当尝试修改 Address 实例后的对象属性时就会报错。
from dataclasses import dataclass
# frozen=不可变;eq=自动按属性判断相等
@dataclass(frozen=True, eq=True)
class Address:
city: str
street: str关键字 frozen 只做一件事:禁止修改实例属性(instance fields)完全不限制类属性(class variables)
通过缓存池,共享相同实例。
from dataclasses import dataclass, field
from typing importDict, ClassVar
@dataclass(frozen=True, eq=True)
classCurrency:
code: str
# Currency的双引号表示前向引用,延迟解析
_instances: ClassVar[Dict[str, "Currency"]] = {}
@classmethod
defof(cls, code: str) -> "Currency":
if code notin cls._instances:
cls._instances[code] = Currency(code)
return cls._instances[code]
# ------------------- 测试共享 -------------------
cny1 = Currency.of("CNY")
cny2 = Currency.of("CNY")
usd = Currency.of("USD")
print(cny1 is cny2) # True → 同一个实例(共享成功)
print(cny1 is usd) # False注意:这里的ClassVar表明 _instances 为类变量,所有实例共享的,也不在frozen的管理范围。
全局共享方式。
from dataclasses import dataclass
@dataclass(frozen=True, eq=True)
classGender:
code: str
# 步骤1:先创建唯一实例
Gender.MALE = Gender("M")
Gender.FEMALE = Gender("F")
# 步骤2:再锁死构造函数
def_forbidden_new(cls, *args, **kwargs):
raise RuntimeError("禁止直接创建 Gender,请使用 Gender.MALE / Gender.FEMALE")
Gender.__new__ = _forbidden_new
# 使用方式:
from value_objects import Gender
# 这里用的是 全局共享的同一个对象!
defcreate_user():
gender = Gender.MALE
print("用户模块使用性别:", gender)
return gender总结:
有时,对象不是一个事物,而是一些特殊的操作。在领域中的某个重要的过程或是转换不是实体或是值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,即服务。
服务强调的是与其他对象的关系,和实体、值对象不同的是,它只是定义了能做什么。
服务往往是以一个活动来命名,是一个动词而不是名词。
具有以下三个特征:
无状态是指,当输入的参数相同时,输出也是相同的。
举例:订单金额计
一个订单(Order)需要计算最终金额,规则如下:
这种跨对象协作、无状态、动词型操作,就是领域服务。
模块也称之包 package,为了解决“认知超载”的问题。
领域层中的模块应该成为模型中有意义的部分,其从更大的角度描述了领域。
模块内高内聚:关联紧密、业务语义一致的实体、值对象、领域服务放一起
模块间低耦合:模块只对外暴露必要接口,隐藏内部细节,依赖尽量单向、精简
每个模块是领域有意义的独立业务片段,从宏观视角划分领域。
围绕 DDD 领域驱动设计,核心讲解领域模型的关键元素及相关概念,助力精准捕获领域含义、规范设计实践。
明确捕获领域概念对象易,但体现其核心含义难,需区分元素含义并结合设计实践。核心涉及三种标识模型的元素:实体(ENTITY),用于表示具有连续性和唯一标识的事物;值对象(VALUE OBJECT),用于描述事物的状态属性;服务(SERVICE),用于标识一系列动作或操作。
此外,还介绍了领域中的普遍关系 —— 关联,以及模块(MODULE)概念,模块是基于对领域深层知识的理解所做的决策,能够反映领域中的核心概念。