第七章 测试、调试和重构

重构候选项

使用Lambda 表达式重构代码有个时髦的称呼:Lambda 化(读作lambda-fi-cation,执行重构的程序员叫作lamb-di-fiers 或者有责任心的程序员)

孤独的覆盖

ThreadLocal能创建一个工厂,为每个线程最多只产生一个值。这是确保非线程安全的类在并发环境下安全使用的一种简单方式。

ThreadLocal thisAlbum = new ThreadLocal () {
@Override 
protected Album initialValue() {
return database.lookupCurrentAlbum();
}
};

在Java 8中,可以为工厂方法withInitial传入一个Supplier对象的实例来创建对象。如:

ThreadLocal thisAlbum
= ThreadLocal.withInitial(() -> database.lookupCurrentAlbum());

好处:

  • 任何已有的Supplier实例不需要重新封装,就可以在此使用,这鼓励了重用和组合
  • 代码短小精悍就是个优势。很容易弄明白代码的意图
  • JVM会少加载一个类

同样的东西写两遍

DRY是一个众所周知的模式,重复的代码难于重构,一改就坏
如果有一个整体上大概相似的模式,只是行为上有所不同,就可以试着加入一个Lambda表达式

public long countRunningTime() {
long count = 0;
for (Album album : albums) {
for (Track track : album.getTrackList()) {
count += track.getLength();
}
}
return count;
}
public long countMusicians() {
long count = 0;
for (Album album : albums) {
count += album.getMusicianList().size();
}
return count;
}
public long countTracks() {
long count = 0;
for (Album album : albums) {
count += album.getTrackList().size();
}
return count;
}

使用Java8中的集合类库来重写上述代码:

public long countRunningTime() {
return albums.stream()
.mapToLong(album -> album.getTracks()
.mapToLong(track -> track.getLength())
.sum())
.sum();
}
public long countMusicians() {
return albums.stream()
.mapToLong(album -> album.getMusicians().count())
.sum();
}
public long countTracks() {
return albums.stream()
.mapToLong(album -> album.getTracks().count())
.sum();
}

Java8核心类库有这样一个类型ToLongFunction:


image.png

使用上述类型,代码可优化为:

public long countFeature(ToLongFunction 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表达式的单元测试

Lambda 表达式给单元测试带来了一些麻烦,Lambda 表达式没有名字,无法直接在测试代码中调用。
你可以在测试代码中复制Lambda 表达式来测试,但这种方式的副作用是测试的不是真正的实现。假设你修改了实现代码,测试仍然通过,而实现可能早已在做另一件事了。
解决方式:
将Lambda表达式放入一个方法测试,这种方式要测那个方法,而不是Lambda表达式本身。如:

public static List allToUpperCase(List words) {
return words.stream()
.map(string -> string.toUpperCase())
.collect(Collectors.toList());
}

在测试替身时使用Lambda表达式

日志和打印消息

  • 幼稚一点的方法:使用forEach
album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.forEach(nationality -> System.out.println("Found: " + nationality));
Set nationalities
= album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.collect(Collectors.toSet());
  • 正规方案:peak
Set nationalities
= album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.peek(nation -> System.out.println("Found nationality: " + nation))
.collect(Collectors.toSet());

使用peek 方法还能以同样的方式,将输出定向到现有的日志系统中,比如log4j、java.util.logging 或者slf4j。

在流中间设置断点

记录日志这是peek 方法的用途之一。为了像调试循环那样一步一步跟踪,可在peek 方法中加入断点,这样就能逐个调试流中的元素了。
此时,peek 方法可知包含一个空的方法体,只要能设置断点就行。有一些调试器不允许在空的方法体中设置断点,此时,我将值简单地映射为其本身,这样就有地方设置断点了,虽然这样做不够完美,但只要能工作就行。

你可能感兴趣的:(第七章 测试、调试和重构)