您是否曾经使用过一个应用程序,在该应用程序中您必须先将数据从一个对象复制到另一个对象,再依此类推,然后才能真正对其进行处理? 您是否曾经编写过将XML数据从DTO转换为业务对象到JDBC语句的代码? 一次又一次地针对每个不同的数据类型进行处理? 然后,您遇到了许多“企业”(读作“过度设计”)应用程序的反通用模式,我们可以将其称为“无尽映射死亡之行”。 让我们看一个受此反模式影响的应用程序,以及如何以一种更好,更精简和易于维护的形式重写它。
注意:这比坏代码与好代码更多的是设计讨论,但我仍然认为它适合本博客。 GitHub上提供了“好”部分的代码。
该应用程序“激动人心的时尚世界”(简称WTF)收集并存储有关新设计的连衣裙的信息,并通过REST API使其可用。 每一件可怜的衣服都必须经过以下转换,才能吸引忠实的时尚迷:
- 从XML解析为特定于XML的XDress对象
- 处理并转换为特定于应用程序的Dress对象
- 转换为MongoDB的DBObject,以便可以将其存储在DB中(作为JSON)
- 从DBObject转换回Dress对象
- 从Dress转换为JSON字符串
乌夫,这是很多工作! 每个转换都是手动编码的,如果我们想扩展WTF以提供有关时髦鞋的信息,我们将需要再次编码所有这些。 (加上我们MongoDAO中的几种方法,例如getAllShoes和storeShoes。)但是我们可以做得更好!
消除手动转换
在您实际上想花费时间来建立业务逻辑而不是一些样板代码时,对所有转换进行编码非常耗时,容易出错并且很烦人。 我们可以通过两种方式消除手工工作:
- 概括转换,以便只需要编写一次(可能利用现有的转换库)
- 消除它们,例如在整个处理链中使用相同的数据格式
公平地讲,我必须承认手动方法也有一些优点:您对对象的形式具有完全的(傻瓜?)功能,并且可以完美地适合所需的处理,而无需引入庞大且复杂的对象。越野车图书馆,而且如果由LOC支付,您将获得更多的收入。
但是,上面提到的缺点几乎总是抵消了优点,特别是如果您重复使用合适的,成熟的,高质量的库,这些库使您可以根据需要将处理自定义为任何细节。
仍然存在一个问题:我们如何表示数据? 我们有两种可能性:
- 具有通用数据结构,即地图。 这在动态函数语言(例如Clojure)中很常见,并且非常容易和舒适。
- 优点:工作量少,非常灵活,可以轻松应用常规操作(地图,过滤器等)
- 具有特定于每种数据类型的对象,即POJO(例如DressVariant,Shoes)
- 优点:类型安全,编译器有助于确保您的代码正确,可能更易于理解
- 缺点:您必须为每个可能处理的数据元素编写并维护一个类
旁注:业务领域
您可能会跳过本节,仅在以后想要了解设计背后的原因时再回来。
WTF必须对其检索到的着装元素进行一些处理,这主要是因为多个元素可能仅在颜色等轻微变化的情况下代表同一件衣服。 因此,WTF将此类相关元素存储为父Dress对象内的DressVariant项目列表,为Dress生成唯一的ID,并将输入元素的ID存储在名为“ externalIds”的属性中。 因此,N个输入元素变为具有1+ DressVariants的M个打扮元素,M <=N。
WTF还必须对其WTF XML输入进行其他处理,例如检测哪些图像是真实的,哪些图像只是伪造的占位符,但我们不会对此进行讨论。
实现静态类型的泛型处理
我决定继续为每个数据类型提供一个类,以免与当前实现产生太大差异。 现在我们如何使手动转换通用且可重用?
首先让我们看看我如何构建处理管道:
fetchFrom("http://wtf.example.com/atom/dresses.xml")
.parseNodesAt("/feed/dress")
.transform(DressVariant.class, new DressDeduplicatingTransformer()); // Transformer
.transform(new PojoToDboTransformer()); // Transformer
.store(new MongoDAO());
// + we'll use DBObject.toMap() + PojoToJson mapper when serving the data via REST
因此,我们从URL提取XML,将其发送到解析器以提取一些节点,这些节点会自动转换为DressVariant对象,接下来我们使用一个转换器将多个DressVariants合并为一个统一的Dress对象,最后将结果POJO转换为一个Mongo DBObject,然后再将其存储到数据库中。 我们将什么用于转换?
- XML-> DressVariant:使用JAXB将Nodes转换为以@XmlRootElement注释的POJO。 请注意,如果需要,您可以自定义JAXB非常执行的转换。 因此,您只需要创建一个简单的POJO并添加一个注释即可。
- DressVariant->连衣裙:我们将检查MongoDB并发送添加了DressVariant的现有Dress或新的Dress对象(如果连衣裙在输入源中确实具有多种格式,则将导致多次更新,但这不是对我们来说是一个问题)。 这种转换是特定于类型的,即,对于每种数据类型,我们都必须编写自己的转换代码。 这很好,因为例如Shoes不需要任何此类重复数据删除处理/转换。
- 着装-> DBObject:我们将使用Jackson Mongo Mapper,以及一流的JSON映射库的扩展,它增加了对Mongo DB的支持。 它还将执行Mongo所需的一些特殊数据清理,例如替换'。 在地图键中以“-”表示。
- DBObject-> MongoDB:我们将有一个通用方法
storeDocument(String collectionName, DBObject doc)
,其中集合名称是从原始对象派生的(例如DressVariant->“ dressVariants”)。 该文档的属性ID应该是其唯一标识符(因此,我们将根据其[missing]值进行更新或插入)。 - MongoDB-> DBObject:还是一个通用方法,
list(String collectionName)
- DBObject-> Map:DBObject自己完成
- Map- > JSON:当将其发送给客户端时,我们将使用Jersey REST库的PojoMapping功能将由我们的方法生成的Map自动转换为JSON。
- JSON->客户端:我们将有一个GenericCollectionResource,其列表方法映射到URL / list / {collectionName}”。 它将按照说明从Mongo加载集合,并返回一个列表,该列表会由Jersey自动转换为JSON。
结果:除了自定义特定于数据类型的转换之外, 对于每种数据类型 ,无需1个POJO,4个手工编码的转换器和2 + 2个方法,现在,每个数据类型仅需要1个POJO加一个通用转换器,4个通用方法和一两个库。 更少的编码,更少的代码,更少的缺陷,更多的生产力,更多的乐趣。
请注意,由于我们选择了库,因此如果默认转换方案对我们来说不够用,我们可以根据需要进行任意调整–尽管我们当然不希望这样做。 最好牺牲一些灵活性和更适合的数据格式,而不要进行太多的调整,而不是利用映射库,而要花很多精力。 明智的人选择自己的战斗。
样例代码
GitHub上提供了示例代码,该示例代码演示了自动的通用映射XML-> Java-> Mongo->带有REST的JSON –generic-pojo-mappers。
总结与结论
许多应用程序迫使开发人员在许多对象之间转换数据,这非常无用且容易出错。 更好的方法是避免转换,并在整个处理过程中尽可能使用相同的对象,仅在确实必要时进行转换。 与对每种数据类型进行手工编码相比,以通用和可重用的方式编写这些转换效果更好,并且为此使用通常使用的成熟映射库通常会有所作为(尽管您必须确保预期用途与其原理和设计保持一致) )。
在整个处理过程中使用相同的对象会导致它不太适合各个处理阶段,但使它们的编写和维护变得更加容易和快捷。 由于使用反射,我们会损失一些性能,但是就I / O(通过HTTP检索文件,将数据发送到DB)和XML解析而言,这可以忽略不计。
在“激动人心的时尚世界”的示例中,我们大大减少了手动编码和方法的数量,结果是更小,更简洁,更灵活的代码(wrt添加了新的数据类型)。
批评
“但是我真的需要为每个处理层使用经过微调的对象!”
您的选择(如果确实需要)可以执行该操作-但请注意您为此付出了多少。
嗯,是。 有时最好手动编码事物,但并非总是如此。 确保不要以与预期不同的方式使用库,因为这样做可能会使您浪费更多的时间而不是提高生产力。
“你是个笨蛋!”
是的, 很多人都这样认为 。 感谢您的阅读。
“如果我写这样的代码,你说我是白痴吗?”
完全没有,您可能有充分的理由这样做。 否则您可能不知道其他选择。 或者,您只是没有像我一样强烈不喜欢编写无意识的代码。 没关系。
有关
Adam Bien提供的丰富的Persistent Domain Object + slim Gateway模式也使以提高生产率的名义在所有应用程序(Web UI – DB)中使用同一对象(JPA实体)成为可能。
从The Holy Java重新发布。