DDD落地的思考:映射偏移模式 -- 知识铺
一、背景
DDD落地的思考系列已经写了两篇,接下来搞点干货,来总结一些新的模式。“映射"这个词在eric的书中出现了很多次,主要表达了各种模型之间的映射问题。由于面向不同层次导致对应的模型对象本身担负的职责出现了一些微妙的变化,因此在构建模型的过程中不得不对模型进行转换,本文将深度揭露不同模型之间映射的问题。另外也将引出一个新的模式–映射偏移模式。
二、模型映射的场景
2.1 模型映射的疑惑
大家在构建业务模型的时候常常会发现,数据库字段名或者数据库表名与代码里定义的entity名称多少会有一些出入,除了类型不太一样,名字也命名的不一样。同时在同一个统一语言下的业务模型命名也不太一样,所以在构建模型的时候不得不处理这部分偏差,同时还要忍受无法重构的状况。那为什么会这样呢,仅仅是把问题归咎于命名的问题?程序员的水平问题?我想还有另外的答案。就是在构建模型的时候有时候你觉得这个字段在这一层这么命名是好的,但是到另外一层可能做了简写或者发现了更合适的其他名字,或者说命名的时候根本没有保持一致。
当代码写的差不多了,注意力就会分散,不再专注于某一层。如果一旦要修改命名,则对所有层次的模型都会产生影响,这可能让人更屈服于让映射偏移继续存在。深入到业务逻辑中才会发现处理这些映射问题会让程序变得臃肿,代码量增多,坏味道也弥漫开来。
在单体应用或者分布式应用中都会出现,但是分布式应用下因为领域划分导致的模型引用变得复杂,这样的情况会变得更糟糕。比如在电商领域商品,优惠券的命名可能在不同的微服务边界下变得模糊,可能A团队在代码里定义为a,B团队在定义引用A团队里负责的a则成了a1。
2.2 ER模型与JavaEntity模型映射
一般情况下我们的ER模型(这里指表字段结构)与JavaEntity模型的映射是非常简单的,但是仍然存在一些问题,比如类型,由于数据库底层的数据类型与开发语言定义的数据类型不同则需要处理这部分映射(0,1对应false,true;但是类型是数字和布尔类型的转换)。另外的命名则是驼峰式命名转为下划线命名。
像Hibernate,JPA等基本上从框架层面自己处理了模型数据类型和模型名称转换的问题,但是在Mybatis中则需要自己处理这两个映射的问题。当然从自研的角度来看,能比较好地处理这个问题算是走好了一小步了。
2.3 JavaEntity模型与业务模型映射
由于ER模型与JavaEntity模型之间的映射更偏向于数据结构方面的描述,同时描述的内容也基本是一致的,但是业务模型可能就会复杂一点,有些业务模型是与JavaEntity模型是一一对应的,但是另外一些则是业务模型衍生出的业务对象,本身可能只是作为上下文参数传递或者作为JSON字段对应数据库字段,到业务模型里是要反序列化的。
通常来说如果业务不复杂的话,业务模型与JavaEntity模型则可以作为一个模型来对待。这样一来,在ORM下则需要更灵活的处理哪些需要持久化。但是大多数场景下还是需要将这二者区分开。一般来说这两个之间的映射则需要一些工具类帮助,不然则会更加复杂,也就是说需要通过MapStruct或者BeanUtils来做对象之间的转换。在工具类没有普及之前,这俩之间的映射则需要手动处理,那么一个保存单据的业务代码就会膨胀很多。
2.4 业务模型与接口参数模型映射
很早之前做web管理系统已经将接口参数与业务模型参数隔离开来,但是业务参数还跟JavaEntity模型是一致的,所以在业务模型与接口参数模型之间的转换则显得更加干脆,因为业务逻辑处理的过程中就是直接的参数模型映射,直到数据库层面。不过由于接口层的特殊性,在模型映射之前还需要保证数据的正确性,这样的前置校验可以避免后续逻辑产生更多的问题。
2.5 业务模型之间的映射
上面的映射过程都没有提到DDD,在分层架构下或者强调多层多模块的架构风格中,不同的层都有不同的业务模型,比如DTO,BO,DO,Context,Bean,MsgBody,Event等等。
有时候相同的层次之间存在多个业务模型,比如领域层可能会包括BO,Bean,ContextBO,或者Event。当模型之间需要转换传递数据的时候就存在映射关系,那么处理这些映射可能用BeanUtils也不一定好使了,就我当前的使用经验来看这些模型之间的转换会更复杂,场景也比较多,属性类型,命名都不一样,同时也牵扯着一些数据处理。当然,使用MapStruct可能稍微好点,不过,依然搞不定这些情况。
一般的做法就是显示转换,另外的方案就是单独构建一个factory或者helper类来辅助完成模型之间的映射,当然也可以叫做对象的构建。
2.6 属性映射
上面整体讲述了不同模型和不同模块层次之间的映射,现在从微观的角度来看一下映射的过程。在属性上的映射无非就下面两种情况。
- 属性名之间的映射
属性名之间的映射的潜在条件是属性名相同,属性类型相同,这样的话在不同的映射转换工具下都是讲得通的,但是复杂情况下的映射也要看映射转换工具是否支持,比如类型是复合数据类型(List)的情况下还麻烦一点。
- 属性类型之间的映射
如果属性名相同,但是属性类型不同的话,做映射则需要借助SDK转换方法来转换,更多的是不同类型之间的转换可能会报错,比如string转换到integer。通常情况下不建议属性是枚举类型,否则转换则需要更多的代码支持。
2.7 复杂场景映射
映射转换工具(也叫bean对象转换复制工具)如BeanUtils对不同的场景支持度不一样,这里不再深入叙述,现在我们看一下复杂场景下的映射问题。
- 列表对象映射
比较常见的一类映射就是列表对象,Listor Map<key,XXX>都是比较常见的,这一类一般来说都需要基础对象的支持才能构建,由于复杂数据结构,在构建之前需要声明下对象数据容器。如果对象中有这一类数据需要转换,那么就会麻烦一点,不过另外一点也说明了存在这种情况的话,其对象本身可能就是一个复杂对象,同时与聚合或者快照有关。
- 换值映射
现在我们看一下什么是换值映射,之前做报表导入的时候深有体会,因为报表中很多列都与枚举有关,有时候是汉字–>数字,有时候是数字–>汉字。这样的话就需要在枚举类中亲自构建映射方法。在页面查询的时候依然如此,用户不希望看到一个数字或者一个字符串。当然,在涉及统计计算的时候最好是数字或者简单的字符串。
- 逻辑映射
这里的逻辑映射场景可能比较少见,当然也是最复杂的一种。举个例子,参数传递进来的dto是(a,b,c,d),但是在业务层的业务模型bo就变成了(a,c,d)。中间的逻辑映射变成了a =a+b。那么到了数据库层面就是a+b。当然更复杂的情况比如存在不同条件下进行不同的属性拼接,截取,合并,拆分等都可能会让模型之间的映射变得不再直观。有时候一旦出问题,或者传参出错都必须要看代码才能解决,同时这种情况下的映射条件也变得严格,需要在映射过程中保持一定的校验。
三、映射偏移模式
3.1 概念说明
不同模型之间出现数据流动,模型相似度较高,在领域和上下文之间进行传递,因而出现模型之间的相互转换。转换的过程可以视为映射偏移。不同的转换过程其实现方式也不一样,对于这种场景可以称为一种现象或者模式。
3.2 映射偏移产生的问题(副作用)
-
对业务产生理解偏差
-
实现复杂,容易出错
-
可能存在性能问题
-
干扰业务核心流程,导致代码可读性比较差
-
数据转换复杂,增加重构难度
3.3 映射偏移在架构工程下的体现
映射偏移在工程架构中的体现-分层架构 (1).png
3.4 映射偏移的作用
-
辅助业务数据流转
-
模型和上下文之间解耦合
-
让模型职责更加单一
-
辅助工程架构进行合理分层
3.5 实现映射偏移的方法
现在我们看一下从代码上来看不同的映射场景下的实现映射偏移的方法。
- mybatis-ResultMap
需要说明的是在查询和插入场景下有些Mybatis.xml文件的sql和ResultMap可以不带类型映射和类型标签,这个应该是跟Mybatis内部的处理机制有关。
- Hibernate(自研ORM框架)
这一类的ORM映射则将JavaEntity与框架或者自定义注解结合在一起,通常来说不需要写sql,查询结果直接按照一定的约定(下划线<–>驼峰)进行相互转换,具体代码不再演示。
- Spring BeanUtils
image.png
- MapStruct
关于MapStruct的原理这里不再过多赘述,但是从上面生成的代码来看,如果使用常规做法进行手动get/set则非常容易出错。
- 手动get/set
当然还有一下其他的衍生实现方法,比如将转换逻辑抽离出到服务私有方法中,或者弄个xxxHelper类或者xxxFactory作为转换逻辑的服务类。具体这里不再深入。
- 模型内部转换
image.png
四、最佳实践
4.1 选择主转换工具
在大型复杂工程下,建议选择一个合适的主转换工具来帮助解决大量模型转换的问题,当然不建议贪快或者省事,不同的转换工具使用都是有成本的,所以选择了一个合适的主转换工具,代码量会少很多。
4.2 建议使用lombok
这里个人建议使用lombok来自动生成get/set方法,就个人的使用经验看lombok+mapstruct已经相对完美了,但是有以下几点需要说明,避免踩坑。
-
存在模型类继承的场景下lombok+mapStruct可能出现转换失效的问题
-
慎用lombok的builder,toString注解
-
mapStruct在编译后,如修改了或者新增属性之后,可能需要清空字节码重新编译生效
4.3 在主流程中少用手动get/set转换
通常情况下,主流程中包括各种调用,数据计算,数据转换,持久化等等。但是由于主流程的业务性比较强,在不同的条件语句,循环语句下建议将手动get/set的模型映射逻辑单独弄出来。当然少量的1-3个的手动get/set也没有太多问题。
4.4 单个服务模型层次之间转换不超过2次
这里从工程架构的角度来看模型层次之间的转换,通常来说如果不用DDD或者业务不复杂的情况下,两层模型即可,也就是说DTO/VO可以直接转换到JavaEntity然后进行持久化。但是用DDD或者存在分层架构下,模型就有了三层,中间则需要转换两次,即DTO/VO<—>BO/DomainEntity<—->JavaEntity/POJO。由于在分布式和微服务的场景下可能存在更多的模型,比如如下转换链路:
DTO/VO<—>BO/DomainEntity<—->MSGBODY/EVENT
DTO/VO<—>CMD<—>BO/DomainEntity<—->DTO/VO(adapterLayer) 所以个人建议在构建服务的时候需要考虑让服务内模型层次之间的转换不要超过2次,尤其是主转换流程。
4.5 使用建造者模式和工厂模式
上面的主转换流程中已经有了一些应对方法,这里看一下复杂场景下的转换如何应对,比如一个聚合对象的保存操作,首先构建聚合对象的DTO/VO在协议层需要进行转换,到应用层反序列化成为Java对象,然后往下层传递的时候可能需要根据参数构建一个对应复杂的BO/DomainEntity,一般而言可以使用工厂模式,来构建一个复杂对象,同DDD书里写读聚合构建方法。
另外一种场景就是我保存的数据对象是聚合对象的一部分,那么在领域层构建的聚合保存则是以聚合对象为入参的,那这样的话就需要构建一个聚合对象,然后把被聚合的对象给装载到聚合对象里,如上图3.5节的mapstruct对应的代码,ProjectBO是聚合对象,ApiBO是有独立Controller对应的,但是ApiBO在领域层却没有独立的保存接口,而是通过ProjectBO来辅助ApiBO进行保存。
通常情况下如果一个数据对象没有父类的话,使用建造者模式进行一定的转换也是可以的,这种情况比较适用于模型内部的转换,如果需要参数可以单独再传,也就是模型本身可以提供静态方法的能力来让自己转换成别的对象或者别的对象转换成自己。这么做的一个代价就是模型之间耦合严重。
4.6 MapStruct使用专场
在处理映射偏移模型转换的过程中对MapStruct的应用也有了一些经验,这里专门用一小节来阐述下MapStruct的使用场景。通常情况下我们会为每个识别出的领域实体构建一个转换接口,如下:
- 常规场景
现在我们看一下常规场景下的对象转换,这样的情况就是最简单的,属性名和属性类型都一样,没有父类继承也没有复杂属性。当然有时候这个转换接口也会被注入到spring容器中,但是个人看这个转换层更像是工具,所以不被spring容器接管也行,以免出现不必要的麻烦。
- 带有枚举类的使用场景
image.png
- 聚合对象转换的使用场景
从上面的转换代码可以看出,StockRecordSearchDTO对象是个聚合对象,但是转换的目标对象不是,从属性之间的关系来看像是被拍平了。 这里我们看一下另外一种聚合对象的转换,上面引用了其他转换层接口的转换方法,这样在聚合对象的转换过程中实现了转换层代码的复用,同时也不需要单独构建引用对象的转换方法。
- 转换过程中存在其他数据处理逻辑
在转换过程中也存在一些其他的数据处理逻辑,比如a=a+b的情况,这样的话可以参考上面的方式在expression中提供静态转换方法或者处理方法,但是个人建议不要过多使用,因为转换方法如果涉及到一些业务性比较强的处理语意,很可能会让代码变得复杂。
五、总结
本文深度讨论了模型映射相关的场景和问题,同时给出映射偏移的基本概念,讨论映射偏移下的代码模型转换的一些工具和可能存在的坑,最后总结如何应对映射偏移的方法。针对映射偏移衍生出的应用场景在数据工厂2.0中也有所体现,相关技术细节会在公众号持续发布,敬请期待。
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20240710/DDD%E8%90%BD%E5%9C%B0%E7%9A%84%E6%80%9D%E8%80%83%E6%98%A0%E5%B0%84%E5%81%8F%E7%A7%BB%E6%A8%A1%E5%BC%8F--%E7%9F%A5%E8%AF%86%E9%93%BA/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com