DDD是与技术无关的,从纯业务的角度分析业务模型和业务流程,面向纯业务进行建模,这个和传统的面向数据库建模是有很大区别的。
一、 传统开发模式 VS DDD开发模式的区别
传统开发模式:基于传统三层框架(Controller+Service+Dao)+SSM+面向数据库建模。
DDD开发模式:基于DDD分层框架(interface+application+domain+infrace)+SSM+面向业务建模。
面向数据库建模的缺点:代码完全丢弃了业务语义,代码和业务流程、业务模型是不匹配的,导致了我们是按照数据库思维写出来的代码(数据库表的CURD),导致代码可维护性极差,哪怕是自己写的代码,过几个月在来看,可能都看不懂业务流程是啥,如果是别人来接手更加看不懂,需要花更多时间来梳理代码逻辑确定业务流程。
传统开发模式采用贫血模型,实体(entity)只保留了属性、setter和getter方法,没有其它任何具有业务逻辑的方法或者行为。
面向DDD建模开发的优点:代码保留了业务语义,代码能够准确的还原业务流程和模型,面向数据库表的CRUD、ES、redis、MQ等逻辑完全隐藏在业务流程里面,代码可读性好、可维护性好,产品经理等非技术人员也能通过代码和注释读懂业务流程
DDD开发模式采用了充血模型,实体保留了属性、setter和getter方法之外,还具有业务逻辑的方法或者行为,这个业务逻辑不依赖外部业务(第三方平台接口)
二、DDD的一些概念
子域、有界上下文、聚合、实体、值对象、仓储、领域服务、六边形架构、清洁架构、CQRS、DDD分层模型。
三、DDD落地方法论
DDD落地分为三个步骤,战略设计、战术设计、代码落地(DDD三层框架、开源框架例如阿里的cola)
3.1 战略设计主要包含有界上下文划分、映射、子域类型确定、通用语言确定
3.1.1 有界上下文:业务边界划分,确定哪些是我们负责的,哪些不是我们负责的
有界上下文通过事件风暴会议进行划分,在做ddd项目的时候,可能是对手头上已有的项目进行DDD建模和重构,对于项目的业务流程、逻辑是比较了解的,那么就可以自己和几个负责开发项目的同事一起进行风暴会议,必须根据自己对业务的理解,把技术相关的东西全部剥离(数据库设计、es、redis、mq),纯粹站在业务的角度去进行风暴会议,梳理出业务相关的流程,如果是新项目,那么开发、产品、跨团队、用户、客户、运营,很多人最好都召集在一起,进行头脑风暴,大家一起对这个业务领域的所有事件进行梳理。
事件风暴会议分为5个步骤:
(1)召集一个会议,程序员、用户、产品经理、管理者、其他人,开始一块儿进行头脑风暴,一块儿来梳理和说出来,履约相关的所有的事件,事件(动词、动作),有多少事件就说多少事件,但凡是跟履约这个事情相关的事件,都可以说出来。
(2)把所有梳理出来的履约相关的事件,按照时间线进行排列,按照时间发生的先后顺序,做一个排列。
(3)梳理出来履约相关的所有的用户界面和命令,针对上面的所有事件的发生,是否有系统用户界面,有用户参与进来发出一个指令、命令(在系统界面里,点击按钮、提交表单、发起操作),驱动了履约各个事件的执行。
(4)把用户界面、命令、事件,按照时间线,先后顺序,交互逻辑,全部串联起来,一环一环的。
(5)在上面的那个大串联的逻辑里,找出来履约有界上下文,哪些事情应该是属于履约要解决的问题,哪些事情明显是属于别的有界上下文,是属于别人要去做的事情
3.1.2 映射:不同的有界上下文之间如何进行交互,例如接口调用或者MQ,有以下几种交互类型:
separate way:完全没关系。
customer-supplier:u-d,可以商量,如果你有一些变化,我有一些需求,大家可以商量一下的,所以这种关系,常见于一个大的项目组内的不同的小项目组之间的依赖关系。
大厂,有几块大的业务,新零售、O2O、B端、云计算、电商,大的事业部里,技术团队,假设有500个人,划分为了n个大的团队,每个团队有几十个人,每个团队几十个人内部又分为了几个小组,每个组10个人,10来个人。
对于几十个人的团队,做一个大的项目,有几个小组,每个小组负责维护一个系统,系统之间有依赖的关系,我要调用你 的接口,你是上游,我是下游,我们的关系和距离,组织结构是比较近的,所以这个时候往往就是我有一些接口的需求,我们是可以很好的进行一个商量的。
publish-subscribe:p-s,有一方发布一个事件,另外一方监听事件以及处理这个事件,这种关系就叫做ddd里面的ps映射关系,这种关系,说句实话,在企业开发里,非常的常见,一般来说,就是对于你一个有界上下文,如果你要跟别的有界上下文进行交互,你可以选择发布一个事件出去,你作为一个有界上下文,你还可以自己发布事件,自己去监听和处理这个事件。
anti corruption layer:acl(防腐层),不是一个孤立、单独使用的映射关系,一般是跟其他的映射关系,一起进行使用,CS关系,PS关系,CS关系来举个例子,supplier给我接口返回的东西,本来这个东西大家约定好了里面有8个字段,但是现在虽然还是8个字段,可是里面有的字段的名称变化了(其实本质没变化,可是名称变化了),或者是字段的类型变化了,或者是字段的值,可选的值变化了,字段可选的值:true、false,加了一种unknown,对于我来说,是不是说直接修改我的代码呢?一般来说可以加一个防腐层,acl层,一般来说会把你返回过来的东西,做一个适配和转化,把他转为我内部的一个我自己固定的一个对象,我固定的对象,他的字段的名称、类型、值,都是不变的,如果你那里返回的东西有变化,此时我可以在我的防腐层的代码里,做一个转化,把他转换位我固定的不变的东西就可以了,防止外部返回结果有变化引起本地代码调整,只要转换逻辑调整一下就可以了。
conformist:u(upstream上游)-d(downstream下游),完全没法商量,
不同的大的部门之间,系统之间要进行调用,电商部门有500个技术,划分为了10个技术部门,每个技术部门是50个人,有的部门50个人专门做订单,有的部门50个人专门做商品,有的部门50个人专门做营销,假设你的订单划分为了多个技术小组,多个技术小组之间维护的系统互相依赖,cs,还好商量。
但是如果说要是跨了部门了以后,商品中心,有50个人,对外提供的给别人调用的接口,可能都是固定死的,一般来说你不太能去跟他们商量,新增一个接口,这种商量可能是极为极为难的,你要用,就直接用就可以了,遵奉他们的调用模式,没法商量,平台技术部,专门提供一些 基础性的系统和接口出来,人家是为全公司服务的,他一般是收集大家的普适性的需求,按照自己的计划迭代和升级的。
open host service+published language:OHS+PL
对于第三方支付平台,第三方物流平台,saas云平台(提供出来API接口),open host service,开放主机服务,开放平台接口,他们会定义一套自己的通信协议和数据格式,通信协议都是HTTP协议,数据格式一般来说都是他们定义好结构的JSON格式的数据。
子域类型:确定有界上下文所属子域类型,划分出核心子域、支持子域、通用子域。
核心子域:对于一个电商领域而言,用于完成用户购物是个核心需求,所需要具备的一些核心的子域,就是属于核心子域,商品子域、订单子域、支付子域、履约子域、会员子域、营销子域。
支持子域:负责支持核心子域,他的作用,明显的是用于支持别的核心子域的功能实现的,属于支持性的子域,仓储子域、物流子域 、财务子域、报表子域、平台技术子域, 履约子域在履约的过程中,需要仓储和物流的支持,财务子域,支持我们的一些业务运作,报表子域支持公司对业务进行分析和决策,平台技术子域支持各个业务服务 。
通用子域:不用自己公司来开发了,都是外购的软件,或者是第三方的通用的平台和系统,第三方支付平台,第三方物流平台,通用子域。
通用语言:当前有界上下文边界范围内要制定一套统一的命名规范。
为什么需要这套通用语言,假设履约有界上下文,里面的技术团队有20个人,划分了几个小组,5个人一个小组,一个是4个小组,每个小组都负责了一个履约系统里的一个微服务,假设对自己负责的服务涉及到的代码,命名规则和规范都有一套自己的理解的话,那么在这个有界上下文会表现的很乱,叫什么的都有,但都表示同一个意思,所以每个有界上下文里面,对所有的名词和动词,业务操作、业务事件、业务语义,都用统一的一套规则和规范去做。
3.2 战术设计包含实体、值对象、聚合、仓储、领域服务、业务组件、领域事件、命令和查询 。
实体 -> 我们之前在项目代码里搞的那种domain类,Order(他是有一个唯一标识,OrderId,他里面的很多数据是可以变化的),我们的上下文里,对于一些核心的数据,一般来说都会设计一些类出来,如果他有唯一标识、而且他的数据是允许变化的,此时在ddd战术设计里,他被称之为:实体,Entity,这样的一个概念,就是一个Class类,有唯一标识,类里面的数据允许变化,例如订单状态
值对象 -> ValueObject,OrderId(建模成一个独立的类),对于一个订单,他的唯一的标识,OrderId而言,里面可能包含和封装了订单再数据库里的唯一的主键以及我们分配给他的一个订单编号,这些东西,是绝对不会变化的,而且OrderId自己就没有必要再有一个自己的唯一标识,他代表的就是一份不会变化的数据,ddd战术设计里称之为值对象,就是一个class类,没有唯一标识,类里面的数据在运行过程中不会变化,例如订单id和订单编号
聚合 -> 把有强关系的一些实体+值对象放在一块,这些实体所谓的强关系是指创建的时候一起创建,更新的时候一起更新,删除的时候一起删除,聚合里的这些实体必须放在一个事务里,要么一起成功,要么一起失败,聚合还有一些贴合业务语义的业务行为,聚合最外层的实体就叫做聚合根,聚合是要进行充血模型设计,也就是要有一些核心符合业务语义的业务逻辑行为(不依赖第三方的服务)
领域服务 -> 有一些业务行为是没办法放在聚合里的,针对跨越多个聚合执行的业务行为,就没办法放在一个聚合里面来做,只能是放在领域服务里面来做, 他的业务行为都是委托给多个聚合来做
业务组件 -> 贴合业务语义的class(类),例如订单状态机、从第三方平台提供的api获取数据的行为,都可以设计成业务组件类
领域事件 -> 某个有界上下文执行的一些动作和操作会驱动其他有界上下文某个方面业务流程的执行,例如订单上下文会发起订单支付事件会驱动履约流程的执行,这就是领域时间
命令(查询) -> 人驱动发起的命令,一些核心的业务动作和操作,应该是人发起的,例如web系统网页界面、手机APP界面,通过UI界面发起的动作和操作,在DDD里面就叫做命令(command),如果是更新数据一类的动作叫命令COMMAND,如果是查询一类的动作叫查询QUERY
仓储 -> 负责与持久层进行交互,聚合和实体数据都是通过仓储来进行持久化和读取,类似于DAO
3.3 DDD代码落地
DDD在代码落地时候借鉴了六边形架构和CQRS,所以DDD分层架构即四层架构,分别是:
接口层(用户层)adapter-> 最外层跟用户界面、其他上下文进行交互的一层,例如通过界面交互的controller层\通过接口调用进行交互的api层\通过MQ进行交互的listener层。
应用服务层application -> 进行业务流程编排,基于仓储、聚合、领域服务、业务组件执行各种各样的动作,完成业务流程中整个业务链路里的各个环节。
领域服务层domain -> 基于仓储、聚合、领域服务、业务组件完成多个聚合操作,实际处理业务细节的地方(面向数据库的CRUD、面向REDIS\MQ\ES的CRUD、业务逻辑处理)。
基础设施层infrastracture -> 负责与具体的基础设施交互的地方(跟DB\REDIS\ES\MQ进行交互),repository依赖反转。
四、DDD是否成功的验证
验证你的ddd做的对不对,成功与否,关键点在于,上面那套东西配合起来,能够实现一个效果就是,用代码,还原出来一套完整的业务语义、业务流程、业务模型 -> 这套还原出来的代码,我有一个验收的标准,这套代码,主流程,如果找PM产品经理来走读代码,让他结合英文单词的含义,来读代码,结合你的一些注释,是否可以直接通过英文单词的含义,把他理解的业务语义和流程能够理解出来才算ddd成功了,ddd成功的一个关键的点,在于说,你的代码做出来,基本贴合业务语义和流程,抛弃掉技术细节不说,底层的crud,sql,缓存,nosql,mq,es,搜索,数据库连接池,单纯是业务代码主流程,能把业务语义还原出来,ddd就成功了。
五、系统和服务的区别,领域和子域的区别
领域是个大的概念,一个领域包含多个子域(子域一般对应一个系统,也可能是一个服务)。
系统是一个大的概念,一个系统包含多个服务
一个系统包含多个服务,商品系统包含商品后台管理服务、商品供需服务、商品B端基础服务、商品C端查询服务、商品质检报告服务、商品定时计算服务、商品生命周期服务、商品价格服务。
六、DDD借鉴了六边形架构和CQRS
把ddd思想跟六边形架构的思想、CQRS思想结合起来使用。
CQRS主要是更新和查询是分离成两套系统,六边形架构主要是提供api与外部系统进行交互,提供 spi与外部基础设施进行交互,提供领域模型层 ,提供应用服务层
DDD分层架构借鉴了六边形架构和CQRS。
简单的业务,纯粹就是界面里的一些增删改查,表单的提交,数据更新,没什么太复杂的业务的不要用DDD,DDD是用来解决复杂业务的