尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:

谈谈你的DDD落地经验?

谈谈你对DDD的理解?

如何保证RPC代码不会腐烂,升级能力强?

微服务如何拆分?

微服务爆炸,如何解决?

你们的项目,DDD是怎么落地实操的?

所以,这里尼恩给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典PDF》V132版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

除了本文,尼恩输出了一个 《从0到1,带大家精通DDD》系列,帮助大家彻底掌握DDD,链接地址是:

阿里DDD大佬:从0到1,带大家精通DDD

阿里大佬:DDD 落地两大步骤,以及Repository核心模式

阿里大佬:DDD 领域层,该如何设计?

极兔面试:微服务爆炸,如何解决?Uber 是怎么解决2200个微服务爆炸的?

阿里大佬:DDD中Interface层、Application层的设计规范

字节面试:请说一下DDD的流程,用电商系统为场景》

DDD如何落地:去哪儿的DDD架构实操之路

DDD落地:从腾讯视频DDD重构之路,看DDD极大价值》

DDD落地:从美团抽奖平台,看DDD在大厂如何落地?》

美团面试:微服务如何拆分?原则是什么?

DDD神药:去哪儿结合DDD,实现架构大调优

DDD落地:从网易新闻APP重构,看DDD的巨大价值》

大家可以先看前面的文章,再来看本篇,效果更佳。

另外,尼恩会结合一个工业级的DDD实操项目,在第34章视频《DDD的学习圣经》中,给大家彻底介绍一下DDD的实操、COLA 框架、DDD的面试题。

DDD现在非常火爆,是有其巨大生产价值,经济价值的, 绝不仅仅是一套概念那么简单。

DDD的绝大价值,具体请参见以下视频:

从腾讯视频DDD重构案例,看看DDD极大价值

本文目录

- 尼恩说在前面

- 阿里单据系统的DDD最佳实践

- 一、前言

- 二、单据

  - 1、生命周期

    - 1)领域概念:贫血实体

    - 2)领域知识:生命周期

    - 3)领域模式:聚合与聚合根

  - 2、隐式概念

    - 1)领域知识:单据字段

    - 2)概念突破:命令实体

  - 3、深层模型

    - 1)领域知识:状态推进本质

    - 2)深层模型:修正状态机模型

  - 4、边界模型

    - 1)领域知识:边界隐式概念

    - 2)领域模式:防腐层

    - 3)隐式概念:重构中发现模型

  - 5、领域服务

- 三、后记

- 参考书籍

- 说在最后

- 部分历史案例

阿里单据系统的DDD最佳实践

作者:少岚,阿里同城履约物流技术团队

本篇以电商购物场景为背景,探讨了领域驱动设计(DDD)在实际应用中的实践过程。你会发现,DDD 的核心理念在于,通过一系列实用技巧,挖掘出能揭示问题本质的领域模型,并通过模型间的协作解决领域问题,从而驾驭问题领域的复杂性。对于 DDD 爱好者来说,它犹如一个充满挑战和智慧的玩具,在深入思考问题本质和构建抽象知识模型的过程中,让人沉浸于心流状态。

一、前言

领域驱动设计(Domain-Driven Design),简称 DDD,并非一种框架或具体的架构设计,而是一种架构设计思想。其代表性著作便是“领域驱动设计之父”Eric Evans 的经典书籍《领域驱动设计》。DDD的核心目标是通过各种实用方法和技巧提炼出具有体现问题实质的领域模型,并通过保护和组织模型协作来解决领域问题,从而掌控问题领域本身的复杂性,也就是为什么DDD会被认为是软件核心复杂性的应对之道。

DDD的理想应用场景是具有固定领域体系且复杂性较高的应用软件系统设计的各个环节和过程,但这无疑是一项艰巨的任务。DDD要求技术人员高度协同,提升建模技巧,精通领域设计,并通过不断的时间推移和领域知识的吸收消化,以达成应对复杂性的目标。只有这样,DDD的价值才能在项目的中后期得到充分体现。本文旨在带领大家从第一视角体验这种实践过程,感受DDD的独特魅力,掌握其精髓,为在DDD中探索的朋友们指明方向。

我个人对面向对象编程有着浓厚的兴趣,编写代码如同孩子玩玩具般充满乐趣。DDD让我有机会玩得更高级、更复杂、更具挑战性的玩具。对于一个始终保持少年心态的程序员来说,构建领域模型极易让人进入心流状态。这种深入思考问题本质,构建抽象知识模型的过程,让我对DDD情有独钟。

我想用两个词来表达我体会到的魅力:知识、思考。

知识:Eric Evans发行的《领域驱动设计》一书中第一章介绍的就是知识,特别指领域知识,但是这里的知识并不是简单的问题的表象,而是深入到问题的本质,只有获取到真正的知识,运用好各种DDD模式和优秀的战术,打造具有丰富知识设计的模型,才能充分发挥领域驱动设计的好处。

思考:获取知识并不容易。例如,给你一批地球日出月落的数据,你可以用地心说、日心说和地平说等不同模型来拟合地球的各种现象,究竟哪个模型的知识最适合呢?产品和业务提出需求时,很多时候难以触及问题的本质。因此,设计模型、选择模型都需要设计者做到深入思考,挖掘概念,并和领域专家(如果存在)达成一致。

本文是一篇关于DDD实践的典型案例文章,读者也可认为它类似于一种多字段单据的设计模式。全文将以一个简化的电商购物背景作为领域上下文,重点介绍领域组件的形成过程,并突出DDD的核心要点。但同时需要注意到,本文专注于单个领域上下文的战术实践,不涉及多个领域上下文的协作。文章核心内容将按照4个小节展开:

  1. 从实体生命周期出发,围绕一个聚合根的设计作介绍,包括原因、好处;

  2. 从单据字段的性质,特点等,挖掘出一类命令对象集合;

  3. 是体现如何从深层领域本质修正一个状态机模型,从而改变了我的组件设计为状态同步模型

  4. 根据防腐层的一些好处,以及如何在防腐层中通过重构去捡回来重要的领域实体

通过本文,希望大家能更好地理解和应用领域驱动设计,为复杂业务场景找到解决问题的方法。

二、单据

1、生命周期

1)领域概念:贫血实体

为了简化问题,本文将以简化的电商交易平台领域为例,探讨其中的核心概念。简而言之,即消费者在某个购物平台下单购买商品,支付完成后,商品按照计划送达消费者手中。

其中系统比较重要的就是订单,订单作为单据,是一种交易凭证,表达了交易关系的事实依据。它主要涵盖了客户、商品、时间、支付等要素,可作为会计核算的原始资料和重要依据。电商交易单据以电子化形式存在于信息系统中,我们统一称之为交易主订单

通常情况下,一个交易主订单代表一次交易行为。其中的交易内容,会用交易子订单表示,例如:用户一次性购买5个苹果,3个梨子,那就对应为一个交易主订单,它刻画了用户的购买行为,其中有两个交易子订单,一个描述5个苹果,一个描述3个梨子。

如果我们系统的子单可以单独发货,甚至多仓发货的,那么我们再加一个发货单的概念,用作和包裹一一对应,一个包裹可以放任意交易子单的物品,例如上面的两个子单可以放到两个包裹,用两个发货单表示,一个发货单4个苹果,另一个发货单1个苹果加3个梨子,当然我们的电商系统还有商品、客户、收件人、供应商等实体,现在我们在系统中有了这些实体,如下图所示。

图片

注意:请点击图像以查看清晰的视图!

在系统中,实体具有各自的生命周期。一个交易主订单可能包含多个交易子订单,一个包裹可以随意组合子订单进行发货。但这些模型相对较弱,因为难以充实。如何将这些需求封装为知识,以设计出更完善的模型,只有在实际操作中才能找到答案。这也是系统初期面临的实际情况,不应过度设计,往往一开始就是一个简单的CRUD系统。

2)领域知识:生命周期

以上介绍的实体都有自己的生命周期,生命周期体现在系统行为中。以简单电商系统为例,从下单到服务结束,基本经历以下过程行为:

下单

  1. 用户提交订单

  2. 商品的库存占用

  3. 用户在规定时间内进行支付

  4. 订单阶段性状态推进:待支付、支付完成、待发货、运输中、配送中、妥投等等

查询

  1. 生命周期中发生查询请求

取消

  1. 订单有效期到期取消订单

  2. 用户取消订单

以上流程,都和上面提到的实体相关,但具有相同生命周期的实体组合较少。例如,订单实体的生命周期与客户完全不同。客户从注册到注销,一直存在,而订单仅在一次完整交易行为中存在。商品和订单也不同,订单被取消生命周期结束,而商品可以重新售卖。因此,在商品、供应商、客户、交易主订单、交易子订单、发货单等实体中,只有交易主订单和交易子订单具有相同生命周期,过程还包括发货单。

另一方面,我们看一下会改变交易主订单和交易子订单状态的一些代码行为(通常我们会封装到服务类中),代码在系统刚开始基本会写成如下这样:

图片
在现代企业运营中,单据实体的依赖关系管理显得尤为关键。以下是对上述问题和解决方案的重新梳理和结构化描述:

事务一致性问题在服务如提交订单、订单支付、取消订单等场景中,事务的一致性是必须保障的。这不仅涉及到并发操作,还包括处理乱序问题。保持一致性的逻辑复杂,且容易出错,给开发和维护带来了挑战。

解决方案- 并发控制:通过锁机制或乐观锁等技术,确保在并发环境下数据的一致性。- 事务管理:使用事务管理工具,如Spring的声明式事务管理,确保操作的原子性。

共同闭包性问题交易主订单的状态往往由子订单或发货单的状态推进。例如,当所有子订单都妥投后,主订单才能标记为完成。这要求系统能够处理这种闭包性关系。

解决方案- 闭包抽象:将具有共同闭包性质的实体进行抽象封装,简化状态管理逻辑。

共性逻辑散落问题在不同服务中,存在大量重复的共性逻辑,如妥投一致性规则、状态变更记录等。这种散落的逻辑增加了维护成本。

解决方案- 逻辑抽象:将共性逻辑抽象成通用服务或组件,减少重复代码,提高代码复用性。

领域模式:聚合与聚合根在DDD(领域驱动设计)中,聚合是一种相关对象的封装,聚合根是对外的唯一引用点。通过构建聚合,可以简化对象间的关系管理。

解决方案- 构建聚合:将交易主订单、子订单和发货单构建为聚合,以交易主订单作为聚合根。- 充血模型:将业务逻辑集中在聚合内部,避免贫血模型中逻辑与数据分离的问题。

知识拓展- 聚合:一种相关对象的封装,作为数据修改的基本单位。- 聚合根:聚合中对外的唯一引用点,定义了聚合的边界。

通过上述措施,可以有效地解决单据实体依赖关系管理中的问题,提高系统的稳定性和可维护性。
图片

聚合根一致性原则

聚合根是聚合模式中的核心,它负责确保所有相关子实体的一致性。以下是一些关键的一致性规则:

主子单一致性- 妥投一致性规则:交易子单的妥投状态独立更新,只有当所有子单都妥投后,交易主单才更新为妥投状态。- 出仓库一致性规则:子单的出仓操作独立进行,当所有发货单出仓完成后,交易主单才更新出仓状态。

发货单与子单一致性- 包裹中包含的子单及其数量需与源交易子订单保持一致,由交易主订单统一保证。

聚合根封装细节聚合根中封装了一些关键逻辑,以提高系统的可维护性和一致性:

节点流水记录- 流水记录逻辑封装在聚合根中,状态变化时生成流水记录。

订单状态推进- 事件处理代码封装在交易主订单中,统一变更子订单和发货单状态。

事务修改的基本单元聚合根的修改是事务的基本单元,这带来以下好处:

无数据库概念- 简化数据库操作,只需取出和放回聚合根。

副作用保护- 限制副作用的产生,并通过监控机制保护。

状态机应用- 考虑使用状态机来管理状态变更,提高系统的健壮性。

总结聚合根的设计和应用确保了交易数据的一致性和系统的可维护性。通过封装关键逻辑和简化数据库操作,提高了系统的效率和稳定性。

图片

聚合模式的优缺点及应用

聚合模式的优势聚合模式通过将多个相关对象组合成一个单一的聚合根,简化了业务逻辑的复杂性,提升了代码的封装性和可维护性。例如,在支付处理、取消逻辑和妥投逻辑中,服务职责单一化,代码逻辑简化,提高了可读性。

聚合模式的实现方法- 添加新状态或逻辑:在交易主订单聚合操作中添加即可。- 新增服务:如拒收回传服务,无需重写事务逻辑,封装性和可维护性得到保障。

聚合模式的缺点及解决方案- 查询性能问题:通过设计优化,如按需更新对象的版本控制,解决加载所有聚合实体的性能问题。- 无谓的更新:通过断言或日志记录每次字段修改,减少错误更新。- 属性访问限制:创建聚合访问视图,允许服务操作视图而非直接访问实体。

聚合根设计建议- 聚合根设计不宜过大,建议包含3-4个实体,以最大化聚合根的优势。

隐式概念的探讨

领域知识:单据字段的管理单据字段的多样性和动态拓展性要求我们在聚合根中进行细致管理。字段的内聚性分析有助于我们更好地分类治理字段。

单据字段的挑战- 字段多样性:单据承载多种属性,若全部由聚合根维护,将导致方法臃肿。- 动态拓展字段:建议使用key-value存储或Map实现,避免影响实体搜索。

字段内聚性的利用- 通过分析订单字段,可以将其归类并分别管理,如联系人信息、购买者信息等。

概念突破:命令实体命令实体在领域驱动设计中承担着改变状态的角色,与CQRS架构中的Command有所区别。建议将Command替换为Operation,以符合领域逻辑。

知识拓展与沟通通过与领域专家的沟通,可以更深入地理解业务需求,从而设计出更符合实际的命令实体。

柔性设计根据Eric Evans的理论,将逻辑代码组织为无副作用的函数,返回Value Object,并由命令对象根据Value Object更改对象状态。

结论聚合模式提供了一种有效的业务逻辑封装和代码组织方式,但也存在一些性能和设计上的挑战。合理地应用聚合模式,并结合领域知识深入分析和管理单据字段,可以提升系统的可维护性和扩展性。同时,命令实体的正确使用和设计,将进一步增强系统的灵活性和响应业务变化的能力。

图片

注意:请点击图像以查看清晰的视图!

命令模式:这个过程和命令模式是差不多的。我们会把命令交给聚合根去执行,对比上面命令模式的图我们可以看出,其实运维人员就是Client,他把封装好的命令间接设置给交易主订单聚合根,而Invoker,则是聚合根,他负责执行具体的命令,同时也会记录命令的执行,改变自身状态。例如下面的代码所示,为聚合根执行命令的过程。

public class SubmitOrderUsercase{  
      
    public void sumit(Request request) {  
        TradeMainOrder mainOrder = getMainOrder();  
        //获取命令的具体实现  
        IPhoneNumberCompleteCommand command = getCommand(request,IPhoneNumberCompleteCommand.class);  
        //聚合根执行手机号完善命令  
        mainOrder.execute(command);  
        // ......  
        //获取命令的具体实现  
        IDiscountCalculateCommand command = getCommand(request,IDiscountCalculateCommand.class);  
        //聚合根执行折扣计算命令  
        mainOrder.execute(command);  
        // ......  
    }  
    
    public IPhoneNumberCompleteCommand getCommand(Request request,Class clazz){  
      // 业务配置好的,什么场景用什么命令.......  
    }  
    
  }  

在设计单据字段管理系统时,可以采用命令对象模式来增强字段内聚性。以下是具体的设计步骤和组件结构:

  1. 定义命令对象:针对单据字段的变更原因,创建相应的命令对象。例如: - 发货单完善命令 - 支付信息完善命令 - 购买者信息完善命令 - 商品信息完善命令
  2. 封装逻辑和参数:每个命令对象封装了变更所需的逻辑和参数。包括: - 入参的封装 - 可能的外部服务查询(查询结果作为入参)
  3. 分类命令对象:根据单据的不同实体,对命令对象进行分类,例如: - 交易主订单变更命令 - 交易子订单变更命令
  4. 聚合根的执行:交易主订单作为聚合根,负责执行上述分类的命令对象。
  5. 组件结构:通过修改依赖关系,形成如下组件结构图(此处应插入组件结构图)。 这种设计方法有助于提高系统的模块化和可维护性,同时保持字段变更逻辑的清晰和一致性。
    图片

注意:请点击图像以查看清晰的视图!

如此的灵机一闪,引入命令实体后,以上所有的字段问题,都刚好被这个模型拟合了,我列举几个好处:

设计良好:很明显的倒置依赖,保护聚合根的独立性;函数式编程,可以组合而不担心逻辑错误,有人可能会质疑,命令内部是不是会直接访问对象呢?如下图的命令接口所示,如果这样设计该接口明显是有副作用的,但如果我们传入的是编辑稿(类似视图),然后我们编辑视图,最后更新回到实体就可以了。

public interface TradeSubOrderChangeCommand {

     String getSubOrderId();

     void execute(TradeSubOrderDraft subOrder);

 }

字段分治管理:有了命令后,加上适当的命令命名,字段的管理再也不混乱。每个字段都应该有其对应的设置命令进行管理,而不是让各种服务类去进行赋值管理。同时,对字段的处理也可以封装到命令中。你可以随时定位一个字段的变更命令,只需要思考一下字段的归类。最重要的是,这种字段的分类的独立性可以让你操作字段的代码独立分离,使其具有更好的开闭性。这一点正好可以解决字段的多样性问题。

命令封装逻辑:命令可以封装action调用。赋值只是命令的目的。既然封装了action的调用,那么对action的入参和结果的处理也可以封装到命令中。更重要的是,只要是符合触发源的目的、职责单一,部分业务逻辑也可以封装到命令中。在以往很多贫血系统中,这些都是由service负责的,似乎没有service不知道该如何安置代码一样。

随时随地跟踪:下面是一个简单版本的聚合根执行命令的代码示例。其中record方法根据命令本身的属性提供有选择性地记录执行结果的能力。如果有重要的字段,你可以找到该单据对应命令的执行流水,并进行可视化管理。这种粒度的管理在业务运维和开发疑难问题排查上都非常有用。

public class TradeMainOrder{  
    
    public void onCommand(TradeSubOrderChangeCommand command) {  
        if (!tradeSubOrderDict.isEmpty()) {  
            TradeSubOrder subOrder = findSubOrder(command.getSubOrderId());  
             // 变更前的快照代码  
            command.execute(subOrder);  
            // 变更后的对比逻辑代码,记录字段变化个数、时间  
            record();  
            // ......  
            makeStateConsistent();  
        } else {  
            log.error('子单变更命令执行失败,子单列表为空,{}', EagleEye.getTraceId());  
        }  
    }  
    public void onCommand(TradeOrderChangeCommand command) {  
        // 变更前的快照代码  
        command.execute(this);  
        // 变更后的对比逻辑代码,记录字段变化个数、时间  
        record();  
        // ......  
        makeStateConsistent();  
    }  
  }  

在软件开发中,无副作用的函数因其可预测性和易于复用的特性,常被用于构建灵活且可靠的系统。以下是对组合命令和深层模型的详细阐述:

组合命令的实现与优势

**1. 组合命令的定义:**组合命令,如Combine命令,是一个容器,它将多个子命令组织起来,形成一个有序的执行序列。例如,在提交订单的过程中,Combine命令可以包含手机信息完善、邮箱信息完善等子命令。 2. 组合命令的执行逻辑:- 组合命令按照既定的顺序执行各个子命令。- 由于子命令具有统一的接口,实现组合变得简单。 3. 组合命令的特点:- 通过为每个子命令分配一个唯一的ID,可以实现命令组合的外部配置化。- 配置化使得命令的执行顺序可以在运行时动态决定,提高了系统的灵活性。- 基于函数式的实现方式,避免了“组合爆炸”的问题,确保了整个过程的透明性和安全性。 **4. 业务应用:**通过组合命令,可以根据业务需求灵活定制命令组合,并快速上线,实现代码的可管理和配置化,这是编程艺术的高级境界。

深层模型:状态机的应用

**1. 发货单的状态:**发货单具有多种状态,例如:已接单、待发货、运输中、揽收、妥投、拒收、取消等。 **2. 状态变化的驱动:**状态的变化是由接收到的外部事件推动的。然而,由于可能存在事件丢失或乱序到达的问题,状态决策变得复杂。 **3. 状态机的引入:**为了解决这一问题,引入了状态机的概念。状态机通过定义状态转换规则,确保即使在事件不完整或乱序的情况下,也能做出正确的状态决策。 **4. 状态图的构建:**构建状态图是实现状态机的关键步骤,它清晰地展示了不同状态之间的转换关系。 通过上述内容,我们可以看到,无论是组合命令还是状态机,都是提高软件开发效率和系统稳定性的重要工具。
图片
在物流实操中,业务流程的设置是严格有序的,不可跳过任何中间步骤。例如,如果货物尚未进入运输阶段,揽收环节就不可能发生。这表明实际业务状态的转换与状态机的解空间存在不匹配问题,状态机的解空间包含了许多不必要的部分。 对比之下,如果我们设计一个游戏机的投币程序,使用状态机来表示游戏机的不同状态,如投币状态、空闲状态和游戏状态,状态机的设计就非常合适。这里的关键区别在于,问题空间本身就是解空间的模型,由模型驱动。

物流实操中的状态转换问题

  1. 业务流程设置:物流业务流程是严格有序的,不允许跳过中间环节。2. 状态转换的不匹配:实际业务状态转换与状态机解空间存在不匹配,状态机包含不必要的部分。

游戏机投币程序的状态机设计1. 状态机的适用性:游戏机的投币程序使用状态机表示,状态包括投币、空闲和游戏状态。2. 模型驱动:问题空间与解空间的一致性,使得状态机设计完全合理。

结论状态机的设计需要根据问题空间的特性来决定。在物流实操中,状态转换的严格性要求状态机设计必须精确匹配实际业务流程。而在游戏机投币程序中,由于问题空间与解空间的一致性,状态机设计得当,能够很好地模拟和控制游戏机的状态。

图片

不纯粹的解空间问题解析

1. 问题概述在软件开发中,解空间可能包含不必要的连线,例如在物流系统中,运输过程可能会异常跳转到妥投状态。这种现象可能由计算机系统和架构的细微差别引起,如事件传播的异常和速度问题。

2. 领域驱动设计(DDD)的挑战在DDD中,将领域逻辑与技术细节混合是不被推荐的。如果事件顺序被保证,一些连线就变得不必要。然而,这可能导致领域实体中混入非领域逻辑,违背DDD原则。

3. 状态机的适用性使用状态机时,需求变更可能引入新状态,进而导致解空间中出现新的连线,这可能给开发和维护带来困扰。

4. 代码职责问题业务要求在每个状态节点记录节点流水,这在状态机实现中如何操作?若事件顺序混乱,如揽收事件先于运输事件到达,应由哪个节点处理记录流水?

4.1 事件顺序问题- 正确顺序:1. 运输事件,2. 揽收事件,3. 妥投事件。- 实际顺序可能不同,如何处理?

4.2 职责分配- messageHandler 处理运输事件,尽管事件顺序混乱。- 揽收节点处理显得不合理,而messageHandler处理则勉强。

5. 深层模型修正### 5.1 状态同步而非推进发货单的状态代表物流操作过程,其状态变更应反馈至订单进度。这里的状态变更更多是同步而非流转。

5.2 解决方案- 采用流程实例或无状态流程来解决状态同步问题。

6. 结论调整模型,去除不必要的逻辑,采用更合适的方法来处理状态变更和事件顺序问题,是优化解空间的关键。

图片

状态同步算法更新概述

1. 算法变更背景我们对更新状态的算法进行了更新,从状态推进变更为状态同步。这一变更基于对问题空间的深入分析,将整个流程视作一个无环的拓扑排序模型。

2. 状态同步与状态机的比较### 2.1 有序性状态机的节点是相对无序的,而状态同步模型则具有明确的顺序,与问题空间中的工序顺序一致。

2.2 拓扑结构状态机可能存在环状结构,但状态同步模型是拓扑排序的,没有环,这符合业务节点的特性。

2.3 运作机制状态机通过事件和当前状态来确定下一个状态,而状态同步模型则以流程实例为核心,事件到来时,相应节点会被标记为已同步。

2.4 计算机无关性状态同步模型不关注事件的乱序或延迟,事件一旦到达,即触发相应节点的业务逻辑。

3. 逻辑封装逻辑封装到节点上,提高了代码的扩展性和灵活性。例如,流水记录和消息发送代码可以封装在运输节点内或通过观察者模式实现。

4. 性能提升与状态机相比,状态同步模型在开发效率和代码维护方面有显著提升。状态机的复杂度是线性的,而状态同步模型的复杂度是常数级别的。

5. 领域驱动设计的核心这个例子展示了领域驱动设计的核心本质:重视领域和知识的重要性。

6. 边界模型讨论### 6.1 领域知识边界模型涉及软件设计中边缘的恰当区分,单一订单处理系统与多个外部系统(如账户中心、商品中心等)的交互。

6.2 交互方式这些交互主要通过调用各系统的接口来实现数据的获取或写入,展示了系统间的依赖关系。

7. 结论通过状态同步模型的引入,我们不仅提升了系统的效率和可维护性,还强调了领域驱动设计的重要性。

图片
在接单系统中,我们发现一些领域概念如赠品、计划、库存和会员等级等,它们通常只作为字段属性存在,而没有被赋予实体的地位。这引发了一个关键问题:是否需要为这些概念创建独立的实体?目前,我们选择不立即为这些领域逻辑设计实体,但同时,我们应为未来可能的需求做好准备。为此,我们引入了防腐层的概念,以确保系统的灵活性和可扩展性。

领域概念的实体化问题

  • 赠品:通常作为订单的一部分,但是否应有独立实体?- 计划:可能涉及促销或特殊订单处理,其独立性如何?- 库存:管理商品的存储和可用性,实体化的必要性?- 会员等级:影响会员的优惠和权限,是否应独立于账号实体?

防腐层的重要性防腐层是一种设计模式,用于隔离系统的内部领域模型和外部系统或接口。通过这一层,可以实现模型之间的转换,同时保护内部逻辑不受外部变化的影响。以下是防腐层的关键作用:- 隔离外部依赖:减少系统对外部系统的依赖。- 模型转换:在内部领域模型和外部系统之间进行数据和行为的转换。- 灵活性和可扩展性:便于未来对系统进行扩展或修改,而不破坏现有逻辑。

实现防腐层1. 识别外部接口:明确系统需要与哪些外部系统或接口交互。2. 设计转换逻辑:开发必要的逻辑,以在不同模型间转换数据和行为。3. 创建隔离层:实现一个明确的层,用于处理所有与外部系统的交互。4. 维护独立性:确保内部逻辑的独立性,便于系统的维护和升级。

通过这种方式,我们可以确保系统的健壮性和适应性,同时为未来可能的实体化需求做好准备。
图片

注意:请点击图像以查看清晰的视图!

设计防腐层会带来一定的编程困扰。你需要在内部设计一个进出参模型或内部接口,并添加一层适配器层。适配器层负责实现内部与外部实体的对接。尽管这种方法较为复杂,但我们仍需了解使用防腐层的理由:

  • 保护核心层概念

  • 例子:

    例如,你在公司中的角色是老板,但在家里的角色是父亲。如果你将老板实体放在家庭中为孩子做饭,这个家庭就会依赖不必要的逻辑,这违反了整洁架构的原则,可能导致变更和稳定性问题;

  • 例子:

    在交易系统这种复杂的系统中,例如一个在供应商系统中代表它自己编码的merchantCode可能来到交易系统这边会变成supplyMerchantCode,同一个值,用角色字段区分他们这自然是很重要的;

  • 关注点分离

  • 说明:外部接口的非逻辑依赖变更不会影响核心逻辑。你只需确保返回字段的含义一致即可;

  • 适配逻辑的代码

  • 说明:有很多代码你只是用来做外部实体的处理的,变成内部可识别的实体,例如决策中心传给你的是2021-07-12 ~ 2021-07-13,但你内部用的是一个stat的Date变量和一个end的Date变量;那就需要适配了,这些代码如果编写在核心逻辑中,那你在维护核心逻辑的时候也不得不多思考一件事,不仅代码臃肿,还消耗你的精力。

  • 说明:有一个点很重要,为什么要做这种设计,因为设计就是需要把代码放在它该呆的地方,这种转换的代码,总要有一个适配器处理;

  • 可随时挖掘隐式概念

  • 说明:例如用户的会员等级,这个会员等级字段属性,就是一个隐藏的概念,它存在于用户账号中,所以你难以发觉。但日后不断的需求变更中,你或许会发现它可能是一个封装性很好的实体。下一节我们将具体介绍如何挖掘除会员等级这个实体。

严格遵守防腐层并不容易,首先要求编写代码的人具备这方面的意识,以免违反规则。其次,编写代码的人应对整个系统架构有一定了解。虽然如此,如果有人把控这部分代码,新手也可以参与系统建设。这一思想来源于《人月神话》中的外科手术医生只有一个的观点。

实际上,划分边缘有两种方式:防腐层和基础设施、应用类业务划分。当将与领域逻辑无关的逻辑划分边界后,六边形架构就出来了。

3)隐式概念:重构中发现模型

以上提到,在划分和外部边界的时候,先不考虑散落的逻辑概念抽象为实体,有了防腐层之后,当有新实体的产生需求即可把这些概念实体化了;这篇我们用一个例子来说明如何通过重构,从防腐层代码中,抽象一个实体出来;首先下面是一个简化版本账号中心域的防腐设计,我们专门为计划的返回做了一个内部的Entity — 用户账号;

@Data public class UserAccount {

    // 其他字段 ...... 

    /**      * 会员等级      */     private int userLevel;

    // 其他字段 ...... 

}

现在有另一段获取账号信息,根据会员等级获取对应折扣比例的信息,这个代码是这样写的:

public class XxxxxxxService {  
  
    public Double getDiscount(UserAccount account) {  
        switch(account.getUserLeval){  
            case 1:  
                return 0.99;  
            case 2:  
                return 0.98;  
            case 3:  
                return 0.97;  
            case 5:  
                return 0.95  
                    default:  
                return 1.00;  
        }  
    }  
}  

特别的,我们在其他service中也发现了一样的代码,当你注意到这点的时候,就是一个领域实体出现的时候了,那么我们可以复用这段逻辑,并把逻辑和账号关联起来,把该行为封装到账号中,如下所示:

@Data public class UserAccount {     /**      * 客户等级      **/     int userLevel;

    public Double getDiscount() {         switch(userLevel){             case 1:                 return 0.99;             case 2:                 return 0.98;             case 3:                 return 0.97;             case 5:                 return 0.95                     default:                 return 1.00;         }     } }

但这还不够,我们忘记了一个领域概念遗留了,那就是会员等级 ,现在是时候把它显性化为一个领域实体了,所以最终的重构结果是:

@Data  
public class UserAccount {  
    /**  
     * 客户等级  
     */  
    private UserLevel userLevel;  
}  
  
public class UserLevel {  
  
    int userLevel;     
  
    public Double getDiscount() {  
        switch(userLevel){  
            case 1:  
                return 0.99;  
            case 2:  
                return 0.98;  
            case 3:  
                return 0.97;  
            case 5:  
                return 0.95  
                    default:  
                return 1.00;  
        }  
    }  
}  

领域驱动设计(DDD)实践指南

领域上下文的重要性在代码重构过程中,我们经常遇到不同领域中相同名称的类,例如UserLevel,在账户中心和交易领域中可能有不同的数据绑定行为。这体现了领域驱动设计(DDD)的核心理念:领域上下文的识别与应用。

重构与领域驱动设计重构不仅是代码优化的手段,更是领域驱动设计的引擎。通过领域知识引导设计,确保逻辑的独立性,发现实体和聚合根,是实现优质设计的关键。

领域服务的设计### 知识拓展在某些情况下,传统的Entity或Value Object可能无法满足需求,这时可以引入领域服务(SERVICE)。领域服务在DDD的各层中都有体现,包括应用层、领域层和基础设施层。

领域服务的划分- 应用层服务:处理输入输出逻辑,调用领域层服务。- 领域层服务:与领域模型交互,组织协调领域模型的工作。

例如,针对订单流程,可以有提交订单、支付、取消等领域服务,它们各自负责不同的逻辑代码。

服务与模型的结合领域或应用层服务通常基于实体和价值对象构建,如提交订单服务涉及单据和命令。它们的行为类似于组织领域功能的脚本。

领域驱动设计的核心目标DDD的目标是通过实用策略和技巧,提炼出反映问题本质的领域模型,并保护模型间的相互作用。我们已探讨了聚合根模式、统一语言交流、防腐层模式和重构技术等,但在实际应用中,需要更多创新和调整。

领域模型的协作与组织虽然本文未详细探讨领域模型的合作与组织,但这是非常关键的。需要考虑模型的纯度、性能和交易属性,以及如何管理领域实体和服务的分离。

演进与重构随着需求的增长,系统将面临复杂性问题。通过Martin Fowler的两顶帽子原则——重构+编写新功能,系统可以逐步演进。DDD重视领域模型的演化。

总结领域驱动设计是一个不断发展和重构的过程。技术人员应具备DDD知识、建模技巧和实践经验。通过重构和功能添加,应对系统发展和复杂性问题,实现成本与业务价值的平衡。

参考书籍1. 《重构》2. 《架构整洁之道》3. 《领域驱动设计》

说在最后DDD架构的落地是常见面试题。掌握DDD的相关知识,能够在面试中给面试官留下深刻印象。尼恩即将发布《第34章:DDD的学习圣经》视频,帮助大家深入理解DDD。

尼恩Java面试宝典在准备面试时,系统化地学习《尼恩Java面试宝典PDF》,并可在遇到问题时与尼恩交流,以提高面试成功率。

最终,通过深入DDD,让面试官对你的表现赞不绝口,从而获得offer。