有句话叫做 “计算机科学领域任何问题,都可以间接的通过添加一个中间层来解决”,但是唯一解决不了的问题,是层次本身过多的问题。每一层内都会维护自己在乎的数据对象模型。层与层之间数据的传递,就不可避免地遇到对象类型转换的问题。
这个话题也和最近的项目有关。我们在重构一个老旧的系统,所做的第一件事情,就是要把数据访问层从原有系统中剥离出来,我们精心设计了这一层的模型和结构,但是要让原有系统平缓地从原有数据访问方式上移植到新的数据访问层上,就涉及到上层(Service)的原有数据对象和数据访问层(DAS)之间的数据传递,而二者模型并不相同,而且原有 Service 的模型并不纯粹,既不是充血模型,model 层也掺杂了很多逻辑,因此也不是纯粹的贫血模型,因此这两层之间对象转换的工作就显得尤为重要。
且看这样的对象转换:
public UserNew transform(UserOld old){ UserNew userNew = new UserNew(); userNew.setName(old.getName()); userNew.setAge(old.getAge()); userNew.setSex(old.getSex()); userNew.setDesc(old.getDesc()); ... ... } public UserOld transform(UserNew newUser){ ... ... }
但是在使用过程中,发现存在着这样一些问题:
- 一个 UserNew/UserOld 对象有 40 个属性,这样的一次 transform 就要写 40+行这样毫无营养的 get/set 代码,而再提供一个反向转换的方法这样的代码需要×2;整个系统存在二三十种 model,这样啰嗦的转换令人恶心;再者,我们发现,层次可能很多——比如我们在使用一些序列化框架时,需要借由类似的方法将当前对象转换成框架需要的 POJO 对象,因此一个 User 就让我们做了很多次这样丑陋的转换。
- 转换并不是那么顺利的,经常遇到类型不同的情况,需要经过类型转换或者简单的逻辑处理。比如对于空值的特殊处理,对于 0 值的特殊处理等。
- 转换甚至都不一定是一对一的,特殊情形的处理被迫使用到的逻辑,让整个转换层和业务模块中的很多发生耦合……这不是我希望看到的。
如何思考和解决这样的问题?其实这个问题有很多种表现形式,比如 PO-VO 对象的互转换等等。这里的争论很多,我整理如下:
1、如果能够尽量保证模型的字段名和和类型一致,可以利用 Spring 的 copyProperties 方法来完成 POJO 对象的拷贝:
BeanUtils.copyProperties(srcObj, desObj);
不过这个方法也有一些缺陷,一个是反射导致的性能损失,一次反射并不明显,对象拷贝可以说是非常频繁的;还有一个是对于一些类型不同的情况,我们需要自定义一些转换逻辑来处理这样的特殊情形。
2、借由一个中间层来承载数据,这样的中间层往往是可序列化的,比如 JSON 格式,每一种 String、int 等基础的类型都有转换成 JSON 的统一处理办法,所有数据的转换都通过通用方法转成 JSON 格式,然后再根据目标对象对各字段格式的要求,把 JSON 表示的对象复原。这种办法需要的框架性代码比较多,而且通过序列化对象作为中间介质,不免存在性能损耗的问题,但是对于存在大量数据转换的情况,也不失为一种好办法:
3、如果是使用 Ruby 之类的动态语言,或者变量定义本身就是弱类型的,那么就会省去很多这样转换的工作,当然,由于编译期间对于对象属性的不确定性,也可能引入更多不可预期的运行时异常,或者是一些丢失精度、显示错乱等等这方面的问题。
4、还有一个走极端的方式,对象变成 Map<String, String> 来存储,这样就免去了对象转换的成本,而且扩展性极强。但是缺点也是极其明显的,这就根本不是面向对象了,这是 “面向无差异数据容器” 编程……而且缺少约束,对于嵌套场景可读性极差。
5、在某些情况下还有一个变通的方式,我们不减少任何这样对象转换的重复代码,但是,我们可以通过注解、工具等等让这些可预期的代码自动生成,这同样减少了程序员的工作量。
最后,我要说的是,保持模型对象的纯粹和单一性,是减小工程重量的一个原则,让不同层次的逻辑使用同一组对象,虽然可能带来一些契合性问题、兼容性问题,但是带来的好处就是大大减小冗余对象类型的数量,减少这种没有营养的转换。这里又是 trade off 了。
除了这些,你还有什么体会和好办法?
————————————————————————————————————–
2012 年 11 月 11 日:
一江春水邀明月回复我可以使用 Dozer 或者 Orika 这样的组件,我第一次听到,就简单学习了一下:
Orika:Orika is a Java Bean mapping framework that recursively copies (among other capabilities) data from one object to another. It can be very useful when developing multi-layered applications.
几个特性:
- Map complex and deeply structured objects
- “Flatten” or “Expand” objects by mapping nested properties to top-level properties, and vice versa
- Create mappers on-the-fly, and apply customizations to control some or all of the mapping
- Create converters for complete control over the mapping of a specific set of objects anywhere in the object graph–by type, or even by specific property name
- Handle proxies or enhanced objects (like those of Hibernate, or the various mock frameworks)
- Apply bi-directional mapping with one configuration
- Map to instances of an appropriate concrete class for a target abstract class or interface
官网上的例子,先创建一个映射规则的 mapper(当然,对于字段名相同的对象可以使用默认 mapper):
MapperFactory factory = new DefaultMapperFactory.Builder().build(); factory.registerClassMap(factory.classMap(Order.class,OrderDTO.class) .field("product.state.type.label", "stateLabel") .field("product.name", "productName").toClassMap()); MapperFacade mapper = factory.getMapperFacade();
然后注册转换器(组件也内置了一些常用的转换器):
// register your converter mapperFactory.getConverterFactory().registerConverter(new CustomConverter<Double, String>() { public BigDecimal convert(Double source, Type<? extends BigDecimal> destinationClass) { return new BigDecimal(source); } });
然后就可以执行转换了,比如:
mapper.map(blah, Blah.class);
至于 Dozer,从功能上也差不多,支持各种类型的映射配置,包括注解、XML 文件和 API 调用等等。
Dozer is a Java Bean to Java Bean mapper that recursively copies data from one object to another. Typically, these Java Beans will be of different complex types.
public class SourceBean { private Long id; private String name; @Mapping("binaryData") private String data; @Mapping("pk") public Long getId() { return this.id; } public String getName() { return this.name; } }
注解这种转换的风格我还是很喜欢的。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
MapStruct
另外,remote 的 User 需要从本地 User 生成的 JSON 中进行复原,同理,本地 User 也需要从 remote User 生成的 JSON 中进行复原,感觉跟定义两个 transform 方法代码及维护成本没有降低啊
hi,对于借由一个中间层来承载数据这个有些疑问,以图例来说,如果 Service 和 DAO 层的 User 定义并不是完全一致,那么生成的 JSON 形式的 User 也不是相同的,这时,remote 的 User 怎么样做到以通用的方法从这两种模型的解析出自己的 User 类型呢?反之亦然。或者说,前提条件就是本地 Service 和 DAO 层的 User 就是一样的,要做的只是与 remote 的 User 模型的转换?
在很多情况下,转换的工作不可避免,但是可以减少一些冗余的转换,还可以减少一些中间对象表示的信息量。
举例来说,模块 A 需要的是 getUserA(),模块 B 是 getUserB() 方法,模块 C 是 getUserC(),
1. 如果没有一个中间转换数据(例如 {"user":{…}}),它们各自都要知道和耦合其它两个模块,转换的工作一共是 3×2=6 件,否则只有 3 件;
2.JSON 不需要自解释,也就不承载任何对象的类型或者约束信息,这是比 SOAP 消息简洁的原因之一,消息的解析和转换只有 JSON 的客户自己知道。