分段式协作分为三个阶段:战略设计、战术设计、战术实现
战略设计在整个设计过程中,务必忽略具体的技术实现细节,做到抽象与实现细节解耦,从而满足”领域驱动“而不是”技术实现驱动“。
通过分析依赖关系,提前识别依赖矛盾,减少低级设计错误。若出现以下依赖关系,需要考虑是否存在未澄清问题:
技术实现环节涉及:UML设计、API详细设计、数据库设计、部署与运维...
对以下知识点一定要熟知:
在落地领域驱动之前,首先明确的问题是选择何种架构去实现。
DIP改进分层所谓的依赖倒置原则指的是:高层模块不应该依赖于低层模块,两者都应该依赖于抽象,抽象不应该依赖于细节,细节应该依赖于抽象。
正如架构图中看到的,基础实施层位于其他所有层的上方,接口定义在其它层,基础实施实现这些接口。或者可以这样来表述:领域层等其他层不应该依赖于基础实施层,两者都应该依赖于抽象。
这也就是意味着一个重要的落地指导原则: 所有依赖基础实施实现的功能,抽象和接口都应该定义在领域层或应用层中。
六边形架构在这种架构风格中,外部客户和内部系统的交互都会通过端口和适配器完成转换,这些外部客户之间是平等的,比如用户web界面和数据库持久化,当需要一个新的外部客户时,只需要增加相应的适配器,比如当我们依赖外部一个RPC的服务时,只需要编写对应的适配器即可。
这种设计角度下,没有前端web和数据库后端之分,统一为外部客户。
如果外部客户时HTTP请求,那么SpringMVC的注解和Controller构成了适配器;如果外部客户时MQ消息,那么适配器就是MQConsumer监听器;如果外部客户时数据库,那么适配器可能就是Mybatis的Mapper。
洋葱架构只允许外层依赖内层,不允许内层知道外层的细节。
驱动模式在领域驱动架构中,通常会将查询和命令操作分开,我们称之为CQRS(命令查询的责任分离Command Query Responsibility Segregation)。
图中读模块Query Model和写模块Command Model只是逻辑分离,物理层面还是使用了同一个数据库,也可以将数据库改成读库和写库做到物理分离。
架构落地架构中,平等地看待Web、RPC、DB、MQ等外部服务,基础实施依赖圆圈内部的抽象。
当一个命令Command请求过来时,会通过应用层的CommandService去协调领域层工作,而一个查询Query请求过来时,则直接通过基础实施的实现与数据库或者外部服务交互。再次强调,所有的抽象都定义在圆圈内部,实现都在基础设施。
代码落地代码结构java
└── com
└── gtw
└── business
├── application -- 用户接口层
│ ├── controller -- HTTP 请求
│ ├── mq -- mq 消费入口
│ ├── report -- 报表类、查询入口
│ ├── rpc -- rpc 服务提供入口(这里指rpc的实现,rpc接口提供单独放在可打包发布的module中)
│ └── scheduler -- 定时任务调度入口
├── common -- 公共通用层
│ ├── component -- 通用基础设施层的接口,如 mq,cache
│ ├── model -- 公用的数据对象和抽象接口
│ └── utils -- 工具类
├── domain -- 领域服务层
│ ├── aggregate -- 领域模型
│ └── service -- 领域服务
├── infrastructure -- 基础设施层
│ ├── cache
│ ├── db -- 对领域服务中的仓储实现
│ │ └── reponsitory
│ ├── event -- 对领域服务中的事件实现
│ │ ├── listener
│ │ └── publisher
│ ├── mq -- mq 生产者
│ └── rpc -- rpc 服务的调用
└── service -- 应用服务层(CQRS)
├── command
│ ├── cmd -- 命令的请求参数XXXCommand对象
│ └── impl -- 命令请求服务的实现(请求接口直接定义在command包下)
└── query
├── dto -- 查询结果DTO对象
├── impl -- 查询服务的实现(查询接口直接定义在command包下)
└── qry -- 查询的条件参数XXXQry对象
用户界面com.gtw.business.application.controller
Controller作为六边形架构中与HTTP端口的适配器,起到了适配请求,委托应用服务处理的任务。对称性架构的好处就在于,当增加新的用户的界面时可以创建一个新包去承载适配器(比如为mq消费,在application目录下新增mq包),然后调用应用层的服务。
这里可以有个默认规范:所有查询的条件封装成XXXQry对象,所有命令的请求封装成XXXCommand对象。
考虑校验逻辑应该放到哪一层的时候确定这一层代码可以有请求参数的基本校验,但是应用服务的校验逻辑是必须存在的,校验和应用服务的耦合是紧密的。
应用服务com.gtw.business.service应用服务的每个方法与用例是一一对应的,典型的处理流程是:
发布领域事件的动作放在了应用层没有放在领域层,而领域事件的定义是在领域层,这是为什么呢?如果不考虑持久化,发布领域事件的确应该在领域模型中,但是在代码落地时,考虑到持久化完成后才代表有了真实的事件,所以将触发事件的代码放到了资源库后面。
领域模型com.gtw.business.domain.aggregate采用了aggregate而不是model,是为了将聚合根的概念显现出来,每个聚合根单独成一个子包,在单个聚合根中包含所需要的值对象、领域事件的定义、资源库的抽象接口等。
领域事件的定义、资源库的抽象接口之所以放在相应聚合根的package中,是因为它更能体现这个领域模型,而且资源库的抽象和聚合根有着对应的关系(不大于聚合根的数量)。
关于聚合根对象的创建,特别提醒的是聚合根对象的创建不应该被Spring容器管理,也不应该在聚合根中注入其它对象。聚合根对象可以通过静态工厂方法来创建,下文还会介绍如何落地资源库进行聚合根的创建。
领域服务com.gtw.business.domain.service业务逻辑什么时候该放在领域模型中,什么时候放在领域服务中,可以从以下几点考虑:
基础设施可以对抽象的接口进行实现,资源库Repository的接口定义在领域层,那么在基础设施中就可以具体实现这个接口。
资源库Repository的实现就是将聚合根对象持久化,往往聚合根的定义和数据库中定义的结构并不一致,数据库的对象称为数据对象DO。
当持久化时就需要将聚合根序列化成数据库数据对象,通过资源库获取(构造)聚合根时,也需要反序列化数据库数据对象。可以基于反射或其它技术手段完成序列化和反序列化操作,这样可以避免聚合根中编写过多的getter和setter方法。
查询服务com.gtw.business.service.query应用服务包含了commond和query两个子包,查询服务是在query包中。
运用CQRS设计,查询服务不会调用应用服务,也不会调用领域模型和资源库Repository,它会直接查询数据库或者ES获取原始数据对象DO,然后组装成数据传输对象DTO给用户界面,这个组装的过程称为Assembler,由于与用户界面有一定的对应关系,所以Assembler放在查询服务中。
是否需要将每个对象都转化成DTO返回给用户界面这个要看情况,个人认为当DO能满足界面需求时是可以直接返回DO数据的。
落地MQ、Event、Cache毫无疑问,MQ、Event、Cache的实现都应该在基础设施层,它们接口的定义放在哪里呢?一个方案是哪一层使用了抽象就在那一层定义接口,另一个方案是放到一个共有的抽象包下,基础设施和对应层依赖这个共有的抽象包。
RPC和防腐层前面提到过,当我们暴露一个RPC服务时和web层是平等对待的,比如暴露一个dubbo协议的服务就和暴露一个http的服务是平等的。这一小节我们将来探讨如何与第三方系统的RPC服务进行交互。
这里涉及到DDD中Bounded Context和Context Map的概念,在领域驱动设计中,限界上下文之间是不能直接交互的,它们需要通过Context Map进行交互,在微服务足够细致的年代,我们可以做到一个微服务就代表着一个限界上下文。
通用做法会再创建一个Translator实现上下文模型之间的翻译功能。其它限界上下文的模型在我们系统中并不是一个模型实体,而是一个值对象,很显然Adapter应该放在基础设施层中,那么这个值对象存放在哪里呢?我们可以将值对象和抽象接口定义在领域层,然后基础设施通过适配器和翻译器实现抽象接口,很明显这个做法是非常可取的。在具体落地时我们发现,这些值对象可能同时又被查询服务依赖,所以值对象和抽象接口定义在shared Data & Service中也是可取的,具体放在那里因看法而异。
Example需求-节选自《领域驱动设计第7章》:假设正在为一家货运公司开发新的软件,最初的需求包括三项基本功能:
演示如何落地:
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved