Java之领域模型转换

应用分成&分层领域模型为什么重要?

我们在软件开发设计及开发过程中,习惯将软件横向拆分为几个层。比如常见的三层架构:表现层(UI)/业务逻辑层(BAL)/数据访问层(DAL)。

图片描述

那应用系统为什么要分层呢?

主要解决以下几个问题:

  • 解耦

    • 有一句计算机名言:软件的所有问题都可以通过增加一层来解决。
    • 当系统越大,团队越多,需求变化越快时,越要保证程序之间的依赖关系越少。而分层/面向接口编程,会使我们在应对变化时越容易。
  • 简化问题

    • 当我们想不明白从用户操作一直到数据落盘整个过程的交互情况时,我们应该换种方式思考。想想各层应该提供哪些支持,通过对各层分工的明确定义,复杂问题就变成了如何将各层功能组合起来的“积木搭建”。
  • 降低系统维护与升级版本

    • 这里体现了面向接口编程的优势。我们抽象出数据访问层后,只需要保证对外提供的接口不变,底层数据库使用Oracle还是MySql,上层结构是感知不到的。
  • 逻辑复用/代码复用

    • 通过分层,明确定义各层职责,再也不会出现系统中多个地方查询同一个数据库表的代码。因为查询某个数据库表的工作只会由一个数据访问层类来统一提供。
  • 提高团队开发效率

    • 如果开发团队很多,通过分层和接口定义。各团队只需要遵循接口标准/开发规范,就可以并行开发。

一个比较形象的比喻:分层化相当于把软件横向切几刀,模块化相当于把软件纵向切几刀。

在《阿里巴巴Java开发手册》中,对应用分层的建议是这样的:

image-20201125104742238

  • 开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口; 网关控制层等。
  • 终端显示层:各个端的模板渲染并执行显示的层。 当前主要是 velocity 渲染, JS 渲染, JSP 渲染,移动端展示等。
  • Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
  • Service 层:相对具体的业务逻辑服务层。
  • Manager 层:通用业务处理层,它有如下特征:
    • 对第三方平台封装的层,预处理返回结果及转化异常信息。
    • 对 Service 层通用能力的下沉,如缓存方案、 中间件通用处理。
    • 与 DAO 层交互,对多个 DAO 的组合复用。
  • DAO 层:数据访问层,与底层 MySQL、 Oracle、 Hbase、 OB 等进行数据交互。
  • 外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,其它公司的 HTTP 接口。

以上的层级只是在原来三层架构的基础上进行了细分,而这些细分的层级仅仅是为了满足业务的需要。千万不要为了分层而分层。过多的层会增加系统的复杂度和开发难度。

应用被细分为多个层次,每个层关注的点不同。所以在这基础上,抽象出不同的领域模型。也就是我们常见的DTO,DO等等。

其本质的目的还是为了达到分层解耦的效果。

典型的领域模型都有哪些?

我们还是来看看《阿里开发手册》提供的分层领域模型规约参考:

  • DO(Data Object):此对象与数据库表结构一一对应,通过DAO层想上传输数据源对象。
  • DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
  • BO(Business Object):业务对象,由Service层输出的封装业务逻辑的对象。
  • AO(Application Object):应用对象,在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
  • VO(View Object):显示层对象,通常是Web向模版渲染引擎层传输的对象。
  • Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类来传输。
    这里的结构图大概是这样:

这里的结构图大概是这样:

图片描述

在给出的参考中并没有对模型对象进行非常明确的划分,特别是对BO、AO、DTO的界限不是非常明确。这也是因为系统处理的业务不同、复杂度不同导致的。所以在设计系统分层和建模的时候,需要综合考虑实际应用场景。

查看以上内容,就会发现为什么会有这么多O,还要转来转去!

为什么有这么多O?

举个例子:我们平时都在网上购物,查询网上购物的订单,会看到这样的信息

image-20201125105721342

其中包含:订单编号,下单日期,店铺名称,用户信息,总金额,支付方式,订单状态还有一个订单商品明细的集合。

终端显示层来说,这些信息是可以封装成一个VO对象的。因为显示层的关注点就是这些信息。为了方便显示层展示,我们可以将所有属性都弄成字符串类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package com.demo.action.domain.vo;

import lombok.Data;

/**
* 显示层订单展示信息
*
* @author jingLv
* @date 2020/11/25
*/
@Data
public class OrderVO {
/**
* 订单编号
*/
Long orderId;
/**
* 下单日期
*/
String orderDate;
/**
* 总金额
*/
String totalMoney;
/**
* 支付方式
*/
String paymentType;
/**
* 订单状态
*/
String orderStatus;
/**
* 商品名称
*/
String shopName;
/**
* 订单商品明细集合
*/
List<ProductVO> orderProductList;
}

再来看看对于业务逻辑层来说,它关心的是什么呢?显然跟显示层关注的不一样,它更加关注的是内部的逻辑关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.demo.action.domain.dto;

import lombok.Data;

import java.math.BigDecimal;
import java.util.Date;
import java.util.List;

/**
* 业务逻辑层订单信息
*
* @author jingLv
* @date 2020/11/25
*/
@Data
public class OrderDTO {
/**
* 订单编号
*/
Long orderId;
/**
* 下单日期
*/
Date orderDate;
/**
* 总金额
*/
BigDecimal totalMoney;
/**
* 支付方式
*/
PaymentType paymentType;
/**
* 订单状态
*/
OrderStatus orderStatus;
/**
* 商品名称
*/
ShopDTO shopInfo;
/**
* 用户信息
*/
UserDTO userInfo;
/**
* 订单商品明细集合
*/
List<ProductDTO> orderProductList;
}

可以看到,下单日期使用的Date类型,金额使用BigDecimal,支付方式和订单状态使用枚举值表示,商铺名称和用户名称变成了商铺信息/用户信息对象,明细集合中的商品也变成了DTO类型的对象。

在业务逻辑层面,更多的是关注由多种信息组合而成的关系。因为它在系统中起到信息传递的作用,所以它携带的信息也是最多的。

数据持久层与数据库是一一对应的关系,而上一层的订单信息其实可以拆解为多个持久层对象,其中包含:订单持久层对象(OrderDO),商铺持久层对象(ShopDO),用户持久层对象(UserDO)还有一堆的商品持久层对象(ProductDO)。

通过上面的描述,应该理解具体的拆分方法了吧!

回过头来想想,如果我们一路拿着最开始的OrderVO对象来操作,当我们想要将它持久化时,会遇到多少坑就可想而知了。

所以分层/拆分的本质还是简化我们思考问题的方式,各层只关注自己感兴趣的内容。

可这样的拆分确实增加了许多工作量,不同模型之间转来转去的确实头疼

模型转换需要注意的问题

通过对比,可以明显看到模型之间的差异

image-20201125115408103

  1. 原对象和目标对象相同属性的类型不一样,有的是Date,有的是BigDecimal,还有的是枚举
  2. 属性的名称也不一样
  3. 集合类属性中的泛型也不一样
  4. 能不能只复制一部分属性
  5. 能不能自定义转换逻辑
  6. 嵌套对象是深拷贝还是浅拷贝

那问题来了,我们如果更好的进行转换呢?

常见的模型转换方法

就以OrderDTO转OrderVO为例:

原对象OrderDTO的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"orderDate": 1570558718699,
"orderId": 202011250001,
"orderStatus": "CREATED",
"orderedProducts": [{
"price": 79.990000000000009094947017729282379150390625,
"productId": 1,
"productName": "羊腿肉",
"quantity": 1
},
{
"price": 40,
"productId": 2,
"productName": "羊排",
"quantity": 1
}
],
"paymentType": "CASH",
"shopInfo": {
"shopId": 20000101,
"shopName": "草原一家"
},
"totalMoney": 139.990000000000009094947017729282379150390625,
"userInfo": {
"userId": 20100001,
"userLevel": 2147483647,
"userName": "小红"
}
}

期望转换后得到的目标对象OrderVO如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"orderDate":"2020-11-25 15:49:24.619",
"orderStatus":"CREATED",
"orderedProducts":[
{
"productName":"羊腿肉",
"quantity":1
},
{
"productName":"羊排",
"quantity":1
}
],
"paymentType":"CASH",
"shopName":"草原一家",
"totalMoney":"139.99",
"userName":"小红"
}

第一种:通过Set/Get方式

也是最简单粗暴的方法,直接通过Set/Get方式来进行人肉赋值。

特点如下:

  • 直观,简单,处理速度快
  • 属性过多的时候,人容易崩溃

第二种:FastJson

利用序列化和反序列化,这里我们采用先使用FastJson的toJSONString的方法将原对象序列化为字符串,再使用parseObject方法将字符串反序列化为目标对象。

使用方法:

1
2
3
4
5
6
7
8
/**
* 第二种:FastJson
*/
@Test
void json() {
// JSON.toJSONString将对象序列化成字符串,JSON.parseObject将字符串反序列化为OderVO对象
orderVO = JSON.parseObject(JSON.toJSONString(orderDTO), OrderVO.class);
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
========OrderDTO========
{
"orderDate":1606286814019,
"orderId":202011250001,
"orderProductList":[
{
"price":79.9899999999999948840923025272786617279052734375,
"productId":1,
"productName":"羊腿肉",
"quantity":1
},
{
"price":30,
"productId":2,
"productName":"羊排",
"quantity":1
}
],
"orderStatus":"CREATED",
"paymentType":"CASH",
"shopInfo":{
"shopId":20000101,
"shopName":"草原一家"
},
"totalMoney":139.990000000000009094947017729282379150390625,
"userInfo":{
"userId":20100001,
"userLevel":2147483647,
"userName":"小红"
}
}
========OrderVO========
{
"orderDate":"1606286814019",
"orderId":202011250001,
"orderProductList":[
{
"productName":"羊腿肉",
"quantity":1
},
{
"productName":"羊排",
"quantity":1
}
],
"orderStatus":"CREATED",
"paymentType":"CASH",
"totalMoney":"139.990000000000009094947017729282379150390625"
}

可以看到

  • 日期不符合我们的要求
  • 金额也有问题
  • 最严重的是,当属性名不一样时,不复制(userName没有复制到)

第三种:Apache工具包PropertyUtils工具类

引入apache工具包

1
2
3
4
5
6
<!--Apache工具包-->
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>

第四种:Apache工具包BeanUtils工具类

第五种:Spring封装BeanUtils工具类