目录
一.专栏简介
本专栏是我学习《head first》设计模式的笔记。这本书中是用Java语言为基础的,我将用C++语言重写一遍,并且详细讲述其中的设计模式,涉及是什么,为什么,怎么做,自己的心得等等。希望阅读者在读完我的这个专题后,也能在开发中灵活且正确的使用,或者在面对面试官时,能够自信地说自己熟悉常用设计模式。
不费话,下面直接开始策略模式的学习吧。
二.从简单的SimUDuck应用开始
在书中,SimUDuck是一款成功的鸭塘模拟游戏。一开始,其中使用了简单OO面向对象技巧来描述其中的各种鸭子。首先有一个鸭子基类,也是抽象类,然后各种不同种类的鸭子去继承它,实现其中的虚函数并且添加自己的属性和方法。

直接展示代码如下:
cpp
// 鸭子抽象基类(接口)
struct Duck
{
void quack()
{
cout << "嘎嘎叫" << endl;
}
void swim()
{
cout << "游泳" << endl;
}
virtual void display() = 0;
// 其他鸭子方法
};
// 绿头鸭派生类
class MallardDuck : public Duck
{
void display() override
{
cout << "看起来像绿头鸭" << endl;
}
};
// 红头鸭派生类
class RedheadDuck : public Duck
{
void display() override
{
cout << "看起来像红头鸭" << endl;
}
};
用struct作为基类,而不是用class主要是利用struct的成员属性和成员方法默认是public的这一特性,少写一行代码哈哈~~其中display()这个函数是纯虚函数,那么就导致Duck这个基类成为了抽象类。在C++中,我们用抽象类作为其它语言所谓的接口。总的来说,这是一段简单的继承和重写,子类必须重写display()这个函数。
【八股文考点】 class和struct的区别:
- struct的默认访问权限是public,class的默认访问权限是private;
- struct的默认继承方式是public,class的默认继承方式是private。
二.但现在我们需要鸭子飞
需要鸭子飞,那还不简单,直接在基类Duck中添加一个fly()方法,那么鸭子就都能飞了。

代码如下:
cpp
// 鸭子抽象基类(接口)
struct Duck
{
void quack()
{
cout << "嘎嘎叫" << endl;
}
void swim()
{
cout << "游泳" << endl;
}
void fly()
{
cout << "起飞,诶,飞" << endl;
}
virtual void display() = 0;
// 其他鸭子方法
};
这样继承Duck的各种鸭子子类就都能飞了。
三.但是,出大问题了
游戏中还有橡皮鸭,橡皮鸭显然是不能飞的。对代码的局部更新导致了非局部的副作用(飞行的橡皮鸭)!这里想用继承来达到复用的目的,但是当涉及到维护时,效果并不那么好。这里就是继承的缺点之一:变更会不经意地影响其它鸭子。

出问题的代码如下:
cpp
// 鸭子抽象基类(接口)
struct Duck
{
virtual void quack()
{
cout << "嘎嘎叫" << endl;
}
void swim()
{
cout << "游泳" << endl;
}
void fly()
{
cout << "起飞,诶,起飞" << endl;
}
virtual void display() = 0;
// 其他鸭子方法
};
// 绿头鸭派生类
class MallardDuck : public Duck
{
void display() override
{
cout << "看起来像绿头鸭" << endl;
}
};
// 红头鸭派生类
class RedheadDuck : public Duck
{
void display() override
{
cout << "看起来像红头鸭" << endl;
}
};
// 橡皮鸭派生类
class RubberDuck : public Duck
{
void quack() override
{
cout << "吱吱叫" << endl;
}
void display() override
{
cout << "看起来像橡皮鸭" << endl;
}
};
这里就出现了上述问题,飞翔的橡皮鸭。因为考虑到橡皮鸭叫声不一样,这里还将基类中quack()函数设置为虚函数,子类可选择性地重写它。
这里我们就容易想到,将fly()函数也变成虚函数,就像quack()函数一样,橡皮鸭对fly()重写,改成自己不能飞。这样确实达到了目的。但再想游戏中添加一个诱饵鸭呢,它既不会叫,也不会飞。诱饵鸭的类中也需要对quack()函数和fly()函数进行重写,改成不做任何事。
橡皮鸭和诱饵鸭中有相同的fly()方法,造成了代码的冗余 。若收到需求,要修改不做任何行为的fly(),就需要在橡皮鸭和诱饵鸭的fly()方法中逐个修改,提高了变更的风险 ,也就是说程序员如果漏改,就造成了bug的产生。另外,鸭子的行为(飞,叫等)散布在各个子类的重写方法中,难以在一处就能掌握所有鸭子的行为实现细节,造成了行为知识碎片化 。最后,我们不能在运行时改变鸭子飞的方式或者叫的方式,也就是运行时无法动态调整鸭子对象的行为。
总结问题如下:
- 代码在多个子类中重复
- 提高了变更的风险
- 运行时行为难以改变
- 难以获得所有鸭子行为的知识
- 变更会不经意地影响其他鸭子
四.接口如何?
哦哦哦!!!可以把fly()从Duck基类拿出来,并且做一个带fly()方法的Flyable接口(抽象类)。这样,只有能飞的鸭子继承该接口,并且重写它的fly()方法......同样,做一个Quackable接口,因为不是所有鸭子都能嘎嘎叫。

这真是最糟糕的主意了,因为同样造成了大量的代码重复,完全摧毁了代码的复用。当需要修改鸭子飞行行为的时候,需要对所有会飞的鸭子的fly()函数进行修改,改死人啊啊啊。
不是所有子类都有飞行或者嘎嘎叫行为,因此继承不是正确答案。
五.软件开发中唯一的不变
就是变化。
不管你的应用设计得有多好,随着时间推移,应用必定成长和变更,否则它就死了。
六.聚焦于问题
如果你需要修改一个行为,常常被迫往下追踪到所有定义了该行为的子类并修改它,在这个过程中,可能会引入新的bug。
幸运的是,针对这种情况有一个设计原则:
识别应用中变化的方面,把它们和不变的方面分开。

取出变化的部分,并把它"封装"起来,这样它就不会影响其他代码。
另一种思考方式是:把会变化的部分取出来并封装,这样,以后你就可以修改或扩展这个部分,而不会影响其他不需要改变的部分。
这样做之后,代码变更引起的不经意后果变少,系统更加有弹性。
七.分离变和不变
变得部分是fly()和quack(),也就是飞行行为和叫行为,除此之外,Duck类中的其他部分没有变化的迹象。为了从Duck类分离这些行为,我们把两个方法都从Duck类抽出,并创建两组新的类来表示两种行为。例如,Quack这组类就实现嘎嘎叫,吱吱叫和沉默不语。

八.设计Duck的行为
另外,很重要的一点,我们需要保证各种东西的弹性。现在我们要将飞行行为和嘎嘎叫行为作为Duck的一部分,也就是在Duck的构造函数中初始化它们。那我们现在就考虑令它们可以在运行时改变,那怎么个改变法呢?令各种飞行行为继承飞行接口,各种飞行行为重写飞行方法,那么就可以利用多态了,Duck类中持有基类指针执行子类,运行时就可以改变这个基类指针的指向,从而动态改变飞行行为。
这样做之后,Duck类甚至不需要知道飞行行为的具体细节。

新的设计原则:针对接口编程,而不是针对实现编程。
在我们的新设计中,Duck子类将使用接口(FlyBehavior和QuackBehavior)所表示的行为,这样,行为实际的实现(换句话说,在类中编码的,实现FlyBehavior或QuackBehavior的特定具体行为)不会被锁定在Duck子类中。

那么针对接口编程和针对实现编程是什么区别呢?下面用代码进行演示。
针对实现编程:
cpp
Dog* d = new Dog();
d->bark();
针对接口编程:
cpp
Animal* animal = new Dog();
animal->makeSound();

更棒的是,子类型实例化不用在代码中硬编码(像new Dog()),而是在运行时分配具体的实现对象:
cpp
a = getAnimal();
a.makeSound();
我们不知道实际的动物子类型,我们在意的只是它知道如何响应makeSound()。
九.实现Duck的行为
这里我们有两个基类接口,FlyBehavior和QuackBehavior,以及相应的实现每个具体行为的类:

通过这个设计,其他类型的对象可以复用飞行和嘎嘎叫行为,因为这些行为不再隐藏在Duck类后面了!我们可以增加新的行为,只需要将相应类别的行为继承相应的接口,然后实现虚函数即可,不用修改任何已有行为类或设计任何使用飞行行为的Duck类。
十.整合Duck的行为
关键在于:Duck现在将委托其飞行和嘎嘎叫行为,而不是使用Duck类(或子类)中定义的嘎嘎叫和飞行方法。
做法如下:
首先,添加两个实例变量,类型为 FlyBehavior 和 QuackBehavior,名称为 flyBehavior 和 quackBehavior。在运行时,每个具体鸭子对象将给这些变量分配特定行为,像飞行的 FlyWithWings,嘎嘎叫的 Squeak。我们也将从 Duck 类(以及任何子类)移除 fly () 和 quack () 方法,因为我们已经把这些行为搬进 FlyBehavior 和 QuackBehavior 类。我们将用两个类似的方法 performFly () 和 performQuack () 来替换 Duck 类中的 fly () 和 quack (),接下来就会看到它们是怎么工作的。

现在,看看performQuack():

相当简单,嗯哼?为了执行嘎嘎叫,Duck 只要让 quackBehavior 所引用的对象为它嘎嘎叫即可。在这部分代码中我们不关心具体 Duck 是哪种对象,只要它知道怎么 quack () 就行了!
十一.测试Duck的代码
代码如下:
Behavior.hpp:
cpp
#pragma once
#include <iostream>
using namespace std;
// 飞行行为接口
struct FlyBehavior
{
virtual void fly() = 0;
};
// 用翅膀飞行为子类
struct FlyWithWings : FlyBehavior
{
void fly() override
{
cout << "用翅膀飞" << endl;
}
};
// 不会飞行为子类
struct FlyNoWay : FlyBehavior
{
void fly() override
{
cout << "不会飞" << endl;
}
};
struct QuackBehavior
{
virtual void quack() = 0;
};
struct Quack : QuackBehavior
{
void quack() override
{
cout << "正常叫唤" << endl;
}
};
struct Squack : QuackBehavior
{
void quack() override
{
cout << "橡皮鸭子嘎嘎叫" << endl;
}
};
struct MuteQuack : QuackBehavior
{
void quack() override
{
cout << "不叫" << endl;
}
};
Duck.hpp:
cpp
#include <iostream>
using namespace std;
#include "Behavior.hpp"
// 鸭子抽象基类(接口)
struct Duck
{
Duck() :
flyBehavior(nullptr), quackBehavior(nullptr)
{}
~Duck()
{
if(flyBehavior) delete flyBehavior;
if(quackBehavior) delete quackBehavior;
}
virtual void display() = 0;
void swim()
{
cout << "游泳" << endl;
}
void performQuack()
{
quackBehavior->quack();
}
void performFly()
{
flyBehavior->fly();
}
// 其他鸭子方法
protected:
FlyBehavior* flyBehavior;
QuackBehavior* quackBehavior;
};
// 绿头鸭派生类
class MallardDuck : public Duck
{
public:
MallardDuck()
{
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
void display() override
{
cout << "看起来像绿头鸭" << endl;
}
};
// 红头鸭派生类
class RedheadDuck : public Duck
{
public:
RedheadDuck()
{
flyBehavior = new FlyWithWings();
quackBehavior = new Quack();
}
void display() override
{
cout << "看起来像红头鸭" << endl;
}
};
// 橡皮鸭派生类
class RubberDuck : public Duck
{
public:
RubberDuck()
{
flyBehavior = new FlyNoWay();
quackBehavior = new Squack();
}
void display() override
{
cout << "看起来像橡皮鸭" << endl;
}
};
main.cpp:
cpp
#include "Duck.hpp"
int main()
{
MallardDuck mallardDuck;
mallardDuck.display();
mallardDuck.swim();
mallardDuck.performFly();
mallardDuck.performQuack();
RedheadDuck redheadDuck;
redheadDuck.display();
redheadDuck.swim();
redheadDuck.performFly();
redheadDuck.performQuack();
RubberDuck rubberDuck;
rubberDuck.display();
rubberDuck.swim();
rubberDuck.performFly();
rubberDuck.performQuack();
return 0;
}
运行结果:

书中Java所写的MallardDuck如下,主要看看图片旁边的文字说明:

MallardDuck 的嘎嘎叫是真真正正活生生的嘎嘎叫,不是吱吱叫,也不是叫不出声。当 MallardDuck 被实例化时,其构造器初始化 MallardDuck 继承来的 quackBehavior 实例变量为一个新的 Quack(QuackBehavior 的一个具体实现类)类型实例。
鸭子的飞行行为也一样,MallardDuck 的构造器初始化继承来的 flyBehavior 实例变量为 FlyWithWings(FlyBehavior 的一个具体实现类)类型的实例。
十二.动态设置行为
在鸭子中建立了这些动态的能力,却没有在使用,实在可惜!想象一下,如果你要通过一个 Duck 类的 setter 方法设置鸭子的行为类型,而不是在鸭子的构造器中实例化。
1.添加两个新的set方法到Duck类中,分别设置飞行行为和叫唤行为。
代码如下:
cpp
// 鸭子抽象基类(接口)
struct Duck
{
Duck() :
flyBehavior(nullptr), quackBehavior(nullptr)
{}
virtual void display() = 0;
void swim()
{
cout << "游泳" << endl;
}
void performQuack()
{
quackBehavior->quack();
}
void performFly()
{
flyBehavior->fly();
}
void setFlyBehavior(FlyBehavior* newFlyBehvior)
{
delete flyBehavior;
flyBehavior = newFlyBehvior;
}
void setQuackBehavior(QuackBehavior* newQuackBehavior)
{
delete quackBehavior;
quackBehavior = newQuackBehavior;
}
// 其他鸭子方法
protected:
FlyBehavior* flyBehavior;
QuackBehavior* quackBehavior;
};
任何时候我们想要改变鸭子的行为,我们都可以调用这些set方法。
2.做一个新的Duck类型,就叫做ModelDuck,模型鸭
代码如下:
cpp
// 模型鸭派生类
class ModelDuck : public Duck
{
public:
ModelDuck()
{
flyBehavior = new FlyNoWay();
quackBehavior = new MuteQuack();
}
void display() override
{
cout << "看起来像模型鸭" << endl;
}
};
3.做一个新的火箭动力的飞行行为,就叫FlyRocketPowered
cpp
struct FlyRocketPowered : FlyBehavior
{
void fly() override
{
cout << "用火箭发射器飞" << endl;
}
};
4.改变测试类,测试模型鸭和火箭动力的飞行行为
现在我们改用Duck这个基类指针来调用,效果是一样的,但这是更好的写法。
cpp
Duck* modelDuck = new ModelDuck();
modelDuck->display();
modelDuck->swim();
modelDuck->performFly();
cout << endl;
modelDuck->setFlyBehavior(new FlyRocketPowered());
modelDuck->performFly();
modelDuck->performQuack();
运行结果如下:

十三.封装行为的全貌
OK,现在我们已经深入研究了鸭子模拟器的设计,是时候浮出水面看一看全貌了。 以下是重新设计后的整个类结构,里面有你所期望的一切:鸭子扩展Duck,飞行行为实现FlyBehavior,嘎嘎叫行为实现QuackBehavior。
也要注意,我们描述事情的方式有了一点不同。不再把鸭子的行为看作一组行为,而是开始把它们看作一个算法家族。想想看:在SimUDuck设计中,算法代表鸭子会做的事(不同的嘎嘎叫或飞行方式),不过我们也可以轻易地把同样的技巧用于一组实现计算不同州销售税的类。 特别要注意类之间的关系。在下面类图中标明合适的关系[IS-A(是一个),HAS-A(有一个),和IMPLEMENTS(实现)]。

十四.HAS-A比IS-A好
HAS-A关系很有趣:每只鸭子有一个FlyBehavior和一个QuackBehavior,以便委托飞行和嘎嘎叫。 当你把两个类像这样放在一起时,你在使用组合。不继承行为,鸭子通过组合正确的行为对象获得它们的行为。
这是一个重要的技巧;事实上,它是我们的第三个设计原则的基础:
设计原则:优先使用组合而不是继承。
正如你已经看到的,使用组合创建系统给了你更大的弹性。不仅是让你把一个算法家族封装进它们自己的一组类,而且让你在运行时改变行为,只要组合的对象实现正确的行为接口。
组合用在许多设计模式中,在本书前前后后,你会看到更多关于其优点和缺点的内容。
书中大师和徒弟的对话说明了设计模式,继承,复用,可扩展的重要性:


头脑风暴:

答案:用组合的方式,鸭鸣器内有一个(has-a)橡皮鸭,会嘎嘎叫而不会飞,完美契合。
十五.总结与定义
恭喜各位,我们学会了第一个设计模式。
我们刚刚应用了第一个设计模式------策略模式。对的,我们使用策略模式改写了SimUDuck应用。 多亏有了这个模式,模拟器可以应对任何变化。为了学会这个模式,我们走了很长一段路,下面是这个模式的正式定义:
策略模式定义了一个算法族,分别封装起来,使得它们之间可以互相变换。策略让算法的变化独立于使用它的客户。