首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >DDD|领域中的模型设计

DDD|领域中的模型设计

作者头像
AI老马
发布2026-05-18 17:16:03
发布2026-05-18 17:16:03
1150
举报
文章被收录于专栏:AI前沿技术AI前沿技术

在 DDD 领域设计里,把业务里的概念变成代码对象一点都不难,但难就难在把这些概念背后真正的业务含义给精准表达出来。想要做好这件事,就得先把各类设计元素的作用掰扯清楚,再配合一套规范的设计思路,一步步打造出适配业务的专用对象。

本节重点讲解搭建领域模型的三大核心角色:实体、值对象、服务。

实体很好理解,就是那些有唯一身份、能一直存续的业务事物;值对象不单独做身份标识,只用来描述事物的状态和属性;服务则专门负责各类业务行为、操作动作。

除此之外,还少不了业务之间普遍存在的关联关系,以及模块这个重要概念。模块不是随便划分的,是吃透业务深层逻辑后做出的规划,能把领域里的概念规整归纳,让整个业务模型更清晰有条理。

1,关联 Association

关联关系是软件设计中最普遍的概念,其本质是把业务里“谁和谁有关、怎样有关,如何约束”,用对象模型精确、可执行的表达出来。如何设计和简化关联是关键。模型中每个可遍历的关联,软件中都要有同样的属性机制。

实际的关联有:一对一,一对多,多对多的关联,不明确的关联对软件实现意义不大。

尽可能的对关联进行约束是非常重要的。减少设计中的关联有助于简化对象之间的遍历,并在某种程度上限制关系的急剧增多。

如何使关联变得可控,可约束?可尝试三种方式。

  • • 指定一个方向遍历
  • • 添加一个限定符,有效的减少多重关联
  • • 消除不必要关联

下面,从一对多的关联,通过添加限定符变为一对一关联的例子。

一个国家可以有多位总统,但是一个国家在一段时间内只有一位总统。通过加入时间的概念,限制了国家和总统一对一的关联。

再比如实际代码中:

图1,限制关联关系。

在最开始的业务中,一个账户 (Brokerage Account) 可以有多笔投资 (Investment),是典型的一对多的关系。后续通过设置限制关系 (stock):一个账户仅允许有一笔投资,那账户和投资之间就变成了一对一的关系。代码的实现也从 set 集合转变成了 map 映射。

同时账户和投资之间有明确的指向关系,账户类中持有投资类的引用,而投资类不能持有账户类的应用。不像账户和客户之间可以相互持有,并没有明确的指向关系。

关联是一个更广泛上层的概念,聚合和组合都是关联的两种特殊形式,是关联的加强版,有约束的版本。

具体的表述为:

  • • 关联 association:对象间独立连接,无所有权,彼此生命周期独立。
  • • 聚合 aggregate:业务相关的聚合簇,含且仅含一个聚合根,外界只能通过聚合根访问,是事物的一致性边界。
  • • 组合 composition:聚合内的强关系,整体与部分的生命周期绑定,部分不共享。比如房子和房间。

小结: 有限制的关联,关系少,指向明确,代码好写,维护容易。关联是连接,组合是绑定,聚合是边界。

2,实体 ENTITY

实体:通过连续性和标识定义的对象。

满足两个条件

  • • 它在整个生命周期中具有连续性
  • • 它的区别是由那些对用户非常重要的属性决定的

举例:员工实体

  • • 连续性:入职→调岗→晋升→离职,始终是同一个员工。
  • • 唯一性:在公司内部,工号是该员工的唯一标识,具有唯一性。外部其身份证号全国唯一。

实体的连续性如何追踪,需要设计实体的标识。

  • • 标识在实体创建时分配,生命周期内永不改变;
  • • 即使实体的其他属性(名称、状态、价格等)频繁变化,标识不变;
  • • 系统通过标识识别 “这是同一个实体”,从而保证连续性。

标识设计最佳实践

  • • 内部用技术 ID,外部用业务号。
  • • 避免使用敏感信息做主键,避免隐私泄露与变更风险。
  • • 单一来源分配,统一 ID 生成服务。
  • • 标识不可回收。实体删除后,ID 不再复用,保证历史数据可追溯。
  • 33,值对象 VALUE OBJECT

值对象:用于描述领域的某个方面而本身没有概念表示的对象。

当只关心一个模型元素的属性时,就应该把它归为值对象。应该使这个模型元素能够表示出其属性的意义,并为他提供相关的功能。值对象应该是不可变的,不能为其分配任何的标识。

值对象被实例化后用来表示一些设计元素,对于这些设计元素,我们只关心他们是什么,而不关心他们是谁。

值对象所包含的属性应该形成一个概念整体。

举例:

图2,值对象的定义。

在客户的类中包含了用户ID,名称,街道等一些属性。显然街道,城市等作为人的属性不合适,可以将其包装成一个地址的总属性,进而形成一个地址的值对象。

设计值对象有多种的选择,包括复制,共享或保持值对象不变。

如何通过不变性和共享,优化程序设计和代码实现?

  • 不可变:用 @dataclass(frozen=True)
  • 可共享:用享元模式 / 实例缓存实现相同值复用同一个对象
  • 相等判断:自动按属性值比较(不是引用)

3.1,值对象不变性设计

以下代码当尝试修改 Address 实例后的对象属性时就会报错。

代码语言:javascript
复制
from dataclasses import dataclass

# frozen=不可变;eq=自动按属性判断相等
@dataclass(frozen=True, eq=True)  
class Address:
    city: str
    street: str

关键字 frozen 只做一件事:禁止修改实例属性(instance fields)完全不限制类属性(class variables)

3.2,值对象的共享设计

通过缓存池,共享相同实例。

代码语言:javascript
复制
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的管理范围。

全局共享方式。

代码语言:javascript
复制
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

总结:

  • • 值对象一律用 @dataclass(frozen=True, eq=True)
  • • 要共享就用静态工厂 + 缓存池
  • • 永远不提供修改方法,要改就新建实例
  • • 相等用属性比较,不是引用比较

4,服务 SERVICE

有时,对象不是一个事物,而是一些特殊的操作。在领域中的某个重要的过程或是转换不是实体或是值对象的自然职责时,应该在模型中添加一个作为独立接口的操作,即服务。

服务强调的是与其他对象的关系,和实体、值对象不同的是,它只是定义了能做什么。

服务往往是以一个活动来命名,是一个动词而不是名词。

具有以下三个特征:

  • • 与领域概念相关的操作不是实体或值对象的一个自然组成部分。
  • • 接口是根据领域模型的其它元素定义的
  • • 操作时无状态的

无状态是指,当输入的参数相同时,输出也是相同的。

举例:订单金额计

一个订单(Order)需要计算最终金额,规则如下:

  1. 1. 订单本身只存商品明细、单价、数量
  2. 2. 最终金额 = 商品总价 - 会员折扣 - 优惠券 + 运费
  3. 3. 折扣、运费、优惠券都不属于订单自己的职责,也不属于商品
  4. 4. 这是一个跨多个领域对象的计算过程

这种跨对象协作、无状态、动词型操作,就是领域服务

5,模块 MODULE

模块也称之包 package,为了解决“认知超载”的问题。

领域层中的模块应该成为模型中有意义的部分,其从更大的角度描述了领域。

  • • 在模块中查看细节,而不会被整个模型淹没
  • • 观察模块之间的关系,而不考虑其他内部细节

模块内高内聚:关联紧密、业务语义一致的实体、值对象、领域服务放一起

模块间低耦合:模块只对外暴露必要接口,隐藏内部细节,依赖尽量单向、精简

每个模块是领域有意义的独立业务片段,从宏观视角划分领域。

总结:

围绕 DDD 领域驱动设计,核心讲解领域模型的关键元素及相关概念,助力精准捕获领域含义、规范设计实践。

明确捕获领域概念对象易,但体现其核心含义难,需区分元素含义并结合设计实践。核心涉及三种标识模型的元素:实体(ENTITY),用于表示具有连续性和唯一标识的事物;值对象(VALUE OBJECT),用于描述事物的状态属性;服务(SERVICE),用于标识一系列动作或操作。

此外,还介绍了领域中的普遍关系 —— 关联,以及模块(MODULE)概念,模块是基于对领域深层知识的理解所做的决策,能够反映领域中的核心概念。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 AI老马啊 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1,关联 Association
    • 3.1,值对象不变性设计
    • 3.2,值对象的共享设计
  • 4,服务 SERVICE
  • 5,模块 MODULE
    • 总结:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档