Java函数式编程(二)

本系列文章译自Venkat Subramaniam的Functional Programming in Java译者注:本篇会有点无聊,希望你不要睡着了。

第一章 你好,lambda表达式!

第二节:函数式编程的最大收获

函数式风格的代码有更高的信噪比;写的代码更少了,但每一行或者每个表达式做的却更多了。比命令式编程相比,函数式编程让我们获益良多:


  • 避免了对变量的显式的修改或赋值,这些通常是BUG的根源,并导致代码很难并行化。在命令行编程中我们在循环体内不停的对totalOfDiscountedPrices变量赋值。在函数式风格里,代码不再出现显式的修改操作。变量修改的越少,代码的BUG就越少。
  • 函数式风格的代码可以轻松的实现并行化。如果计算很费时,我们可以很容易让列表中的元素并发的执行。如果我们想把命令式的代码并行化,我们还得担心并发修改totalOfDiscountedPrices变量带来的问题。在函数式编程中我们只会在完全处理完后才访问这个变量,这样就消除了线程安全的隐患。
  • 代码的表达性更强。命令式编程要分成好几个步骤要说明要做什么——创建一个初始化的值,遍历价格,把折扣价加到变量上等等——而函数式的话只需要让列表的map方法返回一个包括折扣价的新的列表然后进行累加就可以了。
  • 函数式编程更简洁;和命令式相比同样的结果只需要更少的代码就能完成。代码更简洁意味着写的代码少了,读的也少了,维护的也少了——看下第7页的"简洁少就是简洁了吗"。
  • 函数式的代码更直观——读代码就像描述问题一样——一旦我们熟悉语法后就很容易能看懂。map方法对集合的每个元素都执行了一遍给定的函数(计算折扣价),然后返回结果集,就像下图演示的这样。


图1——map对集合中的每个元素执行给定的函数有了lambda表达式之后,我们可以在Java里充分发挥函数式编程的威力。

使用函数式风格,就能写出表达性更佳,更简洁,赋值操作更少,错误更少的代码了。

支持面向对象编程是Java一个主要的优点。函数式编程和面向对象编程并不排斥。真正的风格变化是从命令行编程转到声明式编程。在Java 8里,函数式和面向对象可以有效的融合到一起。我们可以继续用OOP的风格来对领域实体以及它们的状态,关系进行建模。除此之外,我们还可以对行为或者状态的转变,工作流和数据处理用函数来进行建模,建立复合函数。

第三节:为什么要用函数式风格?

我们看到了函数式编程的各项优点,不过使用这种新的风格划得来吗?这只是个小改进还是说换头换面?在真正在这上面花费工夫前,还有很多现实的问题需要解答。


小明问到:代码少就是简洁了吗?

简洁是少而不乱,归根结底是说要能有效的表达意图。它带来的好处意义深远。

写代码就好像把配料堆到一起,简洁就是说能把配料调成调料。要写出简洁的代码可得下得狠工夫。读的代码是少了,真正有用的代码对你是透明的。一段很难理解或者隐藏细节的短代码只能说是简短而不是简洁。

简洁的代码竟味着敏捷的设计。简洁的代码少了那些繁文缛节。这是说我们可以对想法进行快速尝试,如果不错就继续,如果效果不佳就迅速跳过。



用Java写代码并不难,语法简单。而且我们也已经对现有的库和API很了如指掌了。真正难的是要拿它来开发和维护企业级的应用。我们要确保同事在正确的时间关闭了数据库连接,还有他们不会不停的占有事务,能在合适的分层上正确的处理好异常,能正确的获得和释放锁,等等。

这些问题任何一个单独来看都不是什么大事。不过如果和领域内的复杂性一结合的话,问题就变得很棘手了,开发资源紧张,难以维护。如果把这些策略封装成许多小块的代码,让它们各自进行约束管理的话,会怎么样呢?那我们就不用再不停的花费精力去实施策略了。这是个巨大的改进, 我们来看下函数式编程是如何做到的。

疯狂的迭代

我们一直都在写各种迭代来处理列表,集合,还有map。在Java里使用迭代器再常见不过了,不过这太复杂了。它们不仅占用了好几行代码,还很难进行封装。

我们是如何遍历集合并打印它们的?可以使用一个for循环。我们怎么从集合里过滤出一些元素?还是用for循环,不过还需要额外增加一些可修改的变量。选出了这些值后,怎么用它们求出最终值,比如最小值,最大值,平均值之类的?那还得再循环,再修改变量。

这样的迭代就是个万金油,啥都会点,但样样稀松。现在Java为许多操作都专门提供了内建的迭代器:比如只做循环的,还有做map操作的,过滤值的,做reduce操作的,还有许多方便的函数比如 最大最小值,平均值等等。

除此之外,这些操作还可以很好的组合起来,因此我们可以将它们拼装到一起来实现业务逻辑,这样做既简单代码量也少。而且写出来的代码可读性强,因为它从逻辑上和描述问题的顺序是一致的。我们在第二章,集合的使用,第19页会看到几个这样的例子,这本书里这样的例子也比比皆是。

应用策略
策略贯穿于整个企业级应用中。比如,我们需要确认某个操作已经正确的进行了安全认证,我们要保证事务能够快速执行,并且正确的更新修改日志。这些任务通常最后就变成服务端的一段普通的代码,就跟下面这个伪代码差不多:
Transaction transaction = getFromTransactionFactory();
//... operation to run within the transaction ...
checkProgressAndCommitOrRollbackTransaction();
UpdateAuditTrail();
这种处理方法有两个问题。首先,它通常导致了重复的工作量并且还增加了维护的成本。第二,很容易忘了业务代码中可能会被抛出来的异常,可能会影响到事务的生命周期和修改日志的更新。这里应该使用try, finally块来实现,不过每当有人动了这块代码,我们又得重新确认这个策略没有被破坏。还有一种方法,我们可以去掉工厂,把这段代码放在它前面。不用再获取事务对象,而是把执行的代码传给一个维护良好的函数,就像这样:
runWithinTransaction((Transaction transaction) -> {
  //... operation to run within the transaction ...
}); 
这是你的一小步,但是省了一大堆事。检查状态同时更新日志的这个策略被抽象出来封装到了runWithinTransaction方法里。我们给这个方法发送一段需要在事务上下文里运行的代码。我们不用再担心谁忘了执行这个步骤或者没有处理好异常。这个实施策略的函数已经把这事搞定了。我们将会在第五章中介绍如果使用lambda表达式来应用这样的策略。
扩展策略

策略看起来无处不在。除了要应用它们外,企业级应用还需要对它们进行扩展。

我们希望能通过一些配置信息来增加或者删除一些操作,换言之,就是能在模块的核心逻辑执行前进行处理。这在Java里很常见,不过需要预先考虑到并设计好。

需要扩展的组件通常有一个或者多个接口。我们需要仔细设计接口以及实现类的分层结构。这样做可能效果很好,但是会留下一大堆需要维护的接口和类。这样的设计很容易变得笨重且难以维护,最终破坏扩展的初衷。

还有一种解决方法——函数式接口,以及lambda表达式,我们可以用它们来设计可扩展的策略。我们不用非得创建新的接口或者都遵循同一个方法名,可以更聚焦要实现的业务逻辑,我们会在73页的使用lambda表达式进行装饰中提到。

轻松实现并发

一个大型应用快到了发布里程碑的时候,突然一个严重的性能问题浮出水面。团队迅速定位出性能瓶颈点是出在一个处理海量数据的庞大的模块里。团队中有人建议说如果能充分发掘多核的优势的话可以提高系统性能。

不过如果这个庞大的模块是用老的Java风格写的话,刚才这个建议带来的喜悦很快就破灭了。

团队很快意识到要这把这个庞然大物从串行执行改成并行需要费很大的精力,增加了额外的复杂度,还容易引起多线程相关的BUG。难道没有一种提高性能的更好方式吗?

有没有可能串行和并行的代码都是一样的,不管选择串行还是并行执行,就像按一下开关,表明一下想法就可以了?

听起来好像只有纳尼亚里面能这样,不过如果我们完全用函数式进行开发的话,这一切都将成为现实。内置的迭代器和函数式风格将扫清通往并行化的最后一道障碍。JDK的设计使得串行和并行执行的切换只需要一点不起眼的代码改动就可以实现,我们将会在145页《完成并行化的飞跃》中提到。

讲故事

在业务需求变成代码实现的过程中会丢失大量的东西。丢失的越多,出错的可能性和管理的成本就越高。

如果代码看起来就跟描述需求一样,将会很方便阅读,和需求人员讨论也变的更简单,也更容易满足他们的需求。

比如你听到产品经理在说,”拿到所有股票的价格,找出价格大于500块的,计算出能分红的资产总和”。使用Java提供的新设施,可以这么写:

tickers.map(StockUtil::getprice).filter(StockUtil::priceIsLessThan500).sum() 
这个转化过程几乎是无损的,因为基本上也没什么要转化的。这是函数式在发挥作用,在本书中还会看到更多这样的例子,尤其是第8章,使用lambda表达式来构建程序,137页。
关注隔离

在系统开发中,核心业务和它所需要的细粒度逻辑通常需要进行隔离。比如说,一个订单处理系统想要对不同的交易来源使用不同的计税策略。把计税和其余的处理逻辑进行隔离会使得代码重用性和扩展性更高。在面向对象编程中我们把这个称之为关注隔离,通常用策略模式来解决这个问题。解决方法一般就是创建一些接口和实现类。

我们可以用更少的代码来完成同样的效果。我们还可以快速尝试自己的产品思路,而不用上来就得搞出一堆代码,停滞不前。我们将在63页的,使用lambda表达式进行关注隔离中进一步探讨如果通过轻量级函数来创建这种模式以及进行关注隔离。

惰性求值

开发企业级应用时,我们可能会与WEB服务进行交互,调用数据库,处理XML等等。我们要执行的操作有很多,不过并不是所有时候都全部需要。避免某些操作或者至少延迟一些暂时不需要的操作是提高性能或者减少程序启动,响应时间的一个最简单的方式。

这只是个小事,但用纯OOP的方式来实现还需要费一番工夫。为了延迟一些重量级对象的初始化,我们要处理各种对象引用 ,检查空指针等等。不过,如果使用了新的Optinal类和它提供的一些函数式风格的API,这个过程将变得很简单,代码也更清晰明了,我们会在105页的延迟初始化中讨论这个。

提高可测性

代码的处理逻辑越少,容易被改错的地方当然也越少。一般来说函数式的代码比较容易修改,测试起来也较简单。

另外,就像第4章,使用lambda表达式进行设计和第5章资源的使用中那样,lambda表达式可以作为一种轻量级的mock对象,让异常测试变得更清晰易懂。lambda表达式还可以作为一个很好的测试辅助工具。很多常见的测试用例都可以接受并处理lambda表达式。这样写的测试用例能够抓住需要回归测试的功能的本质。同时,需要测试的各种实现都可以通过传入不同的lambda表达式来完成。

JDK自己的自动化测试用例也是lambda表达式的一个很好的应用范例——想了解更多的话可以看下OpenJDK仓库里的源代码。通过这些测试程序可以看到lambda表达式是如何将测试用例的关键行为进行参数化;比如,它们是这样构建测试程序的,“新建一个结果的容器”,然后“对一些参数化的后置条件进行检查”。


我们已经看到,函数式编程不仅能让我们写出高质量的代码,还能优雅的解决开发过程中的各种难题。这就是说,开发程序将变得更快更简单,出错也更少——只要你能遵守我们后面将要介绍到的几条准则。


未完待续,后续文章请继续关注deepinmind


原创文章转载请注明出处:Java译站

Java函数式编程(二),布布扣,bubuko.com

Java函数式编程(二)

上一篇:C++赋值运算符、函数调用运算符、下标运算符(“=”、“()”、“[]”)重载


下一篇:读书笔记_Effective_C++_条款三十六:决不重新定义继承而来的non-virtual函数