为什么要DDD
传统MVC
传统的MVC模型框架拆分成了三层:显示层、控制层、模型层。显示层负责显示用户界面,控制层负责处理业务逻辑、而模型则负责与数据库通信,对数据进行持久化的操作。从代码角度来看,这样的框架结构每个模块职责分离,对小型应用系统十分友好。但是随着业务复杂度的上升,会发现服务层的逻辑以及代码不断增长,变得庞大且复杂,测试成本直线上升。而且,每个Service的逻辑散落在各处,后续新需求的维护迭代成本非常高,导致交付效率越来越低,系统的稳定性风险也越来越高。
除了代码及开发的一些常见问题外,更重要的是整个系统缺乏一定的业务知识沉淀,当文档沉淀或者更新不及时的情况下,新同学来接受一个小需求,面对产品描述的需求改动点,开发同学根本无从下手。一方面是新同学业务的生疏,但真正根本原因还是:开发与产品之间的语言不能保持一致,双方对于同一事物的表达和理解有很大的区别,产品描述的更多是实际的业务场景,而开发则更关注背后的具体实现逻辑,加之文档的缺失和代码的发散式变化,可以说是面对一堆模型和代码逻辑两眼茫然。
为什么需要DDD
对于一个架构师来说,在软件开发中如何降低系统复杂度是一个永恒的挑战。
复杂系统设计:系统多,业务逻辑复杂,概念不清晰,有什么合适的方法指导帮助我们理清楚边界,逻辑和概念。
多团队协同:边界不清晰,系统依赖复杂,语言不统一导致沟通和理解困难。有没有一种方式把业务和技术概念统一,大家用一种语言沟通。例如:风险事件是大家所理解的风险事件吗?
设计与实现一致性:PRD,详细设计和代码实现天差万别。有什么方法可以把业务需求快速转换为设计,同时还要保持设计与代码的一致性?
架构统一,可复用资产和扩展性:当前取决于开发的同学具备很好的抽象能力和高编程的技能。有什么好的方法指导我们做抽象和实现。
DDD的价值
边界清晰的设计方法:通过领域划分,识别哪些需求应该在哪些领域,不断拉齐团队对需求的认知,分而治之,控制规模。
统一语言:团队在有边界的上下文中有意识的形成对事物进行统一的描述,形成统一的概念(模型)。
业务领域的知识沉淀:通过反复论证和提炼模型,使得模型必须与业务的真实世界保持一致。促使知识(模型)可以很好的传递和维护。
面向业务建模:领域模型与数据模型分离,业务复杂度和技术复杂度分离。
DDD核心概念
DDD分为战略设计和战术设计。战略设计侧重于高层次、宏观上去划分和集成限界上下文,非常强调针对业务问题的分析和分解,通过识别核心问题域来降低分析的复杂度。战术设计关注的是在具体的领域模型内部,使用更细粒度的技术来实现领域对象、领域服务、仓储等。它关注的是如何在代码层面实现和组织业务逻辑。维护性和扩展性。下面我们对DDD的核心概念逐个展开介绍下:
战略设计
在战略设计中,讲究的是子域和限界上下文的划分,以及各个限界上下文之间的上下游关系,更偏向于软件架构,帮助我们从一个宏观的角度观察和审视软件系统。首先,一个领域相当于一个问题域,每个领域可以继续拆分为多个子领域,拆分的过程实际就是大问题拆分为小问题的过程,拆到一定程度后,有些子域的领域边界就可能变成限界上下文的边界了。每个领域模型都有它对应的限界上下文,团队在限界上下文内用通用语言交流。
领域 Domain
领域表示的是一种特定的范围或区域,用来确定范围和边界,将业务上的问题限定归属在特定的边界内,领域就是这个边界内要解决的业务问题域。比如,一个保险公司的领域中包含了保险、理赔等概念。在DDD中,我们对系统的划分是基于领域的,也是基于业务的。
子域 Subdomain
为了降低业务理解和系统实现的复杂度,可以将领域进一步划分成更小范围的问题,称为子域,如果需要可以对子域进一步划分形成子子域。有一个文章的例子挺好的,比如研究的对象是桃树,那么果实、根、茎叶是领域,但是如果在果实领域进一步研究,还需要研究组织甚至细胞,那么组织就是果实的子域、细胞是组织的子域。
核心域 & 通用域 &支撑域
核心域:决定产品和公司核心竞争力的子域是核心域,它是业务成功的主要因素和公司的核心竞争力。例如风险流转的创建,处理,完结。直接对业务产生价值。
通用域:没有太多个性化的诉求,同时被多个子域使用的通用功能子域是通用域。例如,权限,登录,审批,通知等等。间接对业务产生价值。
支撑域:支撑其他领域业务,具有企业特性,但不具有通用性。例如模板,规则等等。间接对业务产生价值。
为什么要划分核心域、通用域和支撑域,主要目的是什么?
一个业务一定有它最重要的部分,在日常做业务判断和需求优先级判断的时候,可以基于这个划分来做决策。例如:一个风险流转的需求和一个通知的需求排优先级,很明显风险流转是核心域,通知是通用域。同样,我们认为是支撑域或者通用域的在其他公司可能是核心域,例如权限审批对于我们来说是通用域,但是对于专业做ACL、或者BPMS的部门,这些就是核心域。
限界上下文 Bounded Context
限界上下文就是在复杂系统中,为了确保模型和概念的一致性,划定的一个清晰明确的边界。
业务边界的划分,这个边界可以是一个领域或者多个领域的集合。复杂业务需要多个域编排完成一个复杂业务流程。限界上下文可以作为微服务划分的方法。例如,正向和逆向进行划分,风险流转和自动规则进行划分。其本质还是高内聚低耦合,只是限界上下文只是站在更高的层面进行划分。如何进行划分,我的方法是一个限界上下文必须支持一个完整的业务流程,保证这个业务流程所涉及的领域都在一个限界上下文中。
例如:正向的风险创建,收到风险创建请求进行创建,处理,自动流转通知的过程。前置条件:风险创建请求,流程:创建->规则 -> 流转->通知。对于这一自动创建流转并通知的流程,我们分为了风险域和流转域。但完成一个风险创建的流程,需要风险域和流转域配合才能完成,所以,我们把风险域和流转域划为正向限界上下文。实际在这个过程中,我们可能用到了规则域和模板域,那是不是这些东西也要划进来呢?规则和模板实际是一个支撑域,是信息的共享,风险创建流程不会影响这些信息的变化。
通用语言 Ubiquitous Language
为什么需要通用语言?
开发同学和业务、产品、领域专家等同学交流以及协作的过程中,业务、领域专家一般都是使用业务行话表达他们的这些概念,开发同学没有领会到业务知识,反而满脑子想的都是具体的类、方法、怎么实现等等,迫切的想将实际生活中的业务概念和程序模块进行对应,在这个痛苦的交流过程中,怎么对齐所有人使用同一种语言和概念?
因此,当我们有了限界上下文以后,就需要在该限界上下文中使用一种通用语言用于表达业务、软件模型等概念,它可以是任何计算机语言、人类语言或者白板上的图形,只要能让团队内的每个人都能看懂。通用语言的产出其实就是需求分析的过程,也是理解领域知识的过程,提炼领域知识过程的产出物,也是团队中每个角色就系统目标、范围与具体功能达成一致的过程。通用语言可以定义一些公共术语,减少概念混淆,达到概念和代码的统一语言,连接概念和实现。
战术设计
如果说战略设计为我们提供一种高层视野来审视我们的软件系统更偏向于软件架构,那么战术设计则更偏向于技术层面的具体化和细节化,更偏向于编码实现,也是开发同学最关注的地方。战术设计的目的是使得业务能够从技术中分离并突显出来,让代码直接表达业务的本身。领域驱动设计围绕着领域模型进行设计,通过分层架构将领域独立出来,包含了实体、值对象和领域服务等概念,领域逻辑都应该封装在这些对象中。聚合是一种边界,它可以封装一到多个实体与值对象,并维持该边界范围之内的业务完整性。工厂和资源库都是对领域对象生命周期的管理。工厂负责领域对象的创建,用于封装复杂或者可能变化的创建逻辑。资源库负责从存放资源的持久层获取、添加、删除或者修改领域对象。
模型 Model
领域反映到代码里就是模型,模型是对领域某个方面的抽象,并且可以用来解决相关域的问题,模型分为实体和值对象两种。
实体 Entities
实体有唯一的标识,有生命周期且具有延续性。例如一个风险的创建,从创建风险我们会给他一个业务编号并且是唯一的,这就是实体唯一标识。同时,风险实体会从创建,处理,完结,复盘,改进等过程最终走到终态这就是实体的生命周期。风险实体在这个过程中属性发生了变化,但风险还是那个风险,不会因为属性的变化而变化,这就是实体的延续性。
实体的业务形态:实体能够反映业务的真实形态,实体是从用例提取出来的。领域模型中的实体是多个属性、操作或行为的载体。例如:正向的风险创建流程,系统收到用户创建风险的请求后,跑规则,创建流转,发送通知,进行复盘,并做改进措施这一个整个过程,很容易找到一个风险实体,风险有创建流转的行为和创建改进措施行为。
实体的代码形态:我们要保证实体代码形态与业务形态的一致性。那么实体的代码应该也有属性和行为,也就是我们说的充血模型,但实际情况下,我们使用的是贫血模型。贫血模型缺点是业务逻辑分散,更像数据库模型,充血模型能够反映业务,但过重依赖数据库操作,而且复杂场景下需要编排领域服务,会导致事务过长,影响性能。所以,我们使用充血模型,但行为里面只涉及业务逻辑的内存操作。比如,我们可以在风险实体做高风险判断,状态完结判断等等。
实体的运行形态:实体有唯一ID,当我们在流程中对实体属性进行修改,但ID不会变,实体还是那个实体。
实体的数据库形态:实体在映射数据库模型时,一般是一对一,也有一对多的情况。例如风险的风险实体,1个风险对应多个流转任务。数据库模型我们有风险数据模型和流转任务数据模型对应一个风险实体。我们海可以把风险任务实体通告序列化的方式存储到风险数据模型的一个字段里,但是,相应会有很多局限性。
值对象 Value Object
通过对象属性值来识别的对象,它将多个相关属性组合为一个概念整体。在DDD中用来描述领域的特定方面,并且是一个没有标识符的对象,叫做值对象。值对象没有唯一标识,没有生命周期,不可修改,当值对象发送改变时只能替换。
值对象的业务形态:值对象是描述实体的特征,大多数情况一个实体有很多属性,一般都是平铺,这些数据进行分类和聚合后能够表达一个业务含义,方便沟通而不关注细节。
值对象的代码形态:实体的单一属性是值对象,例如:字符串,整型,枚举。多个属性的集合也是值对象,这个时候我们把这个集合设计为一个Class,但没有ID。例如,风险实体下的创建人,就是一个值对象。它是花名和工号的集合,不需要ID,可以整体替换。流转任务为什么是一个实体,而不是描述风险特征,因为需要表达谁创建了什么样的任务,所以我们需要知道哪一个任务,因此需要ID来标识唯一性。
我们看一下下面这段代码,SysFlowDTO这个实体有若干个单一属性的值对象,比如eventCode、title等属性;同时它也包含多个属性的值对象,比如用户 SimpleUserModel。
public class SysFlowDTO {/*** 标题*/private String title;/*** 流转代码*/private String flowCode;/*** 建议*/private String recommend = StringUtils.EMPTY;/*** 创建人*/private SimpleUserModel creator;
}
值对象的运行形态:值对象创建后就不允许修改了,只能用另外一个值对象来整体替换。当我们修改创建人时,从页面传入一个新的创建人替换调用SysFlowDTO对象的创建人即可。如果我们把创建人设计成实体,必然存在ID,那么我们需要从页面传入的创建人的ID与SysFlowDTO里面的创建人的ID进行比较,如果相同就更新,如果不同先删除数据库在新增数据。
值对象的数据库形态:有两种方式嵌入式和序列化大对象。
案例1:以属性嵌入的方式形成的流转实体对象,创建人值对象直接以属性值嵌入流转实体中。
当我们只有一个人员属性的时候使用嵌入式比较好,如果多个人员关系必须有序列化大对象。同时可以支持搜索。
id | flow_code | title | recomend | creator_display_name | creator_work_no |
1 | FL_342_34 | 流转标题 | 流转建议 | 耀康 | 360197 |
案例2:以序列化大对象的方式形成的流转实体对象,人员值对象被序列化成大对象json串后,嵌入流转实体中。
支持多个人员存储,不支持搜索。
id | flow_code | title | recomend | creator |
1 | FL_342_34 | 流转标题 | 流转建议 | {"displayName": "耀康","workNo": "360197"} |
值对象的优势和局限:
1.简化数据库设计,提示数据库操作的性能(多表新增和修改,关联表查询)
2.虽然简化数据库设计,但是领域模型还是可以表达业务
3.序列化的方式会使搜索实现困难(通过搜索引擎可以解决)
值对象与实体的区别是什么?
值对象没有唯一标识和连续性,任何属性发生变化, 都可以认为是新的值对象。
判断对象是否相同:值对象需要判断所有属性是否相同,而实体只需要判断唯一标识是否相同。
值对象一般依附于实体而存在,是实体属性的一部分,而非独立存在。值对象属性是只读的,可以被安全的共享。
聚合 Aggregate
聚合就是一组相关对象的集合,即将领域中高度内聚的概念放到一起组成一个整体。聚合根就是聚合在一起的基础,并提供对这个聚合的操作与外界打交道。聚合通过定义清晰的所属关系和边界,避免错综复杂的对象关系⽹来实现模型的内聚。在聚合边界内,对象之间可以相互引用。聚合之间,聚合根是聚合中唯一允许被外部引用的元素,比如风险编号。
聚合根 AggreagateRoot
聚合根也称为根实体,是业务的载体。聚合根本身也是实体,拥有实体的属性和业务行为,有着自己的业务逻辑。只不过这个实体,还用来作为聚合的管理者,在聚合内部负责协调实体和值对象按照固定的业务规则,协同完成共同的业务逻辑。聚合根必须是实体而非值对象,因为它需要整体持久化,所以一定会有标识,而聚合根里的各个元素,既可能是实体,也可能是值对象。
如何识别聚合根?
如果一个聚合只有一个实体,那么这个实体就是聚合根;如果有多个实体,需要思考聚合内哪个对象有独立存在的意义并且可以和外部直接进行交互。聚合内任何对其他元素的操作都必须通过聚合根来进行。聚合根拥有自己独立的生命周期,其实体的生命周期从属于其所属的聚合,值对象因为只是值而已,并没有生命周期。
工厂 Factory
当创建一个对象或者创建一个聚合很复杂或者暴露了过多内部结构,可以使用工厂进行封装。工厂的作用是将创建对象的细节隐藏起来,这样可以不会让领域层的业务逻辑泄露到应用层,同时也减轻了应用层的负担,它只需要简单的调用领域工厂即可创建期望的对象。
工厂是生命周期的开始阶段,它知道先怎样实例化一个对象,然后在对这个对象做哪些初始化操作,这些知识就是创建对象的细节,如果传递进来的参数符合创建对象的业务规则,则可以顺利创建相应的对象,但是如果由于参数无效等原因不能创建出期望的对象时,应该抛出一个异常,以确保不会创建出一个错误的对象。整个创建过程可简单可复杂,可能只需要调用构造函数,也有可能需要调用其他系统获取数据等。实体和值对象的工厂不太一样,因为值对象是不可变的,所以需要工厂一次性创建一个完整的值对象出来,而实体工厂则可以选择创建之后再补充一些细节。
库 Repository
资源库是聚合根的家,外部世界只能通过资源库来完成对聚合的访问。资源库里面存放的对象一定是聚合,资源库以聚合的整体管理对象,不会单独对某个聚合内的子对象进行单独查询或做更新操作。
资源库是生命周期的结束,它封装了基础设施以提供查询和持久化聚合的操作。这样能够让我们始终聚焦于模型,而把对象的存储和访问都委托给资源库来完成。通俗来将,资源库就是用来持久化聚合根的,但是需要注意的是,资源库并不是数据库的封装,而是领域层与基础持久层之间的桥梁。DDD关心的是领域内的模型,而并非任何技术细节,比如数据库的操作,理论上资源库对客户隐藏了内部的工作细节,委托持久层来进行存储,至于具体使用关系型数据库、NOSQL、Excel甚至内存里读取和存储数据并不重要。
贫血和充血模型
关于DDD的失血、贫血、充血和涨血模型,失血和胀血模型应该是不被提倡的,一般讨论最多的还是贫血模型和充血模型。从技术上来说,这两者都是可行的。贫血模型就是类似平常用的Bean,一般只有getter和setter方法,只作为保存状态或者传递状态,但并不包含业务逻辑,这种只有数据没有行为的对象不是真正的领域对象, DDD中的实体属于充血模型,会封装包含这个实体相关的所有业务逻辑,它不仅是多个业务属性的载体,也是操作或行为的载体。
失血模型:领域对象模型仅包含对象属性的定义和操作对象属性的getter/setter方法的纯数据类,所有的业务逻辑以及行为完全由业务逻辑层中的服务来完成。优点是领域对象结构非常简单,缺点是业务服务代码逻辑比较复杂、散落、难于理解和维护,无法良好的应对复杂变化的业务逻辑和场景。
胀血模型:领域对象模型包含对象属性的定义和操作对象属性的getter/setter方法并包含了所有相关的的业务逻辑,包括不相关的其它应用逻辑(如事务等)。胀血模型取消了业务逻辑层,只剩下领域和持久层两层,在领域层的领域服务上面封装事务,授权逻辑等。好处是简化了代码分层结构,符合面向对象设计。缺点是应用逻辑混到领域逻辑里了,取消了业务逻辑层,在领域层的领域服务上面封装事务、授权等很多本不应该属于领域对象的逻辑,导致模型的不稳定,代码理解和维护性差。
贫血模型
贫血模型相对于失血模型包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。因此相当于在包含getter/setter方法上并包含了对象的行为。例如:人是一个领域对象,首先具有一些属性,如姓名、性别等,还具有一些行为能力,如走路、吃饭、打架等, 但不包含依赖持久层的业务逻辑。
优点:设计简单易行,底层模型稳定,各层结构清楚,易于实现和维护,一般简单业务逻辑的应用很适用。
缺点: 领域逻辑泄露到应用逻辑, 部分依赖持久化的逻辑分离到service层可能导致service层过重,不够面向对象,无法良好的应对非常复杂的逻辑和场景。
哪些逻辑应该放入领域逻辑?
可重用度高的,和领域对象状态密切关联的,此外该逻辑独立于持久层框架之外,仍然可以脱离持久层进行单元测试,这个领域对象仍然是一个完备的、自包含、不依赖于外部环境的领域对象。
如何区分领域逻辑层和业务逻辑层?
领域逻辑层只应该和这一个领域对象的实例状态有关,而不应该和多个领域对象的状态有关,因为其他对象只应该关联唯一标识。所以,逻辑只和这个对象的状态有关,就是领域逻辑层;如果,逻辑和对个领域对象状态有关,就该放业务逻辑层。
充血模型
充血模型相对于贫血模型包含了依赖于持久化的领域逻辑, 相当于不仅包含getter/setter方法上,也包含了大多数相关的业务逻辑,包括依赖于持久层的业务逻辑。业务逻辑层是很薄的一层,仅仅简单封装少量业务逻辑以及控制事务、权限隔离等,不和持久层打交道。充血模型的思路就是把比较稳定的领域逻辑分离出来。
优点:更加符合面向对象的原则;业务逻辑层比较薄,不和持久层打交道。
缺点:持久层和领域对象形成了复杂的双向依赖可能导致很多潜在的问题;如何划分业务层逻辑和领域层逻辑是非常模糊;Service层非常薄,Service层必须对所有的领域对象的逻辑提供相应的事务封装方法,几乎重新定义了一遍领域层服务,做了很多多余的工作,非常烦琐。
读写操作
DDD写操作
DDD的写操作,需要将领域实体通过工厂方式完整的构建出来,然后调用聚合的服务或者领域服务完成写操作,尽量不要使用单个的原子服务暴露给外界使用,尽量按照“外部接口 -> 应用服务 -> 聚合根 -> 资源库”的结构进行写服务实现。需要注意的点:首先,聚合根的创建,需要通过Factory完成;其次,各种写操作的业务逻辑要尽量使用聚合本身的服务,如果能够在聚合根边界内完成更好;最后,在聚合根中实在不适合放置的业务逻辑,才考虑放到领域服务。
DDD读操作
在DDD的写操作中,我们需要构建领域实体之后再进行写操作。但是,如果读操作也严格采用与写操作相同的结构时,会发现有时不但得不到好处,反而使整个过程变得冗繁,查询性能非常差。在日常的工作中读操作更倾向于基于数据直接查询操作,因为在互联网企业,每个域的划分比较细、查询的依赖也比较单一;其次,对性能的要求比较高。如果有条件的话,可以使用CQRS,读写模型可以各自设计,但是需要考虑数据的延迟、一致性的关键问题。
领域模型的复杂读:这种方式不区分读模型和写模型,先通过统一的资源库获取到对象,然后将其转换为外部查询DTO。这种方式优点是直接明了,也不用创建新的数据读取机制,直接使用资源库读取数据即可,消费方根据自己所需从返回的大对象获取数据。然而,缺点也很明显:一是领域模型中的对象不能直接返回给客户端,因为这样领域模型的内部便暴露给了外界,而对领域模型的修改将直接影响到客户端。二是读操作完全束缚于聚合根的边界划分,为了加载查询所需的数据,经常将整个聚合根或者相关的数据加载到内存中再做转换,这种方式既繁琐又低效,关键性能还差。三是在读操作中,通常需要基于不同的查询条件返回数据,如果都通过资源库,导致仓库上处理了太多的查询逻辑,变得越来越复杂,也逐渐偏离了Repository本应该承担的职责。关于最后两点缺点,早期,咱们在做盒马的权限的时候就深受其害,导致后期性能特别差,一个用户的数据权限太多,连缓存都放不下。
数据库的简单读:这种方式绕开了资源库和聚合,直接从持久层中读取客户端所需要的数据,此时写操作和读操作共享的只是数据库。这种方式的优点是读操作的过程不用考虑领域模型,而是基于查询本身的需求直接获取需要的数据即可,一方面简化了整个流程,另一方面大大提升了性能。但是,由于读操作和写操作共享了数据库,而此时的数据库主要是对应于聚合根的结构创建的,因此读操作依然会受到写操作的数据模型的牵制,当涉及到多个join的条件查询性能会比较差。
CQRS的读:CQRS(Command Query Responsibility Segregation),即命令查询职责分离,这里的命令可以理解为写操作,而查询可以理解为读操作。与“数据库的简单读”不同的是,在CQRS中写操作和读操作使用了不同的数据库模型,数据从写模型数据库同步到读模型数据库,通常通过领域事件的形式同步变更信息。这样一来,读操作便可以根据自身所需独立设计数据结构,而不用受写模型数据结构的牵制,性能也会大大提升。
无论哪种读操作的形式,都是需要基于特定的场景才能发挥最大的优势,但是对外标准服务的一些好的设计原则是经久不变的,比如:服务要有明确的业务边界;服务要有明确清晰的契约设计;服务内部要保持高度模块化,才能够容易的被拆分;服务提供要有明确的SLA等。
领域建模
建模方法论
领域建模之前首先要理解的几个基本概念:领域是什么,模是什么以及如何建。
领域是什么?建模首先要有限定范围、建模目标、建模的限定词,比如商品领域建模、优惠券领域建模等。
模是什么?模是业务场景的映射,想要建模首先就要了解业务、了解客户的真正需求。
怎么建模?领域建模方法论有很多,常见的有:用例建模、四色建模、事件风暴等。
既然了解业务是建模的必要条件,那么如何开始?任何业务都存在一条稳定的业务流程,找到业务流程节点就成功了一半,业务流程中的每个节点都会有相应的产物出现,每个节点的产物就是业务骨架。常见的建模基本步骤:
找出业务主流程;比如优惠券业务,业务流程就是建券、发券、用券。并且业务主流程每个流程节点都会有一个产物就是业务的骨架;
细分业务主流程;主流程基础上继续分析子流程,主流程能让我们知道整体的业务流程,但还有些细节流程是在子流程中,比如建券是一个大流程;
进行一定的抽象:领域建模源于业务,又服务于业务。
那么具体到领域驱动设计,每一步每一重边界该是如何?
确定项目的愿景与目标,确定问题空间、确定核心子领域、通用子领域、支撑子领域(额外功能如数据统计、导出报表);
解决方案空间里的限界上下文就是一道进程隔离层面的物理边界;
每个限界上下文内,使用分层架构划分为:接口层、领域层、应用层、基础设施层之间的最小隔离;
领域层里为了保证各个领域的完整性和一致性,引入聚合的设计作为隔离领域模型的最小单元 。
常见的一个完整领域驱动设计的演进过程如下图所示,面对客户的业务需求,由领域专家与开发团队展开充分的交流,经过需求分析与知识提炼,获得清晰的问题域。通过对问题域进行分析和建模,识别限界上下文,利用它划分相对独立的领域,再通过上下文映射建立它们之间的关系,辅以分层架构划分系统的逻辑边界与物理边界,界定领域与技术之间的界限。之后,进入战术设计阶段,深入到限界上下文内对领域进行建模,并以领域模型指导程序设计与编码实现。若在实现过程中,发现领域模型存在重复、错位或缺失时,再重新对已有模型进行重构,甚至重新划分限界上下文。
用例分析法
用例分析是比较通用的领域建模方法,在传统需求调研过程中结合领域模型的设计思路进行,核心是通过通过需求、场景、规则、流程等梳理用例,进而规划领域模型,大致可以分为获取用例、收集实体、添加关联、添加属性、模型精化几个步骤。
收集用例:在进行用例分析法进行建模之前,关键要梳理业务,产出用例,提取领域规则描述,从领域、规则、场景、流程中进行提取,收集相应的名词、动词、形容词,梳理出业务用例;
梳理领域概念:梳理出领域内我们关注的概念、用例的关系,并统一交流词汇,形成统一语言;
梳理业务规则:梳理出领域内我们关注的各种业务规则,业务规则通常具有不变性;
梳理业务场景:梳理出领域内的核心业务场景;
梳理业务流程:梳理出领域内的关键业务流程,比如订单处理流程;
梳理业务用例:梳理出业务用例,收集用例进而进行建模。
收集实体:从名词中定位出主要实体,比如商品、SKU、品类等;
添加关联:动词添加实体和实体之间的关联,比如商品“包含”SKU,卖家“开设”店铺等;
添加属性:从形容词中添加实体属性,比如颜色、价格等;
模型精化:识别出初步模型,验证并迭代模型,同时补充用例验证模型、业务流程验证模型。
四色建模法
四色建模法是一种模型的分析和设计方法,通过把所有模型分为四种类型,帮助模型做到清晰、可追溯。简单说,四色法关注的是:某个人(Party)的某个角色(PartyRole)在某个地点(Place)的某个角色(PlaceRole)用某个东西(Thing)的某个角色(ThingRole)做了某件事情(MomentInterval)。
时间(MomentInterval):表示在某个时刻或某一段时间内发生的某个活动,具有可追溯性的记录运营或管理数据的时刻或时段对象,用粉红色表示‘
角色(Role):角色就是我们平时所理解的“身份”,往往由人或者物来承担,会有相应的责任和权利,用黄色表示。为什么会有角色这个概念?因为很多活动或行为,只允许具有特定角色的参与者(PPT)才能参与该活动,比如,一个人只有具有教师的角色才能上课;
人-事-物(Party-Place-Thing Archetype):表示参与某个活动的人或物,地点则是活动的发生地,也叫PPT,代表参与到流程中的参与方、地点、物,用绿色表示;
描述(Description):表示对PPT的本质描述,对PPT 对象的一种补充描述,用蓝色表示, 它不是PPT的分类。有个例子解释的挺好的,有一个人叫张三,如果外星人问你张三是什么?你可能会说,张三是个人,但是外星人不知道“人”是什么。你会补充到:张三是个由一个头、两只手、两只脚,以及一个身体组成的人。在这个例子中,张三就是一个PPT,而“由一个头、两只手、两只脚,以及一个身体组成的客观存在”就是对张三的Description,头、手、脚、身体则是人的本质的不变的共性的属性的集合,只是大家抽象总结和命名,已经把这个Description用一称呼人来代替了。
使用“四色建模法”进行分析建模的基本步骤,感兴趣实战的可以参考下:从领域模型提取数据架构:
第一步:寻找要追溯的事件;
第二步:识别“时标对象”:识别“时标对象” ,按时间发展的先后顺序,用红色所表示的起到“追溯单据”作用的“时标”概念;
第三步:寻找时标对象周围的“人、地、物”:在“时标”对象周围的用绿色所表示的“人、地、物”概念;
第四步:抽象“角色”;
第五步:补充“描述”信息。
事件风暴法
事件风暴法类似头脑风暴,是一项团队活动,简单来说就是谁在何时基于什么做了什么,产生了什么,影响了什么事情。事件风暴是快速的设计技术,以更专注的方式发现与提取领域事件,短时间内高度可视化交流、集中思考,可以快速分析复杂业务领域,完成领域建模的目标。
事件风暴如果没有领域专家参与,可以寻找业务、产品、开发或者其他对问题域及其中的各种可行方案有深入理解同学参与。在开始之前,需要对业务场景或者业务场景背后的问题域有一定的事先准备,找到问题域的本质后再展开事件风暴。其次,在处理复杂问题时,一个有效又好用的方法就是分而治之,拆分成多组或者组织多轮事件风暴,就跟开会一样,每次要有明确的主题、目标以及会议结果。整个过程中,业务代表或领域专家用自己的语言表达业务,要先对齐上下文及语言,以统一的语言与统一的业务视角讨论或者验证领域事件。最后一点,事件风暴可能识别不出来所有领域事件,因此不需要担心是不是找出所有领域事件,只要真正解决了业务问题就好了。
事件风暴关注如下元素:简单理解就是谁(实体)在何时(时间)基于什么(输入)做了什么(命令、动作)产生了什么(输出)影响了什么(事件)。事件即某个动作的结果;属性即事件的输入、输出;命令即某个动作;实体即命令的触发者。事件风暴要先做如下准备:
正确的人(业务⼈员、领域专家、技术⼈员、架构师、测试⼈员等关键⻆色);
开放空间(有足够的空间可以将事件流可视化,让⼈们可以交互讨论);
即时贴(至少三种颜色),关联的人充分讨论、集体决策,从价值角度来审视业务流程的合理性,达成创建业务人员和非业务人员的共识。
事件风暴的基本步骤:
1.在便利贴上写领域事件,梳理出业务流程,一般是橘色。写好的便利贴按照时间顺序放到建模平面上,从左往右逐步发生;
2.创建导致领域事件发生的命令,命令应该是指令式的。创建领域事件的便利贴是浅蓝色的,按照时间顺序,将命令和事件的关系处理好;
3.把命令和领域事件通过实体、聚合联系起来。由于建模没完,因此没有真正的实体和聚合,而是领域专家思想里的业务概念和概念群。用淡黄色的便利贴来表示聚合,其左下角是命令,右下角是事件。聚合的名字应该是名词。
4.在建模平面上画出边界和事件流动的箭头。
5.识别用户执行操作所需的各种视图,以及客户不同用户的关键角色。
分层架构
四层架构
经典的三层架构是目前最普遍的架构,展现层、应用层、数据访问层,前面第一节传统框架MVC已经介绍过优缺点。这样的框架结构每个模块职责分离,很适合小型的应用系统。但是随着业务复杂度的上升,这种分层架构的弊端也日益凸显。
那DDD整洁架构里面介绍最多的就是四层架构了:
用户界面层,负责向用户显示信息和解释用户命令,完成前端界面逻辑。展示层的组件实现用户与应用交互的功能,处理Controller的VO层展示、Restful消息处理、配置文件解析等。有时候大家纠结Controller层与Service层参数校验的区别是什么,Controller需不需要校验?校验应该取决于校验的内容,一般推荐尽早校验,不过这里主要是进行一些简单的、不涉及业务规则的校验。具体的业务规则的校验放在领域层。
应用层,主要组件是Service,因为主要职责是协调各组件工作,所以通常会与多个组件交互,如其他Service,领域对象,Repostitory等。核心负责展现层与领域层之间的协调,也是与其它系统应用层进行交互的必要渠道,例如事务、执行单位操作、调用应用程序的任务。应用层要尽量简单,不包含业务规则或者知识,不保留业务对象的状态,只保留有应用任务的进度状态,更注重流程性的东西。它只为领域层中的领域对象协调任务,分配工作,使它们互相协作。
领域层,负责表达业务概念,业务状态信息以及业务规则。领域层是业务软件的核心,领域模型位于这一层。包含了前面提到的核心概念,如领域对象(实体、值对象)、领域服务以及它们之间的关系,负责表达业务概念、业务状态信息以及业务规则,具体表现形式就是领域模型。领域驱动设计提倡充血模型,即尽量将业务逻辑归属到领域对象上。
基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制(如数据库资源、中间件交互、缓存、MQ消息)等,屏蔽技术底座能力。
张建飞搭建的COLA框架希望成为应用架构的最佳实践,接口层、应用层、领域层、基础设施层。其中最主要的属于领域层,其中引入了DDD领域模型的概念,领域模型中不再是按照分层去组织代码,而是按照领域模型去组织代码。本质上,其实后面会发现不管是六边形架构、洋葱圈架构、整洁架构、还是COLA架构,都提倡以业务为核心,解耦外部依赖,分离业务复杂度和技术复杂度。
简单的可以直接按照功能进行不同层级的分包,如果复杂的也可以按照COLA的分包策略,我们在每一个module下面首先按照领域做一个顶层划分,然后在领域内,再按照功能进行分包。
六边形架构
依赖倒置原则:高层模块不应该依赖于底层模块,两者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。根据该定义,DDD分层架构中的低层组件应该依赖于高层组件提供的接口,即无论高层还是低层都依赖于抽象,整个分层架构好像被推平了,如果我们把分层架构推平,再向其中加入一些对称性,就会出现一种具有对称性特征的架构风格,这种架构风格被称为六边形架构,也叫端口适配器架构。在六边形架构中,让业务与基础设施分离,内部是业务的核心,也就是核心领域模型,外部则是API、数据库以及其他基础组件,实际上已经不存在分层的概念,所有组件都是平等的。六边形架构让业务领域的边界更加清晰,从而有更好的可扩展性,对测试也更加友好。
六边形每条不同的边代表了不同类型的端口,端口要么处理输入,要么处理输出。对于每种外界类型,都有一个适配器与之对应,外界通过应用层API与内部进行交互。如果是主动适配表示的是用户如何使用应用,它们接收用户输入,调用端口并返回输出, controller就是⼀种端⼝,端⼝的具体实现就是应⽤逻辑⾃身。因此端⼝和具体实现都在应⽤系统的内部,Rest API是目前最常见的应用使用方式。如果是被动适配表示的是实现应用的出口端口,向外部工具执行操作,指访问存储设备,外部服务等。每种访问就是⼀种端⼝,具体实现是各个具体的中间件。因此端⼝在整个应⽤系统的⾥部,具体实现在系统的外部。
洋葱架构
洋葱架构针对六边形架构更进⼀步把内层的业务逻辑分为了DDD概念的应⽤服务层、领域服务层和领域模型层。根据依赖原则,定义了各层的依赖关系,越往里依赖程度越低,代码级别越高,越是核心能力。外圆代码依赖只能指向内圆,内圆不需要知道外圆的情况,这种架构也是典型的分层架构,体现了高内聚,低耦合的设计特性。洋葱架构的主要特点:围绕独⽴的领域模型构建应⽤;内层定义接⼝,外层实现接⼝;依赖的⽅向指向圆⼼(注意:洋葱架构提倡不破坏耦合⽅向的依赖都是合理的,外层可以依赖直接内层,也可以依赖更⾥⾯的层);所有的应⽤代码可以独⽴于基础设施编译和运⾏
CQRS
CQRS(Command-Query Responsibility Segregation) 是一种读写分离的模式,代表命令和查询责任隔离,用于分隔数据存储的读取和更新操作,读模型和写模型之间不再是强事务一致性,而是最终一致性。 在应用程序中实现 CQRS 可以最大限度地提高其性能、可缩放性和安全性。在传统的体系结构中,使用同一数据模型查询和更新数据库,非常适用于基本的 CRUD 操作。 但是,某些复杂的应用程序中,这样的体系结构可能会导致一定的问题。例如,数据读取和写入形式不匹配、复杂查询性能无法满足、同一组数据集并行执行操作时导致的数据争用等。
参考文档
DDD领域驱动设计是什么?入门篇
迄今为止最完整的DDD实践
DDD的知与行-记一次完整的DDD系统实践
从软件复杂度的角度去理解DDD
DDD实践-质检平台(kayle)建设
领域驱动设计从0到1之事件风暴