放下对各种XO的执念,你才能获得愉悦

文章讨论了在Java开发中DTO、VO等概念的使用,指出严格的XO划分可能导致代码复杂度增加和心智负担。作者提倡使用request/response替代DTO、VO,并建议根据实际需求选择转换方法,如BeanUtils或MapStruct,避免过度设计。同时提醒开发者谨慎处理ORM框架中的实体类职责,保持代码清晰和职责单一。
摘要由CSDN通过智能技术生成

前言

首先说明,这里所说的XO,实际是开发中经常遇到的DTO、VO等概念。我最开始是从阿里巴巴的Java开发手册接触这些概念,下面是手册中对于各种XO的解释:

1. POJO(Plain Ordinary Java Object): 在本规约中,POJO 专指只有 setter/getter/toString 的 简单类,包括 DO/DTO/BO/VO 等。

2. DO(Data Object):阿里巴巴专指数据库表一一对应的 POJO 类。此对象与数据库表结构一 一对应,通过 DAO 层向上传输数据源对象。

3. DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。

4. BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。

5. Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。

6. VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。

7. AO(Application Object): 阿里巴巴专指 Application Object,即在 Service 层上,极为贴近业务的复用代码。

最开始接触到这些概念,我感到异常兴奋——终于有规范可以约束命令了。

俗话说:“无规矩不成方圆”。往往代码的混乱,就是因为没有规则和规范。有一个权威的规范被发布,势必可以改善这种情况。

因此我在后面的开发中,开始大量实践这种命名方式。起初,并没有什么问题,但是随着自己的开发经验逐渐丰富,我发现这种命名方式时常给我带来痛苦。这种痛苦主要集中在三个方面:

1. 各种XO的命令并不优雅,也不具备语义。(很多人直接在实体上加上DTO或者VO作为类名,你无法从中看到任何有用信息。而且一堆XO在代码里面乱飘,简直让人头皮发麻。)
2. 各种XO之间的转换代码,最典型的就是从DTO转成VO。
3. 严格区分各种XO所带来的心智负担。

于是我开始分析,究竟是什么让我变得痛苦。后面我得出结论,这不是规范的问题,而是人的。我把它归结为对规范的“执念”,这种执念体现在,把规范奉为圭臬,不懂得变通。

 

忆往昔

我记得我开始入行的时候,是实施一款Sap的产品,名为Hybris。我不知道有多少知道它。

Hybris的业务代码分成四层:

  1. controller:这个不必多少,就是控制器
  2. facade:门面,负责封装负责的业务逻辑,或对返回数据进行转换,给controller提供统一入口。
  3. service:业务逻辑层
  4. dao:持久层,封装数据库操作。

和大家平时写业务代码不太相同的就是,它增加了一个facade层,这个对应了设计模式中门面模式。这一层要发挥作用还少不了两个配套设施:Data类和Converter。

  • Data类:它其实充当了DTO、VO和BO的作用,只是我们当时习惯性命名为XXXData。
  • Converter:又叫Populator,主要负责数据转化,典型的就是将实体类转成Data类型。

整个应用架构如下图所示:

04d585b562c04c7e9fc0483c5b549c79.png

 你会发现,除了controller和facade之间传递Data,其他各层之间都可以传递Entity和Data两种类型。

以dao层为例:

  • 保存和创建时接收Entity;复杂连表查询时,接收Data;
  • 简单查询时返回Entity;复杂连表查询是返回Data;

这里没有对Data进行细致的职责划分,比如DTO、VO、BO等,它能干的事情几乎都干了,代码也并没有因此变得混乱。

现在回忆起来这种写法,就是两个字——舒服。

这不禁让我发问,我们真的需要这么多XO吗?

 

分的越清楚,往往越痛苦

我曾今看过一个项目,它严格遵循DTO、VO的职责划分。为了控制controller只返回VO,哪怕两个类的属性是一致的,都要定义转换逻辑。代码里充斥这各种转换逻辑,甚至盖过了本身的业务逻辑。

一些项目不堪其害,索性放弃VO,直接使用DTO代替VO,这样得到了很大的解脱——至少不用写DTO到VO的转化逻辑了。

另外一个让人头大的是BO,它的问题在于在实际开发中,有时很难识别一个类到底是BO还是DTO。这给开发带来了不少的心智负担。我更倾向于放弃使用BO。

可是,放弃了VO,又放弃了BO,结果常用的就只剩DTO了。如果只剩DTO,那干嘛还要用这些XO。

所以慢慢的,我开始放弃这种命令方式。取而代之的是我认为更“亲切”的命名方式。

 

request/response

我放弃了VO、DTO,转而使用request和response。

  • request表示接口请求类型,跟DTO很像;
  • response表示接口返回类型,跟VO很像;

虽然功能和DTO和VO类型,但是request和response具备更好的语义。

你可能会问request和response可以用在service层吗,我的选择是可以。实际上是把service层同时当facade来使用,不用再单独定义一个facade层。

这样做带来的好处就是可以减少无意义的转换逻辑,同时没有引入新的层次,从而降低了代码结构的复杂度。

当然只有request和response是不够的,有时确实需要一些数据的中间状态,用来组装response。类似于BO,但是我会选择更朴实的命名,比如record、info、data等,这样我就可以降低识别BO的心智负担。在一些简单的场景,接口可以直接输出record,一切都是这么的简单自然。

当然entity还是必要的。你还可以加上domian,这个对应DDD中的领域对象,它是充血模型。虽然你可能并没有使用DDD,但是充血模型我认为有很大价值的,它的本质还是面向对象,可以把一些简单的逻辑,内聚到领域对象上。

最终的结构可能想下面这样:

model
      req
      resp
      record (可选)
      entity 
      domain (可选) 

 

当转换不可避免,该如何抉择

不管用哪种命名方式,做什么样的取舍,转化的逻辑都是不能完全消除的。应该怎么做才能尽可能保证代码的简单,不过度设计呢?

我觉得程序员的问题,往往是对规则或框架充满执念,不轻易妥协。比如,为了进行对象转化,引入了mapstruct这个库,于是不分青红皂白,清一色使用mapstruct,导致定义了一大堆转化接口。

请放弃这种执念。

实际经常用到的转化方案无外乎三种:

  1. 使用BeanUtils的copyProperties方法
  2. 使用mapstruct等开源库
  3. 自定义Converter接口

这三者的应用场景是有差别的:BeanUtils适用于属性名和类型都相同的类型;Converter适用与复杂的转化逻辑,比如需要其他Service进行相关的查询和计算;mapstruct介于两者之间,可以使用声明的方式解决属性名不同或格式不同的问题。

选择那种方式取决你的真实场景。总之,能用BeanUtils,就不要用mapstruct;能用mapstruct,就别用自定义Converter。尽可能降低代码复杂度,不要引入新的不必要的类。

 

小心ORM框架的“陷阱”

我记得最开始写代码的时候,嫌麻烦,就只会定义一个Entity。当需要增加显示列的时候,就在在Entity里定义非表字段。在jpa和mybatis可以使用@Transient注解或transient修饰符实现;mybatis plus可以使用@TableField(exist = false)实现。

请谨慎在正式项目中使用这种特性——它不是“糖”,可能是“毒药”。

应该尽可能保证实体类的职责单一,不要用于显示等用途。如果是为了复用实体类的属性,继承和组合都能实现。

 

总结

放下执念,立地成佛。

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值