Skip to content

第二十五章 设计模式 翻译更新(“函数对象”小节翻译完成) #377

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 8, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions docs/book/25-Patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,258 @@ KungFuGuy now battles a NastyWeapon

## 函数对象

一个 *函数对象* 封装了一个函数。其特点就是将被调用函数的选择与那个函数被调用的位置进行解耦。

*《设计模式》* 中也提到了这个术语,但是没有使用。然而,*函数对象* 的话题却在那本书的很多模式中被反复论及。

### 命令模式

从最直观的角度来看,*命令模式* 就是一个函数对象:一个作为对象的函数。我们可以将 *函数对象* 作为参数传递给其他方法或者对象,来执行特定的操作。

在Java 8之前,想要产生单个函数的效果,我们必须明确将方法包含在对象中,而这需要太多的仪式了。而利用Java 8的lambda特性, *命令模式* 的实现将是微不足道的。

```java
// patterns/CommandPattern.java
import java.util.*;

public class CommandPattern {
public static void main(String[] args) {
List<Runnable> macro = Arrays.asList(
() -> System.out.print("Hello "),
() -> System.out.print("World! "),
() -> System.out.print("I'm the command pattern!")
);
macro.forEach(Runnable::run);
}
}
/* Output:
Hello World! I'm the command pattern!
*/
```

*命令模式* 的主要特点是允许向一个方法或者对象传递一个想要的动作。在上面的例子中,这个对象就是 **macro** ,而 *命令模式* 提供了将一系列需要一起执行的动作集进行排队的方法。在这里,*命令模式* 允许我们动态的创建新的行为,通常情况下我们需要编写新的代码才能完成这个功能,而在上面的例子中,我们可以通过解释运行一个脚本来完成这个功能(如果需要实现的东西很复杂请参考解释器模式)。

*《设计模式》* 认为“命令模式是回调的面向对象的替代品”。尽管如此,我认为"back"(回来)这个词是callback(回调)这一概念的基本要素。也就是说,我认为回调(callback)实际上是返回到回调的创建者所在的位置。另一方面,对于 *命令* 对象,通常只需创建它并将其交给某种方法或对象,而不是自始至终以其他方式联系命令对象。不管怎样,这就是我对它的看法。在本章的后面内容中,我将会把一组设计模式放在“回调”的标题下面。

### 策略模式

*策略模式* 看起来像是从同一个基类继承而来的一系列 *命令* 类。但是仔细查看 *命令模式*,你就会发现它也具有同样的结构:一系列分层次的 *函数对象*。不同之处在于,这些函数对象的用法和策略模式不同。就像前面的 `io/DirList.java` 那个例子,使用 *命令* 是为了解决特定问题 -- 从一个列表中选择文件。“不变的部分”是被调用的那个方法,而变化的部分被分离出来放到 *函数对象* 中。我认为 *命令模式* 在编码阶段提供了灵活性,而 *策略模式* 的灵活性在运行时才会体现出来。尽管如此,这种区别却是非常模糊的。

另外,*策略模式* 还可以添加一个“上下文(context)”,这个上下文(context)可以是一个代理类(surrogate class),用来控制对某个特定 *策略* 对象的选择和使用。就像 *桥接模式* 一样!下面我们来一探究竟:

```java
// patterns/strategy/StrategyPattern.java
// {java patterns.strategy.StrategyPattern}
package patterns.strategy;
import java.util.function.*;
import java.util.*;

// The common strategy base type:
class FindMinima {
Function<List<Double>, List<Double>> algorithm;
}

// The various strategies:
class LeastSquares extends FindMinima {
LeastSquares() {
// Line is a sequence of points (Dummy data):
algorithm = (line) -> Arrays.asList(1.1, 2.2);
}
}

class Perturbation extends FindMinima {
Perturbation() {
algorithm = (line) -> Arrays.asList(3.3, 4.4);
}
}

class Bisection extends FindMinima {
Bisection() {
algorithm = (line) -> Arrays.asList(5.5, 6.6);
}
}

// The "Context" controls the strategy:
class MinimaSolver {
private FindMinima strategy;
MinimaSolver(FindMinima strat) {
strategy = strat;
}
List<Double> minima(List<Double> line) {
return strategy.algorithm.apply(line);
}
void changeAlgorithm(FindMinima newAlgorithm) {
strategy = newAlgorithm;
}
}

public class StrategyPattern {
public static void main(String[] args) {
MinimaSolver solver =
new MinimaSolver(new LeastSquares());
List<Double> line = Arrays.asList(
1.0, 2.0, 1.0, 2.0, -1.0,
3.0, 4.0, 5.0, 4.0 );
System.out.println(solver.minima(line));
solver.changeAlgorithm(new Bisection());
System.out.println(solver.minima(line));
}
}
/* Output:
[1.1, 2.2]
[5.5, 6.6]
*/
```

`MinimaSolver` 中的 `changeAlgorithm()` 方法将一个不同的策略插入到了 `私有` 域 `strategy` 中,这使得在调用 `minima()` 方法时,可以使用新的策略。

我们可以通过将上下文注入到 `FindMinima` 中来简化我们的解决方法。

```java
// patterns/strategy/StrategyPattern2.java // {java patterns.strategy.StrategyPattern2}
package patterns.strategy;
import java.util.function.*;
import java.util.*;

// "Context" is now incorporated:
class FindMinima2 {
Function<List<Double>, List<Double>> algorithm;
FindMinima2() { leastSquares(); } // default
// The various strategies:
void leastSquares() {
algorithm = (line) -> Arrays.asList(1.1, 2.2);
}
void perturbation() {
algorithm = (line) -> Arrays.asList(3.3, 4.4);
}
void bisection() {
algorithm = (line) -> Arrays.asList(5.5, 6.6);
}
List<Double> minima(List<Double> line) {
return algorithm.apply(line);
}
}

public class StrategyPattern2 {
public static void main(String[] args) {
FindMinima2 solver = new FindMinima2();
List<Double> line = Arrays.asList(
1.0, 2.0, 1.0, 2.0, -1.0,
3.0, 4.0, 5.0, 4.0 );
System.out.println(solver.minima(line));
solver.bisection();
System.out.println(solver.minima(line));
}
}
/* Output:
[1.1, 2.2]
[5.5, 6.6]
*/
```

`FindMinima2` 封装了不同的算法,也包含了“上下文”(Context),所以它便可以在一个单独的类中控制算法的选择了。

### 责任链模式

*责任链模式* 也许可以被看作一个使用了 *策略* 对象的“递归的动态一般化”。此时我们进行一次调用,在一个链序列中的每个策略都试图满足这个调用。这个过程直到有一个策略成功满足该调用或者到达链序列的末尾才结束。在递归方法中,一个方法将反复调用它自身直至达到某个终止条件;使用责任链,一个方法会调用相同的基类方法(拥有不同的实现),这个基类方法将会调用基类方法的其他实现,如此反复直至达到某个终止条件。

除了调用某个方法来满足某个请求以外,链中的多个方法都有机会满足这个请求,因此它有点专家系统的意味。由于责任链实际上就是一个链表,它能够动态创建,因此它可以看作是一个更一般的动态构建的 `switch` 语句。

在上面的 `StrategyPattern.java` 例子中,我们可能想自动发现一个解决方法。而 *责任链* 就可以达到这个目的:

```java
// patterns/chain/ChainOfResponsibility.java
// Using the Functional interface
// {java patterns.chain.ChainOfResponsibility}
package patterns.chain;
import java.util.*;
import java.util.function.*;

class Result {
boolean success;
List<Double> line;
Result(List<Double> data) {
success = true;
line = data;
}
Result() {
success = false;
line = Collections.<Double>emptyList();
}
}

class Fail extends Result {}

interface Algorithm {
Result algorithm(List<Double> line);
}

class FindMinima {
public static Result leastSquares(List<Double> line) {
System.out.println("LeastSquares.algorithm");
boolean weSucceed = false;
if(weSucceed) // Actual test/calculation here
return new Result(Arrays.asList(1.1, 2.2));
else // Try the next one in the chain:
return new Fail();
}
public static Result perturbation(List<Double> line) {
System.out.println("Perturbation.algorithm");
boolean weSucceed = false;
if(weSucceed) // Actual test/calculation here
return new Result(Arrays.asList(3.3, 4.4));
else
return new Fail();
}
public static Result bisection(List<Double> line) {
System.out.println("Bisection.algorithm");
boolean weSucceed = true;
if(weSucceed) // Actual test/calculation here
return new Result(Arrays.asList(5.5, 6.6));
else
return new Fail();
}
static List<Function<List<Double>, Result>>
algorithms = Arrays.asList(
FindMinima::leastSquares,
FindMinima::perturbation,
FindMinima::bisection
);
public static Result minima(List<Double> line) {
for(Function<List<Double>, Result> alg :
algorithms) {
Result result = alg.apply(line);
if(result.success)
return result;
}
return new Fail();
}
}

public class ChainOfResponsibility {
public static void main(String[] args) {
FindMinima solver = new FindMinima();
List<Double> line = Arrays.asList(
1.0, 2.0, 1.0, 2.0, -1.0,
3.0, 4.0, 5.0, 4.0);
Result result = solver.minima(line);
if(result.success)
System.out.println(result.line);
else
System.out.println("No algorithm found");
}
}
/* Output:
LeastSquares.algorithm
Perturbation.algorithm
Bisection.algorithm
[5.5, 6.6]
*/
```

我们从定义一个 `Result` 类开始,这个类包含一个 `success` 标志,因此接收者就可以知道算法是否成功执行,而 `line` 变量保存了真实的数据。当算法执行失败时, `Fail` 类可以作为返回值。要注意的是,当算法执行失败时,返回一个 `Result` 对象要比抛出一个异常更加合适,因为我们有时可能并不打算解决这个问题,而是希望程序继续执行下去。

每一个 `Algorithm` 接口的实现,都实现了不同的 `algorithm()` 方法。在 `FindMinama` 中,将会创建一个算法的列表(这就是所谓的“链”),而 `minima()` 方法只是遍历这个列表,然后找到能够成功执行的算法而已。

<!-- Changing the Interface -->
## 接口改变
Expand Down