DDD
领域驱动设计
- DDD是一种通过领域模型驱动复杂系统设计的软件方法论。其核心在于将业务逻辑与技术实现紧密结合,确保软件系统能够准确反映业务需求
- 目的是为了对软件设计的领域进行建模,以应对系统规模过大引起的软件复杂性问题。
- 什么是领域建模?
- 领域模型跟技术毫无关系,而是为了更有结构化的拆解和表达业务逻辑。业务逻辑来自现实世界里的具体场景,涉及可视画面、操作动作和流程。要准确表达业务逻辑需要先讲清楚每个概念是什么,再建立概念之间的联系,基于这些关系再组合出更多的流程;
- 概念、联系、流程就是领域模型。围绕领域模型去表达业务时也自然而然地把技术实现细节分离出去了。后续代码实现就是将业务架构映射到系统架构的过程,以后业务架构调整了能快速的调整技术架构。
- DDD用哪些领域概念表达业务?
- 表示业务逻辑的是:实体、值对象、领域服务、领域事件。这意味着所有领域逻辑都应该在这四种对象里,统一称为领域模型对象,这将极大减少业务逻辑的蔓延;
- 引入聚合进一步封装实体和值对象,让领域逻辑更内聚,起到边界保护的作用。聚合的引入使得业务对象间的关联变少。如何设计聚合见下面实践部分;
- 围绕聚合的操作引入工厂和资源库。工厂负责复杂聚合的创建,资源库负责聚合的加载、添加、修改、删除。聚合内的实体状态变更通过领域事件来推动;
- 应用服务处于应用层,对领域逻辑编排、封装之后对上层接口层暴露。一次编排就是一个用户用例。
战略设计
- 战略设计:主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
Bounded Context(限界上下文)
- 限界上下文是围绕应用程序和/或项目部分的概念边界,涉及业务领域、团队和代码。它将相关组件和概念分组,避免歧义,因为其中一些可能在没有明确上下文的情况下具有相似的含义。
- 比如电商领域的商品,商品在不同的阶段有不同的术语,在销售阶段是商品,而在运输阶段则变成了货物。同样的一个东西,由于业务领域的不同,赋予了这些术语不同的涵义和职责边界,这个边界就可能会成为未来微服务设计的边界。领域边界就是通过限界上下文来定义的。
- 限界上下文定义了解决方案的明显边界,边界里的每一个领域概念,包括领域概念内的属性和行为都有特殊含义。出了限界上下文这个边界这层含义就不复存在。
- 如何划分限界上下文?
- 根据相关性做归类。一般是优先考虑功能相关性而不是语义相关性,比如创建订单和支付订单都是订单语义,但功能相差比较大,应该划分为两个限界上下文。
- 根据团队粒度做裁剪、根据技术特点做裁剪。一些通用的技术功能应该尽可能归拢到一个限界上下文,比如每个业务限界上下文都有监控,但监控能力应该归拢到监控限界上下文。
Context Mapping(上下文映射)
- 识别并以图形方式记录项目中的每个限界上下文称为上下文映射。上下文映射有助于更好地理解有界上下文和团队如何相互关联和沟通。它们给出了实际边界的清晰概念,并帮助团队直观地描述系统设计的概念细分。
上下文映射的关系
- Anti-corruption Layer 防腐层
- 下游限界上下文实现了一个转换来自上游上下文的数据或对象的层,确保它支持内部模型
- Conformist 跟随者关系
- 下游有界上下文符合并适应上游上下文,如果需要,必须进行更改。在这种情况下,上游环境对满足下游需求不关心。
- Customer/Supplier 客户/供应商关系
- 上游向下游提供服务,下游上下文充当客户,确定需求并向上游请求更改以满足其需求。
- Shared kernel 共享内核
- 有时,两个(或更多)上下文不可避免地重叠,最终共享资源或组件。这种关系要求两个上下文在需要更改时保持连续同步,因此如果可能的话应该避免。
战术设计
- 战术设计:从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
服务架构
四层架构

六层架构

- 四层架构和流程架构的区别
特性 四层架构 (Layered) 六边形架构 (Hexagonal / Ports & Adapters) 核心目标 按技术职责分层,单向依赖 隔离核心领域,通过端口/适配器连接外部 结构 纵向分层 (UI -> 应用 -> 领域 <- 基础设施) 同心圆 (核心领域 + 外围适配器) 依赖方向 上层依赖下层 (领域 <- 基础设施实现) 所有依赖指向核心 (适配器依赖/实现核心端口) 关键原则 关注点分离,依赖倒置 (领域接口) 依赖倒置 (核心定义端口,适配器实现) 外部世界 主要视为基础设施层的职责,UI是顶层 所有外部平等,通过适配器连接核心端口 领域位置 核心层之一 (领域层) 绝对核心,被严格保护,无外部依赖 可测试性 高 (利用接口解耦) 极高 (核心纯POJO+接口,天然易测) 灵活性 基础设施实现可替换 任意适配器可独立替换,技术栈变更风险更低 DDD契合 经典,明确领域层核心 极致领域核心,适应复杂外部集成 层级 名称 举例 应用层 应用服务 钱包充值应用服务,该服务会编排客户校验、钱包聚合创建、主钱包扣款、子钱包充值、充值已完成领域事件发送等行为 领域层 实体 主钱包/子钱包是个实体,主钱包/子钱包的状态随着跟进活动推进需随时变化,需要根据唯一标识来追踪变化 领域层 值对象 钱包上的客户联系方式信息是值对象,不需要唯一标识去追踪联系方式的变化过程 领域层 聚合 钱包是个聚合,主钱包实体是该聚合的根实体,状态信息、联系方式信息等是聚合的附属 领域层 领域事件 钱包创建后会产生钱包已创建领域事件,后续的赠送优惠券、应收应付等功能可以监听该事件启动相应操作 领域层 领域服务 钱包的还款行为属于领域服务,涉及到主钱包和子钱包的余额计算,单独某个钱包自身没法完成还款行为 基础层 资源库 钱包这个聚合的访问通过钱包资源库提供,资源库的实现因技术选型不同而不同,可以是数据库、文件等
应用层(application)
- 应用服务位于应用层。用来表述应用和用户行为,负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装。
- 应用层的服务包括应用服务和领域事件相关服务。
- 应用服务可对微服务内的领域服务以及微服务外的应用服务进行组合和编排,或者对基础层如文件、缓存等数据直接操作形成应用服务,对外提供粗粒度的服务。
- 领域事件服务包括两类:领域事件的发布和订阅。通过事件总线和消息队列实现异步数据传输,实现微服务之间的解耦。
- 构成结构
- 入参: CQE对象
- 出参:DTO对象
领域层(domain)
- 领域服务位于领域层,为完成领域中跨实体或值对象的操作转换而封装的服务,领域服务以与实体和值对象相同的方式参与实施过程。
- 领域服务对同一个实体的一个或多个方法进行组合和封装,或对多个不同实体的操作进行组合或编排,对外暴露成领域服务。领域服务封装了核心的业务逻辑。实体自身的行为在实体类内部实现,向上封装成领域服务暴露。
- 为隐藏领域层的业务逻辑实现,所有领域方法和服务等均须通过领域服务对外暴露。
- 为实现微服务内聚合之间的解耦,原则上禁止跨聚合的领域服务调用和跨聚合的数据相互关联。
- 构成结构
- 入参: 实体、聚合根、基础的数据结构
- 出参:实体、聚合根、基础的数据结构
基础层(infrastructure)
- 基础服务位于基础层。为各层提供资源服务(如数据库、缓存等),实现各层的解耦,降低外部资源变化对业务逻辑的影响。
- 基础服务主要为仓储服务,通过依赖反转的方式为各层提供基础资源服务,领域服务和应用服务调用仓储服务接口,利用仓储实现持久化数据对象或直接访问基础资源。
- 基础设施层完成资源库的实际实现,以及领域层定义的其他接口的实现如对外部服务的访问,领域事件发布到消息队列中间件等
用户接口层(interfaces)
- 接口服务位于用户接口层,用于处理用户发送的Restful请求和解析用户输入的配置文件等,并将信息传递给应用层。
- 构成结构
- 入参: CQE对象;(Command,Query,Event)
- 出参:DTO对象
- 常用功能
- 鉴权
- 网络协议的转化
- session的管理
- 限流
- 异常处理
- 日志打印
- CQE的校验
领域模型
- 领域模型包含实体、值对象、聚合
- 领域模型本质是业务认知,承载核心业务逻辑,如订单状态的流转、库存扣减规则
- 领域模型定义了领域内的关键概念及这些概念之间的关系
- 领域模型描述的是现实世界的事物和他们之间的关系
- 高质量模型的本质是持续演进
实体
- 定义:具有唯一标识和生命周期的对象,通过标识而非属性区分不同实例。
- 特点:
- 唯一性:通过唯一标识(如订单ID、用户ID)进行跟踪。
- 保证实体的不变性(Invariants),也就是说要确保无论外部怎么操作,一个实体内部的属性都不能出现相互冲突,状态不一致的情况。
- 通过聚合根对子实体进行访问,子实体的一致性交由聚合根保证;
值对象
- 定义:描述特征的对象,不具有任何唯一性的对象称为价值对象,它们只关心自己是什么,而不关心自己是谁,值对象是多个实体的属性,可以由多个实体共享
- 特点:
- 无标识:通过属性值定义,如金额(Money)包含数值和货币类型。
- 不可变性:值对象必须是不可变的,当需要更新时,强制系统用新实例替换它们。
- 价值对象的创建应始终取决于用于创建它们的数据的有效性,以及它如何尊重业务不变量。
领域服务
- 定义:
- 在许多情况下,领域模型需要某些与实体或值对象不直接相关的动作或操作,将这些动作或操作强制到它们的实现中会导致它们的定义失真
- 封装不归属于实体或值对象的业务逻辑,通常涉及多个聚合的协作
- 一些业务并不是和领域对象相关,它本身代表了一种商业策略或业务处理过程
- 服务应该精心设计,始终确保它们不会剥夺实体和价值对象的直接责任和行为。
- 特点:
- 无状态:不持有领域对象的状态。
- 跨聚合协调:例如支付服务需协调订单和账户聚合
领域事件
- 领域事件是领域模型内部发生的、对业务有重要意义的、值得领域专家关心的事件或状态变化的记录。它代表了过去发生的、不可更改的事实(Fact)。简单来说,它就是“领域内发生了某件重要事情”的通知
- 关键特性
- 描述性
- 事件名称应清晰、明确地描述发生了什么业务事实。
- 通常使用过去时态的动词短语命名,例如
OrderPlaced
(订单已下单)、PaymentReceived
(付款已收到)、InventoryLevelAdjusted
(库存水平已调整)、CustomerAddressUpdated
(客户地址已更新)。 - 名称应使用通用语言(Ubiquitous Language),让领域专家和开发者都能理解其含义。
- 携带信息
- 事件对象本身是一个数据结构,包含描述该事件所需的最小必要信息。
- 避免携带整个聚合根状态!只携带必要数据和引用。
- 不可变性
- 一旦事件被创建并发布,它就不能被修改。因为它代表的是已经发生的历史事实。如果业务状态需要修正,应该发布一个新的事件(如
OrderCancelled
来修正OrderPlaced
的最终结果),而不是修改旧事件。
- 一旦事件被创建并发布,它就不能被修改。因为它代表的是已经发生的历史事实。如果业务状态需要修正,应该发布一个新的事件(如
- 幂等性
- 事件处理器应设计成幂等的。因为网络问题或重试机制可能导致同一个事件被多次投递。处理器需要能够识别并安全地处理重复事件(例如,通过检查事件ID是否已处理过)。
- 描述性
聚合(Aggregate)
- 定义:
- 一组相关对象的集合,通过聚合根(Aggregate Root)统一管理,确保业务规则的一致性
- 将实体和值对象划分为聚合并围绕着聚合定义边界
- 作为一个整体来定义聚合的属性和不变量,并把其执行责任赋予聚合跟或者指定的框架机制
- 选择一个实体作为每个聚合的根,并仅允许外部对象持有聚合跟的引用
- 每个聚合都有一个朝外的实体,控制对边界内对象的所有访问,该实体称为聚合根,是其他对象可以交互的唯一对象
- 特点:
- 一致性边界:外部只能通过聚合根操作内部对象。
- 事务边界:聚合内的所有对象需作为一个整体持久化。
- 聚合的划分的生命周期一致性原则
- 聚合边界内的对象,和聚合跟之间存在“人身依附”关系,即:如果聚合根消失,聚合内的其他元素都应该同时消失
- 在满足生命周期一致性的前提下,应该划分为尽量小的聚合
资源库(Repository)
- 作用:
- 管理聚合的持久化与检索,隔离领域模型与数据库技术细节
- 以聚合的整体管理对象,一个聚合只能有一个资源库对象,那就是以聚合根命名的资源库。除此之外的其他对象,都不应该提供资源库对象
- 为了能够从持久性中检索对象,无论是在内存、文件系统还是数据库中,我们需要提供一个对客户机隐藏实现细节的接口,以便它不依赖于基础架构细节,而仅仅依赖于抽象
- 资源库模式不等价于持久化,更不是数据库访问层,仓储应基于领域模型设计接口,而非直接映射数据库表结构,
- 所有存储库接口定义都应该位于domain层,但它们的具体实现属于基础架构层。
- 特点:
- 以聚合为操作单位:保存或加载整个聚合
- 提供领域友好的接口:如
findById
、save
工厂
- DDD上下文中,聚合需要保证业务一致性,因为聚合都是有业务完整性的,我们需要一次性把它构造出来,所以采用工厂模式,来保证聚合的构造
- 作用:封装复杂对象的创建逻辑,确保对象构建符合业务规则
- 类型:
- 领域工厂:创建聚合根或复杂值对象
- 基础设施工厂:如数据库连接工厂
子领域划分
- 核心子领域
- 通用子领域
- 支撑子领域
充血模型和贫血模型
- 贫血模型即事务脚本模式。
- 充血模型即领域模型模式。
维度 贫血模型 充血模型 适用场景 简单CRUD应用 复杂业务逻辑领域 业务逻辑归属 集中在Service层 内聚在领域对象(实体/值对象) 可维护性 逻辑分散,易产生重复代码 高内聚,易于扩展和维护 技术框架依赖 强依赖ORM和Service框架 领域模型独立于框架
贫血模型
- 贫血模型最早广泛应用源于EJB2,最强盛时期则是由Spring创造将:
- “行为”(逻辑、过程);
- “状态”(数据,对应到语言就是对象成员变量)。
分离到不同的对象中: - 只有状态的对象就是所谓的“贫血对象”(常称为VO——Value Object);
- 只有行为的对象就是,我们常见的N层结构中的Logic/Service/Manager层(对应到EJB2中的Stateless Session Bean)。
- 优点
- 对于只有少量业务逻辑的应用来说,使用起来非常自然;
- 开发迅速,易于理解;
- 注意:也不能完全排斥这种方式。
- 缺点
- 无法良好的应对复杂逻辑:
VO(View Objec)
- 视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
DTO(Data Transfer Object)
- 数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
DO(Domain Object)
- 领域对象,就是从现实世界中抽象出来的有形或无形的业务实体
PO(Persistent Object)
- 持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。
转换流程
- 用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。
- 展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。
- 服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。
- 服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作。
- 对于一个逆向操作,如读取数据,也是用类似的方式转换和传递,略。
VO和DTO的区别与应用
区别
- 大家可能会有个疑问(在笔者参与的项目中,很多程序员也有相同的疑惑):既然DTO是展示层与服务层之间传递数据的对象,为什么还需要一个VO呢?对!对于绝大部分的应用场景来说,DTO和VO的属性值基本是一致的,而且他们通常都是POJO,因此没必要多此一举,但不要忘记这是实现层面的思维,对于设计层面来说,概念上还是应该存在VO和DTO,因为两者有着本质的区别,DTO代表服务层需要接收的数据和返回的数据,而VO代表展示层需要显示的数据。
用一个例子来说明可能会比较容易理解:例如服务层有一个getUser的方法返回一个系统用户,其中有一个属性是gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?
对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的DTO,不应该出现与表现形式的耦合。
理论归理论,这到底还是分析设计层面的思维,是否在实现层面必须这样做呢?一刀切的做法往往会得不偿失,下面我马上会分析应用中如何做出正确的选择。
应用
- 在以下才场景中,我们可以考虑把VO与DTO二合为一(注意:是实现层面):
- 需求非常清晰稳定,而且客户端很明确只有一个的时候,没有必要把VO和DTO区分开来,这时候VO可以退隐,用一个DTO即可,为什么是VO退隐而不是DTO?回到设计层面,服务层的职责依然不应该与展示层耦合,所以,对于前面的例子,你很容易理解,DTO对于“性别”来说,依然不能用“帅哥美女”,这个转换应该依赖于页面的脚本(如JavaScript)或其他机制(JSTL、EL、CSS)
- 即使客户端可以进行定制,或者存在多个不同的客户端,如果客户端能够用某种技术(脚本或其他机制)实现转换,同样可以让VO退隐
- 以下场景需要优先考虑VO、DTO并存:
- 上述场景的反面场景
- 因为某种技术原因,比如某个框架(如Flex)提供自动把POJO转换为UI中某些Field时,可以考虑在实现层面定义出VO,这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升与设计多一个VO所多做的事情带来的开发和维护效率的下降之间的比对。
- 如果页面出现一个“大视图”,而组成这个大视图的所有数据需要调用多个服务,返回多个DTO来组装(当然,这同样可以通过服务层提供一次性返回一个大视图的DTO来取代,但在服务层提供一个这样的方法是否合适,需要在设计层面进行权衡)。
DTO与DO的区别与应用
区别
- 首先是概念上的区别,DTO是展示层和服务层之间的数据传输对象(可以认为是两者之间的协议),而DO是对现实世界各种业务角色的抽象,这就引出了两者在数据上的区别,例如UserInfo和User,对于一个getUser方法来说,本质上它永远不应该返回用户的密码,因此UserInfo至少比User少一个password的数据。而在领域驱动设计中,正如第一篇系列文章所说,DO不是简单的POJO,它具有领域业务逻辑。
应用
- 从上一节的例子中,细心的读者可能会发现问题:既然getUser方法返回的UserInfo不应该包含password,那么就不应该存在password这个属性定义,但如果同时有一个createUser的方法,传入的UserInfo需要包含用户的password,怎么办?
在设计层面,展示层向服务层传递的DTO与服务层返回给展示层的DTO在概念上是不同的,但在实现层面,我们通常很少会这样做(定义两个UserInfo,甚至更多),因为这样做并不见得很明智,我们完全可以设计一个完全兼容的DTO,在服务层接收数据的时候,不该由展示层设置的属性(如订单的总价应该由其单价、数量、折扣等决定),无论展示层是否设置,服务层都一概忽略,而在服务层返回数据时,不该返回的数据(如用户密码),就不设置对应的属性。
对于DO来说,还有一点需要说明:为什么不在服务层中直接返回DO呢?这样可以省去DTO的编码和转换工作,原因如下:- 两者在本质上的区别可能导致彼此并不一一对应,一个DTO可能对应多个DO,反之亦然,甚至两者存在多对多的关系。
- DO具有一些不应该让展示层知道的数据
- DO具有业务方法,如果直接把DO传递给展示层,展示层的代码就可以绕过服务层直接调用它不应该访问的操作,对于基于AOP拦截服务层来进行访问控制的机制来说,这问题尤为突出,而在展示层调用DO的业务方法也会因为事务的问题,让事务难以控制。
- 对于某些ORM框架(如Hibernate)来说,通常会使用“延迟加载”技术,如果直接把DO暴露给展示层,对于大部分情况,展示层不在事务范围之内(Open session in view在大部分情况下不是一种值得推崇的设计),如果其尝试在Session关闭的情况下获取一个未加载的关联对象,会出现运行时异常(对于Hibernate来说,就是LazyInitiliaztionException)。
- 从设计层面来说,展示层依赖于服务层,服务层依赖于领域层,如果把DO暴露出去,就会导致展示层直接依赖于领域层,这虽然依然是单向依赖,但这种跨层依赖会导致不必要的耦合。
对于DTO来说,也有一点必须进行说明,就是DTO应该是一个“扁平的二维对象”,举个例子来说明:如果User会关联若干个其他实体(例如Address、Account、Region等),那么getUser()返回的UserInfo,是否就需要把其关联的对象的DTO都一并返回呢?
如果这样的话,必然导致数据传输量的大增,对于分布式应用来说,由于涉及数据在网络上的传输、序列化和反序列化,这种设计更不可接受。如果getUser除了要返回User的基本信息外,还需要返回一个AccountId、AccountName、RegionId、RegionName,那么,请把这些属性定义到UserInfo中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”,笔者目前参与的项目是一个分布式系统,该系统不管三七二十一,把一个对象的所有关联对象都转换为相同结构的DTO对象树并返回,导致性能非常的慢。
DO与PO的区别及应用
区别
- DO和PO在绝大部分情况下是一一对应的,PO是只含有get/set方法的POJO,但某些场景还是能反映出两者在概念上存在本质的区别:
- DO在某些场景下不需要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不同折扣策略实现类,这些折扣策略实现类可以算是DO,但它们只驻留在静态内存,不需要持久化到持久层,因此,这类DO是不存在对应的PO的。
- 同样的道理,某些场景下,PO也没有对应的DO,例如老师Teacher和学生Student存在多对多的关系,在关系数据库中,这种关系需要表现为一个中间表,也就对应有一个TeacherAndStudentPO的PO,但这个PO在业务领域没有任何现实的意义,它完全不能与任何DO对应上。这里要特别声明,并不是所有多对多关系都没有业务含义,这跟具体业务场景有关,例如:两个PO之间的关系会影响具体业务,并且这种关系存在多种类型,那么这种多对多关系也应该表现为一个DO,又如:“角色”与“资源”之间存在多对多关系,而这种关系很明显会表现为一个DO——“权限”。
- 某些情况下,为了某种持久化策略或者性能的考虑,一个PO可能对应多个DO,反之亦然。例如客户Customer有其联系信息Contacts,这里是两个一对一关系的DO,但可能出于性能的考虑(极端情况,权作举例),为了减少数据库的连接查询操作,把Customer和Contacts两个DO数据合并到一张数据表中。反过来,如果一本图书Book,有一个属性是封面cover,但该属性是一副图片的二进制数据,而某些查询操作不希望把cover一并加载,从而减轻磁盘IO开销,同时假设ORM框架不支持属性级别的延迟加载,那么就需要考虑把cover独立到一张数据表中去,这样就形成一个DO对应对个PO的情况。
- PO的某些属性值对于DO没有任何意义,这些属性值可能是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO存在一个version的属性,这个version对于DO来说是没有任何业务意义的,它不应该在DO中存在。同理,DO中也可能存在不需要持久化的属性。
应用
- 由于ORM框架的功能非常强大而大行其道,而且JavaEE也推出了JPA规范,现在的业务应用开发,基本上不需要区分DO与PO,PO完全可以通过JPA,Hibernate Annotations/hbm隐藏在DO之中。虽然如此,但有些问题我们还必须注意:
- 对于DO中不需要持久化的属性,需要通过ORM显式的声明,如:在JPA中,可以利用@Transient声明。
- 对于PO中为了某种持久化策略而存在的属性,例如version,由于DO、PO合并了,必须在DO中声明,但由于这个属性对DO是没有任何业务意义的,需要让该属性对外隐藏起来,最常见的做法是把该属性的get/set方法私有化,甚至不提供get/set方法,但对于Hibernate来说,这需要特别注意,由于Hibernate从数据库读取数据转换为DO时,是利用反射机制先调用DO的空参数构造函数构造DO实例,然后再利用JavaBean的规范反射出set方法来为每个属性设值,如果不显式声明set方法,或把set方法设置为private,都会导致Hibernate无法初始化DO,从而出现运行时异常,可行的做法是把属性的set方法设置为protected。
- 对于一个DO对应多个PO,或者一个PO对应多个DO的场景,以及属性级别的延迟加载,Hibernate都提供了很好的支持,请参考Hibnate的相关资料。
总结
- 分析设计层面和实现层面完全是两个独立的层面,即使实现层面通过某种技术手段可以把两个完全独立的概念合二为一,在分析设计层面,我们仍然(至少在头脑中)需要把概念上独立的东西清晰的区分开来,这个原则对于做好分析设计非常重要(工具越先进,往往会让我们越麻木)。
参考文章
DDD
https://x-leonidas.github.io/2022/02/01/14软件工程/DDD/