1. 领域
1.1 领域的定义
- 领域用来确定业务的边界与范围。领域是这个边界和范围内要解决的业务问题域。
- 领域的定义是为了精确地捕捉和描述显示世界中的具体业务问题,从而使得软件设计更贴切地服务实际业务。
举例
:如果开发一个电子商务系统,首先电子商务本身是一个领域,它应该包括但不限于以下子领域:商品管理
:商品管理是电子商务的核心领域,负责处理与商品相关的所有操作,如商品的新增、删除、修改等购物车管理
:购物车应该是电子商务中另一个关键领域,购物车处理的是与用户选购商品相关的所有操作。订单管理
:订单处理涉及到与订单相关的所有操作,如订单生成、支付、发货、收货等。支付
: 支付是电子商务的一个支撑域。用户管理
:用户的登陆登出、资料的新增和修改等
1.2 领域的划分
- 核心域
- 决定产品和公司核心竞争力的子域。如腾讯的社交。阿里的电商。
- 支撑域
- 用于支持和协作建立核心域的其他子域。如很多公司的支付业务。
- 通用域
- 一般已有开源解决方案,不能直接给公司带来价值但又不可或缺。如认证系统,日志管理。
1.3 为何需要划分领域
- 公司的资源是有限的,为了最优分配资源,实现利益最大化。
- 优先做好核心域。还有余力可以将支撑域也做成核心域。等到公司体力做够大了再精力投入通用域。
2. 限界上下文
2.1 限界上下文的定义
- 限界上下文定义了领域的边界
- 在同一界限上下文中,我们可以使用统一的语言进行交互。
- 用来封装通用语言和领域对象,提供
上下文环境
,保证在领域之内的一些术语、业务相关对象等有一个确切的含义,没有二义性。 我们将限界上下文内的领域模型映射到微服务,就完成了从问题域到软件的解决方案
- 一个限界上下文理论上就可以设计为一个微服务。
2.2 如何划分限界上下文
- 限界上下文是一个围绕
业务概念、角色和行为
定义系统边界
的概念 其主要工作就是将大的领域分解成高内聚低耦合的子域,找出最理想的微服务划分策略
- 划分限界上下文的常见步骤如下:
1. 详细理解业务领域:首先深入理解你所面向的业务领域,学习领域专家的语言,弄清各种业务规则和流程。
2. 定义通用语言:建立一个通用语言,即由领域专家和开发者一起定义的、在限界上下文内部统一使用的语言。
3. 识别领域模型:识别出领域内的各种实体
、值对象
、领域服务
和事件
。
4. 描绘业务流程:描绘出你的系统中的业务流程,描绘关键业务过程可以帮助你识别和定义限界上下文。
5. 找出潜在的限界上下文:可能源于业务功能的划分
、业务模型的聚合
或者系统的职责界限
。
6. 明确限界上下文的责任:每个限界上下文都需明确其职责
,并针对这些职责来描述其对外提供的接口和行为
。
7. 评估划定的限界上下文:评估划分是否合理,是否能有效的封装出业务领域的逻辑
,不足则继续调整。
8. 定义上下文间的关系:定义各限界上下文之间的交互关系
,例如共享内核
、客户/供应者
和防腐层
等。
2.3 限界上下文的交互方式
通俗点理解就是多个服务之间的交互方式,或者多个团队、多个业务之间的交互方式。
2.3.1 上下游关系(Upstream/Downstream)
在这种关系中,上游的变化会影响到下游,但下游的改变不会影响到上游。实质上这是一种单向依赖关系
。
-
要求
:- 上游通常要提供接口或者信息,供下游使用。
- 当上游的模型有改变时,下游可能需要进行相应的改变以适应上游的变化。
-
特点
:- 这种关系是单向的,上游的改变可能会影响到下游,但下游的改变对上游没有影响。
- 下游通常需要依赖于上游的数据或者服务。
-
开发与部署的依赖关系
:- 下游上下文的开发基于上游上下文的设计和模型。一旦上游模型发生更改,下游可能也需要相应地进行调整。
- 下游通常需要在上游部署完毕并运行正常后再进行部署。因为下游需要上游的服务或数据,上游需要能在下游部署之前准备好这些。
-
适用性
:- 在依赖关系明显,且一方明显具有主导地位的情况下,更适合将关系定义为上下游关系。
- 比如,一个核心业务系统(上游)可能有许多其他服务系统需要依赖它(下游)。
-
例子
:- 一个订单系统(上游)和一个审计系统(下游)可以形成上下游关系。
- 订单系统负责处理和管理用户的所有订单,其中包含了许多重要的业务逻辑。
- 审计系统需要对订单进行审计,以确保所有的交易都是合法和透明的。
- 在这个关系中,审计系统需要依赖订单系统提供的数据和接口。因此,我们可以说审计系统处于下游,订单系统处于上游。
- 如果订单系统改变了数据格式或者业务逻辑,审计系统可能需要进行相应的更新,以满足新的需求。
- 而反过来,订单系统一般不需要因为审计系统的变动而进行修改。
2.3.2 客户-供应者关系(Customer-Supplier)
客户依赖于供应者提供的服务或数据,同时供应者要考虑客户的情况。是双向依赖
。
要求
:- 供应者需要提供接口(
这个接口是代之,可能是rpc接口,http访问链接,或者是上游提供的数据
)以供客户使用,且在设计和实现这些接口时,需要考虑到客户的需求
; - 当供应者计划修改这些接口时,
需要与客户进行充分的沟通
,并在修改完成后告知客户。
- 供应者需要提供接口(
特点
:- 具有非常强的偶合,任何修改都可能导致大规模的波动;
- 所有的修改都需要大量的沟通成本;
- 供应者通常按照客户需求提供服务,因此在某种程度上,服务质量受限于客户的需求。
部署和开发的依赖
- 客户和供应者需要达成明确的接口契约。供应者提供的接口(可能是API、数据模型等)必须被客户理解和接受。
- 若供应者提供的接口发生改变,需要提前通知客户。供应者不能单方面去更改接口,除非客户被明确得通知并同意。
- 供应者需要首先部署并确保其提供的服务或数据可用,然后客户才能进行部署。因为客户的系统通常需要在启动时就访问供应者提供的服务或数据。
- 供应者进行升级或者维护时,需要考虑避免或者最小化对客户的影响。需要考虑合适的时间和方式进行升级和维护。
适用性
:- 客户-供应者关系通常适用于明显的依赖关系中,供应者可以控制服务或数据的产出,但
需考虑到客户的需求
。
- 客户-供应者关系通常适用于明显的依赖关系中,供应者可以控制服务或数据的产出,但
2.3.3 合作关系(Partnership)
两个限界上下文需要密切协作以完成共同的目标。
要求
:- 合作的双方需要频繁沟通,并在接口、数据、过程等方面达成共识。
- 共享成功的责任:当解决方案成功时,双方都能分享成功的喜悦;失败时,双方都需要分担责任。
特点
:- 高度的协作和沟通:由于双方需要共同完成一些目标,这就要求双方需有高度的协作和沟通。
- 扩展性:如果双方的通信和合作机制被良好地定义和维护,那么当需求变更或规模增大时,系统的扩展性将更好。
开发与部署的依赖关系
:- 都是强依赖关系,需要双方有密切的协调与沟通
适用性
:- 当两个限界上下文被紧密绑定,共享一个明确的业务目标,且没有明确的主导方时,一般适合采用合作关系。
- 通常在同一公司的不同团队之间,或者在互有信任且高度协作的团队之间出现。
2.3.4 共享内核(Shared Kernel)
两个或更多的限界上下文共享相同的模型或代码段(内核)。
要求
:- 所有共享内核的上下文都需要约定并遵循共享部分的设计和实现。
- 不允许在没有其他共享该内核上下文同意的情况下更改共享内核。
特点
:- 合作是关键:必须在所有共享此内核的上下文之间进行频繁的沟通和协调。
- 变更控制:更改共享内核需要谨慎对待,并必须得到所有相关上下文的同意。
开发与部署的依赖关系:
- 开发任何一个与共享内核相关的功能,共享该内核的所有上下文都需要高度协调,以保证改动不会破坏其他上下文的功能。
- 升级或修改共享内核可能需要所有涉及的上下文同时部署,以避免不同版本的共享内核导致的问题。
适用性
:- 这种关系常出现在同一个项目或团队的不同限界上下文中。
2.3.5 不对称关系(Conformist)
在这种关系中,一个限界上下文(下游)选择完全遵守另一个限界上下文(上游)的模型、规则和策略。
要求
:- 下游限界上下文需要完全接受并遵循上游的模型和策略,即使这并不符合下游的最佳实践或优化。
- 上游通常没有义务对下游进行任何特殊考虑,也不需要对下游的变化做出响应。
特点
:- 明确的依赖关系:下游完全依赖上游,需要接受并遵守上游的模型和策略。
- 沟通成本比较低:由于下游需要遵守上游的模型,所以这种关系可以减少上下文间的沟通和协调。
开发与部署的依赖关系
:- 下游限界上下文需要按照上游的模型进行开发,需要密切关注上游的变化,以确保及时进行必要的调整和更新。
- 下游通常需要在上游部署并运行正常之后才能部署,因为下游直接依赖于上游提供的服务或数据。
适用性
:- 当下游不能影响上游的设计或决策,而且与上游的紧密交互是不可避免的情况下,通常选择不对称关系。
- 这种关系通常出现在内部团队与外部团队,或者大团队与小团队之间。
2.3.6 防腐层(Anticorruption Layer)
防腐层位于两个限界上下文之间,用于隔离它们的模型和交互方式,防止一个上下文的模型被另一个上下文“污染”。
要求
:- 防腐层需要
将上游限界上下文的模型或数据转
换为下游限界上下文可以理解的模型或数据
,反之亦然。 - 任何对上游限界上下文的调用都应通过防腐层进行。
- 防腐层需要
特点
:- 隔离和转换:防腐层的主要任务就是对于上游和下游之间交互的隔离和转换,确保下游不直接依赖上游的模型和实现。
- 保护自身模型:通过防腐层,下游限界上下文可以保护其自身的模型,防止被上游的模型“污染”。
开发与部署的依赖关系
:- 防腐层需要同时理解上下游的模型,并实现这两者之间的转换。如果上游的模型发生了变化,防腐层可能需要进行相应的更新。
- 因为防腐层直接依赖于上游限界上下文,所以在部署的时机上,需要确保上游限界上下文已经被正确部署和运行。
适用性
:- 当下游限界上下文需要与一个外部系统、遗留系统或者无法控制的系统进行交互,同时又不希望这个系统对其有太大影响时,可以使用防腐层。
例子
:- 假设一个电商系统(新系统)需要整合一个遗留库存管理系统。这个库存管理系统的数据模型已经和新系统的模型存在显著的差异,直接使用将会对新系统产生不良影响。
- 为了解决这个问题,可以在新系统(即下游限界上下文)和遗留库存管理系统(即上游限界上下文)之间建立一个防腐层。这个防腐层负责将库存管理系统的数据模型转换为新系统可以理解的模型,并封装所有对库存管理系统的调用。
- 这样,新系统可以只和防腐层进行交互,而无需直接依赖于库存管理系统的模型和实现。同时,如果以后需要替换库存管理系统,那么只需要更新防腐层即可,新系统无需进行任何更改,这大大提高了系统的灵活性和稳定性
2.3.7 开放主机服务(Open Host Service)
在这种关系中,一个限界上下文(主机)对外提供一组标准化的公开API,其他的限界上下文(客户端)可以通过这些API与主机进行交互。
-
要求
:- 主机需要提供一套稳定、可用、标准化的API,这些API需要满足其他限界上下文的业务需求。
- 客户端需要遵循主机的API规范进行开发,以确保正确地与主机进行交互。
- 主机需要及时更新和维护API,同时需保证向后兼容以减小对客户端的影响。
-
特点
:- 明确的接口:主机提供清晰、明确并且稳定的API,以满足客户端的需求。
- 低耦合:客户端并不直接依赖主机的内部实现,只需要与公开的API进行交互,这帮助降低了耦合度。
- 一对多交互:多个客户端限界上下文可以同时使用主机的API,与主机进行交互。
-
开发与部署的依赖关系
:- 主机侧需要定义并实现API,同时也需要为客户端提供相应的
文档和支持
。客户端则需要依据API文档
进行开发,实现与主机的交互。 - 通常情况下,主机需要先部署,以便客户端可以使用主机提供的API。
- 主机侧需要定义并实现API,同时也需要为客户端提供相应的
-
适用性
:- 当一个限界上下文需要为其他多个限界上下文提供服务,同时
又需要保持一致性和控制权
时,通常采用开放主机服务模式。 - 具备良好的API设计和管理能力的大团队或者重要的核心服务团队,通常更适合作为主机方。
- 当一个限界上下文需要为其他多个限界上下文提供服务,同时
2.3.8 发布语言(Published Language)
发布语言(Published Language是限界上下文之间沟通的一种方式。它是一个预定义的、标准化的数据交换格式。发布语言可以是任何公认的模型或数据格式,如XML, JSON,甚至可以是特定的业务语言。
-
要求
:- 发布语言应该是一个公认的,标准化的,通用的数据交换格式。
- 通过发布语言交换的数据应当是上下文之间都能理解的信息。
- 所有参与交互的限界上下文都应遵循使用同一种发布语言。
-
特点
:- 标准化:使用公认和标准化的语言,使得跨上下文通信变得可靠和一致。
- 抽象:提供了一种中立的方式表示和交换信息,屏蔽了内部模型的复杂性。
- 易于互操作:通过使用通用数据格式,可以简化不同上下文间的交互。
-
开发与部署的依赖关系
:- 开发阶段:各限界上下文应该根据发布语言提供的规范来进行开发,以确保能正确地解析或生成符合发布语言规定的数据。
- 部署阶段:由于发布语言是一种标准化的数据格式,开发和部署上的依赖性相对较低。
-
适用性
:- 适用于有跨不同限界上下文交互需求,且需要一致性和标准化交互方式的场景,例如在多个团队或服务间分享数据或命令时。
- 或者在系统与外部实体(如其他服务或应用)交互时也常用。
-
例子
:- 如利用RESTful API进行系统间交互。在这样的系统中,一种常见的发布语言是JSON。例如,订单系统(Order System)和库存系统(Inventory System)间的交互。订单系统在创建新订单后,需要通知库存系统进行库存扣减。
- 订单系统生成一个满足此JSON格式的消息,并发布到库存系统。而库存系统在接收到这个消息后,依据JSON中的信息进行相应的库存扣减操作。
- 在这个过程中,这个JSON就充当了订单系统和库存系统间的标准化交流语言。
{
"orderId": "123",
"items": [
{
"productId": "abc",
"quantity": 2
},
{
"productId": "def",
"quantity": 1
}
]
}
2.4 限界上下文与共享语言的关系
2.4.1 共享语言(统一语言)
共享语言是开发人员和非技术团队成员共同沟通业务概念的手段。以下是共享语言的一些要求和特点:
- 基于业务领域:共享语言应该是基于业务领域的术语和概念。它应该反映来自领域专家的深度知识,并用于描述业务规则和流程。
- 全团队使用:无论在团队内部交流还是在团队与业务专家之间的交流中,都需使用这个共享语言。这意味着无论是开发人员、项目经理、测试人员、需求分析师等,还是业务持有人,都要使用并理解这种语言。
- 项目全周期使用:它应该在整个项目生命周期中使用,包括在需求讨论、系统设计、代码实现、测试验证、文档制作以及后期维护等所有阶段。
- 语言的一致性:共享语言的表达应当是一致的,不论是在口头表述还是在代码中实现,用到的词汇表达都应该保持一致。例如,如果在业务讨论中将某个概念称为"订单",那么在编写代码、制定测试用例或编写项目文档时,都应当称之为"订单"。
- 适应性强和易于演变:随着业务的发展和变化以及项目需求的变动,共享语言需要具有足够的适应性和易变性。它需要随着项目的推进和知识的积累进行不断的修正和完善。
创建和维护一个良好的共享语言,可以有效地协助团队更深入地理解业务领域,减少沟通误解,并为创建精确的领域模型铺平道路。
2.4.2 限界上下文与共享语言的关系
- 特定语境使用:共享语言不是在所有场合下都适用,通常是在特定的限界上下文中使用。
同一词汇或概念,可能在不同限界上下文中有不同的含义
。 - 边界情境定义:限界上下文帮助我们定义了共享语言的使用范围和边界,有助于避免在不同业务领域之间造成语意混淆。
- 假设我们正在一个酒店管理系统中工作,系统中有两个明确的限界上下文:
预订管理
和客房服务
。 - 在"预订管理"这个限界上下文中,"客户"一词可能指的是通过网站预订房间的客人,对应的业务操作可能包括预订房间、取消预订等。
- 而在"客房服务"这个限界上下文中,"客户"可能被理解为已经到达酒店并入住的客人,对应的业务操作可能是清洁客房、提供早餐等。
- 落地到代码层面上,两个服务的
客户实体对象
有着不同的职责
,所以有着不同的方法。
- 假设我们正在一个酒店管理系统中工作,系统中有两个明确的限界上下文:
- 共享语言影响:限界上下文内的模型,例如实体或聚合,共享语言的词汇直接影响这些模型的设计和实现。
- 假设我们在开发一个图书馆管理系统,我们有一个"借阅"的限界上下文。在这个上下文中,我们的共享语言可能包括"书籍"、“读者”、“借出”、"归还"等词汇。
- 在这个上下文中,我们可能会有一个"书籍"的实体,它的属性可能包含书名、作者、ISBN号等。
- 我们也可能有一个"读者"的实体,它的属性可能包含读者名称、借阅证号、已借阅的书籍列表等。
- 在这个上下文中,"借出"和"归还"可能会被设计成对"书籍"实体的操作。
- 为了处理借阅和归还操作的复杂性,我们可能会创建一个"借阅"的聚合,它由"书籍"实体、"读者"实体以及他们之间的借阅关系组成。
- 语境转换:在不同的限界上下文之间,需要转换数据和行为,这种转换也是基于各自的共享语言进行的。
- 假设我们有一个电商系统,其中有两个限界上下文:“订单管理"和"库存管理”。
- 在"订单管理"的上下文中,"商品"可能包含商品名称、价格、购买数量等信息;
- 在"库存管理"的上下文中,"商品"可能只包含商品的库存编号和库存数量。
- 当一个新订单被创建时,"订单管理"上下文需要与"库存管理"上下文交互,以便减少相应商品的库存。
- 这时,就需要对"商品"数据进行转换:将"订单管理"上下文中的商品名称和购买数量,转换为"库存管理"上下文能理解的库存编号和需要减少的数量。
- 总结:虽然两个限界上下文都是用来商品这个概念,但是在不同的上下文中关注商品的属性侧重点不同。在建模时,自然不能建立为一种对象。但两个限界上下文中的商品概念又有联系,所以在交互过程中,需要进行转换,这种转换通常发生在适配层或应用层。
3. 领域模型
3.1 领域模型的组成
Entity(实体)
:是一种具有唯一不变标识符
的对象,并且通过时间和不同的表示形式保持连续性。例如,一个人(即使他们的姓名、地址等信息可能随着时间改变)或者一个帐户(即使帐户余额变动)都是实体。Value Object(值对象)
:是没有唯一标识符
的对象,通常是来定义属性。例如,日期、颜色或者金额都可以是值对象。Aggregate(聚合)
:将一组实体和值对象组织在一起形成一种边界,并且规定所有外部对象
只能持有聚合根的引用
。聚合根
是聚合中的某个实体
,用来封装聚合的内部实现,对外提供统一的访问入口。Domain Service(领域服务)
:当一些操作既不属于实体,也不属于值对象,而且会跨越多个聚合的时候,我们通常会使用领域服务来进行这样的操作。例如,用户注册、支付等操作就可以是领域服务。领域服务通常是全局的
,可以被任何实体、值对象或其他服务
调用。Domain Event(领域事件)
:领域事件是领域中发生的重要变化,这些变化可能会影响到其他部分。例如,订单已经被支付就可以定义为一个领域事件。Repository(仓库)
:用于存储、检索和管理聚合或实体的集合
。它可以看作是一种持久化机制,对外提供统一的存储接口。
3.2 领域模型划分举例(电商系统)
用户 (User)
:实体,每个用户都有唯一的标志(用户ID)来区分,其他信息更改也不影响其用户身份(如收获地址,昵称)收货地址 (Address)
:值对象。收货地址是由街道、城市、州/省、邮编这些属性组成的,我们通常不会直接去定位一个地址,而总是在定位到实体后再去查找其某个值对象。订单 (Order)
:聚合,其中包含多个订单项和一个用户,这整个聚合的聚合根就是订单本身
。订单可以保障其内部所有订单项的数据一致性
。库存扣减 (Inventory Deduction)
:领域服务,它涉及到的操作可能跨越用户、订单等多个聚合的边界
。订单已支付 (Order Paid)
:领域事件,当用户完成支付动作,订单状态
变为已支付,触发一个订单已支付的领域事件。
4. 领域对象
4.1 什么是领域对象
- 领域模型由领域对象组成。
- 领域对象包括以下几种
- 实体
- 值对象
- 聚合
- 领域事件
- 领域服务
- 领域对象都是为了解决业务问题而创建的,它们应当
包含足够的业务逻辑
,而不仅仅是数据的容器。
4.2 什么是实体
- 实体
对象
拥有唯一标识符,该对象在经历各种状态变更仍能保证标识符一致。 - 实体对象强调唯一标识。如商品是商品上下文的一个实体,通过商品的唯一ID标识一个商品,不管商品其他数据如何变化,只要这个ID不变,他始终代表同一个商品。
在领域驱动设计中,实体类通常采用充血模型。即实体要包含足够的业务逻辑
4.3 实体的数据库形态
DDD设计中是先有领域模型,再针对业务场景构建实体对象,最后将实体对象映射与数据持久化
- 所以,实体对象和数据持久化对象之间不一定是一一对应。可能是一对一,多对一,一对多,以及一对零。
- 如权限实体对应user和role两个持久化对象
- 如为了减少链表查询,将客户和账户两个实体映射为一个持久化对象。
- 在领域驱动设计中,由
Repository(仓库)
进行数据持久化管理
4.3 什么是值对象
- 没有标识符的对象叫做值对象。
- 值对象是若干相互关联的属性集合。
- 如
省、市、县和街道等属性
构成了地址
这个值类型。该类的一个具体实例就是一个值对象。
- 如
- 值对象一般不会涉及到直接定位,比如我们不会直接去定位一个地址,通常是先去定位到一个用户,再去找这个用户的地址。
- 实体类会包含业务逻辑,包含修改数据的行为。而值类型则基本不包含业务逻辑,很少涉及修改数据的行为。
- 值类型一般是实体类型的一部分,用于描述实体的特征。
// 实体类
public class Person {
public String ID; // 实体类的标识字段
public String name;
public int age;
public boolean gender;
public Address address; // 值类型作为实体类的属性。
// 各种实体方法,省略
}
public class Address {
public String province;
public String city;
public String country;
public String streat;
// 值对象方法,省略。
}
4.4 实体如何持有值对象
属性嵌入
// 实体类
public class Person {
public String ID; // 实体类的标识字段
public String name;
public int age;
public boolean gender;
public Address address; // 值类型作为实体类的属性。
// 各种实体方法,省略
}
public class Address {
public String province;
public String city;
public String country;
public String streat;
// 值对象方法,省略。
}
序列化值对象
// 实体类
public class Person {
public String ID; // 实体类的标识字段
public String name;
public int age;
public boolean gender;
public String address; // json类型,将Address序列化后嵌入实体中。
// 各种实体方法,省略
}
public class Address {
public String province;
public String city;
public String country;
public String streat;
// 值对象方法,省略。
}
-
属性嵌入值对象
这种方式是直接将值对象作为实体的属性。这样可以更直观地表示实体与值对象之间的关系,并有利于在程序中理解和操作。
优点:
- 更直观和易于理解。值对象与实体之间的关联关系可以更清晰地体现在代码中,有助于理解系统的运作原理。
- 较好的性能。不需要经过序列化和反序列化处理,可以直接从实体对象访问值对象的属性。
缺点:
- 在持久化麻烦,如果需要将数据存储到关系型数据库,则需要设定映射策略。
- 存储效率可能略低。因为每个实体在数据库中都需要为值对象的每个字段分别创建一列。字段过多。
-
序列化值对象
这种方式是将值对象序列化为一种可存储的格式(如 JSON 或者 XML),然后将其存储在实体的一个字段中。当我们需要使用该值对象时,可以再将其反序列化回对象。
优点:
- 方便存储。无论值对象的属性如何复杂,只需要一个字段就可以存储整个值对象。
灵活性高
。在对值对象的结构修改时,只需要相应地调整序列化和反序列化的操作即可。
缺点:
- 需要进行序列化和反序列化。这会使得程序变得复杂,也可能对性能产生影响。
- 读取难度增加。直接从数据库查询特定值对象的某个属性时,需要先进行反序列化操作。
总结,属性嵌入值对象:代码容易理解且性能好;序列化值对象:存储方便且灵活性高。
4.5 值对象如何持久化
- 在DDD中,我们将值对象嵌入到实体对象对应的数据库表中。
- 实体对象通过序列化大对象将值对象嵌入,简化了数据库的设计。
- 总结:
在领域建模时,我们将部分对象设计为值对象,保留对象的业务含义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计
4.6 值对象的优缺点
- 优点
- 简化数据库设计,减少实体表的数量。
- 缺点
- 无法满足基于值对象的快速查询。
- 值对象缺乏概念完整性。(一般值对象作为实体表中的一个json字段)
5. 聚合
5.1 聚合的定义
- 聚合是一个业务概念的完整集合,它包含了实体、值对象,以及它们之间的关系。每个聚合都具有清晰的边界,并且代表一个有特定业务含义的完整概念。
- 每一个聚合里面一定要包含一个聚合根,以及它的上下文边界。
- 70%的场景下,一个聚合内都只有一个实体,那就是聚合根。
- 一个限界上下文可能包含多个聚合,但一个聚合只能存在于一个限界上下文
- 一个重要的原则是:
业务规则和数据的一致性保证必须在聚合内部实现,聚合的任何实体的状态变化都必须产生一致的、有效的聚合状态。
5.2 聚合根
- 每个聚合中都会有一个特定的实体,被选定为聚合根。
- 聚合根是整个聚合中
外部对象
唯一能够直接引用的对象。 - 外部对象对聚合的操作都
必须通过聚合根
来进行,其他的实体和值对象
则对外部对象
不可见。(即外部对象不能直接修改
聚合内部的其他对象,这个外部对象
通常指的是领域服务
)- 例如,要添加一个新的
订单项(非聚合根实体)
,领域服务不应该直接创建并添加到订单项集合中,而应该调用订单(聚合根)
的一个方法(如 addOrderItem(product, quantity)),由订单聚合根来保证添加的合法性和一致性
。
- 例如,要添加一个新的
- 聚合根有责任
确保聚合的内部状态的一致性和完整性
,聚合根的任何方法都不应让聚合处于无效的状态。 - 聚合内
聚合根对实体和值对象采用直接对象引用
的方式进行组织和协调。(所以当我们拿到聚合根实体后,那么该聚合下的所有实体和值对象也可以得到) - 一般来说,
领域服务
操作聚合根
,聚合根
操作聚合内的其他对象
5.3 领域层包的划分规则
com
└── mycompany
└── myproject
└── domain
├── model # 包含所有的领域模型,通常是按照业务上下文进行模块化组织
│ ├── order # "订单"业务上下文
│ │ ├── Order.java # 订单聚合根
│ │ └── OrderItem.java # 在订单聚合内的其他实体
│ └── customer # "客户"业务上下文
│ └── Customer.java # 客户聚合根
├── repository # 数据访问接口,一个聚合通常对应一个仓库
│ ├── OrderRepository.java
│ └── CustomerRepository.java
├── service # 领域服务,通常包含跨领域模型的业务逻辑
│ └── OrderService.java
└── event # 领域事件,描述领域中重要的业务事件
└── OrderCreatedEvent.java
上面给了一个通常的包目录划分规则,实际的项目可能会根据具体的业务需求和团队的编码规范来进行适当的调整。
以上的目录结构中包含了以下几个关键的部分:
-
model:按照业务上下文来组织的领域模型,每个业务上下文下包含了相应的聚合、实体、值对象等。
-
repository:数据访问接口,一般来说,一个聚合对应一个仓库。仓库用来封装领域对象的持久化细节。
-
service:领域服务层,包含了涉及到多个领域对象的复杂业务逻辑。
-
event:领域事件,描述了领域中的重要业务事件。
5.4 一次业务处理的经过
应用服务 ->领域服务 ->通过资源库获取聚合根
->通过资源库持久化聚合根
->发布领域事件
5.5 聚合根之间如何相互引用
- 一个聚合根通常
不直接引用
另一个聚合根的对象。 - 聚合之间的引用通常通过
唯一识别符
进行。- 降低耦合:由于聚合根之间通过ID引用,而不是直接关联对象,降低了聚合之间的直接依赖,也就减少了系统中的耦合。
- 保持聚合边界清晰:每个聚合都保持独立,维持了DDD的一个基本指导原则:聚合应该是事务的一致性边界。这有助于正确维护和理解每个聚合的业务规则。
- 提升性能:直接引用可能导致
无意识地加载过多的对象
,从而浪费内存和计算资源。通过ID引用,可以根据实际需要加载所需的聚合实例
(通过仓库(Repository)和该唯一识别符获取到具体的聚合实例)。 - 增强灵活性和可扩展性:通过ID引用,你可以更灵活地处理关联聚合的生命周期和存储,比如可以采用延迟加载策略或者根据需要将关联聚合存储在不同的存储媒介。
- 便于分布式系统设计:在微服务或者分布式系统中,不同的聚合可能位于不同的服务或者物理机器,通过ID引用可以方便地跨服务或者物理机器进行调用或查询。
比如,一个"订单"聚合根(Order)可能需要引用"用户"聚合根(User),在订单聚合中,我们保存用户的ID,而不是用户对象的直接引用。当我们需要在订单中使用用户信息时,领域服务
通过用户ID
去用户仓库
中获取实际的用户对象
。
5.4 调用原则
- 聚合根不能直接操作其他聚合根,聚合根之间只能通过聚合根ID引用
同限界上下文内的聚合之间的领域服务可直接调用
。- 两个限界上下文的交互必须通过应用服务层抽离接口(适配层适配)
6. 领域服务
6.1 什么是领域服务
领域服务是被用来处理那些不适合放在实体或者值对象
中进行的业务逻辑,或者涉及到多个聚合
的业务逻辑。
这样的好处是:
- 有助于
保持实体和值对象的简洁和单一职责
。对于某些业务逻辑无法归类到实体和值对象中。- 比如一种复杂的校验逻辑,不能直接归属到任何实体或值对象,那它就适合放在一个领域服务里。
- 多个聚合之间的交互通过领域对象,减少了聚合之间的耦合。
- 强化了领域语义,帮助管理复杂性,增强了模型的可读性和可理解性
6.2 举例说明
业务需求:有"订单"(Order)和"库存"(Inventory)两个聚合。现在有一个业务需求,就是当下订单时,需要扣减库存。
简单方法
:可以直接在"订单"聚合中调用"库存"聚合的扣减方法,缺点
:聚合间的耦合增加。一旦"库存"聚合的扣减逻辑发生改变,"订单"聚合也必须修改,这违反了聚合的设计原则。
DDD设计方法
:引入领域服务,称为OrderProcessingService。定义placeOrder方法,接收订单和商品数量作为参数,它首先调用"订单"聚合的方法生成订单,然后调用"库存"聚合的方法扣减库存。优点
:保持了"订单"和"库存"聚合的独立性和封闭性。
补充一下,这里面消灭了变化吗?其实没有,原先库存逻辑发生改变,订单聚合也需要修改。采用DDD后,库存逻辑发生改变,虽然订单聚合不需要修改了,但是引入的领域服务却需要修改。所以变化没有被消灭,只是将变化转移到特定的地方,其实我们消灭不了变化,但我们能做到的事分门别类地管理变化,让变化在合适的位置,即封装变化到特定位置。
7. 领域事件
7.1 什么是领域事件
领域事件表示在业务领域内发生的重要事件
,这些事件可能触发其他业务行为
或在业务流程中扮演重要角色
。具有以下好处:
- 切断领域模型之间的强依赖关系,事件发布完成后,发布方不必关心后续订阅方事件处理是否成功,实现了领域模型之间的解耦。
- 在领域模型映射到微服务系统架构时,领域事件可以解耦微服务,微服务之间的数据不必要求
强一致性
,而是基于事件的最终一致性
。 事件驱动本质是将同步调用转为异步调用,由强一致性转为最终一致性
- 领域对象主要的
目的是表示某种重要的业务状态变更
,它主要被用作数据传输对象
,因此领域事件对象本身通常不包含业务逻辑
- 领域事件对象生成后,事件中的业务数据不再修改,它通常被视为值对象。
- 领域事件通常代表系统中发生的一种重要状态变化,而这种变化往往是瞬态的,并且领域事件自身也没有长期持久化的需求。
- 一旦系统中发生相应的状态变化,领域事件就会被创建,并且在创建后,它的状态是不可变的,因为它描述的是当时的状态事实。
- 领域事件并不具有自己的生命期,即我们通常不关心领域事件对象本身的变化,只关心它代表的业务含义。
7.2 领域事件要符合的要求
- 反映业务语义:领域事件应该代表着业务领域中一个显著的、有意义的事情的发生,它反映的应该是领域专家关心的
业务变化
。 - 记录状态改变:领域事件通常用来
描述系统中的状态改变
,例如,“订单创建”、“付款成功”、“货物邮寄”等。 - 不可变性:一旦领域事件被创建,就不能更改。领域事件通常包含触发事件时的状态信息,比如订单支付事件会记录支付的金额、支付时间等信息,而这些信息在创建后是不应更改的。
- 有明确的事件源:领域事件需要有
明确的源头
,以便跟踪
到究竟是哪个聚合或实体
发起的这个事件。这通常通过记录事件源的唯一标识符来实现。 - 可序列化:事件需要能够序列化和反序列化,因为它常常需要在网络中传输,或者被存储在数据库中。
- 包含时间戳:领域事件
应该记录其发生的时间
。这在很多情况下都是很重要的,比如进行审计、排序事件、或在某些业务规则中该事件必须在某个特定时间内被处理。
以上特性确保了领域事件可以准确地反映业务的状态变化,同时也使得领域事件在系统中的流转变得更加安全、可追踪。
假设我们正在设计一个电商系统,我们可以定义一个"订单支付成功"的领域事件。根据领域事件的要求,这个领域事件应该包含以下属性:
-
反映业务语义:订单支付成功是一个重要的业务事件,表示用户已经对订单进行了支付,系统需要对此做出响应。
-
记录状态改变:支付成功显著改变了订单的状态,使其从“未支付”变为“已支付”。
-
不可变性:一旦订单支付成功事件被创建,其携带的信息如支付金额、支付方式等就应该被锁定,不能更改。
-
有明确的事件源:订单支付成功事件的源头是具体的订单,可以用订单ID来表示。
-
可序列化:该事件需要被序列化成如 JSON 的格式,以便存储或在网络间传输。
-
包含时间戳:应该记录下订单何时支付成功,以便进行如退款等后续操作在适当的时间。
定义一个订单支付成功的领域事件可以如下:
{
"eventType": "OrderPaid",
"orderId": "123456",
"paymentMethod": "Credit Card",
"paymentAmount": "100.00",
"paidAt": "2022-01-01T10:00:00Z", // 使用ISO 8601格式的时间戳
}
系统中的其他服务如邮件通知服务,库存服务等可以监听这个OrderPaid
事件,并根据自身的业务逻辑进行相应处理,如发邮件给用户,更新库存等。
7.3 领域事件的使用范围
- 微服务内的领域事件
- 用的不多,同一个微服务进程内,编程语言层面上几乎可以很好地进行业务编排交互。
微服务之间的领域事件
- 要考虑
事件构建,发布与订阅,事件数据持久化,消息中间件以及分布式事务机制
如果微服务之间不使用领域事件,则微服务之间的访问也可以采用应用服务直接调用的方式,但是这个需要引用分布式事务机制,以确保数据的强一致性。
- 要考虑
7.4 事件的基本属性与业务属性
基本属性
- 事件唯一标识
- 发生时间
- 事件类型
- 事件源
业务属性
- 记录是按发生那一刻的业务数据。会随事件传输到订阅方
7.5 事件构建与发布
- 事件
基本属性
和业务属性
一起构成了领域事件
- 领域事件通常在以下几个部分进行构建:
- 聚合:聚合是业务领域模型的核心部分,它们通常封装了较为复杂的业务规则,当业务状态发生变更时,聚合内部可能需要创建并发布领域事件。例如,在一个电子商务系统中,当一个订单被创建后,订单聚合可能需要创建一个“订单已创建”事件。
- 领域服务:领域服务处理那些不属于任何特定实体或值对象的业务逻辑,它们也可能会创建领域事件。例如,一个处理支付的领域服务,在接收到支付确认后,可能会创建一个“支付完成”事件。
- 应用服务:应用服务协调并驱动领域层完成业务需求,它们负责处理更高层次的业务流程,因此可能会在合适的时机创建并发布领域事件。
public class DomainEvent {
public String ID;
public long timeStamp;
public String source;
public String type;
public String data;
// 领域事件一般只用作传输对象,一般不包含业务逻辑。
}
7.6 事件处理流程
- 事件构建
- 事件发布
- 事件持久化
- 事件接收
- 事件处理
8. DDD分层架构
8.1 用户接口层
- 负责处理流量的输入和系统的输出。用户接口层不应包含业务逻辑。
- 针对于
消息队列的消费者服务
,其用户接口层可能的工作:监听消息队列
消息解包和预处理
:比如反序列化成领域事件对象
、验证消息的完整性
。委托应用层处理消息
接收应用层的反馈
错误处理或日志记录与处理监控
:如统计处理时长、打点监控、错误重试。
- 针对
web服务、rpc服务
,其用户接口层主要可能工作- 接收用户请求
- 路由请求:如将一个POST请求根据URL转发到特定路由。
- 验证和处理输入数据:数据解码/解析、验证数据内容等。
- 调用应用层服务
- 格式化和返回响应:如构建并发送HTTP响应。
- 错误处理和记录:处理来自应用层或领域层的错误。记录到日志系统。
8.2 应用层
- 应用层是
很薄的一层
,理论上不应该有业务规则和逻辑。 - 处理业务的高层流程和协调领域对象来实现业务需求的地方。应用层将各种领域对象组合,驱动领域层完成具体的业务需求。
协调/编排
应用的各种领域对象(实体、值对象、领域服务等);- 可能进行安全验证,权限校验,事务控制,发送领域事件等;
- 不包含任何业务规则或者业务知识,只委托给领域对象处理;
8.3 领域层
- 实现企业核心业务逻辑。
- 领域层包含聚合根、实体、值对象、领域服务等领域模型中的领域对象
- 领域中的某些功能单一实体不能实现时,领域服务就会出马,它可以组合聚合内的多个实体,实现复杂的业务逻辑。
- 领域层里面的对象普遍采用充血模型,即对象要负责相应的业务逻辑。
8.4 基础设施层
- 基础设施层贯穿所有层,它的作用是为其他各层提供通用的技术和基础服务。
- 如第三方工具,驱动,消息中间件,网关,文件,缓存以及数据库,还有数据持久化。
- 面向
领域层、应用层以及用户界面层
,为它们提供支持,使它们专注于执行业务逻辑。
8.5 分层架构的原则
- 不能跨层访问
- 业务规则和业务知识应该被主要定义在领域层,并由应用层进行协调和驱动。
- 而用户界面层和基础设施层只需提供他们自身的职责,例如提供用户交互,持久化数据等,并不需要理解业务规则。
9 一些概念
9.1 领域对象之间的关联
9.1.1 关联的分类
- 方向性
- 单向关联:A对象持有B对象,B对象不持有A对象
- 双向关联:A对象持有B对象,B对象也持有A对象
- 数量
- 一对多
- 多对一
- 一对一
- 多对多
9.1.2 双向关联的特点
-
互相引用:在双向关联中,两个对象都拥有对方引用的权力。例如,在面向对象设计中,某个类的实例中可以包含指向另一个类实例的引用。
-
数据一致性:一对双向关联的实体需要处理的一个
主要问题
是数据的一致性
。例如,当你更改一端的关联时,你必须确保另一端的关联也做出了相应的更改。否则,就可能导致数据不一致。 -
性能考虑:双向关联有助于提高查询效率。例如,当你拥有一个对象引用时,可以直接通过它来访问相关的其他对象,而不需要执行额外的查询。
-
复杂性考虑:虽然双向关联有其优点,但也增加了设计的复杂性,并可能引入
循环引用
的风险。因此在使用时需要谨慎。 -
内存管理问题:在某些情况下,双向关联可能导致对象不能被正确地垃圾回收,因为它们互相持有对方的引用,这可能会导致内存泄露。
在一个博客系统中,存在“博文”和“评论”两种实体。一个博文可以拥有多个评论,而每一个评论都对应于一个特定的博文。在这个例子中,一个“博文”实体可能会维护一个包含其所有“评论”的列表,反过来,“评论”实体也会持有其对应的“博文”的引用。这就构成了一个双向关联,即,可以从“博文”访问它的“评论”,也可以从“评论”访问其对应的“博文”。
9.1.3 对关联关系比较好的约束方法
在DDD中,关联的管理是一个非常重要的问题。设计得当,可以使得代码更清晰、更易理解;否则,可能会导致代码复杂难懂。常见有效管理和约束关联的方法:
- 使用聚合:聚合是一种将相关实体和值对象组合成一个单一的、可通过聚合根访问的单元的方式。在一个聚合内部,对象可以自由关联;但是跨聚合的关联只能通过聚合根。这样可以大大限制关联的数量和复杂性。
- 在一个电商系统中,一个订单(Order)聚合可能包括订单项(OrderItems)和配送信息(ShippingInformation)。
- 在此情况下,“订单项”和“配送信息”只能通过“订单”进行访问和操作,而不能独立存在。
- 限制关联的方向:通常来说,尤其是在多对多关联情况下,关联都是有方向性的。有时,我们可以规定只能从一个实体向另一个实体的方向遍历,而不能反向。这样可以减少关联的复杂性。
- 在社交网络系统中,用户(User)和好友(Friend)之间存在多对多关系
- 但在实际操作中,我们可以规定只能从用户端遍历到其好友,而不能从好友端反向遍历到用户,从而降低了关联的复杂性。
- 使用限定符减少多重关联:为了管理那些潜在的大量关联,我们可以添加限定条件(如过滤条件),以减少实际需要处理的关联数量。
- 在图书管理系统中,书(Book)与读者之间存在多对多的关联关系
- 如果书籍种类很多,某读者只对科幻类书籍感兴趣,那么我们可以通过限定书的种类为“科幻”,来有效减少需要处理的关联数量。
- 工厂和仓储:当创建新实体或寻找已存在的实体进行关联时,可以利用领域工厂和仓储。这样的话,关联的行为就可以通过这些模式进行封装,从而降低系统复杂度。
- 在汽车制造系统中,我们需要生产一辆汽车,这涉及到产品线上数百个部件(包括引擎、车轮、车身等)的装配和关联。
- 在这种情况下,我们可以使用一个汽车工厂(CarFactory)来封装创建新汽车的过程,直至所有部件关联完全。
- 之后,我们可以把创建好的汽车对象存储在汽车仓储中,如果以后其他业务流程需要用到这辆车,只需要从仓储中取即可。
- 消除不必要的关联:代码复杂性和维护难度通常随着关联数量的增加而增加。因此,如果发现某个关联在业务情境中并无实际价值,那么最好的做法就是消除它。
- 在一个学生和老师的关联系统中,学生可以选多个老师作为导师,老师也可以带多个学生。
- 然而,如果系统中的某个功能只关心学生当前的主要导师是谁,那么与其他非主导师的关联就可以视为不必要的,可以被消除。
9.2 工厂和仓储的区别
- 领域工厂主要负责创建复杂的领域对象
- 领域仓储主要负责查询、持久化已有领域对象
- 工厂负责处理对象生命周期的开始
- 仓储帮助管理生命周期的中间和结束
- 但通常有一部分对象存储在关系数据库、文件或其他非面向对象的系统中。在这些情况下,检索出来的数据必须被重建为对象形式。但重建一个已存储的对象并不是创建一个新的概念对象。仓储通过查询和重建对象让客户感觉到那些对象就好像驻留在内存中一样。
9.3 规则范式与对象范式结合
9.3.1 什么是规则范式
- 规则范式是一种逻辑编程范式,它基于规则来表示知识并进行推理,其中的规则往往表现为 “如果…那么…” (If…then…) 的形式。规则范式的主要精神在于将编程任务视为一系列的规则,并让机器通过逻辑推理来执行这些规则。
- 在领域驱动设计(DDD)中,规则范式可以与对象范式结合在一起,这样就可以既利用对象范式为我们提供的状态和行为封装,又能够使用规则范式处理复杂的业务规则。
9.3.2 规则范式与对象范式结合的方式
-
领域服务中使用规则引擎:
领域服务
是没有明显的状态和行为逻辑,属于一种无状态服务,可以包含规则引擎来处理复杂的业务决策。领域服务可以接收特定的领域对象,然后应用规则引擎以决定如何处理这些对象。 -
在领域对象中嵌入规则:如果某些复杂的业务规则直接对应于特定的领域对象,那么这些规则可以直接写入领域对象的方法中。比如,如果有一个"用户"对象,并且存在一个规则,描述了"如果用户积分超过1000,那么用户成为VIP",那么这个规则可以直接写入"用户"对象的方法中。
-
使用策略模式:策略模式是一种行为设计模式,允许在运行时选择对象的行为。如果在领域模型中有多个规则可供选择,那么可以使用策略模式。每一种策略都封装了一个特定的规则,运行时可以动态选择使用哪一种策略。
9.3.3 什么是规则引擎
规则引擎,即业务规则引擎,是一种计算机软件系统,专门用于执行业务规则。业务规则是逻辑描述,比如 “如果条件A满足,那么进行操作B”。规则引擎的目标是管理和执行这些规则。
规则引擎主要由两大部分组成:一个用于业务规则的定义和管理(比如添加、修改、删除规则)的规则管理系统
,以及一个用于匹配和执行规则的规则执行系统
。
规则管理
:例如规则的版本管理、权限控制,以及规则的导入导出等。规则执行
:规则引擎在接收到某个领域对象时,会根据定义的规则来指导这个对象应该怎么处理。处理方式可以是计算、执行动作,或将结果引导到其他流程。
规则引擎有以下特点:
-
独立于业务流程:规则引擎使得业务逻辑可以从业务流程和应用程序代码中提取出来,独立管理。这样就可以在不改变业务流程和应用代码的情况下,灵活地修改业务逻辑。
-
逻辑表达能力:规则引擎通常包含一种或几种对业务逻辑进行表达的方式(如决策表、决策树、规则链、脚本等)。通过这些工具,业务人员或开发人员可以用相对直观的方式定义业务规则。
-
自动匹配与执行:当规则引擎接收到需要处理的数据(通常包装为一种称为"事实"的数据结构)时,它会自动匹配其业务规则库中所有与这些数据相关的规则,并对这些匹配的规则进行执行。
-
万能适用性:规则引擎不仅可以应用于业务系统,也可以用于各种需要决策逻辑的场景,如推荐系统、风险控制系统等。
通过使用规则引擎,企业能更方便地管理和调整其业务逻辑,提高业务处理效率,同时降低对IT资源的依赖。
9.3.4 DDD中如何使用规则引擎
确保领域对象范式的逻辑清晰性,同时让规则引擎灵活地处理多变的业务规则。
-
确定何处使用规则引擎:并非所有的业务规则都需要由规则引擎处理。对于稳定、不经常变更的核心业务规则,可以内置在领域对象中。对于经常变更或需要由业务人员直接管理的业务规则,可以交由规则引擎处理。
-
规则引擎与领域模型的交互:将
规则引擎
作为领域服务
,允许它操作和决策领域对象
。规则引擎可以在需要时接收领域对象,根据业务规则做出判断,然后更改领域对象的状态或者引导对象进行相应的行为。 -
明确领域模型的边界:明确哪些操作应该由领域模型完成,哪些决策应该由规则引擎来执行。
领域对象应该封装业务的固有逻辑和行为
,规则引擎主要侧重于处理可配置和可变的业务决策
。 -
保持规则引擎的透明性:应对规则引擎进行足够的记录和审计,确保当规则执行产生结果时,我们能理解为何会产生该结果。这也对于调试和分析业务规则执行过程非常有帮助。
-
避免规则冲突:
需要避免创建与领域对象内置业务逻辑冲突的规则
。我们做领域模型设计时,已经对业务产生了深入理解。如果规则引擎中的规则与领域对象范式内置逻辑存在冲突,可能会导致错误的业务行为。
在实践过程中,我们的目标是平衡领域驱动设计和规则引擎的使用,两者合作可以让我们的应用更加灵活,并更好地符合业务的需求。
9.4 隐式概念转换为显示概念
9.4.1 隐式概念和显示概念的区别
在领域驱动设计(DDD)中,隐式概念和显示概念是关于如何在代码中表示业务规则
和业务逻辑
的概念。
-
隐式概念(Implicit Concept):这些是在业务规则或业务逻辑中存在,却并未明确地在代码中体现出来的概念。它们通常存在于代码的某个特定部分,或分散在代码的多个地方。
- 例如,假设有一个在线购物系统,在代码中你可能会在多个地方看到 “items * price + shipping fee” 这样的逻辑来计算订单总价。
- 这里,“订单总价的计算逻辑” 就是一个隐式概念,因为它并没有被封装到一个独立的方法或者类中,而是在代码的多个地方都有相同的计算过程。
-
显示概念(Explicit Concept):这些是直接在代码中定义的,明确体现了业务规则和业务逻辑的概念。这通常体现在定义的类、结构、方法、变量、常量等代码元素中。
- 将订单总价的计算逻辑封装到一个名为 “CalcTotalPrice” 的订单类方法中,这样 “订单总价的计算逻辑” 就从一个隐式概念转变为了一个显示概念。
- 这样一来,后续在任何需要计算订单总价的地方,都可以调用这个方法,而不是在各个地方重复相同的逻辑。
在DDD中,推崇将隐式概念转变为显示概念,因为它可以简化代码,降低维护难度,提高代码质量,使得代码对业务需求的表述更加精确,也能更好地支持业务的变化和增长。
9.4.2 如何将隐式概念转换为显示概念
-
引入新的领域对象:如果当前的模型无法清晰地表示某个业务概念,可以考虑引入一个新的领域对象。例如,如果代码中到处都在对一个"日期范围"进行操作,那你可以创建一个新的"日期范围"领域对象。
-
重构领域对象:如果一个对象包括了太多职责或概念,那么可以尝试将它进行拆分,每个对象只表达一个概念。例如,如果你有一个表示"用户"的对象,但它同时也包括了"账户"相关的逻辑,那你可以将"账户"部分独立为一个新的领域对象。
-
引入领域服务:有时,某个特定的业务操作无法归属于任何一个特定的实体,而是跨越了多个实体,这时可以创建一个新的领域服务来承担这个业务操作。
-
使用值对象:如果有一些关联的属性被经常一起使用,那么这些属性可以被封装为一个值对象。例如,“地址"包括"街道”、“城市”、“省份"和"邮编”,它们通常被一起使用,所以可以把它们封装为一个"地址"值对象。
通过将隐式概念转换为显式概念,我们能够使代码更加符合业务需求,更易于阅读和维护,更容易适应和响应业务的变化。
9.5 柔性设计
柔性设计指的是建立一个能够适应变化、方便扩展和维护的设计。其目标是使得当业务需求或技术环境发生变化时,系统可以方便地进行适应,降低系统变化的成本。
9.5.1 无副作用函数(Side-Effect-Free Function)
- 无副作用函数指的是函数的执行不会影响程序的其他部分,例如更改全局状态或修改输入。
- 无副总用函数的特点:
- 纯粹性:函数的输出仅取决于输入,不受外部状态的影响。
- 不可变性:函数不会修改输入值,不会改变
非局部变量
,也不会操作IO。 - 可预测性:对于同样的输入,函数将始终产生相同的输出。
- 优点:
- 代码更易于理解、测试和重用
- 使得并行和分布式计算更为简单。
- 如果能设计成无副作用函数就尽可能设计成为无副作用函数。
9.5.2 断言(Assertion)
- “Assertion”(断言)指的是在代码中对某些条件进行检查的声明,通常用于验证领域模型中的业务规则。
- 断言主要用于确保在程序执行过程中,一些关键条件必须满足。
- 如果关键条件未满足,程序将抛出错误或异常,阻止程序以
不正确的状态或数据
继续运行。 - 断言会马上暴露问题,使得问题更易于排查和修复。
9.5.3 概念轮廓(Conceptual Contour)
- 概念轮廓主要涉及到如何确定和区分领域模型中有关概念的边界。
- 明确的轮廓可以帮助我们划分边界,
确定哪些概念和操作应该归组在一起,哪些应该分开
。 - 例如,在一个电子商务系统,需要定义"用户",“商品”,"订单"等概念的轮廓,以及它们如何相互交互。在这种情况下,“订单"可能包含"用户"和"商品"的信息,但每个概念都有自己的明确边界。我们可能不会直接在"订单"中修改"用户"的信息,而是要首先处理"用户”,然后更新订单中的相关信息。
9.5.4 独立类(Standalone Class)
- 独立类表示那些
独立存在、不需要依赖于其他类
就能完成自己的功能的类。 - 优点:
- 提高模块化
- 易于理解和维护
- 促进代码复用
public class Product {
private String id;
private String name;
private double price;
public Product(String id, String name, double price) {
this.id = id;
this.name = name;
this.price = price;
}
// Getters and setters...
public void increasePrice(double increase) {
this.price += increase;
}
public void decreasePrice(double decrease) {
this.price -= decrease;
}
}
9.5.5 揭示意图的接口(Intention-Revealing Interfaces)
- 强调接口应该能够清晰地表达出其预期的用途和操作方式。
- 当我们设计类和方法时,揭示意图的接口要求我们使用具有描述性的名称,以及表达出预期行为的参数和返回值。
9.5.6 操作的封闭性(Closure of Operations)
- 领域操作的结果应仍然在模型的有效状态集中进行。
- 比如转账在资金不足的情况下也不可能出现负值。
9.5.7 声明式设计(Declarative Design)
- 声明式设计主张描述"程序应该做什么"而不是"程序应该怎么做"。
- 最大的好处就是容易理解,屏蔽了复杂实现细节。