精练代码:一次Java函数式编程的重构之旅

一、基础知识

二、重构前

三、重构过程

从小处着手

重复的foreach代码

lambda取代内部类

简单而有益的隔离

回调接口改造成函数接口

新的需求

抽离异常处理

抽离并发处理

过程式改函数式

更函数式的风格

模拟柯里化

四、小结

五、重构后

ConcurrentDataHandlerFrameRefactored

ExecutorUtil

TaskUtil

CatchUtil

StreamUtil

摘要:通过一次并发处理数据集的Java代码重构之旅,展示函数式编程如何使得代码更加精练。

难度:中级

一、基础知识

在开始之前,了解“高阶函数”和“泛型”这两个概念是必要的。

高阶函数就是接收函数参数的函数,能够根据传入的函数参数调节自己的行为。类似C语言中接收函数指针的函数。最经典的就是接收排序比较函数的排序函数。高阶函数不神秘哦!在Java8之前,就是那些可以接收回调接口作为参数的方法;在本文中,那么接收 Function, Consumer, Supplier 作为参数的函数都是高阶函数。高阶函数使得函数的能力更加灵活多变。

泛型是能够接纳多种类型作为参数进行处理的能力。很多函数的功能并不限于某一种具体的类型,比如快速排序,不仅可以用于整型,也可以用于字符串,甚至可用于对象。泛型使得函数在类型处理上更加灵活。

高阶函数和泛型两个特点结合起来,可使得函数具备强大的抽象表达能力。

二、重构前

基本代码如下。主要用途是根据具体的业务数据获取接口 IGetBizData ,并发地获取指定Keys值对应的业务数据集。

代码本身写得不坏,没有拗口的地方,读起来也比较流畅。美中不足的是,不够通用化。 心急的读者可以看看最后面重构后的代码。这里还是从重构过程开始。

三、重构过程

1、从小处着手

如果面对一大块代码不知如何下手,那么就从小处着手,先动起来。 对于如下代码,了解 Java8 Stream api 的同学肯定知道怎么做了:

可以写成一行代码:

不过, 写多了, collect(Collectors.toList()) 会大量出现,占篇幅,而且当 map 里的函数比较复杂时,IDE 有时不能自动补全。注意到这个函数其实就是传一个列表和一个数据处理函数,因此,可以抽离出一个 StreamUtil ,之前的代码可以写成:

看上去是一个很平常的改动,实际上是一大步。注意到 map(keys, key -> Integer.valueOf(key) % 1000000000) 并没有展示该如何去计算,只是表达了要做什么计算。 从“关注计算过程” 到“描述计算内容”,实现了计算“描述” 与“执行”的关注点分离。

好滴,已经走出了第一步!

2、重复的foreach代码

自从了解了函数编程,似乎对重复的foreach代码生出“仇”了,恨不得消灭干净。 读者可以看到方法 getKeys 和 getAllData (从completionService获取数据时) 分别有一段foreach循环,通过计数然后添加元素并返回一个列表(具体就不贴代码了)。这样的代码看多了也会厌倦的。 实际上,可以抽离出一个 ForeachUtil 的公用类来做这个事情。为避免代码占篇幅,读者可以看重构后的 ForeachUtil, 然后 getKeys 的实现就可以凝练为一行代码:

棒! 每次将多行代码变成一行代码是不是很爽?更重要的是,每次都抽离出了通用的部分,可以让后面的代码更好写。

注意到,由于 lambda 表达式无法处理受检异常,因此,将 get 函数抽离出来成为一个函数,lambda 表达式就显得更好看一点。

3、lambda取代内部类

注意到 getAllData 里有一个比较难看的内部类,是为了根据一段逻辑生成一个任务类:

实际上,优秀的IDE工具比如 Intellj 会自动提示要不要替换成 lambda 。 就依它的建议:

又是一行代码! 干净利落!

4、简单而有益的隔离

这里有一段代码,根据任务划分的区段范围,获取数据集的指定子集:

本来是一段容易编写单测的独立逻辑块,混在 getAllData 方法里,一来让这段代码的单测难写了,二来增加了整个方法 getAllData 的单测编写麻烦度。真是两不讨好。抽离出去更好。可参见重构后的TaskUtil. 很多程序猿都有这个容易导致单测难写的不良习惯。

5、回调接口改造成函数接口

接下来做什么呢? 看上去小的改动似乎到尽头了。 现在,可以考虑改造回调接口了。实际上,函数接口是回调接口的非常有效的替代者。可以把 getAllData 的参数 final IGetBizData iGetBizData 改成 Function iGetBizDataFunc,表示这个函数将作用于一个列表keys,返回指定的数据集。相应的,iGetBizData.getData(tmpRowkeyList) 就可以改成 iGetBizDataFunc.apply(tmpRowkeyList) 。 就是这么简单!

读者可能会疑惑,这样改究竟有什么益处呢?第一个好处就是可以移除 iGetBizData 接口定义了。 java8之前,每次写回调,都得定义一个接口,再写实现类,烦不烦?

6、新的需求

假设现在我不仅需要并发获取数据,还需要并发处理数据得到一个数据列表,该怎么办呢?看上去 getAllData 已经有潜力满足需求了,可是还有一些细节要处理。实际上,无非就是给定一个T类型列表,以及一个处理列表并返回另一个R类型列表的函数,然后利用 getAllData 已有的功能就可以实现。 可以抽离出一个底层的 public staticListhandleAllData(ListallKeys, Function handleBizDataFunc) 方法,然后将 getAllData 的实现移入其中,对类型略加改造,就可以实现。然后 getAllData 就可以依赖 handleAllData 来实现了。泛型很强大!

7、抽离异常处理

我们常常会在代码里看到很多 try-catch 语句块。大多数程序猿可能并不觉得有什么,可是,重复就是代码罪恶之源。实际上,消除这些重复有一个简单的技巧:首先看这些重复函数里有哪几行语句是不一样的(通常是一行或两三行),抽离出 Function (单参数单返回函数) 或 Consumer (单参数无返回函数) 或 BiFunction (双参数单返回函数) 或 BiConsumer (双参数无返回函数) , 然后将这个函数接口作为参数传进去。 function 的方法是 apply, consumer 的方法是 accept ;

重构后的代码可见 CatchUtil 。 实际上很像 Python 里的装饰器,通过封装函数的 try-catch ,给任何函数添加异常处理。 不过 Python 有万能函数 func(*args, **keyargs) , Java 没有可以表示所有函数接口的万能函数。可参见文章: python使用装饰器捕获异常。

8、抽离并发处理

接下来,我们需要抽离出并发处理。客户端代码不需要知道数据处理的细节,它只需要传一个数据列表和一个数据处理函数,其他都交给框架层。略加修改后,可参见重构后的代码 ExecutorUtil. 原来一团代码经过精练后,长度减少了很多。handleAllData 现在变成了这样:

抽离并发处理的益处在于,可以在后续使用策略模式,提供串行计算策略和并发计算策略,在不同场景下选择不同的计算策略。重构后的代码没有展示这一点。读者可以一试。

9、过程式改函数式

10、更函数式的风格

注意到 handleAllData 需要传一个数据列表 allKeys ; 更函数式的风格,这个列表应该是一个数据提供函数来获得的。可以使用 Supplier 来抽象。它有一个 get 函数。 可以将 参数改成 Supplier getAllKeysFunc,然后用 getAllKeysFunc.get() 来取代之前的列表 allKeys.

这样有什么益处呢? 抽离了列表 allKeys 的来源,现在可以从任意地方获取,比如从文件或网络中获取,只要传入一个数据提供函数即可,这使得 handleAllData 的处理范围更加灵活了。

11、模拟柯里化

了解柯里化的同学知道,柯里化是将多元函数分解为多个单元函数的多次调用的过程,在每一次分解的过程中,都会产生大量的副产品函数,是一个强大的函数工厂。柯里化的简单介绍可参见文章: 函数柯里化(Currying)示例 。

(http://www.cnblogs.com/lovesqcc/p/5398758.html)

如何使用 Java 模拟柯里化呢? 这要求一个并发数据处理函数返回一个函数 Function 而不是一个值列表,而返回的函数是可定制化的。这部分通过尝试及IDE的提示,而完成的。见如下代码:

然后,客户端的代码就更加有函数式风格了(甚至显得有点“另类”)。 第一个 handleAllData 接受一个数据处理函数,并返回一个封装了并发处理的数据处理函数,可以对任意指定数据集进行处理; 第二个 handleAllData 接受一个数据提供函数, 并返回一个封装了并发处理的数据处理函数,通过指定定制化的数据处理函数来实现计算。apply 里的对象是一个 Function ! 是不是有点思维反转 ? ^_^ 仔细再体味一下

当然,这里并不是真正的柯里化,因为参数只有一个。Scala 的柯里化是指 f(x)(y) = x+y ; f(x) = f(x)(1) = x+1 ; f(y) = f(1)(y) = 1+y ; 可以通过 f(x)(y) 将x或y代入不同的变量得到任意多的函数。利用柯里化很容易写成简洁的微框架,比如一个文件集合处理框架。 filesHandler(files)(handler) 与 filesHandler(hanler)(files) 是不一样的。这里不再过多讨论。

四、小结

通过使用函数式编程对过程/对象混合式代码进行重构,使得代码更凝练而有表达力了。虽然函数式编程尚未广泛推广于大型工程中,只有一部分程序猿开始尝试使用,在理解上也需要一定的思维转换,不过适度使用确实能增强代码的抽象表达力。仅仅是“高阶函数+泛型+惰性求值”的基本使用,就能产生强大而凝练的表达效果。 函数式编程确有一套自己独特的编程设计理念。 推荐阅读《Scala函数式编程》。

现代软件开发已经不仅仅是单纯地编写代码实现逻辑,而是含有很强的设计过程。需要仔细提炼概念、对象、操作,仔细设计对象之间的交互,有效地组合一系列关联对象成为高内聚低耦合的模块,有效地隔离对象关联最小化依赖关系,如此才能构建出容易理解和扩展、更容易演进的长久发展的软件。编程即是设计,从具象到抽象再到具象的过程。

五、重构后

重构后的代码是这样子滴:

1、ConcurrentDataHandlerFrameRefactored

2、ExecutorUtil

3、TaskUtil

4、CatchUtil

5、StreamUtil

如果您觉得不错,请别忘了转发、分享、点赞让更多的人去学习, 您的举手之劳,就是对小乐最好的支持,非常感谢!

来自:琴水玉

链接:

http://www.cnblogs.com/lovesqcc/p/7077971.html

著作权归作者所有。本文已获得授权。欢迎来投稿。

每日英文

If you wait to do everything until you're sure it's right, you'll probably never do much of anything.

如果你等到每件事都确定是对的才去做,那你也许永远都成不了什么事。

乐乐有话说

再难也要坚持,再好也要淡泊,再差也要自信,再多也要节省。

看完本文有收获?请转发分享给更多人

关注「杨守乐」,提升技能

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180122G02Y5Q00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券