From 6e20cd1f6e87cc745a4a9094fc223097bc5eac4c Mon Sep 17 00:00:00 2001 From: confucianzuoyuan Date: Sat, 8 Feb 2020 12:21:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=8D=81=E4=BA=94?= =?UTF-8?q?=E7=AB=A0=20=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F=20=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E6=9B=B4=E6=96=B0=EF=BC=88=E2=80=9C=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E2=80=9D=E5=B0=8F=E8=8A=82=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/25-Patterns.md | 252 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md index 22ce3cfb..65b09573 100644 --- a/docs/book/25-Patterns.md +++ b/docs/book/25-Patterns.md @@ -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 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> 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 minima(List 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 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> 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 minima(List line) { + return algorithm.apply(line); + } +} + +public class StrategyPattern2 { + public static void main(String[] args) { + FindMinima2 solver = new FindMinima2(); + List 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 line; + Result(List data) { + success = true; + line = data; + } + Result() { + success = false; + line = Collections.emptyList(); + } +} + +class Fail extends Result {} + +interface Algorithm { + Result algorithm(List line); +} + +class FindMinima { + public static Result leastSquares(List 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 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 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, Result>> + algorithms = Arrays.asList( + FindMinima::leastSquares, + FindMinima::perturbation, + FindMinima::bisection + ); + public static Result minima(List line) { + for(Function, 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 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()` 方法只是遍历这个列表,然后找到能够成功执行的算法而已。 ## 接口改变 From 3c0c15fa0243badfe2966e1f016e812133420c5d Mon Sep 17 00:00:00 2001 From: confucianzuoyuan Date: Tue, 11 Feb 2020 11:02:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E5=8D=81=E4=BA=94?= =?UTF-8?q?=E7=AB=A0=20=E8=AE=BE=E8=AE=A1=E6=A8=A1=E5=BC=8F=20=E7=BF=BB?= =?UTF-8?q?=E8=AF=91=E6=9B=B4=E6=96=B0=EF=BC=88=E2=80=9C=E6=94=B9=E5=8F=98?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E2=80=9D=E5=B0=8F=E8=8A=82=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/book/25-Patterns.md | 138 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/docs/book/25-Patterns.md b/docs/book/25-Patterns.md index 65b09573..d8a8bf87 100644 --- a/docs/book/25-Patterns.md +++ b/docs/book/25-Patterns.md @@ -1043,8 +1043,144 @@ Bisection.algorithm 每一个 `Algorithm` 接口的实现,都实现了不同的 `algorithm()` 方法。在 `FindMinama` 中,将会创建一个算法的列表(这就是所谓的“链”),而 `minima()` 方法只是遍历这个列表,然后找到能够成功执行的算法而已。 -## 接口改变 +## 改变接口 +有时候我们需要解决的问题很简单,仅仅是“我没有需要的接口”而已。有两种设计模式用来解决这个问题:*适配器模式* 接受一种类型并且提供一个对其他类型的接口。*外观模式* 为一组类创建了一个接口,这样做只是为了提供一种更方便的方法来处理库或资源。 + +### 适配器模式(Adapter) + +当我们手头有某个类,而我们需要的却是另外一个类,我们就可以通过 *适配器模式* 来解决问题。唯一需要做的就是产生出我们需要的那个类,有许多种方法可以完成这种适配。 + +```java +// patterns/adapt/Adapter.java +// Variations on the Adapter pattern +// {java patterns.adapt.Adapter} +package patterns.adapt; + +class WhatIHave { + public void g() {} + public void h() {} +} + +interface WhatIWant { + void f(); +} + +class ProxyAdapter implements WhatIWant { + WhatIHave whatIHave; + ProxyAdapter(WhatIHave wih) { + whatIHave = wih; + } + @Override + public void f() { + // Implement behavior using + // methods in WhatIHave: + whatIHave.g(); + whatIHave.h(); + } +} + +class WhatIUse { + public void op(WhatIWant wiw) { + wiw.f(); + } +} + +// Approach 2: build adapter use into op(): +class WhatIUse2 extends WhatIUse { + public void op(WhatIHave wih) { + new ProxyAdapter(wih).f(); + } +} + +// Approach 3: build adapter into WhatIHave: +class WhatIHave2 extends WhatIHave implements WhatIWant { + @Override + public void f() { + g(); + h(); + } +} + +// Approach 4: use an inner class: +class WhatIHave3 extends WhatIHave { + private class InnerAdapter implements WhatIWant { + @Override + public void f() { + g(); + h(); + } + } + public WhatIWant whatIWant() { + return new InnerAdapter(); + } +} + +public class Adapter { + public static void main(String[] args) { + WhatIUse whatIUse = new WhatIUse(); + WhatIHave whatIHave = new WhatIHave(); + WhatIWant adapt= new ProxyAdapter(whatIHave); + whatIUse.op(adapt); + // Approach 2: + WhatIUse2 whatIUse2 = new WhatIUse2(); + whatIUse2.op(whatIHave); + // Approach 3: + WhatIHave2 whatIHave2 = new WhatIHave2(); + whatIUse.op(whatIHave2); + // Approach 4: + WhatIHave3 whatIHave3 = new WhatIHave3(); + whatIUse.op(whatIHave3.whatIWant()); + } +} +``` + +我想冒昧的借用一下术语“proxy”(代理),因为在 *《设计模式》* 里,他们坚持认为一个代理(proxy)必须拥有和它所代理的对象一模一样的接口。但是,如果把这两个词一起使用,叫做“代理适配器(proxy adapter)”,似乎更合理一些。 + +### 外观模式(Façade) + +当我想方设法试图将需求初步(first-cut)转化成对象的时候,通常我使用的原则是: + +>“把所有丑陋的东西都隐藏到对象里去”。 + +基本上说,*外观模式* 干的就是这个事情。如果我们有一堆让人头晕的类以及交互(Interactions),而它们又不是客户端程序员必须了解的,那我们就可以为客户端程序员创建一个接口只提供那些必要的功能。 + +外观模式经常被实现为一个符合单例模式(Singleton)的抽象工厂(abstract factory)。当然,你可以通过创建包含 **静态** 工厂方法(static factory methods)的类来达到上述效果。 + +```java +// patterns/Facade.java + +class A { A(int x) {} } + +class B { B(long x) {} } + +class C { C(double x) {} } + +// Other classes that aren't exposed by the +// facade go here ... +public class Facade { + static A makeA(int x) { return new A(x); } + static B makeB(long x) { return new B(x); } + static C makeC(double x) { return new C(x); } + public static void main(String[] args) { + // The client programmer gets the objects + // by calling the static methods: + A a = Facade.makeA(1); + B b = Facade.makeB(1); + C c = Facade.makeC(1.0); + } +} +``` + +《设计模式》给出的例子并不是真正的 *外观模式* ,而仅仅是一个类使用了其他的类而已。 + +#### 包(Package)作为外观模式的变体 + +我感觉,*外观模式* 更倾向于“过程式的(procedural)”,也就是非面向对象的(non-object-oriented):我们是通过调用某些函数才得到对象。它和抽象工厂(Abstract factory)到底有多大差别呢?*外观模式* 关键的一点是隐藏某个库的一部分类(以及它们的交互),使它们对于客户端程序员不可见,这样那些类的接口就更加简练和易于理解了。 + +其实,这也正是 Java 的 packaging(包)的功能所完成的事情:在库以外,我们只能创建和使用被声明为公共(public)的那些类;所有非公共(non-public)的类只能被同一 package 的类使用。看起来,*外观模式* 似乎是 Java 内嵌的一个功能。 + +公平起见,*《设计模式》* 主要是写给 C++ 读者的。尽管 C++ 有命名空间(namespaces)机制来防止全局变量和类名称之间的冲突,但它并没有提供类隐藏的机制,而在 Java 里我们可以通过声明 non-public 类来实现这一点。我认为,大多数情况下 Java 的 package 功能就足以解决针对 *外观模式* 的问题了。 ## 解释器