《Head First 设计模式》学习笔记 | 策略模式
- 2020 年 3 月 18 日
- 笔记
前言
我最近在看大名鼎鼎的《Head First 设计模式》。这本“OO 圣经”用 Java 实现各类设计模式,对于我 —— 一个非 Java 爱好者而言,读起来并不过瘾。
有人读完这本书可能会误解设计模式就是设计 Interface,而事实并非如此。在知乎的一个问题《Python 里没有接口,如何写设计模式?》[1]中,vczh 轮子哥是这样回答的:
❝设计模式搞了那么多东西就是在告诉你“如何在各种情况下解耦你的代码,让你的代码在运行时可以互相组合”。这就跟兵法一样。难道有了飞机大炮,兵法就没有用了吗? ❞
我觉得这个比喻很好,不同的语言就像不同的兵器,各有各的特点与使用方式,而设计模式就是那套“兵法”,无论你使用何种兵器,不过是“纵横不出方圆,万变不离其宗”。而只看书中一种“兵器”未免太少,不如我们多试几样?
本篇就来看一看第一章“兵法” —— 策略模式(Strategy Pattern)。
定义
书中对策略模式的定义如下:
❝策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。 ❞
下面以书中的“模拟鸭子应用”为例。
继承的弊端
你要设计一个鸭子游戏,游戏里有各种各样的鸭子,它们会游泳(swim()
),还会呱呱叫(quack()
),每种鸭子拥有不同的外观(display()
)。
一开始,你可能会设计一个鸭子的超类 Duck
,然后让所有不同种类的鸭子继承它:

设计一个鸭子超类(Superclass)
如果此时我们想让鸭子飞起来,就要在超类中增加一个 fly()
方法:

让鸭子飞
此时,鸭子家族来了一只擅于代码调试工作的小黄鸭。

此时,一切都乱套了,这位代码调试工作者会发出“吱吱”的叫声,但却不会飞,然而它却从鸭子超类继承了 quack()
和 fly()
方法。为了让它尊重客观事实,我们需要在小黄鸭类中覆盖超类的 quack()
和 fly()
方法,让它变得不会叫也不会飞。

在小黄鸭中覆盖原有的方法
虽然我们用“覆盖方法”的手段解决了小黄鸭的问题,但未来我们可能还会制造更多奇奇怪怪的鸭子。例如周黑鸭或北京烤鸭,它们显然既不会叫,也不会游泳,还不会飞,这时我们又要为它们重写所有的行为吗?利用继承的方式来为不同种类的鸭子提供行为显然不够灵活。
抽离可变行为
不同的鸭子具有不同的行为,“鸭子的行为应当是灵活可变的”。
❝“设计原则一”:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。 ❞
因此,利用上述原则,我们把“鸭子的行为”从鸭子类(Duck)中抽离出来。

取出容易变化的行为
实现被抽离的行为
❝“设计原则二”:针对接口编程,而不是针对实现编程。 ❞
我们将这些被抽离出的行为归类:
- 所有具体的飞行行为属于飞行策略
- 所有具体的叫声行为属于叫声策略
- 所有具体的游泳行为属于游泳策略
- ……
我们可以利用接口或抽象类代表这些“策略”,然后“让特定的具体行为来实现这些策略中的方法”。
例如,我们的飞行策略名为 FlyBehavior
,我们将它设计为一个抽象类(当然也可以是接口)。然后,我们有两种具体的飞行方式 FlyWithWings
(会飞)和 FlyNoWay
(不会飞),它们需要实现飞行策略中的 fly()
方法:

整合
此时,我们已经将可变的行为从鸭子超类(Duck
)中抽离,并把它们用具体的“行为类”进行表示。我们希望:“如果鸭子要执行某个行为,它不需要自己处理,而是将这一行为委托给具体的“行为类””。
因此,我们可以在鸭子超类(Duck
)中加入“行为类”的实例变量,从而通过这些实例变量来调用具体的行为方法。

在 Class Duck
的 fly()
方法中,我们可以使用实例 flyBehavior
调用具体的行为方法,从而达成“委托”的目的:
public function fly() { $this->flyBehavior->fly(); }
具体实现
下面来看看不同语言的具体实现:
PHP
PHP 有抽象类也有接口,语法和 Java 比较接近。实现方法中规中矩,和书中的并无二致。只不过这里我把行为接口改成了抽象类。类图如下:

UML 类图关系
具体实现:
<?php // 飞行行为类 abstract class FlyBehavior { abstract public function fly(); } // “飞”的具体行为 class FlyWithWings extends FlyBehavior { public function fly() { echo "会飞n"; } } class FlyNoWay extends FlyBehavior { public function fly() { echo "不会飞n"; } } // 叫声行为类 abstract class QuackBehavior { abstract public function quack(); } // “叫”的具体行为 class Quack extends QuackBehavior { public function quack() { echo "呱呱n"; } } class Squeak extends QuackBehavior { public function quack() { echo "吱吱n"; } } class MuteQuack extends QuackBehavior { public function quack() { echo "不会叫n"; } } // 鸭子类 abstract class Duck { protected $flyStrategy; protected $quackStrategy; public function fly() { $this->flyStrategy->fly(); } public function quack() { $this->quackStrategy->quack(); } } // 有只小黄鸭 class YellowDuck extends Duck { public function __construct($flyStrategy, $quackStrategy) { $this->flyStrategy = $flyStrategy; $this->quackStrategy = $quackStrategy; } } $yellowDuck = new YellowDuck(new FlyNoWay(), new Squeak()); $yellowDuck->fly(); $yellowDuck->quack(); /* Output: 不会飞 吱吱 */ ?>
Python
Python 就没有所谓的抽象类和接口了,当然你也可以通过 abc
模块来实现这些功能。
比较简单的做法是:将具体行为直接定义为函数,在初始化鸭子时通过构造函数传入行为函数,赋值给对应的变量。当执行具体行为时,将直接调用被赋值的变量,这时具体的行为动作就被委托给了传入的行为函数,达到了“委托”的效果。
class Duck: def __init__(self, fly_strategy, quack_strategy): self.fly_strategy = fly_strategy self.quack_strategy = quack_strategy def fly(self): self.fly_strategy() def quack(self): self.quack_strategy() def fly_with_wings(): print("会飞") def fly_no_way(): print("不会飞") def quack(): print("呱呱") def squeak(): print("吱吱") def mute_quack(): print("不会叫") # 一只会飞也不会叫的小黄鸭 yellow_duck = Duck(fly_no_way, mute_quack) yellow_duck.fly() yellow_duck.quack() # Output: # 不会飞 # 不会叫
Golang
在 Go 语言中没有 extends
关键字,但可以通过“在结构体中内嵌匿名类型”的方式实现继承关系。此处,将 FlyBehavior
飞行行为和 QuackBehavior
行为声明为接口。
package main import "fmt" // FlyBehavior 飞行行为接口 type FlyBehavior interface { fly() } // QuackBehavior 呱呱叫行为接口 type QuackBehavior interface { quack() } // FlyWithWings 会飞的类 type FlyWithWings struct { } func (flyWithWings FlyWithWings) fly() { fmt.Println("会飞") } // FlyWithWings 不会飞的类 type FlyNoWay struct{} func (flyNoWay FlyNoWay) fly() { fmt.Println("不会飞") } // Quack 呱呱叫 type Quack struct{} func (quack Quack) quack() { fmt.Println("呱呱") } // Squeak 吱吱叫 type Squeak struct{} func (squeak Squeak) quack() { fmt.Println("吱吱") } // MuteQuack 不会叫 type MuteQuack struct{} func (muteQuack MuteQuack) quack() { fmt.Println("不会叫") } // Duck 鸭子类 type Duck struct { FlyBehavior FlyBehavior QuackBehavior QuackBehavior } func (d *Duck) fly() { d.FlyBehavior.fly() // 委托给飞行行为 } func (d *Duck) quack() { d.QuackBehavior.quack() // 委托给呱呱叫行为 } func main() { yellowDuck := Duck{FlyNoWay{}, Squeak{}} yellowDuck.fly() yellowDuck.quack() } /* Output: 不会飞 吱吱 */
总结
三种设计原则:
- 封装变化
- 多用组合,少用继承
- 针对接口编程,不针对实现编程
注意此处的“针对接口编程”,书中也有强调:
❝“针对接口编程”真正的意思是“针对超类型(supertype)编程”。这里所谓的“接口”有多个含义,接口是一个“概念”,也是一种 Java 的 interface 构造。你可以在不涉及 Java interface 的情况下“针对接口编程”,关键就在“多态”。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为。 ❞
因此,你不用拘泥于 interface
,你所用的语言就算没有 interface
也能实现设计模式。