我最近在看大名鼎鼎的《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 有抽象类也有接口,语法和 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 就没有所谓的抽象类和接口了,当然你也可以通过 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:
# 不会飞
# 不会叫
在 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
也能实现设计模式。