《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 Duckfly() 方法中,我们可以使用实例 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:  不会飞  吱吱  */    

总结

三种设计原则:

  1. 封装变化
  2. 多用组合,少用继承
  3. 针对接口编程,不针对实现编程

注意此处的“针对接口编程”,书中也有强调:

❝“针对接口编程”真正的意思是“针对超类型(supertype)编程”。这里所谓的“接口”有多个含义,接口是一个“概念”,也是一种 Java 的 interface 构造。你可以在不涉及 Java interface 的情况下“针对接口编程”,关键就在“多态”。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为。 ❞

因此,你不用拘泥于 interface,你所用的语言就算没有 interface 也能实现设计模式。