亚马逊|领域驱动编程,代码怎么写?

亚马逊|领域驱动编程,代码怎么写?

文章图片

亚马逊|领域驱动编程,代码怎么写?

文章图片

亚马逊|领域驱动编程,代码怎么写?

文章图片

亚马逊|领域驱动编程,代码怎么写?

文章图片

亚马逊|领域驱动编程,代码怎么写?


一 前言 相较于大家熟练使用的 MVC 分层架构 , 领域驱动设计更适用于复杂业务系统和需要持续迭代的软件系统的架构模型 。 关于领域驱动设计的概念及优势 , 可以参考的文献非常多 , 大多数的同学都看过相关的书籍 , 所以本文不讨论领域驱动概念层面的东西 , 而是试图从编程实践的层面 , 对领域驱动开发做一些简单的介绍 。
加入阿里健康之后 , 我所在的团队也在积极推进领域驱动设计的应用 , 相关同学也曾给出优秀的脚手架代码 , 但目前看起来落地情况并不太理想 , 个人浅见 , 造成这种结果主要有四个原因 。































































【亚马逊|领域驱动编程,代码怎么写?】
大家更熟悉 MVC 的编程模式 , 需要快速实现某个功能的时候 , 往往倾向于使用较为稳妥、熟悉的方式 。大家对领域驱动编程应该怎么编写并没有一个统一的认知(Axon Framework[1
对领域驱动设计实现的非常好 , 但它太“重”了) 。DDD 落地本身就比较难 , 往往需要事件驱动和 Event Store 来完美实现 , 而这二者是我们不常用的 。领域驱动设计是面向复杂系统的 , 业务发展初期看上去都比较简单 , 一上来就搞领域驱动设计有过度设计之嫌 。 这也是领域驱动设计常常在系统不得不重构的是时候才被拿出来讨论的原因 。笔者曾在研发过程中研究、实践过领域驱动编程 , 对领域驱动框架 Axon Framework 也做了深入的了解 , (也许是因为业务场景相对简单)当时落地效果还不错 。 抛却架构师的视角 , 从一线研发同学的角度来看 , 基于领域驱动编程的核心优势在于: 实施面向对象的编程模式 , 进而实现高内聚、低耦合 。在复杂业务系统的迭代过程中 , 保证代码结构不会无限制地变得混乱 , 因此保证系统可持续维护 。领域驱动开发最重要的当然是正确地进行领域拆解 , 这个拆解工作可以在理论的指导下 , 结合设计者对业务的深入分析和充分理解进行 。 本文假定开发前已经进行了领域划分 , 侧重于研究编码阶段具体如何实践才能体现领域驱动的优势 。二 保险领域知识简介 以保险业务为例来进行编程实践 , 一个高度抽象的保险领域划分如图所示 。 通过用例分析 , 我们把整个业务划分成产品域、承保、核保、理赔等多个领域(Bounded-Context) , 每个领域又可以根据业务发展情况拆分子域 。 当然 , 完备保险业务要比图中展现的复杂太多 , 这里我们不作为业务知识介绍的篇章 , 只是为了方便后续的代码实践 。三 领域驱动开发的代码结构 1 领域驱动的代码分层 可以使用不同的 Java 项目发布不同的微服务对领域进行隔离 , 也可以在同一个 Java 项目中 , 使用不同 module 进行领域隔离 。 这里我们使用 module 进行领域隔离的实现 。 但是无论采用何种方式进行领域隔离 , 领域之间的交互只能使用对方的二方包或者 API 层提供的 HTTP 服务 , 而不能直接引入其他领域的其他服务 。在每个领域内部 , 相对于 MVC 对应用三层架构的拆分 , 领域驱动的设计将应用模块内部分为如图示的四层 。用户接口层 负责直接面向外部用户或者系统 , 接收外部输入 , 并返回结果 , 例如二方包的实现类、Spring MVC 中的 Controller、特定的数据视图转换器等通常位于该层 。 在代码层面常常使用的包命名可以是 interface api facade 等 。 用户接口层的入参、出参类定义采用 POJO 风格 。用户接口层是轻的一层 , 不含业务逻辑 。 安全认证 , 简单的入参校验(例如使用 @Valid 注解) , 访问日志记录 , 统一的异常处理逻辑 , 统一返回值封装应当在这层完成 。用户接口层所需要的功能实现是由应用层完成 , 这里一般不需要进行依赖倒置 。 编码时 , 该层可以直接引入应用层中定义的接口 , 因而该层依赖应用层 。 需要注意的是 , 虽然理论上用户接口层可以直接使用领域层和基础设施层的能力 , 但这里建议大家在对这种用法熟练掌握前 , 最好采用严格的分层架构 , 即当前层只依赖其下方相邻的一层 。应用层 应用层具体实现接口层中需要功能 , 但该层并不实现真正的业务规则 , 而是根据实际的 use case 来协调调用领域层提供的能力 。消息发送、事件监听、事务控制等建议在这一层实现 。 在代码层面常常使用的包命名可以是 application service manager 等 。 它用来取代 Spring MVC 中 service 层 , 并把业务逻辑转移到领域层 。领域层 领域层面向对象的 , 它主要用来体现和实现领域里的对象所具备的固有能力 。 因此 , 在领域驱动编程中 , 领域层的编程实现是不允许依赖其他外部对象的 , 领域层的编程是在我们对领域内的对象所具备的固有能力和它要在当前业务场景下展现什么样的能力有一定了解后 , 可以直接编码实现的 。例如我们最开始接触面向对象的编程的时候 , 常常会遇到的一个例子是鸟会飞、狗会游泳 , 假设我们的业务域只关心这些对象的运动 , 我们可以做如下的实现 。public interface Moveable { void move();public abstract class Animal implements Moveable {public class Bird extends Animal { public void move(){ //try to fly System.out.println(\"I'am flying\"); public class Dog extends Animal { public void move(){ //try to swim System.out.println(\"I'am swimming\");基于领域驱动的编程需要这样(充血模型)去实现对象的能力 , 而不是像我们在 MVC 架构中常常使用贫血模型 , 把业务逻辑写在 service 中 。当然 , 即使采用了这样的编程方式 , 距离实现领域驱动还差的远 , 一些看似简单的问题就可能给我们带来巨大的不安感 。 例如复杂的对象应当如何初始化和持久化?同样一个事物在不同领域都存在 , 但其关注点不同时这个事物应当分别怎么抽象?不同领域的对象需要对方的信息时 , 应当怎么获取? 这些问题 , 我们也会在代码示例部分尝试给出一些参考的方案 。基础设施层 基础设施层为上面各层提供通用的技术能力 , 例如监听、发送消息的能力 , 数据库/缓存/NoSQL数据库/文件系统等仓储的 CRUD 能力等 。2 小结 根据对领域驱动设计各层的进一步分析 , 一个更加具体化的分层结构如下 。基于上面的分层原则 , 前述保险领域一个可以参考的代码结构如下 , 我们将在下面编码示例详细讲解每一个分包的理念和作用 。四 领域驱动开发的代码 理论上 , DOMAIN 不依赖其他层次且是业务核心 , 我们应当先编写领域层代码 , 但是一则由于我们对保险领域知识的欠缺 , 可能不清楚保单到底有哪些固有能力;二则为了便于讲解 , 因此我们直接借助一个用例来展示代码 。1 用例 用户在前端页面选择保险产品 , 选择可选的保障责任 , 输入投/被保人信息 , 选择支付方式(分期/趸交等)并支付后提交投保请求; 服务端接受投保请求 -核保 -出单 -下发保单权益 。这里用例 1 是用例 2 的前置用例 , 我们假定用例 1 已经顺利完成(用例 1 中完成了费率计算) , 只来实现用例 2 , 并且用例 2 也只是大略的实现 , 只要能把代码样式展示即可 。2 用户接口层编程实践 分包结构 其中 client 是对 inusurance-client (公共二方包) 部分的实现 , web 是 rest 风格接口的实现 。用例代码 @AllArgsConstructor@RestController@RequestMapping(\"/insure\")public class PolicyController { private final InsuranceInsureService insuranceInsureService; /** * 投保出单 * @param request * @return 保单 ID */ @RequestMapping(value = https://mparticle.uc.cn/"/issue-policy\" method = RequestMethod.POST) public String issuePolicy(IssuePolicyRequest request){ return insuranceInsureService.issuePolicy(request);这里用到的入参和返回值的类都在应用层中定义 。3 应用层编程实践 1、分包结构 其中最外层接口是面向具体业务场景的 , 可以根据业务发展再进行分包 。pojo 包中定义了应用层用到的各种数据类(上面的 IssuePolicyRequest 就在这里)及其向其他层传播时需要进行类型转换的转化器 。tasks 包中定义了一些定时任务的入口 。注意 , 在领域编程实践中 , 会需要非常多的类型转换 , 我们可以借助一些框架(例如 MapStruct[2