DDD
领域驱动设计 (DDD) 是指代码的结构和语言(类名、类方法、类变量)应与业务领域相匹配的概念
微服务DDD设计
软件架构模式演进
- 在单机和集中式架构这两种模式下,软件无法快速响应需求和业务的迅速变化,最终错失发展良机
- 微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。换句话说,确定了业务边界和应用边界,这个困境也就迎刃而解了。
DDD核心思想可以总结为:通过领域驱动设计方法论定义领域模型,明确物理边界、逻辑边界、代码边界
战略设计和战术设计
DDD 包括战略设计和战术设计两部分。
- 战略设计主要从业务视角出发,建立业务领域模型,划分领域边界(核心域、通用域、支撑域),建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
- 战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现
DDD 战略模式经常被跳过。原因很简单:我们都是开发人员,我们喜欢编写代码而不是与“业务人员”交谈。在大多数情况下,这是由于缺乏像 Event Storming 这样的适当技术造成的。他们可以为双方带来优势。同样与企业交谈可能是工作中最令人愉快的部分之一
事件风暴
事件风暴是建立领域模型的主要方法,它是一个从发散到收敛的过程。它通常采用用例分析、场景分析和用户旅程分析,尽可能全面不遗漏地分解业务领域,并梳理领域对象之间的关系,这是一个发散的过程。事件风暴过程会产生很多的实体、命令、事件等领域对象,我们将这些领域对象从不同的维度进行聚类,形成如聚合、限界上下文等边界,建立领域模型,这就是一个收敛的过程。
划定领域模型和微服务的边界:
第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。
第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。
第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型
DDD 与微服务的关系
两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。 DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。 微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。
领域
DDD 的知识体系需要理解的概念,像:领域、子域、核心域、通用域、支撑域、限界上下文、聚合、聚合根、实体、值对象等等。
DDD 会按照一定的规则将业务领域进行细分,当领域细分到一定的程度后,DDD 会将问题范围限定在特定的边界内,在这个边界内建立领域模型,进而用代码实现该领域模型,解决相应的业务问题。简言之,DDD 的领域就是这个边界内要解决的业务问题域。
领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域,每个子域对应一个更小的问题域或更小的业务范围。
领域建模和微服务建设的过程和方法基本类似,其核心思想就是将问题域逐步分解,降低业务理解和系统实现的复杂度。
(对一个问题不断地划分,直到划分为我们熟悉的、能够快速处理的小问题。然后再对小问题的处理排列一个优先级。)
子域划分
子域可以根据自身重要性和功能属性划分为三类子域,它们分别是:核心域、通用域和支撑域
划分这几个子域的主要目的是为了确定战略资源的投入,一般来说战略投入的重点是核心域,因此后面我们就可以暂时不严格区分支撑域和通用域了。
核心域
决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。
通用域
没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。
支撑域
还有一种功能子域是必需的,但既不包含决定产品和公司核心竞争力的功能,也不包含通用功能的子域,它就是支撑域。
为什么要划分核心域、通用域和支撑域,主要目的是什么呢?
还是拿上图的桃树来说吧。我们将桃树细分为了根、茎、叶、花、果实和种子等六个子域,那桃树是否有核心域?有的话,到底哪个是核心域呢?不同的人对桃树的理解是不同的。如果这棵桃树生长在公园里,在园丁的眼里,他喜欢的是“人面桃花相映红”的阳春三月,这时花就是桃树的核心域。但如果这棵桃树生长在果园里,对果农来说,他则是希望在丰收的季节收获硕果累累的桃子,这时果实就是桃树的核心域。
核心域、支撑域和通用域的主要目标是:通过领域划分,区分不同子域在公司内的不同功能属性和重要性,从而公司可对不同子域采取不同的资源投入和建设策略(确定 IT 战略投入),其关注度也会不一样。
领域边界
通用语言定义上下文含义,限界上下文则定义领域边界。
事件风暴
在事件风暴过程中,通过团队交流达成共识的,能够简单、清晰、准确描述业务涵义和规则的语言就是通用语言。通用语言是团队统一的语言,不管你在团队中承担什么角色,在同一个领域的软件生命周期里都使用统一的语言进行交流。
有效方式: 设计过程中我们可以用一些表格,来记录事件风暴和微服务设计过程中产生的领域对象及其属性。 比如,领域对象在 DDD 分层架构中的位置、属性、依赖关系以及与代码模型对象的映射关系等。
案列:在这个表格里面我们可以看到,DDD 分析过程中所有的领域对象以及它们的属性都被记录下来了,除了 DDD 的领域对象,我们还记录了在微服务设计过程中领域对象所对应的代码对象,并将它们一一映射
DDD 分析和设计过程中的每一个环节都需要保证限界上下文内术语的统一,在代码模型设计的时侯就要建立领域对象和代码对象的一一映射,从而保证业务模型和代码模型的一致,实现业务语言与代码语言的统一。
限界上下文
实体和值对象
DDD 战术设计中的两个重要概念:实体和值对象。
实体和值对象是组成领域模型的基础单元。
实体一般对应业务对象,它具有业务属性和业务行为;而值对象主要是属性集合,对实体的状态和特征进行描述
实体代码形态
在代码模型中,实体的表现形式是实体类,这个类包含了实体的属性和方法,通过这些方法实现实体自身的业务逻辑。这些实体类通常采用充血模型,与这个实体相关的所有业务逻辑都在实体类的方法中实现,跨多个实体的领域逻辑则在领域服务中实现。
实体的运行形态
实体以 DO(领域对象)的形式存在,每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改,修改后的数据和原来的数据可能会大不相同。但是,由于它们拥有相同的 ID,它们依然是同一个实体。比如商品是商品上下文的一个实体,通过唯一的商品 ID 来标识,不管这个商品的数据如何变化,商品的 ID 一直保持不变,它始终是同一个商品。
实体数据库形态
DDD 是先构建领域模型,针对实际业务场景构建实体对象和行为,再将实体对象映射到数据持久化对象。
值对象
通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体 值对象描述了领域中的一件东西,这个东西是不可变的,它将不同的相关属性组合成了一个概念整体。当度量和描述改变时,可以用另外一个值对象予以替换
简单来说,值对象本质上就是一个集合。值对象可以保证属性归类的清晰和概念的完整性,避免属性零碎。
值对象具有业务形态、代码形态、运行形态、数据库形态
在领域建模时,我们可以将部分对象设计为值对象,保留对象的业务涵义,同时又减少了实体的数量;在数据建模时,我们可以将值对象嵌入实体,减少实体表的数量,简化数据库设计。
DDD 提倡从领域模型设计出发,而不是先设计数据模型。前面讲过了,传统的数据模型设计通常是一个表对应一个实体,一个主表关联多个从表,当实体表太多的时候就很容易陷入无穷无尽的复杂的数据库设计,领域模型就很容易被数据模型绑架。可以说,值对象的诞生,在一定程度上,和实体是互补的。
在领域模型中人员是实体,地址是值对象,地址值对象被人员实体引用。在数据模型设计时,地址值对象可以作为一个属性集整体嵌入人员实体中,组合形成上图这样的数据模型;也可以以序列化大对象的形式加入到人员的地址属性中,前面表格有展示。
同样的对象在不同的场景下,可能会设计出不同的结果。有些场景中,地址会被某一实体引用,它只承担描述实体的作用,并且它的值只能整体替换,这时候你就可以将地址设计为值对象,比如收货地址。而在某些业务场景中,地址会被经常修改,地址是作为一个独立对象存在的,这时候它应该设计为实体,比如行政区划中的地址信息维护。
聚合和聚合根
在事件风暴中,我们会根据一些业务操作和行为找出实体(Entity)或值对象(ValueObject),进而将业务关联紧密的实体和值对象进行组合,构成聚合,再根据业务语义将多个聚合划定到同一个限界上下文(Bounded Context)中,并在限界上下文内完成领域建模。
聚合
领域模型内的实体和值对象就好比个体,而能让实体和值对象协同工作的组织就是聚合,它用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。
聚合就是由业务和逻辑紧密关联的实体和值对象组合而成的,聚合是数据修改和持久化的基本单元,每一个聚合对应一个仓储,实现数据的持久化。
聚合有一个聚合根和上下文边界。
一个聚合包含了多个实体对象和值对象,其中有一个实体对象做为聚合根。这些对象聚集在一起形成了一个比较完整独立的业务边界,称为上下文边界。边界根据业务单一职责和高内聚原则,定义了聚合内部应该包含哪些实体和值对象,而聚合之间的边界是松耦合的。
跨多个实体的业务逻辑通过领域服务来实现,跨多个聚合的业务逻辑通过应用服务来实现。 比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成,我们就可以将这段业务逻辑用领域服务来实现“ 这么说聚合其实仅仅是把多个实体、值对象组合在一起,并提供其对应的安全访问入口,实际并不组织业务逻辑。这个事情是交给领域服务完成的。比如: 聚合:Item 实体1:Item.sku 实体2:Item.category 领域服务:ItemModifyDomainService 商品信息修改方法: ItemModifyDomainService.modify(){ item.sku.update(); item.category.update(); }
聚合根
聚合根的主要目的是为了避免由于复杂数据模型缺少统一的业务规则控制,而导致聚合、实体之间数据不一致性的问题。
如果把聚合比作组织,那聚合根就是这个组织的负责人。聚合根也称为根实体,它不仅是实体,还是聚合的管理者。
首先它作为实体本身,拥有实体的属性和业务行为,实现自身的业务逻辑。
其次它作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。
最后在聚合之间,它还是聚合对外的接口人,以聚合根 ID 关联的方式接受外部任务和请求,在上下文内实现聚合之间的业务协同。也就是说,聚合之间通过聚合根 ID 关联引用,如果需要访问其它聚合的实体,就要先访问聚合根,再导航到聚合内部实体,外部对象不能直接访问聚合内实体。(传统开发上,每个实体都可以被其他业务逻辑引入和修改,增加了维护一致性的难度。例如,A实体表也对外暴露了一个对应的interface并公开,很多外部逻辑可以直接引用进行update,没统一归口就会增加代码的维护难度)
设计聚合
DDD 领域建模通常采用事件风暴,它通常采用用例分析、场景分析和用户旅程分析等方法,通过头脑风暴列出所有可能的业务行为和事件,然后找出产生这些行为的领域对象,并梳理领域对象之间的关系,找出聚合根,找出与聚合根业务紧密关联的实体和值对象,再将聚合根、实体和值对象组合,构建聚合
以保险的投保业务场景为例:
第 1 步:采用事件风暴,根据业务行为,梳理出在投保过程中发生这些行为的所有的实体和值对象,比如投保单、标的、客户、被保人等等。
第 2 步:从众多实体中选出适合作为对象管理者的根实体,也就是聚合根。判断一个实体是否是聚合根,你可以结合以下场景分析:是否有独立的生命周期?是否有全局唯一 ID?是否可以创建或修改其它对象?是否有专门的模块来管这个实体。图中的聚合根分别是投保单和客户实体。
第 3 步:根据业务单一职责和高内聚原则,找出与聚合根关联的所有紧密依赖的实体和值对象。构建出 1 个包含聚合根(唯一)、多个实体和值对象的对象集合,这个集合就是聚合。在图中我们构建了客户和投保这两个聚合。
第 4 步:在聚合内根据聚合根、实体和值对象的依赖关系,画出对象的引用和依赖模型。这里我需要说明一下:投保人和被保人的数据,是通过关联客户 ID 从客户聚合中获取的,在投保聚合里它们是投保单的值对象,这些值对象的数据是客户的冗余数据,即使未来客户聚合的数据发生了变更,也不会影响投保单的值对象数据。从图中我们还可以看出实体之间的引用关系,比如在投保聚合里投保单聚合根引用了报价单实体,报价单实体则引用了报价规则子实体。
第 5 步:多个聚合根据业务语义和上下文一起划分到同一个限界上下文内。
聚合设计原则
两个重要的设计模式:工厂(Factory)模式和仓储(Repository)模式
工厂模式:
有些聚合内可能含有非常多的实体和值对象,我们需要确保聚合根以及所有被依赖的对象实例同时被创建。如果都通过聚合根来构造,将会非常复杂。因此我们可以通过工厂模式来封装复杂对象的创建过程。
使用工厂具有以下好处:
- 工厂将领域对象的使用和创建分离。
- 通过使用工厂类,可以隐藏创建复杂领域对象的业务逻辑。
- 工厂类可以根据调用者的需要,创建相应的领域对象。
- 工厂方法可以封装聚合的内部状态。
最重要的一点就是隐藏创建对象的细节
仓储模式:
目的:依赖倒置,实现了应用逻辑与基础资源的解耦
在传统的 DDD 四层架构中,所有层都是依赖基础层的。应用逻辑对基础层依赖太大的话,基础层中与资源有关的代码可能会渗透到应用逻辑中。
为了解耦应用逻辑和基础资源,在基础层和上层应用逻辑之间会增加一层,这一层就是仓储层。一个聚合对应一个仓储,仓储实现聚合内数据的持久化。聚合内的应用逻辑通过接口来访问基础资源,仓储实现在基础层实现。这样应用逻辑和基础资源的实现逻辑是分离的。如果变更基础资源组件,只需要替换仓储实现就可以了,不会对应用逻辑产生太大的影响,这样就实现了应用逻辑与基础资源的解耦,也就实现了依赖倒置。
领域事件
事件发生后通常会导致进一步的业务操作,在 DDD 中这种事件被称为领域事件。
领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。
举例来说的话,领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。
微服务内的领域事件
当领域事件发生在微服务内的聚合之间,领域事件发生后完成事件实体构建和事件数据持久化,发布方聚合将事件发布到事件总线,订阅方接收事件数据完成后续业务操作。
但微服务内的事件总线,可能会增加开发的复杂度,因此你需要结合应用复杂度和收益进行综合考虑。
微服务内应用服务,可以通过跨聚合的服务编排和组合,以服务调用的方式完成跨聚合的访问,这种方式通常应用于实时性和数据一致性要求高的场景。这个过程会用到分布式事务,以保证发布方和订阅方的数据同时更新成功。
微服务之间的领域事件
跨微服务的领域事件会在不同的限界上下文或领域模型之间实现业务协作,其主要目的是实现微服务解耦,减轻微服务之间实时服务访问的压力。
领域事件通过什么技术实现?消息队列
架构选型
DDD 分层架构、整洁架构、六边形架构都是以领域模型为核心,实行分层架构,内部核心业务逻辑与外部应用、资源隔离并解耦。
DDD分层架构
微服务架构模型有好多种,例如整洁架构(洋葱架构)、CQRS 和六边形架构等等。每种架构模式虽然提出的时代和背景不同,但其核心理念都是为了设计出“高内聚低耦合”的架构,轻松实现架构演进。 而 DDD 分层架构的出现,使架构边界变得越来越清晰。
在最早的传统四层架构中,基础层是被其它层依赖的,它位于最核心的位置,那按照分层架构的思想,它应该就是核心,但实际上领域层才是软件的核心,所以这种依赖是有问题的。后来我们采用了依赖倒置(Dependency inversion principle,DIP)的设计,优化了传统的四层架构,实现了各层对基础层的解耦。
在 DDD 分层架构中,这些要素被重新归类,重新划分了层,确定了层与层之间的交互规则和职责边界。
三层架构向 DDD 分层架构演进,主要发生在业务逻辑层和数据访问层。DDD 分层架构在用户接口层引入了 DTO,给前端提供了更多的可使用数据和更高的展示灵活性。DDD 分层架构对三层架构的业务逻辑层进行了更清晰的划分,改善了三层架构核心业务逻辑混乱,代码改动相互影响大的情况。DDD 分层架构将业务逻辑层的服务拆分到了应用层和领域层。应用层快速响应前端的变化,领域层实现领域模型的能力。
另外一个重要的变化发生在数据访问层和基础层之间。三层架构数据访问采用 DAO 方式;DDD 分层架构的数据库等基础资源访问,采用了仓储(Repository)设计模式,通过依赖倒置实现各层对基础资源的解耦。
微服务之间服务调用和领域事件驱动
微服务的服务是从领域层逐级向上封装、组合和暴露的。
整洁架构
整洁架构最主要的原则是依赖原则,它定义了各层的依赖关系,越往里依赖越低,代码级别越高,越是核心能力
在洋葱架构中,各层的职能是这样划分的:
- 领域模型实现领域内核心业务逻辑,它封装了企业级的业务规则。领域模型的主体是实体,一个实体可以是一个带方法的对象,也可以是一个数据结构和方法集合。
- 领域服务实现涉及多个实体的复杂业务逻辑。应用服务实现与用户操作相关的服务组合与编排,它包含了应用特有的业务流程规则,封装和实现了系统所有用例。
- 最外层主要提供适配的能力,适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。领域服务和应用服务都可以调用仓储服务接口,通过仓储服务实现数据持久化。
- 红圈内的领域模型、领域服务和应用服务一起组成软件核心业务能力。
松散耦合架构
清洁架构方法结合了两个想法:分离端口(Ports)和适配器(Adapters)以及限制代码结构相互引用的方式
当您播放一些音乐时,您可以听到它从扬声器中传出。如果您插入耳机,音频将自动更改为耳机。你的音乐应用不在乎。它不是直接与硬件对话,而是使用操作系统提供的适配器之一。
通常使用的层是适配器、端口、应用程序和域。
- 适配器是您的应用程序与外部世界对话的方式。您必须使内部结构适应外部 API 的期望。想想 SQL 查询、HTTP 或 gRPC 客户端、文件读取器和写入器、Pub/Sub 消息发布者。
- 端口是应用程序的输入,也是外部世界访问它的唯一途径。它可以是 HTTP 或 gRPC 服务器、CLI 命令或 Pub/Sub 消息订阅者。 • 应用程序逻辑是一个将其他层“粘合在一起”的薄层。它也被称为“用例”。
- 遵循领域驱动设计8,引入仅包含业务逻辑的领域层
Dependency Inversio规则规定外层(实现细节)可以引用内层(抽象),但不能反过来。内层应该依赖于接口。 • 域对其他层一无所知。它包含纯业务逻辑。 • 应用程序可以导入域但对外层一无所知。它不知道它是被 HTTP 请求、Pub/Sub 处理程序还是 CLI 命令调用的。 • 端口可以导入内层。端口是应用程序的入口点,因此它们经常执行应用程序服务或命令。但是,他们不能直接访问适配器。 • 适配器可以导入内层。
https://threedots.tech/post/introducing-clean-architecture/?from_wecom=1
应用层
基本上是将领域层与外层绑定的“粘合剂”。它几乎就像一个中间层。应用层声明了代表基础设施、持久性和表示组件的接口和其他抽象。这些组件的实际实现不在这一层中声明,而是通过依赖注入提供给应用程序组件。
该层还负责编排:它实现了操作域对象和启动域工作流的高级逻辑。这样,它本身不包含任何一流的业务逻辑,而是通过对领域层的调用来组织该逻辑。它可以协调任务并将工作委托给域,但它不包含业务规则或维护业务状态
应用层同样使用注入的持久化接口执行持久化操作。这就是存储库模式或 CQRS 发挥作用的地方(解释如下)。由于不同的编排操作,它将数据传输对象(DTO) 传递到表示层。同样,它还使用注入的基础设施接口与操作系统和其他外部资源进行通信。
六边形架构
代码模型
使用DDD设计微服务代码模型
用户接口层:面向前端提供服务适配,面向资源层提供资源适配。这一层聚集了接口适配相关的功能。
应用层职责:实现服务组合和编排,适应业务流程快速变化的需求。这一层聚集了应用服务和事件相关的功能。
领域层:实现领域的核心业务逻辑。这一层聚集了领域模型的聚合、聚合根、实体、值对象、领域服务和事件等领域对象,以及它们组合所形成的业务能力。
基础层:贯穿所有层,为各层提供基础资源服务。这一层聚集了各种底层资源相关的服务和能力。
领域对象与代码对象建立映射
需要将领域模型作为微服务设计的输入,对领域对象进行设计和转换,让领域对象与代码对象建立映射关系。
领域对象整理
一个领域模型会包含多个聚合,一个聚合包含多个领域对象,每个领域对象都有自己的领域类型。领域类型主要标识领域对象的属性,比如:聚合根、实体、命令和领域事件等类型。
从领域模型到微服务的设计
关注: 分析微服务内有哪些服务? 服务所在的分层? 应用服务由哪些服务组合和编排完成? 领域服务包括哪些实体的业务逻辑? 采用充血模型的实体有哪些属性和方法? 有哪些值对象? 哪个实体是聚合根等? 最后梳理出所有的领域对象和它们之间的依赖关系,我们会给每个领域对象设计对应的代码对象,定义它们所在的软件包和代码目录。
领域层的领域对象
微服务的聚合内一般会有:聚合、聚合根、实体、值对象、领域事件、领域服务和仓储等领域对象。
实体
大多数情况下,领域模型的业务实体与微服务的数据库实体是一一对应的。但某些领域模型的实体在微服务设计时,可能会被设计为多个数据实体,或者实体的某些属性被设计为值对象。
- 实体方案封装 实体方法是最底层的原子业务逻辑。如果单一实体的方法需要被跨层调用,你可以将它封装成领域服务,这样封装的领域服务就可以被应用服务调用和编排了。如果它还需要被用户接口层调用,你还需要将这个领域服务封装成应用服务。经过逐层服务封装,实体方法就可以暴露给上面不同的层,实现跨层调用。
找出聚合根
个人客户聚合根通过工厂和仓储模式,实现聚合内地址、银行账号等实体和值对象数据的初始化和持久化。 聚合根是一种特殊的实体(聚合中的关键实体),它有自己的属性和方法。聚合根可以实现聚合之间的对象引用,还可以引用聚合内的所有实体。
设计值对象
根据需要将某些实体的某些属性或属性集设计为值对象。值对象类放在代码模型的 Entity 目录结构下。在个人客户聚合中,客户拥有客户证件类型,它是以枚举值的形式存在,所以将它设计为值对象。
如果这个对象是多条且需要基于它做查询统计,我建议将它设计为实体。
领域事件
如果领域模型中领域事件会触发下一步的业务操作,我们就需要设计领域事件。首先确定领域事件发生在微服务内还是微服务之间。然后设计事件实体对象,事件的发布和订阅机制,以及事件的处理机制。判断是否需要引入事件总线或消息中间件。
所以如果有的实体方法需要被前端应用调用,我们会将它封装成领域服务,然后再封装为应用服务。
应用和资源的解耦与适配-设计仓储
每一个聚合都有一个仓储,仓储主要用来完成数据查询和持久化操作。仓储包括仓储的接口和仓储实现,通过依赖倒置实现应用业务逻辑与数据库资源逻辑的解耦。
传统以数据为中心的设计模式,应用会对数据库、缓存、文件系统等基础资源产生严重依赖。正是由于它们之间的这种强依赖的关系,我们一旦更换基础资源就会对应用产生很大的影响,因此需要为应用和资源解耦。
在微服务架构中,应用层、领域层和基础层解耦是通过仓储模式,采用依赖倒置的设计方法来实现的。在应用设计中,我们会同步考虑和基础资源的代码适配,那么一旦基础设施资源出现变更(比如换数据库),就可以屏蔽资源变更对业务代码的影响,切断业务逻辑对基础资源的依赖,最终降低资源变更对应用的影响。
适配能力分为主动适配和被动适配。主动适配主要实现外部用户、网页、批处理和自动化测试等对内层业务逻辑访问适配。被动适配主要是实现核心业务逻辑对基础资源访问的适配,比如数据库、缓存、文件系统和消息中间件等。
领域服务
领域服务会对多个实体和实体方法进行组合和编排,供应用服务调用。如果它需要暴露给用户接口层,领域服务就需要封装成应用服务。
应用服务
应用服务会对多个领域服务进行组合和编排,暴露给用户接口层,供前端应用调用。
领域对象与微服务代码对象的映射
建立了领域对象与微服务代码对象的映射关系
层:定义领域对象位于分层架构中的哪一层,比如:接口层、应用层、领域层以及基础层等。 领域对象:领域模型中领域对象的具体名称。 领域类型:根据 DDD 知识体系定义的领域对象的类型,包括:限界上下文、聚合、聚合根、实体、值对象、领域事件、应用服务、领域服务和仓储服务等领域类型。 依赖的领域对象:根据业务对象依赖或分层调用的依赖关系,建立的领域对象的依赖关系,比如:服务调用依赖、关联对象聚合等。 包名:代码模型中的包名,对应领域对象所在的软件包。 类名:代码模型中的类名,对应领域对象的类名。 方法名:代码模型中的方法名,对应领域对象实现或操作的方法名。
DRY原则
Don’t Repeat Yourself,直译就是不要重复你自己。
简单来讲,写代码的时候,如果出现雷同片段,就要想办法把他们提取出来,成为一段独立的代码。这样的抽象,可以保证任何调用这段代码的程序都能得到一致的结果;同时在需要修改时也能保证所有调用处都能获得更新。
CQRS
命令查询职责分离。将应用程序服务分为两种不同的类型:命令和查询处理程序。
查询不应该修改任何东西,只返回数据。命令是相反的:它应该在系统中进行更改,但不返回任何数据。多亏了这一点,我们的查询可以更有效地被缓存,并且我们降低了命令的复杂性。这听起来像是一个严重的限制,但实际上并非如此。我们执行的大部分操作都是读或写。很少,两者都有。
命名
DDD Lite描述了一条规则,即你应该坚持使用尽可能接近非技术人员(通常称为“业务”)说话方式的语言。它也适用于命令和查询名称。您应该避免使用“创建培训”或“删除培训”之类的名称。这不是企业和用户理解您的域的方式。您应该改用“安排培训”和“取消培训”。
微服务要有合理的架构分层
微服务设计要有分层的设计思想,让各层各司其职,建立松耦合的层间关系。
不要把与领域无关的逻辑放在领域层实现,保证领域层的纯洁和领域逻辑的稳定,避免污染领域模型。也不要把领域模型的业务逻辑放在应用层,这样会导致应用层过于庞大,最终领域模型会失焦。如果实在无法避免,我们可以引入防腐层,进行新老系统的适配和转换,过渡期完成后,可以直接将防腐层代码抛弃。
领域模型的核心逻辑在领域层实现,服务的组合和编排在应用层实现,通过 API 网关为前台应用提供服务,实现前后端分离。
中台
业务中台的建设可采用领域驱动设计方法,通过领域建模,将可复用的公共能力从各个单体剥离,沉淀并组合,采用微服务架构模式,建设成为可共享的通用能力中台。
中台的本质其实就是提炼各个业务板块的共同需求,进行业务和系统抽象,形成通用的可复用的业务模型,打造成组件化产品,供前台部门使用。前台要做什么业务,需要什么资源,可以直接找中台,不需要每次都去改动自己的底层。
中台业务抽象的过程就是业务建模的过程,对应 DDD 的战略设计。系统抽象的过程就是微服务的建设过程,对应 DDD 的战术设计。
中台业务建模的过程:
第一步:按照业务流程(通常适用于核心域)或者功能属性、集合(通常适用于通用域或支撑域),将业务域细分为多个中台,再根据功能属性或重要性归类到核心中台或通用中台。核心中台设计时要考虑核心竞争力,通用中台要站在企业高度考虑共享和复用能力。
第二步:选取中台,根据用例、业务场景或用户旅程完成事件风暴,找出实体、聚合和限界上下文。依次进行领域分解,建立领域模型。
第三步:以主领域模型为基础,扫描其它中台领域模型,检查并确定是否存在重复或者需要重组的领域对象、功能,提炼并重构主领域模型,完成最终的领域模型设计。
第四步:选择其它主领域模型重复第三步,直到所有主领域模型完成比对和重构。
第五步:基于领域模型完成微服务设计,完成系统落地。
DDD 的战略设计可用于中台业务建模,战术设计可指导中台微服务设计
中台是抽象出来的业务模型,微服务是业务模型的系统实现,DDD 作为方法论可以同时指导中台业务建模和微服务建设,三者相辅相成,完美结合。
总结
总结下聚合、聚合根、实体和值对象它们之间的联系和区别。
聚合的特点:高内聚、低耦合,它是领域模型中最底层的边界,可以作为拆分微服务的最小单位,但我不建议你对微服务过度拆分。但在对性能有极致要求的场景中,聚合可以独立作为一个微服务,以满足版本的高频发布和极致的弹性伸缩能力。一个微服务可以包含多个聚合,聚合之间的边界是微服务内天然的逻辑边界。有了这个逻辑边界,在微服务架构演进时就可以以聚合为单位进行拆分和组合了,微服务的架构演进也就不再是一件难事了。
聚合根的特点:聚合根是实体,有实体的特点,具有全局唯一标识,有独立的生命周期。一个聚合只有一个聚合根,聚合根在聚合内对实体和值对象采用直接对象引用的方式进行组织和协调,聚合根与聚合根之间通过 ID 关联的方式实现聚合之间的协同。
实体的特点:有 ID 标识,通过 ID 判断相等性,ID 在聚合内唯一即可。状态可变,它依附于聚合根,其生命周期由聚合根管理。实体一般会持久化,但与数据库持久化对象不一定是一对一的关系。实体可以引用聚合内的聚合根、实体和值对象。
值对象的特点:无 ID,不可变,无生命周期,用完即扔。值对象之间通过属性值判断相等性。它的核心本质是值,是一组概念完整的属性组成的集合,用于描述实体的状态和特征。值对象尽量只引用值对象。
领域对象