测试、调试和重构
重构、测试驱动开发(TDD)和持续集成(CI)越来越流行,如果我们需要将 Lambda 表达式应用于日常编程工作中,就得学会如何为它编写单元测试。通过这一章,我们来看看如何使用 Lambda 表达式提高非集合类代码的质量。
重构候选项(Lambda Refactoring Candidates)
下面的一些要点,可以帮助你确定什么时候应该 Lambda 化(lambdafication)自己的应用或类库。其中的每一条都可看作一个局部的反模式或代码异味,它们可以借助于 Lambda 化进行修复。
进进出出、摇摇晃晃(In, Out, In, Out, Shake It All About)
在程序中记录日志,例:
Logger logger = new Logger();
if (logger.isDebugEnabled()) {
logger.debug("Look at this: " + expensiveOperation());
}
这段代码先调用 isDebugEnabled
方法抽取布尔值,用来检查是否启用调试级别,如果启用,则调用 Logger
对象的相应方法记录日志。如果你发现自己的代码不断地查询和操作某对象,目的只为了在最后给该对象设个值,那么这段代码就本该属于你所操作的对象。
这种反模式通过传入代码即数据的方式很容易解决。与其查询并设置一个对象的值,不如传入一个 Lambda 表达式,该表达式按照计算得出的值执行相应的行为。当程序处于调试级别,并且检查是否使用 Lambda 表达式的逻辑被封装在 Logger
对象中时,才会调用 Lambda 表达式。例:
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());
上述记录日志的例子也展示了如何使用 Lambda 表达式更好地面向对象编程(OOP),面向对象编程的核心之一是封装局部状态,比如日志的级别。通常这点做得不是很好, isDebugEnabled
方法暴露了内部状态。如果使用 Lambda 表达式,外面的代码根本不需要检查日志级别。
孤独的覆盖(The Lonely Override)
这个代码异味是使用继承,其目的只是为了覆盖一个方法。例如:
ThreadLocal<Album> thisAlbum = new ThreadLocal<Album> () {
@Override
protected Album initialValue() {
return database.lookupCurrentAlbum();
}
};
在Java8中,可以为工厂方法withInitial传入一个Supplier对象的实例来创建对象。例如:
ThreadLocal<Album> thisAlbum
= ThreadLocal.withInitial(() -> database.lookupCurrentAlbum());
第二个例子优于第一个的原因:
- 任何已有的 Supplier 实例不需要重新封装,就可以在此使用,这鼓励了重用和组合。
- 代码更加清晰。
- JVM 会少加载一个类。
同样的东西写两遍(Behavioral Write Everything Twice)
不要重复你劳动(Don’t Repeat Yourself,DRY)是一个众所周知的模式,它的反面是同样 的东西写两遍(Write Everything Twice,WET)。
不是所有 WET 的情况都适合 Lambda 化。有时,重复是唯一可以避免系统过紧耦合的方 式。什么时候该将 WET 的代码 Lambda 化?这里有一个信号可以参考。如果有一个整体上大概相似的模式,只是行为上有所不同,就可以试着加入一个 Lambda 表达式。
Lambda表达式的单元测试
单元测试是测试一段代码的行为是否符合预期的方式。
Lambda 表达式给单元测试带来了一些麻烦,Lambda 表达式没有名字,无法直接在测试代码中调用。
你可以在测试代码中复制 Lambda 表达式来测试,但这种方式的副作用是测试的不是真正的实现。假设你修改了实现代码,测试仍然通过,而实现可能早已在做另一件事了。
解决该问题有两种方式。第一种是将 Lambda 表达式放入一个方法测试,这种方式要测那个方法,而不是 Lambda 表达式本身。第二种是用方法引用,任何 Lambda 表达式都能被改写为普通方法,然后使用方法引用直接引用。
在测试替身时使用Lambda表达式
编写单元测试的常用方式之一是使用测试替身描述系统中其他模块的期望行为。这种方式
很有用,因为单元测试可以脱离其他模块来测试你的类或方法,测试替身让你能用单元测试来实现这种隔离。
【测试替身也常被称为模拟,事实上测试存根和模拟都属于测试替身。区别是模 拟可以验证代码的行为。比如Java中使用的Mokito
和PowerMokito
等】
惰性求值和调试
调试时通常会设置断点,单步跟踪程序的每一步。使用流时,调试可能会变得更加复杂,
因为迭代已交由类库控制,而且很多流操作是惰性求值的。
日志和打印消息
假设你要在集合上进行大量操作,你要调试代码,你希望看到每一步操作的结果是什么。
可以在每一步打印出集合中的值,这在流中很难做到,因为一些中间步骤是惰性求值的。
解决方案:peak
幸运的是流有一个方法让你能查看每个值,同时能继续操作流。这就是 peek
方法。peek
可以输出流中的值,同时避免了重复的流操作。使用 peek
方法还能以同样的方式,将输出定向到现有的日志系统中,比如 log4j
、java.util.logging
或者 slf4j
。
在流中间设置断点
记录日志这是 peek
方法的用途之一。为了像调试循环那样一步一步跟踪,可在 peek
方法中加入断点,这样就能逐个调试流中的元素了。
参考资料:
Java 8函数式编程 作者:(英)沃伯顿著
备注:
转载请注明出处:http://blog.csdn.net/wsyw126/article/details/52765629
作者:WSYW126