在开始DDD之旅之前,我想让大家看看经过DDD设计后的代码是什么样的。我想这是所有遵循“说话很容易,showmeyourcode”理念的程序员更关心的一个理念。为此,特将“团购”生鲜电商系统服务器代码的新旧代码结构进行展示,让大家看到原来的旧代码——也就是“交易脚本”代码是什么lookslike(应该是目前大部分java程序员写代码),然后让大家看看经过DDD改造设计后的新代码长什么样。那么通过分析就很清楚为什么传统的“事务脚本”代码不是对现实世界的“同构映射”,而DDD代码的“同构映射”在哪里。需要提醒你的是:从今天的话题开始,你可能需要花更多的时间,深入阅读我写的代码和文字的每一句,并反复对比,甚至来回看几遍,然后才看懂才能真正明白明白这些话。1.旧代码:交易脚本(贫血模型)代码我们先来看旧代码目录结构截图。注意下面1、2、3、4标注的位置(为了说明,我使用的是spring-boot开发框架,MyBatisPlus数据持久化框架,MySql5.6数据库):你注意到1、2、43这里标出,4是代码位置?代码结构是不是和大多数spring-boot应用框架的代码结构很相似?为了避免大家看不懂这段代码结构,我简单说明一下。标签1的位置:这里是Controller(控制器)层代码,即所有的前端访问接口都在这里实现。根据MVC的分层原则,一般来说,这里只放一些对客户端输入参数的解析和对服务层的业务方法调用(见下)。一般来说,这里的代码是这样的:标签2位置:这里是实体(数据bean)层代码,其实就是POJO代码,所有的类都和数据库表一一对应。一般来说,这里的代码是这样的:label3位置:mapper层,针对mybatis持久层框架,mapper和entity共同实现ORM(从对象模型到关系模型的映射)。一般来说,这里的代码是这样的(这里的CustomerMapper类只定义了实体类Customer和自定义数据操作方法的映射关系):和this(在MP中,只需要实现自定义SQL操作方法,This需要CustomerMapper.xml文件):标签4位置:服务(service)层,这里是所有业务逻辑实现的核心代码,几乎所有的业务逻辑都在这里实现。一般来说,会有接口+实现的组合。例如:OrderService和OrderServiceImpl,分别是这样的:OrderService接口类:OrderServiceImpl实现类;从上面的代码中,我们可以清楚地看到以下几点:Controller/entity/mapper基本上都是使用框架的注解(Annotations),公共工具代码(如json解析等)都是用很少的代码实现的;显然,大部分业务逻辑是在Service层的实现类中实现的;Service层实现类代码的逻辑很长,而且完全“平白无故”。我在这里展示的OrderSeriveImple的创建方法——创建一个订单,只写了135行。从我代码截图中的注释可以看出,我已经想通了如何一步步对数据库进行增删改查,先填注释,再写代码。这种代码,说白了就是“增删改查+计算逻辑”的结合;脑细胞太多,团队很容易开始实施项目工程。找有基本java编程经验(一般一年以上经验)的人开始业务代码的开发;这种代码,我们称之为“事务脚本”代码,或者“贫血模型”代码。之所以叫“事务脚本”,是我个人的理解:本质上和20年前写数据库存储过程代码是一样的(只是用不同的语言写的,代码运行位置参考来自数据库服务器内部的应用程序服务器);之所以叫“贫血模型”代码,是因为实体层的那些POJO对象,比如Order,并没有对业务行为进行任何封装(例如:Order类要自己生成订单号,发货号,等),只有属性没有行为对象就是“贫血”对象,基于“贫血”对象实现的业务逻辑代码称为“贫血模型”代码。根据这里的代码分析,我们是否可以发现一个关键问题:这里的Controller/entity/mapper/service与现实世界中的业务之间的关系没有任何映射——也就是说:“代码世界”和“现实世界”是异构的,具体可以看以下几点。首先,从业务模块划分的“粗”粒度来看,我们其实可以简单直观地划分模块。没必要把一个项目中的所有业务模块,但是我们可以按照业务模块(例如:店铺管理、订单管理、商品管理等)来划分项目目录,也就是项目组进行分组。其实,目前市面上的大多数软件公司只是简单的根据业务经验或者直觉将项目分成多个团队进行开发。但这种划分方式,虽然也可以做到全方位精准——但我们需要认识到的是这种基于经验和直觉的简单粗暴的划分,与DDD方法论(划分为有界上下文的粒度)设计(DDD中称为“战略设计”)所做的设计划分相比,至少存在三个不足:如何划分软件代码是一个严格的“工程问题”,所有的工程问题往往都是“千里之行”!这种经验和直觉的划分很可能会遗漏一些非常重要的“限界上下文”标识。正是因为遗漏了这些重要的“限界上下文”,造成了一些歧义,发现要么是不必要的模块间耦合,要么是不必要的重复。DDD“限界上下文”的识别不仅需要区分分成多少个模块(其实“模块”是一个很模糊的词,可以用来划分微服务,也可以用来划分代码目录结构,取决于需求),还需要确定这些“限界上下文”之间的协作关系和边界。而这些协作关系是真正“清晰、准确、代码行级”的,定义了哪些代码属于模块A,哪些代码属于模块B——即边界,以及这些模块是通过RPC协作还是本地协作调用关系,或者说异步消息事件是协作的,甚至是直接不协作的。一般来说,DDD的“限界上下文”需要对应业务子域,而业务子域的重要性将决定限界上下文的重要性。对于业务子领域的具体软件系统,可以从业务的角度判断哪些必须构建为软件的核心竞争力,哪些可以作为二级模块甚至外包来实现。这些对于“限界上下文”模块“重要性”的不同定义,会促使项目管理从效率的角度出发,采用不同的技术栈。例如:目前市场上不同的程序员,薪资水平不同,招聘难度也不同;不同的技术栈有不同的成熟度和适用的编程特性(例如:java比较成熟适合企业级应用开发,而python适合数据处理开发,node.js适合对接第三方互联网系统,ETC。)。其次,在模块内部,对其代码的层次结构进行了划分。如果遵循mvc思想,最终还是会回到controller/entity/mapper/service这样的划分方式。而这种划分方式与“现实世界”的同构映射关系又是怎样的呢?可以说没有!因此,最终我们还是可以得出一个结论:这种传统的代码架构没有考虑“真实世界”的同构映射。而这种“同构映射”的缺失是我们产生“真实业务有变化不大,但为什么某个需求会导致软件代码发生翻天覆地的变化?”——DDD方法论,就是用来解决这个问题的!2.新代码:DDD设计代码(拥塞模型)让我们看看是什么新的代码结构采用DDD设计后的样子下面是新代码结构的截图(还要注意下面的1~8标签):我将上面代码标签的位置一一解释为如下(需要注意的是,这里的目录排序是IDEA开发工具自动按字母排序的,不是代码设计顺序):标签1位置:这里是边缘层(edge)代码。由于“前端界面”团购”小程序开发完成,这是一个前后端分离的项目,我不打算修改前端代码,所以这里多了一个“接口适配”的代码工作。一般来说,这种代码称为“边缘层”。放在边缘层的代码类似于这种前端接口适配和第三方系统接口适配的代码。这种代码也可以称为“前端后端(BFF)”。理论上,这个BFF层的代码可以由前端团队开发。我可以选择技术栈是Node.js,用js或者ts语言开发。标记2位置:这显示了“基础层”(foundation)。在DDD的系统架构中,boundedcontext(具体概念见后面的介绍,这里你只需要理解类似于子系统或者业务模块的划分)可以分为“BaseLayer”和“BusinessValue”层”。一般来说,“业务价值层”对应的是核心业务模块,是一个软件系统的核心竞争力。需要严格按照DDD的理念进行设计,采用测试驱动开发模式,投入最懂业务的程序员去工作;而“基础层”一般是非核心业务模块,例如:业务相关的基础类、工具类、配套系统的对接等——需要注意的是:“基础层”不是“基础层”资源层”,基础层是指非核心业务模块,基础资源是指数据库、中间件等技术组件。标签3的位置:这里展示了多个限界上下文,都是以xxxcontext之类的目录命名的。在“基础层”和“业务价值层”中,都出现了多个“限界上下文”。每个限界上下文都可以分到不同的项目组去负责,甚至分到不同的微服务中心。还是那句话,现在不需要对“限界上下文”理解太深了。暂时只需要理解为一种模块划分即可(后面会深入讲解)。标签4位置:这里是“业务价值层”的代码——也就是那些需要成为软件系统核心竞争力的模块,下面也会有多个“限界上下文”。标签5位置:在DDD战术设计软件的分层“菱形结构”下,“领域”层的代码就放在这里,也是业务逻辑的核心代码——所有“血腥”模型代码。从这里开始,我们解释了某个“限界上下文”中的代码结构。稍后我们将讨论如何设计这些代码的细节。现在你只需要知道“业务逻辑核心”就放在这里了。标签6和8的位置:在DDD战术设计软件的分层“菱形结构”下,为了“限界上下文”满足各种外部调用需求,需要与其他“限界上下文”进行调用或通信,不应该针对与本模块业务逻辑无关的各种外部因素变化引起的本模块代码逻辑的“乱流”,引入了“北向网关”和“南向网关”的概念。说明如下:标签6为“北向网关”代码,分为本地和远程两个典型目录。“北向网关”的作用是让限界上下文可以输出各种应用服务。本地目录下面是这个限界上下文提供的“应用服务”,是将域中各种“拥塞模型”代码封装后的完整业务逻辑;在远程目录下,它是针对本地目录的。满足“远程调用”的代码封装——如RPC调用、跨服务器消息事件订阅等,没有任何业务逻辑。Label8是“SouthboundGateway”的代号,分为“port”和“adaper”两个典型目录。“南向网关”的作用是让限界上下文通过它请求外部资源。典型的三种外部资源请求是:访问数据持久层(关系型或非关系型数据库)、调用其他限界上下文服务(在微服务架构中,常为RPC远程调用)、向其他限界上下文发布消息。我们都知道,由于外部资源的技术背景不同,这些对外部资源的请求可能会有不同的实现方式。为了隔离“领域层”对底层技术的依赖,将端口层和适配层分离。在java语言实现中,端口层就是接口,没有任何实现代码,只有方法定义;而adaper层是implemetaion,分别在不同的持久层(如不同的关系型数据库oracle/mysql等,不同的nosql数据库redis/mongodb等)实现。然后根据IoC(DependencyInversion)原理,将adaper目录下的具体实现与领域层的代码通过java中的“依赖注入”连接起来。Mark7Position:这是“出版语言”(publishedlanguage,pl)层。“发布语言”说白了就是让“北向网关”在导出服务时与服务调用者有一种“统一的语言”,比如:输入输出参数的结构定义,事件的格式定义消息等。因为我们不需要把限界上下文里面的“领域”层的内部对象结构“泄露”到外面,所以我们必须要有这个“发布语言”层。3、结论好了,在解释完按照DDD的代码结构设计之后,我们还要回答一个问题:DDD对现实世界进行“同构映射”后的代码逻辑在哪里?答案是:在“领域”(Domain)层!所谓“NorthboundGateway”、“PublishingLanguage”、“SouthboundGateway”层的作用只是为了让外部请求和请求资源的底层技术不要“干扰”我们的“业务逻辑”“同构”测绘!这相当于说:领域层是DDD映射“业务逻辑”的核心,其他只是对这些“核心业务逻辑”层级的“包装”!那么,很显然,从技术角度来说,知道如何设计领域层是DDD战术设计层面最重要的技能!因为“北向网关”、“发布语言”、“南向网关”三层的代码开发都是套路Routine,没有“业务知识”的内容,甚至可以用机器人来实现(即自动化通过代码生产工具)。最后解释一下反复提到的DDD战略设计和战术设计的区别:一般来说,DDD战略设计就是识别哪些限界上下文,明确界定限界上下文的关系和边界,就基本完成了(虽然也有一些修补工作,例如是否使用边缘层,软件系统与哪些第三方外部系统接口等,但这些已经不能称为“设计”,因为它不需要太多的大脑);DDD战术设计,核心是在“领域”层完成“聚合”和“领域服务”的设计,即“核心业务逻辑”的设计。具体怎么玩,后面会稍微演示一下。
