阅读 282

《Head First 设计模式》学习笔记 | 策略模式

前言

我最近在看大名鼎鼎的《Head First 设计模式》。这本「OO 圣经」用 Java 实现各类设计模式,对于我 —— 一个非 Java 爱好者而言,读起来并不过瘾。

有人读完这本书可能会误解设计模式就是设计 Interface,而事实并非如此。在知乎的一个问题《Python 里没有接口,如何写设计模式?》中,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 也能实现设计模式。