重构以提高可读性和灵活性

提高代码的可读性

我们介绍三个简单的重构,使用lambda、方法引用和流,将它们应用到代码中,以提高代码的可读性:

  • 将匿名类重构为lambda表达式
  • 将lambda表达式重构为方法引用
  • 将命令式数据处理重构为流处理

从匿名类到lambda表达式

下面是使用匿名类创建一个Runnable对象和对应的lambda表达式:

1
2
3
4
5
6
7
8
// 使用匿名类
Runnable r1 = new Runnable() {
    public void run() {
        System.out.println("Hello");
    }
};
// 使用lambda
Runnable r2 = () -> System.out.println("Hello");

匿名类和lambda表达式有如下区别:

  • this和super的含义不一样:在匿名类里面,this指的是匿名类本身,但在lambda里面,this指向包含lambda表达式的类。
  • 匿名类允许隐藏它外围作用域中的变量,lambda表达式不行。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int a = 10;
Runnable r1 = () -> {
    int a = 2; // Compile error
    System.out.println(a);
};

Runnable r2 = new Runnable() {
    public void run() {
        int a = 2; // Everything is fine!
        System.out.println(a);
    }
};
  • 将匿名类转换为lambda表达式会使代码在重载上下文中具有歧义。考虑下面的例子:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface Task {
    public void execute();
}

public static void doSomething(Runnable r) {
    r.run();
}

public static void doSomething(Task a) {
    r.execute();
}

这里使用匿名类没有问题,但是使用lambda表达式会有歧义,因为RunnableTask都是合法目标类型:

1
doSomething(() -> System.out.println("Danger danger!!")); // 歧义

你可以提供显式转换解决歧义:

1
doSomething((Task)() -> System.out.println("Danger danger!!"));

从lambda表达式到方法引用

Lambda表达式对于少量代码非常有用,但是方法引用更能清楚地说明代码的意图。比如下面这个例子:

1
2
3
4
5
6
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel =
    menu.stream().collect(groupingBy(dish -> {
        if (dish.getCalories() <= 400) return CaloricLevel.DIET;
        else if (dish.getCalories() <= 700) return CaloricLevel.NORMAL;
        else return CaloricLevel.FAT;
    }));

你可以将lambda表达式提取到一个单独的方法中,并将其作为参数传递给groupingBy。代码变得更加简洁,其意图也更加明确:

1
Map<CaloricLevel, List<Dish>> dishesByCaloricLevel = menu.stream().collect(groupingBy(Dish::getCaloricLevel));

此外,尽可能使用辅助静态方法,比如comparing和maxBy。这些方法是为了方法引用而设计的:

1
2
3
4
// lambda表达式
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight()));
// 方法引用
inventory.sort(comparing(Apple::getWeight));

此外,对于许多常见的约简操作(如sum、maximum),有一些内置的辅助方法可以与方法引用相结合。

1
2
int totalCalories = menu.stream().map(Dish::getCalories).reduce(0, (c1, c2) -> c1 + c2);
int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));

从命令式数据处理到流处理

理想情况下,你应该将使用迭代器处理集合的典型数据处理模式的所有代码转换为Streams API。因为Streams API更清楚地表达了数据处理的意图,并能在幕后进行优化,利用短路和惰性求值以及多核架构。

下面的命令式代码表达了两个混杂在一起的模式(过滤和提取),迫使程序员在弄清楚代码功能之前必须弄清楚整个实现。此外,并行执行的代码很难编写。

1
2
3
4
5
6
List<String> dishNames = new ArrayList<>();
for (Dish dish : menu) {
    if (dish.getCalories() > 300) {
        dishNames.add(dish.getName());
    }
}

另一种方法使用Streams API,读起来就像问题陈述,并且可以很容易地并行化:

1
2
menu.parallelStream().filter(d -> d.getCalories() > 300)
.map(Dish::getName).collect(toList());

改进代码的灵活性

采用函数接口

首先,没有函数接口就不能使用lambda表达式,因此你应该开始在代码库中引入它们。我们讨论两个可以重构以利用lambda表达式的常见代码模式:条件延迟执行和Execute around。

条件延迟执行

控制流语句在业务逻辑代码中混合出现是很常见的。典型的场景包括安全检查和日志记录:

1
2
3
if (logger.isLoggable(Log.FINER)) {
    logger.finer("Problem: " + generateDiagnostic());
}

有几个问题:

  • isLoggable方法将logger的状态暴露给用户代码

  • 为什么每次在记录消息之前都必须查询logger对象的状态?它会打乱你的代码

更好的方法是使用log方法在内部检查logger对象是否设置为正确的级别,然后记录消息:

1
logger.log(Level.FINER, "Problem: " + generateDiagnostic());

这种方法更好,代码不会被if检查搞得乱七八糟,而且logger的状态不再暴露。但是这段代码仍然存在一个问题:日志消息总是被计算,即使logger没有启用。

Lambda表达式可以提供帮助。你需要的是一种延迟消息构造的方法,以便只在给定的条件下生成消息。可以使用如下log方法:

1
public void log(Level level, Supplier<String> msgSupplier)

现在可以这样调用:

1
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());

只有在日志级别正确的情况下,log方法才会执行lambda表达式。

Execute around

如果不同代码使用相同的准备和清理阶段,则通常可以将其放入lambda。好处是可以重用准备和清理阶段的逻辑,从而减少代码重复。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
String oneLine = processFile((BufferedReader b) -> b.readLine());
String twoLines = processFile((BufferedReader b) -> b.readLine() + b.readLine());

public static String processFile(BufferedReaderProcessor p) throws IOException {
    try(BufferedReader br = new BufferedReader(
        new FileReader("ModernJavaInAction/chap9/data.txt"))) {
        return p.process(br);
    }
}

public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}

使用lambda重构设计模式

策略模式

策略模式是一种常见的设计模式,用于表示一系列算法,并让你在运行时从中进行选择。如下图:

strategy.png

策略模式包含3个部分:

  • 一个代表某种算法的接口
  • 一个或多个接口的实现用于表示多个算法
  • 一个或多个使用策略对象的用户

假设您想验证文本输入对不同的条件是否正确格式化。首先定义一个接口来验证文本:

1
2
3
public interface ValidationStrategy {
	boolean execute(String s);
}

接下来定义接口的2个实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class IsAllLowerCase implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("[a-z]+");
    }
}

public class IsNumeric implements ValidationStrategy {
    public boolean execute(String s) {
        return s.matches("\\d+");
    }
}

然后你可以使用这些不同的验证策略:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Validator {
    private final ValidationStrategy strategy;

    public Validator(ValidationStrategy v) {
        this.strategy = v;
    }

    public boolean validate(String s) {
        return strategy.execute(s);
    }
}

Validator numericValidator = new Validator(new IsNumeric());
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator(new IsAllLowerCase());
boolean b2 = lowerCaseValidator.validate("bbbb");

使用lambda表达式

ValidationStrategy是一个函数接口,因此可以直接传递更简洁的lambda表达式,来实现不同的策略:

1
2
3
4
Validator numericValidator = new Validator((String s) -> s.matches("[a-z]+"));
boolean b1 = numericValidator.validate("aaaa");
Validator lowerCaseValidator = new Validator((String s) -> s.matches("\\d+"));
boolean b2 = lowerCaseValidator.validate("bbbb");

模板方法模式

模板方法模式是一种常见的模式,当你需要表示算法的大纲并具有更改其中某些部分的额外灵活性时。下面是一个模板方法模式的例子:

1
2
3
4
5
6
7
8
abstract class OnlineBanking {
    public void processCustomer(int id) {
        Customer c = Database.getCustomerWithId(id);
        makeCustomerHappy(c);
    }

    abstract void makeCustomerHappy(Customer c);
}

使用lambda表达式

你可以使用lambda表达式解决相同的问题,需要更改的部分可以使用lambda表达式:

1
2
3
4
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy) {
    Customer c = Database.getCustomerWithId(id);
    makeCustomerHappy.accept(c);
}

现在你可以通过lambda表达式插入不同的行为,而不用继承OnlineBanking类:

1
new OnlineBankingLambda().processCustomer(1337, (Customer c) -> System.out.println("Hello " + c.getName());

观察者模式

观察者模式是一种常见的设计模式,当主体需要在发生某些事件时自动通知其他观察者。假设你要设计一个Twitter通知系统,几家报社订阅了一个新闻,并希望能收到特定新闻通知。

首先定义观察者接口:

1
2
3
interface Observer {
    void notify(String tweet);
}

接下来定义不同的报社实现观察者接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class NYTimes implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("money")) {
            System.out.println("Breaking news in NY! " + tweet);
        }
    }
}

class Guardian implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("queen")) {
            System.out.println("Yet more news from London... " + tweet);
        }
    }
}

class LeMonde implements Observer {
    public void notify(String tweet) {
        if (tweet != null && tweet.contains("wine")) {
            System.out.println("Today cheese, wine and news! " + tweet);
        }
    }
}

定义主体接口:

1
2
3
4
interface Subject {
    void registerObserver(Observer o);
    void notifyObservers(String tweet);
}

主体使用registerObserver方法注册观察者,使用notifyObservers方法通知观察者,接下来实现订阅类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Feed implements Subject {
    private final List<Observer> observers = new ArrayList<>();

    public void registerObserver(Observer o) {
        this.observers.add(o);
    }

    public void notifyObservers(String tweet) {
        observers.forEach(o -> o.notify(tweet));
    }
}

使用lambda表达式

观察者接口可以使用lambda表达式替换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
f.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("money")) {
        System.out.println("Breaking news in NY! " + tweet);
    }
});

f.registerObserver((String tweet) -> {
    if (tweet != null && tweet.contains("queen")) {
        System.out.println("Yet more news from London... " + tweet);
    }
});

职责链模式

责任链模式是创建处理对象链的设计模式。一个处理对象可以做一些工作并将结果传递给另一个对象,另一个对象也做一些工作并将结果传递给下一个处理对象,以此类推。

通常该模式是通过定义一个抽象类来实现的。这个类表示一个处理对象,并定义一个字段来跟踪后继对象。当它完成它的工作时,处理对象将它的工作移交给它的后继对象。如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public abstract class ProcessingObject<T> {
    protected ProcessingObject<T> successor;

    public void setSuccessor(ProcessingObject<T> successor) {
        this.successor = successor;
    }

    public T handle(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handle(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}

下面是使用此模式的例子,你可以创建两个处理对象来执行一些文本处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class HeaderTextProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return "From Raoul, Mario and Alan: " + text;
    }
}

public class SpellCheckerProcessing extends ProcessingObject<String> {
    public String handleWork(String text) {
        return text.replaceAll("labda", "lambda");
    }
}

现在你可以连接两个处理对象来创建一个操作链:

1
2
3
4
5
ProcessingObject<String> p1 = new HeaderTextProcessing();
ProcessingObject<String> p2 = new SpellCheckerProcessing();
p1.setSuccessor(p2);
String result = p1.handle("Aren't lambdas really sexy?!!");
System.out.println(result);

使用lambda表达式

这个模式看起来像组合函数,可以将处理对象表示为Function<String、String>UnaryOperator<String>的实例。要将它们链接起来,可以使用andThen方法组合这些函数:

1
2
3
4
UnaryOperator<String> headerProcessing = (String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
String result = pipeline.apply("Aren't lambdas really sexy?!!");

工厂模式

工厂设计模式允许你创建对象,而无需暴露实例化逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class ProductFactory {
    public static Product createProduct(String name) {
        switch (name) {
            case "loan": return new Loan();
            case "stock": return new Stock();
            case "bond": return new Bond();
            default:
                throw new RuntimeException("No such product " + name);
        }
    }
}

使用lambda表达式

我们可以像使用方法引用一样使用构造器引用,如下:

1
2
Supplier<Product> loanSupplier = Loan::new;
Loan loan = loanSupplier.get();

使用这个方法可以将前面的工厂模式重写为:

1
2
3
4
5
6
final static Map<String, Supplier<Product>> map = new HashMap<>();
static {
    map.put("loan", Loan::new);
    map.put("stock", Stock::new);
    map.put("bond", Bond::new);
}

像工厂模式一样,你可以使用这个Map来初始化不同的产品:

1
2
3
4
5
public static Product createProduct(String name) {
    Supplier<Product> p = map.get(name);
    if (p != null) return p.get();
    throw new IllegalArgumentException("No such product " + name);
}

测试lambda

测试可见lambda的行为

lambda表达式生成函数接口的实例,因此你可以测试该实例的行为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final static Comparator<Point> compareByXAndThenY =
    comparing(Point::getX).thenComparing(Point::getY);

@Test
public void testComparingTwoPoints() throws Exception {
    Point p1 = new Point(10, 15);
    Point p2 = new Point(10, 20);
    int result = Point.compareByXAndThenY.compare(p1, p2);
    assertTrue(result < 0);
}

关注使用lambda的方法的行为

我们认为应该测试使用lambda表达式的方法的行为

1
2
3
4
5
6
7
8
9
@Test
public void testMoveAllPointsRightBy() throws Exception {
    List<Point> points =
        Arrays.asList(new Point(5, 5), new Point(10, 5));
    List<Point> expectedPoints =
        Arrays.asList(new Point(15, 5), new Point(20, 5));
    List<Point> newPoints = Point.moveAllPointsRightBy(points, 10);
    assertEquals(expectedPoints, newPoints);
}

将复杂的lambda放到单独的方法中

也许你会遇到一个非常复杂的lambda表达式,包含很多逻辑。如何测试这个lambda表达式?一种策略是将lambda表达式转换为方法引用。然后可以像测试任何常规方法一样测试新方法的行为。

测试高阶函数

高阶函数比较难测试,可以用不同的lambda测试它的行为:

1
2
3
4
5
6
7
8
@Test
public void testFilter() throws Exception {
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
    List<Integer> even = filter(numbers, i -> i % 2 == 0);
    List<Integer> smallerThanThree = filter(numbers, i -> i < 3);
    assertEquals(Arrays.asList(2, 4), even);
    assertEquals(Arrays.asList(1, 2), smallerThanThree);
}

调试

记录日志信息

peek的目的是在使用流的每个元素时对其执行一个操作。它不像forEach那样消耗整个流,而是将其操作的元素转发给管道中的下一个操作。在下面的代码中,peek在流管道中的每个操作之前和之后打印中间值:

1
2
3
4
5
6
7
8
9
List<Integer> result = numbers.stream()
    .peek(x -> System.out.println("from stream: " + x))
    .map(x -> x + 17)
    .peek(x -> System.out.println("after map: " + x))
    .filter(x -> x % 2 == 0)
    .peek(x -> System.out.println("after filter: " + x))
    .limit(3)
    .peek(x -> System.out.println("after limit: " + x))
    .collect(toList());