Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

对象转换的问题

Posted on 09/29/201210/08/2024 by 四火

image 有句话叫做 “计算机科学领域任何问题,都可以间接的通过添加一个中间层来解决”,但是唯一解决不了的问题,是层次本身过多的问题。每一层内都会维护自己在乎的数据对象模型。层与层之间数据的传递,就不可避免地遇到对象类型转换的问题。

这个话题也和最近的项目有关。我们在重构一个老旧的系统,所做的第一件事情,就是要把数据访问层从原有系统中剥离出来,我们精心设计了这一层的模型和结构,但是要让原有系统平缓地从原有数据访问方式上移植到新的数据访问层上,就涉及到上层(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){
    ... ...
}

但是在使用过程中,发现存在着这样一些问题:

  1. 一个 UserNew/UserOld 对象有 40 个属性,这样的一次 transform 就要写 40+行这样毫无营养的 get/set 代码,而再提供一个反向转换的方法这样的代码需要×2;整个系统存在二三十种 model,这样啰嗦的转换令人恶心;再者,我们发现,层次可能很多——比如我们在使用一些序列化框架时,需要借由类似的方法将当前对象转换成框架需要的 POJO 对象,因此一个 User 就让我们做了很多次这样丑陋的转换。
  2. 转换并不是那么顺利的,经常遇到类型不同的情况,需要经过类型转换或者简单的逻辑处理。比如对于空值的特殊处理,对于 0 值的特殊处理等。
  3. 转换甚至都不一定是一对一的,特殊情形的处理被迫使用到的逻辑,让整个转换层和业务模块中的很多发生耦合……这不是我希望看到的。

如何思考和解决这样的问题?其实这个问题有很多种表现形式,比如 PO-VO 对象的互转换等等。这里的争论很多,我整理如下:

1、如果能够尽量保证模型的字段名和和类型一致,可以利用 Spring 的 copyProperties 方法来完成 POJO 对象的拷贝:

BeanUtils.copyProperties(srcObj, desObj);

不过这个方法也有一些缺陷,一个是反射导致的性能损失,一次反射并不明显,对象拷贝可以说是非常频繁的;还有一个是对于一些类型不同的情况,我们需要自定义一些转换逻辑来处理这样的特殊情形。

2、借由一个中间层来承载数据,这样的中间层往往是可序列化的,比如 JSON 格式,每一种 String、int 等基础的类型都有转换成 JSON 的统一处理办法,所有数据的转换都通过通用方法转成 JSON 格式,然后再根据目标对象对各字段格式的要求,把 JSON 表示的对象复原。这种办法需要的框架性代码比较多,而且通过序列化对象作为中间介质,不免存在性能损耗的问题,但是对于存在大量数据转换的情况,也不失为一种好办法:

image

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;
    }
}    

注解这种转换的风格我还是很喜欢的。

文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》

×Scan to share with WeChat

你可能也喜欢看:

  1. 泛型趣谈
  2. 关于 if (someobject != null) 的问题
  3. 运行时动态增加枚举类型
  4. 泛型传递
  5. 看 JDK 源码,解几个疑问

4 thoughts on “对象转换的问题”

  1. Anonymous says:
    07/28/2020 at 7:02 AM

    MapStruct

    Reply
  2. ZhanGNer says:
    10/27/2012 at 10:47 PM

    另外,remote 的 User 需要从本地 User 生成的 JSON 中进行复原,同理,本地 User 也需要从 remote User 生成的 JSON 中进行复原,感觉跟定义两个 transform 方法代码及维护成本没有降低啊

    Reply
  3. ZhanGNer says:
    10/27/2012 at 10:44 PM

    hi,对于借由一个中间层来承载数据这个有些疑问,以图例来说,如果 Service 和 DAO 层的 User 定义并不是完全一致,那么生成的 JSON 形式的 User 也不是相同的,这时,remote 的 User 怎么样做到以通用的方法从这两种模型的解析出自己的 User 类型呢?反之亦然。或者说,前提条件就是本地 Service 和 DAO 层的 User 就是一样的,要做的只是与 remote 的 User 模型的转换?

    Reply
    1. 四火 says:
      10/28/2012 at 11:01 PM

      在很多情况下,转换的工作不可避免,但是可以减少一些冗余的转换,还可以减少一些中间对象表示的信息量。

      举例来说,模块 A 需要的是 getUserA(),模块 B 是 getUserB() 方法,模块 C 是 getUserC(),

      1. 如果没有一个中间转换数据(例如 {"user":{…}}),它们各自都要知道和耦合其它两个模块,转换的工作一共是 3×2=6 件,否则只有 3 件;

      2.JSON 不需要自解释,也就不承载任何对象的类型或者约束信息,这是比 SOAP 消息简洁的原因之一,消息的解析和转换只有 JSON 的客户自己知道。

      Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Spark 互联网 亚马逊 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 眼界 程序员 管理 系统设计 缓存 编程范型 美股 英语 西雅图 设计 问题 面向对象 面试

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Machine Learning and Artificial Intelligence (6)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 聊一聊分布式系统中的时间
  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • Ticket: TRANSACTION 1.922915 BTC. Go to withdrawal >> https://yandex.com/poll/enter/BXidu5Ewa8hnAFoFznqSi9?hs=20bd550f65c6e03103876b28cabc4da6& on 倔强的程序员
  • panshenlian.com on 初涉 ML Workflow 系统:Kubeflow Pipelines、Flyte 和 Metaflow
  • panzhixiang on 关于近期求职的近况和思考
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme