《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);
}

优点:

  1. 任何已有的Supplier实例不需要重新封装就可以在此使用,鼓励了重用和组合。
  2. 代码更加清晰。
  3. 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方法能记录中间值,在调试时非常有用。