DDD学习与感悟——向屎山冲击
软件体系是经过软件开发来处理某一个事务范畴或问题单元而发生的一个交给物。而经过软件规划可以协助咱们开宣布愈加强健的软件体系。因而,软件规划是从事务范畴到软件开发之间的桥梁。而DDD是软件规划中的其间一种思维,旨在供给一种大型杂乱软件的规划思路和标准。经过DDD思维可以让咱们的事务架构、体系架构、布置架构、数据架构、工程架构等都具有高扩展性、高保护性和高测验性。
可是落地DDD是一件很困难的工作。首先在思维认知层面就比较难以打破。
DDD自身是一种思维,不是某种详细的技能,因而在代码完结和体系架构层面没有束缚。而因为市面上老练的ORM结构(比方hibernate、mybatis等),使得大部分软件开发都是直接面向数据库开发。在传统开发中的运用分层架构又和DDD思维的分层架构很类似。然后导致许多人在初学DDD时有必定的了解误差,然后导致无法落地DDD思维。
这篇文章记载我对DDD的学习、感悟与项目工程代码重构实战心得!
一、Domin Primitive
范畴“元数据”的意思。首要是解说范畴的基本准则。这也是运用DDD思维的基本准则。
1.1 隐性的概念显性化
exp:电话号码一般是由区号编码+号码组成。在实践的事务中会有许多需求电话号码的事务。比方登录认证、导购分销等事务;咱们需求对电话号码进行根底性校验;获取区号编码等;在惯例操作下,会在每一个用到电话号码的办法进口都会写许多的这种校验代码和判别代码,尽管咱们可以将它的校验和获取区号编码抽离成util类(实践上大大都工程中都是这么做的),但这种办法治标不治本。依据DDD思维可以发现这儿有一个隐性概念:区号编码。
咱们可以依据DDD思维,将电话号码创立为一个具有独立概念和行为的值目标:PhoneNumber,将根底性校验和获取编码等无状况行为封装在值目标中。这样在办法中就不需求再充满着写许多的校验和判别。
1.2 隐性的上下文显性化
exp:在银行转账场景中,一般咱们会说A账户给B账户转1000元。这儿的1000元实践上有两层意义,数字1000,钱银元。但咱们一般会疏忽钱银单位元。导致在完结转账功用时,没有考虑到单位。一旦有世界转账时,就又会堕入到许多的if else中。
咱们依据DDD思维,将钱创立为一个具有独立概念和行为的值目标:Money,这样咱们所说的钱才具有完好的概念。经过这种办法就可以将钱银这个隐性上下文显性化,然后防止当时未识别到可是未来或许会爆雷的bug。
1.3 封装多目标行为
exp:在跨境转账的场景中,需求转化汇率,咱们可以将转化汇率封装成一个值目标。经过封装金额核算逻辑和各种校验逻辑,使得整个办法极端简略。
1.4 DP和值目标的差异
DP是阿里大神提出来的概念;值目标是DDD思维中的概念。
学习之后,我个人以为DP是对值目标的进一步弥补,使其具有了愈加完好的概念。在值目标【不变性】的根底上弥补了【可校验性】和【独立行为】。当然也是要求【无副效果】。所谓的无副效果便是【状况不行变】。
1.5 DP和DTO的差异
DTO | DP | |
---|---|---|
功用 | 数据传输目标,归于技能细节 | 归于范畴中的事务概念 |
数据关联性 | 不具有数据关联性 | 数据之间有强关联性 |
行为 | 无行为 | 具有非常丰厚的行为和事务逻辑 |
1.6 运用DP VS 不运用DP
不运用DP | 运用DP | |
---|---|---|
API接口明晰度 | 含混不清 | 办法签名明晰易懂 |
数据校验、错误处理 | 校验逻辑散布多个当地、许多重复代码 | 校验逻辑内聚,在办法鸿沟外完结 |
事务代码的明晰度 | 充满许多胶水代码,吞没事务中心逻辑 | 代码简洁明了,事务逻辑一望而知 |
测验杂乱度 | TC数量:N_M_P(N个参数,每个参数M种校验,有P个办法在调用) | TC数量:N+M+P |
其他优点 | 全体安全性大大进步、不行变性、线程安全 |
二、运用架构
2.1 DDD思维下的标准运用架构
传统的MVC架构分为展示层、事务逻辑层和数据拜访层,愈加重视从展示层到数据拜访层自上而下的交互,编写出来的代码像是脚本式代码。
而依据DDD准则,工程架构被分为运用层、范畴层和根底设施层。将工程中不同的功用和责任区别到不同的层级中。中心的事务逻辑放在范畴层中。
2.1.1 运用层
依照DDD的思维,运用层担任和谐用户界面和范畴层之间的交互。可以浅显的以为是对范畴服务的编列,其自身不包括任何事务逻辑。
2.1.2 范畴层
范畴层担任完结中心事务的逻辑和规矩。依照DDD的思维,这一层包括实体模块、值目标模块、工作、范畴服务。
2.1.3 根底设施层
根底设施层不处理任何事务逻辑,只包括根底设施,一般包括数据库、守时使命、MQ、南向网关、北向网关等。
2.2 我对演进出六边形架构的了解
2.2.1 再谈运用层
在实践事务逻辑傍边,除了用户界面层之外,还有其他外部体系会调用本服务,比方xxljob、MQ、或许供给给外部体系调用http或许rpc接口等。因而在实践傍边,运用层应当是和谐外部体系与范畴层之间的交互。
依照标准架构层级依靠联系来看,运用层依靠了范畴层和根底设施层。因为依靠了根底设施层,因而损坏了运用层自身的可保护性和测验性。因而咱们需求依据接口进行依靠倒置。
为了防止范畴概念外泄,需求对运用层进一步的笼统为外部服务和内部服务,一切外部服务有必要经过内部服务调用范畴层。这样就可以防止范畴模型的外泄。
2.2.2 再谈范畴层
相同的,依照标准架构层级依靠联系,范畴层依靠根底设施层,但这也损坏了范畴层自身的可保护性和可测验性。因而咱们依据DDD中的资源库思维,笼统repository层,经过接口完结依靠回转。让范畴层不再依靠根底设施层。然后进步范畴层自身的可保护性和可测验性。
2.2.3 再谈根底设施层
关于根底设施层而言,它首要效果是供给根底设施的才干,比方数据库、MQ、长途服务调用等。进一步笼统可以发现它们便是端口和适配器。经过端口完结与外部体系的交互,经过适配器完结数据和概念的转化。
2.2.4 演进出六边形架构
经过依靠回转,奇特的工作发生了。根底设施层变成了最外层。
咱们结合对运用层、范畴层和根底设施层进一步的了解再加上回转后的运用架构,便可以得到六边形架构:
2.3 东西类、配备类的代码应该放在哪里?
在一个实践的工程傍边,除了上面所说的三层之外,一般会运用到一些东西类(JSON解析东西类、字符串东西类等)。各层或许都会运用到东西类。
从东西类的定位来看,它应当归于根底设施层,可是根底设施层归于最上层,假如放在根底设施层,那么就会损坏依靠次序。因而咱们在逻辑区别上可以把东西类归类为根底设施或许通用域,在详细的工程结构中,可以独自一个模块放东西类。
在实践工程中还有一种类型的代码是配备相关的。从事务维度区别的话可以分为事务类配备和根底设施类配备。因而咱们需求依据配备的类型将其放在对应的方位。比方为了灵敏应对事务,咱们一般会配备一个动态开关,来动态调整事务的逻辑,这种事务开关类的配备就应该放在范畴层;再比方数据库的配备归于根底设施配备,这类配备就应当放在根底设施层。
2.4 我关于项目的六边形架构的实践
咱们团队做的的责任是事务底座,包括一系列的根底才干建造。其间关于IDaaS体系而言,依据六边形架构完结出以下工程结构:
三、repository形式
3.1 什么是repository形式?
在DDD思维中,repository表明资源库的概念,用于区别数据模型和范畴模型。它操作的目标是聚合根,因而它归于范畴层。
3.2 为什么要运用repository形式?
repository形式有两个非常重要的效果:1、与底层存储进行解偶;2、为处理贫血模型供给了一种标准。
3.3 什么是贫血模型?
因为曩昔ER模型以及干流ORM结构的开展,让许多开发者对实体的概念还停留在与联系形数据库映射这个层面。然后导致实体只要空泛的特点,而实体的事务逻辑散落各个service、util、helper、handler等各种旮旯中。这种现象就被称为贫血模型现象。
怎样判别自己的工程是否有贫血模型现象?
1、许多的XxxDO或许Xxx:实体目标只包括与数据库表映射的特点,没有行为或许及其少数的行为;
2、事务逻辑在各种service、controller、util、helper、handler中:实体的事务逻辑散落在不同层级、不同类、不同办法中,类似场景有许多的重复代码。
3.4 为什么贫血模型欠好?
无法确保实体目标的完好性和共同性:贫血模型下,实体特点的状况和值只能由调用方确保,可是特点的get和set是揭露的,因而一切调用方都可以调用。所以无法确保目标的完好性和共同性。
操作实体目标的鸿沟很难发现:因为目标只要特点,特点的鸿沟值、调用规模不受实体自身操控,各个当地都可以调用,鸿沟值和规模也只能由调用方自行确保。假如实体的鸿沟值有所改动,那么一切调用方都需求调整,这种状况下很简略导致bug的发生。
强依靠底层:贫血模型下的实体和数据库模型映射、协议等。因而假如底层改动,那么上层逻辑需求悉数跟着改动。“软件”变成了“固件”。
总结一句话:贫血模型下,软件的可保护性、可扩展性、可测验性极差!
扩展:
软件的可保护性=底层根底设施改动时,需求新增/修正的代码量是多少(越少可保护性越好)
软件的可扩展性=新增或改动事务逻辑时,需求新增/修正的代码量是多少(越少可扩展性越好)
软件的可测验性=每条TC履行的时长 * 新增或改动事务逻辑时发生的TC(时长越低/TC越少,测验性好)
3.5 实践状况中,为什么贫血模型难以消除?
1、数据库思维
跟着ER和ORM结构的开展,让大都开发者在刚入门的时分(自学、训练等办法),就以为实体便是数据库表映射;然后简略的将面向事务范畴开发改动成了面向数据库开发,渐渐地就以为软件开发便是CRUD。
2、简略
尽管有些架构师或许开发人员知道贫血模型欠好,可是企业为了占领市场,需求快速推出产品。因而工期被紧缩的很厉害。而贫血模型刚好简略,在软件初期阶段,可以快速完结事务逻辑。然后迫使开发人员不得不“先完结了再说”。这种现象也是职业的普遍现象。
3、脚本思维
有些开发人员具有必定的笼统思维,将一些共性的代码写成util、helper、handler等类。但写代码依然是脚本思维。比方一个办法中,先来个字段校验代码,再来个目标转化代码,然后调用长途服务,对长途服务回来的成果再来个目标转化,……终究调用Dao类的办法保存目标。这种代码在许多工程中太常见了。
依据这些要素,导致贫血模型难以消除。
这些要素的根本原因是什么?
根本原因便是,大部分的开发人员混杂了数据模型和范畴模型这两个概念。
数据模型(Data Model):数据模型处理的是数据怎样耐久化、怎样传输的问题;
范畴模型(Domin Model):范畴指的是某一个独立的事务范畴或许问题空间,范畴模型便是处理这个事务范畴或许问题空间而规划的模型;处理的是事务范畴的问题。
在DDD中,repository便是用于区别数据模型和范畴模型提出来的概念。
3.6 运用repository之后,数据模型和范畴模型怎样转化?
运用repository之后,数据模型和范畴模型都各司其职。经过Assembler和Converter进行模型之间的转化。
在代码中,动态转化映射 VS 静态转化映射
尽管Assembler/Converter对错常好用的目标,可是当事务杂乱时,手写Assembler/Converter是一件耗时且简略出bug的工作,所以业界会有多种Bean Mapping的处理计划,从本质上分为动态和静态映射。
动态映射计划包括比较原始的 BeanUtils.copyProperties、能经过xml配备的Dozer等,其间心是在运行时依据反射动态赋值。动态计划的缺点在于许多的反射调用,性能比较差,内存占用多,不适合特别高并发的运用场景。而BeanUtils等copy类东西躲藏了内部copy的进程,很简略引发bug且不易排查。
MapStruct经过注解,在编译时静态生成映射代码,其终究编译出来的代码和手写的代码在性能上完全共同,且有强壮的注解等才干。会节约许多的本钱。
3.7 代码层面模型标准和比较
DO | Entity | DTO | |
---|---|---|---|
命名标准 | XxxDO | Xxx | XxxDTO/XxxRequest/XxxVO/XxxCommand等 |
代码层级 | 根底设施层 | 范畴层 | 运用层 |
字段称号标准 | 于数据库字段坚持共同 | 事务言语 | 和调用方商定 |
字段类型标准 | 和数据库字段坚持共同 | 依据事务特征确认事根底类型仍是值目标 | 和调用方商定 |
是否需求序列化 | 不需求 | 不需求 | 需求 |
转化器 | Assembler | Assembler/Converter | Converter |
3.8 代码层面repository标准
1、接口名命名标准
repository中的接口名不要运用底层存储的称号(insert、update、add、delete、query等),而是尽量运用具有事务意义的命名。比方save、remove、find等。
2、接口的参数标准
repository操作的目标是聚合根。因而只能操作聚合根或许实体。这样才干屏蔽底层的数据模型,防止数据模型渗透到范畴层。
四、范畴层规划标准
4.1 实体类
大大都DDD架构的中心都是实体类,实体类包括了一个范畴里的状况、以及对状况的直接操作。Entity最重要的规划准则是确保实体的不变性(Invariants),也便是说要确保不管外部怎样操作,一个实体内部的特点都不能呈现彼此抵触,状况不共同的状况。
4.1.1 创立即共同
constructor参数要包括一切必要特点,或许在constructor里有合理的默认值。
4.1.2 运用Factory形式来下降调用方杂乱度
因为创立即共同的准则,导致实体的结构办法或许会很杂乱,因而可以运用Factory形式来快速的结构出一个新的实体。下降调用方的杂乱度。
4.1.3 尽量防止public setter
一个最简略导致不共同性的原因是实体暴露了public的setter办法,特别是set单一参数会导致状况不共同的状况。假如需求改动状况,尽量语义化办法称号。
4.1.4 经过聚合根确保主子实体的共同性
一般主实领会包括子实体,这时分主实体就需求起到聚合根的效果,即:
-
子实体不能独自存在,只能经过聚合根的办法获取到。任何外部的目标都不能直接保存子实体的引证
-
子实体没有独立的Repository,不可以独自保存和取出,有必要要经过聚合根的Repository实例化
-
子实体可以独自修正自身状况,可是多个子实体之间的状况共同性需求聚合根来确保
exp:常见的电商域中聚合的案例如主子订单模型、产品/SKU模型、跨子订单优惠、跨店优惠模型等。
4.1.5 不可以强依靠其他聚合根实体或范畴服务
一个实体的准则是高内聚、低耦合,即一个实体类不能直接在内部直接依靠一个外部的实体或服务。
对外部目标的依靠性会直接导致实体无法被单测;
以及一个实体无法确保外部实体改动后不会影响本实体的共同性和正确性。
正确依靠外部的办法
只保存外部实体的ID:这儿我再次强烈建议运用强类型的ID目标,而不是Long型ID。强类型的ID目标不单单能自我包括验证代码,确保ID值的正确性,一起还能确保各种入参不会因为参数次序改动而出bug。
针关于“无副效果”的外部依靠,经过办法入参的办法传入。比方上文中的equip(Weapon,EquipmentService)办法。
4.1.6 任何实体的行为只能直接影响到本实体(和其子实体)
这个准则更多是一个确保代码可读性、可了解的准则,即任何实体的行为不能有“直接”的”副效果“,即直接修正其他的实体类。这么做的优点是代码读下来不会发生意外。
另一个恪守的原因是可以下降不知道的改动的危险。在一个体系里一个实体目标的一切改动操作应该都是预期内的,假如一个实体能随意被外部直接修正的话,会添加代码bug的危险。
4.1.7 可以运用enum来替代承继联系,后续也可以运用Type Object规划形式来做到数据驱动
4.2 范畴服务
当一个事务逻辑需求用到多个范畴目标作为输入,输出成果是一个值目标时,就阐明需求运用到范畴服务。
4.2.1 单目标战略型
这种范畴目标首要面向的是单个实体目标的改动,但涉及到多个范畴目标或外部依靠的一些规矩。
在这种类型下,实体应该经过办法入参的办法传入这种范畴服务,然后经过Double Dispatch来回转调用范畴服务的办法。
什么是Double Dispatch
exp:关于“玩家”实体而言,有一个“equip()”配备兵器的办法。
依照惯例思路,“玩家”实体需求注入一个EquipmentService,可是实体只能保存自己的状况,
除此之外的其他目标实体无法确保其完好性,因而咱们不经过注入的办法运用EquipmentService;
而是经过办法参数引进的办法来运用。即“玩家”实体的"equip()"办法界说为:
public void equip(Weapon weapon, EquipmentService equipmentService) {
if(equipmentService.canEquip(this, weapon)) {
this.weaponId = weapon.getId();
}
}
这种办法就称为Double Dispatch办法。
Double Dispatch是一个运用Domain Service常常会用到的办法,类似于调用回转。
4.2.2 跨目标事务型
当一个行为会直接修正多个实体时,不能再经过单一实体的办法作处理,而有必要直接运用范畴服务的办法来做操作。在这儿,范畴服务更多的起到了跨目标事务的效果,确保多个实体的改动之间是有共同性的。
4.2.3 通用组件型
这种类型的范畴服务供给了组件化的行为,但自身又不直接绑死在一种实体类上。他的优点是可以经过组件化服务下降代码的重复性。
接口组件化来完结通用范畴服务
exp:在游戏体系中,原价、NPC、怪物都是可移动的。因而可以规划一个Movable接口,
让玩家、NPC、怪物实体完结Movable接口。然后再完结一个MoveService,然后完结一个移动通用服务。
4.3 战略目标(Domain Policy)
Policy或许Strategy规划形式是一个通用的规划形式,可是在DDD架构中会常常呈现,其间心便是封装范畴规矩。
一个Policy是一个无状况的单例目标,一般需求至少2个办法:canApply 和 一个事务办法。
canApply办法用来判别一个Policy是否适用于当时的上下文,假如适用则调用方会去触发事务办法。
一般,为了下降一个Policy的可测验性和杂乱度,Policy不应该直接操作目标,而是经过回来核算后的值,
在Domain Service里对目标进行操作。
4.4 副效果的处理办法 - 范畴工作
什么是副效果?
“副效果”也是一种范畴规矩。一般的副效果发生在中心范畴模型状况改动后,同步或许异步对另一个目标的影响或行为。比方:当用于积分到达100时,会员等级升1级。
在DDD中,处理“副效果”的手法是范畴工作。经过EventBus工作总线可以完结范畴工作的传达。
现在范畴工作的缺点和展望
因为实体需求确保完好性,因而不可以直接依靠EventBus,所以EventBus只能坚持大局singleton。可是大局singleton目标很难被单测,这就简略导致Entity目标很难被完好单测掩盖全。
五、写在终究
经过关于DDD的学习与实践,越来越可以领会到它作为一种软件规划思维和辅导,关于大型杂乱软件的建造非常有协助。关于前史留传屎山工程的重构也供给了一个很好的辅导方向。
作者:京东科技 孙拂晓
来历:京东云开发者社区 转载请注明来历