《Java8函数式编程》学习笔记 - 第7章
第七章 测试,调试,和重构
重构,测试驱动开发(TDD)和持续继承(CI)越来越流行。 <=这三个概念不是很明白
本章主要探讨如何在代码中使用Lambda表达式的技术,
也会说明什么情况下不应该(直接)使用lambda表达式。
如何调试大量使用Lambda表达式和流的程序。
如何使用Lambda表达式提高非集合类代码的质量。
7.1 重构候选项
在选择内部设计模型时,想想以何种形式向外展示API是大有裨益的。
什么时候应该Lambda化自己的应用或类库的要点:
其中每一条都可看作一个局部的反模式或者代码异味,借助于Lambda来修复。
7.1.1 进进出出,摇摇晃晃
//例7-1 logger 对象使用isDebugEnabled 属性避免不必要的性能开销 Logger logger = new Logger(); if (logger.isDebugEnabled()) { logger.debug("Look at this: " + expensiveOperation()); }
这种反模式通过传入代码即数据的方式很容易解决。
isDebugEnabled 方法暴露了内部状态。
如果使用Lambda 表达式,外面的代码根本不需要检查日志级别。
//例7-2 使用Lambda 表达式简化记录日志代码 Logger logger = new Logger(); logger.debug(() -> "Look at this: " + expensiveOperation());
当程序处于调试级别,并且检查是否使用Lambda表达式的逻辑被封装在Logger对象中时,才会调用Lambda 表达式。
7.1.2 孤独的覆盖
这个代码异味是使用继承,其目的只是为了覆盖一个方法。
ThreadLocal 就是一个很好的例子。ThreadLocal能创建一个工厂,为每个线程最多只产生一个值。 这是确保非线程安全的类在并发环境下安全使用的一种简单方式。
//例7-3 在数据库中查找艺术家 ThreadLocal<Album> thisAlbum = new ThreadLocal<Album> () { @Override protected Album initialValue() { return database.lookupCurrentAlbum(); } }; //例7-4 使用工厂方法 //为工厂方法withInitial 传入一个Supplier 对象的实例来创建对象 ThreadLocal<Album> thisAlbum = ThreadLocal.withInitial(() -> database.lookupCurrentAlbum()); public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) { return new SuppliedThreadLocal<>(supplier); }
优点:
- 任何已有的Supplier
实例不需要重新封装就可以在此使用,鼓励了重用和组合。 - 代码更加清晰。
- JVM会少加载一个类
7.1.3 同样的东西写两遍
不要重复你劳动(Don’t Repeat Yourself,DRY) 同样的东西写两遍(Write Everything Twice,WET)
//例7-7 使用领域方法重构Order 类 public long countFeature(ToLongFunction<Album> function) { return albums.stream() .mapToLong(function) .sum(); } public long countTracks() { return countFeature(album -> album.getTracks().count()); } public long countRunningTime() { return countFeature(album -> album.getTracks() .mapToLong(track -> track.getLength()) .sum()); } public long countMusicians() { return countFeature(album -> album.getMusicians().count()); }
传入一个代表了领域知识的Lambda 表达式
7.2 Lambda表达式的单元测试
单元测试是测试一段代码的行为是否符合预期的方式。 Lambda表达式没有名字,
7.3 在测试替身时使用Lambda表达式
测试替身也常被称为模拟,事实上测试存根和模拟都属于测试替身。 区别是模拟可以验证代码的行为。读者若想了解更多有关这方面的信息, 请阅读Martin Fowler的相关文章。
//例7-14 使用Lambda 表达式编写测试替身,传给countFeature 方法 @Test public void canCountFeatures() { OrderDomain order = new OrderDomain(asList( newAlbum("Exile on Main St."), newAlbum("Beggars Banquet"), newAlbum("Aftermath"), newAlbum("Let it Bleed"))); assertEquals(8, order.countFeature(album -> 2)); }
OrderDomain是Order类,其中有上面例7-7里面的4个方法。
现在测试的是countFeature方法。其参数为Funtion接口。
入力为Album集合,出力为固定整数2,最后求和为8
//例7-15 结合Mockito 框架使用Lambda 表达式 List<String> list = mock(List.class); when(list.size()).thenAnswer(inv -> otherList.size()); assertEquals(3, list.size());
Mokito会在别的博客中介绍。
7.4 惰性求值和调试
因为惰性求值的存在,没法简单的调试。
7.5 日志和打印消息
因为是惰性求值,是无法看中间值的。
一旦调用了forEach操作,因为是及早求值,使得无法继续流操作。
7.6 解决方案:peek
流有一个方法让你能查看每个值,同时能继续操作流。这就是peek 方法。
使用peek 方法还能以同样的方式,将输出定向到现有的日志系统中,
比如log4j、java.util.logging 或者slf4j。
7.7 在流中间设置断点
记录日志这是peek方法的用途之一。
为了像调试循环那样一步一步跟踪,可在peek方法中加入断点,这样就能逐个调试流中的元素了。
peek方法可知包含一个空的方法体,只要能设置断点就行。
有一些调试器不允许在空的方法体中设置断点时,将值简单地映射为其本身,这样就有地方设置断点了,
7.8 要点回顾
- Lambda表达式,有一些通用的模式。
- 如果想要对复杂一点的Lambda表达式编写单元测试,将其抽取成一个常规的方法。
- peek方法能记录中间值,在调试时非常有用。