学习软件设计,向OO高手迈进!
设计模式(Design pattern)是软件开发人员在软件开发过程中面临的一般问题的解决方案。
这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
是前辈大神们留下的软件设计的"招式"或是"套路"。
什么是策略模式
在本文末尾会给出解释,待耐心看完demo再看定义,相信你会有更深刻的印象
实例讲解
背景
假设我们正在开发一款类似植物大战僵尸的游戏,产品经理告诉我们说:所有僵尸的必要条件是可以动、可以攻击、外观上可以区分就可以了
没问题,写一个抽象类 Character,然后让所有角色(僵尸)继承这个类就可以了。So easy
Version 1.0
类图
抽象类
class Character {
public:
virtual void Move(void) {
printf("Move\n");
}
virtual void Attack(void) {
printf("Attack\n");
}
// 纯虚函数, 由子类实现
virtual void Display(void) = 0;
};
红头僵尸
class RedHeadZombie : public Character {
public:
virtual void Display(void) {
printf("My head is Red\n");
}
};
绿头僵尸
class GreenHeadZombie : public Character {
public:
virtual void Display(void) {
printf("My head is Green\n");
}
};
main函数
int main(int argc, char *argv[])
{
Character *p_objRedHeadZombie = new RedHeadZombie();
Character *p_objGreenHeadZombie = new GreenHeadZombie();
p_objRedHeadZombie->Move();
p_objRedHeadZombie->Attack();
p_objRedHeadZombie->Display();
p_objGreenHeadZombie->Move();
p_objGreenHeadZombie->Attack();
p_objGreenHeadZombie->Display();
return 0;
}
运行结果
Move
Attack
My head is Red
Move
Attack
My head is Green
写完了,问题解决,正准备看看新闻资讯的时候,产品经理走过来对我说,需要给每个僵尸设置一个前进速度,而且速度要有区别,不能所有僵尸都一样的。好吧,设置速度嘛,也简单,我在抽象类里面添加一个 Speed 的方法,让红头僵尸和绿头僵尸都分别实现这个方法。也不难,分分种搞定
Version 1.1
类图
抽象类
class Character {
public:
......
// 纯虚函数, 由子类实现
virtual void Speed(void) = 0;
};
红头僵尸
class RedHeadZombie : public Character {
public:
......
virtual void Speed(void) {
printf("speed is 3.0\n");
}
};
绿头僵尸
class GreenHeadZombie : public Character {
public:
......
virtual void Speed(void) {
printf("speed is 1.0\n");
}
};
main函数
int main(int argc, char *argv[])
{
Character *p_objRedHeadZombie = new RedHeadZombie();
Character *p_objGreenHeadZombie = new GreenHeadZombie();
p_objRedHeadZombie->Move();
p_objRedHeadZombie->Attack();
p_objRedHeadZombie->Display();
p_objRedHeadZombie->Speed();
p_objGreenHeadZombie->Move();
p_objGreenHeadZombie->Attack();
p_objGreenHeadZombie->Display();
p_objGreenHeadZombie->Speed();
return 0;
}
运行结果
Move
Attack
My head is Red
speed is 3.0
Move
Attack
My head is Green
speed is 1.0
好吧,该有的速度也有了,提交代码,这下满足你了吧,产品经理这时走过来告诉你,僵尸的攻击方式不能一样的,你怎么让所有僵尸的攻击方式都一样了,这肯定不行的。好像有点道理的样子,那我就改咯。我重写所有的 Attack 方法不就行了
Version 1.2
类图
红头僵尸
class RedHeadZombie : public Character {
public:
......
virtual void Attack(void) {
printf("RedHeadZombie's attack\n");
}
};
绿头僵尸
class GreenHeadZombie : public Character {
public:
......
virtual void Attack(void) {
printf("GreenHeadZombie's attack\n");
}
};
main函数,不变
运行结果
Move
RedHeadZombie's attack
My head is Red
speed is 3.0
Move
GreenHeadZombie's attack
My head is Green
speed is 1.0
修改完成,攻击方式也不一样了
这时候我偷偷瞄了一眼产品经理,发现这货正在向我走过来…此处略掉一万字。反正最后结果就是,他觉得需要再添加两种僵尸(短腿僵尸,无攻击力僵尸),短腿僵尸速度很慢,但是可以远程攻击,无攻击力僵尸(可能是后勤部队,用来给其他僵尸运送军火的)可以移动,但是不能攻击。还是乖乖写吧,继续写两个类 ShortLegZombie(短腿),NoAttackZombie(无攻击力)继承自 Character
Version 1.3
类图
短腿僵尸
class ShortLegZombie : public Character {
public:
virtual void Display(void) {
printf("I'm ShortLegZombie\n");
}
virtual void Speed(void) {
printf("speed is slow\n");
}
virtual void Attack(void) {
printf("ShortLegZombie's attack\n");
}
};
无攻击力僵尸
class NoAttackZombie : public Character {
public:
virtual void Display(void) {
printf("I'm NoAttackZombie\n");
}
virtual void Speed(void) {
printf("speed is 5.0\n");
}
virtual void Attack(void) {
// nothing to do
}
};
main函数
int main(int argc, char *argv[])
{
Character *p_objRedHeadZombie = new RedHeadZombie();
Character *p_objGreenHeadZombie = new GreenHeadZombie();
Character *p_objShortLegZombie = new ShortLegZombie();
Character *p_objNoAttackZombie = new NoAttackZombie();
p_objRedHeadZombie->Move();
p_objRedHeadZombie->Attack();
p_objRedHeadZombie->Display();
p_objRedHeadZombie->Speed();
p_objGreenHeadZombie->Move();
p_objGreenHeadZombie->Attack();
p_objGreenHeadZombie->Display();
p_objGreenHeadZombie->Speed();
p_objShortLegZombie->Move();
p_objShortLegZombie->Attack();
p_objShortLegZombie->Display();
p_objShortLegZombie->Speed();
p_objNoAttackZombie->Move();
p_objNoAttackZombie->Attack();
p_objNoAttackZombie->Display();
p_objNoAttackZombie->Speed();
return 0;
}
运行结果
Move
RedHeadZombie's attack
My head is Red
speed is 3.0
Move
GreenHeadZombie's attack
My head is Green
speed is 1.0
Move
ShortLegZombie's attack
I'm ShortLegZombie
speed is slow
Move
I'm NoAttackZombie
speed is 5.0
完成了,还好,不算太慢。搞定
过一会儿,产品经理又走过来和我说,刚刚开会了,他们最后的决定是,需要再多添加下面几种僵尸来增加游戏的可玩性,丰富界面,提高用户可选性,辛苦你们了,最近加加班咯。好吧,一看,四十几种!
又添加了几个僵尸类,先拿给产品经理看一下。产品经理一看,眼睛微眯,皱着眉头:你这不行呀,你这僵尸怎么速度都是不变的?你遇到障碍物肯定得减速,没有障碍物得加速呀,这个要修改。僵尸在起跑线应该都不具有攻击方式,你这个攻击方式需要修改下。 听你这么一说好像有点道理,当僵尸满足某个条件时修改下速度和攻击方式。但是我现在已经创建十几个僵尸类了,这要修改的工作量有点大呀!而且还有几十种都没写上去呢!
思考改进
在软件开发上,有什么是你可以深信不疑的?
不管当初软件设计得多好,一段时间之后,总是需要成长与改变,否则软件就会“死亡”
能不能优化一下代码,让新建一个僵尸类的时候尽量用几行代码就搞定?而且可以随时修改其行为
这样我们重新梳理下思路:我们编写一个抽象类(Character),让所有的僵尸都继承这个类。速度和攻击方式因为是可以动态改变的,所以我们将需要动态改变的部分抽离出来。我们分别编写 ISpeedBehavior 和 IAttackBehavior 两个接口,将所有的速度和攻击行为都抽离出来。速度和攻击这里暂时写三种吧
设计原则:找出应用中可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起
类图
我们想要产生一个新的红头僵尸实例,并指定特定攻击方式给它,所以干脆让僵尸的行为可以动态的改变好了。换句话说,我们应该在 Character 类中包含设定行为的方法,这样就可以在运行时动态的改变红头僵尸的攻击方式了。我们分别编写 SetAttackBehavior 和 SetSpeedBehavior 两个方法,用于设置僵尸的攻击和速度行为
类图
回想一下之前的做法:速度行为和攻击行为来自 Character 抽象类的具体实现,或是继承某个接口并由子类自行实现而来,这两种做法都是依赖于“实现”,我们被实现绑得死死的,没办法更改行为(除非写更多代码)
在我们的新设计中,Character 的子类将使用接口(ISpeedBehavior 和 IAttackBehavior)所表示的行为,所以实际的“实现”不会被绑死在 Character 的子类中,换句话说,特定的攻击行为和速度行为的代码写在了实现 ISpeedBehavior 和 IAttackBehavior 的类中,即 SuperAttack 和 FastSpeed 等类中
设计原则:针对接口编程,而不是针对实现编程
接下来我们重新整理下代码
Version 2.0
类图
IAttackBehavior 接口及其实现类
class IAttackBehavior {
public:
// 纯虚函数, 由子类实现
virtual void Attack(void) = 0;
};
class NoAttack : public IAttackBehavior {
public:
virtual void Attack(void) {
printf("NoAttack\n");
}
};
class OrdinaryAttack : public IAttackBehavior {
public:
virtual void Attack(void) {
printf("OrdinaryAttack\n");
}
};
class ReinforceAttack : public IAttackBehavior {
public:
virtual void Attack(void) {
printf("ReinforceAttack\n");
}
};
class SuperAttack : public IAttackBehavior {
public:
virtual void Attack(void) {
printf("SuperAttack\n");
}
};
ISpeedBehavior 接口及其实现类
class ISpeedBehavior {
public:
// 纯虚函数, 由子类实现
virtual void Speed(void) = 0;
};
class SlowSpeed : public ISpeedBehavior {
public:
virtual void Speed(void) {
printf("SlowSpeed\n");
}
};
class NormalSpeed : public ISpeedBehavior {
public:
virtual void Speed(void) {
printf("NormalSpeed\n");
}
};
class FastSpeed : public ISpeedBehavior {
public:
virtual void Speed(void) {
printf("FastSpeed\n");
}
};
抽象类
class Character {
public:
IAttackBehavior *m_pAttackBehavior;
ISpeedBehavior *m_pSpeedBehavior;
virtual void Move(void) {
printf("Move\n");
}
virtual void PerformAttack(void) {
m_pAttackBehavior->Attack();
}
virtual void PerformSpeed(void) {
m_pSpeedBehavior->Speed();
}
// 随时调用这个方法改变僵尸的攻击行为
virtual void SetAttackBehavior(IAttackBehavior *i_pAttackBehavior) {
m_pAttackBehavior = i_pAttackBehavior;
}
// 随时调用这个方法改变僵尸的速度行为
virtual void SetSpeedBehavior(ISpeedBehavior *i_pSpeedBehavior) {
m_pSpeedBehavior = i_pSpeedBehavior;
}
// 纯虚函数, 由子类实现
virtual void Display(void) = 0;
};
Character 的子类
class RedHeadZombie : public Character {
public:
RedHeadZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
m_pAttackBehavior = i_pAttackBehavior;
m_pSpeedBehavior = i_pSpeedBehavior;
}
virtual void Display(void) {
printf("My head is Red\n");
}
};
class GreenHeadZombie : public Character {
public:
GreenHeadZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
m_pAttackBehavior = i_pAttackBehavior;
m_pSpeedBehavior = i_pSpeedBehavior;
}
virtual void Display(void) {
printf("My head is Green\n");
}
};
class ShortLegZombie : public Character {
public:
ShortLegZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
m_pAttackBehavior = i_pAttackBehavior;
m_pSpeedBehavior = i_pSpeedBehavior;
}
virtual void Display(void) {
printf("I'm ShortLegZombie\n");
}
};
class NoAttackZombie : public Character {
public:
NoAttackZombie(IAttackBehavior *i_pAttackBehavior, ISpeedBehavior *i_pSpeedBehavior) {
m_pAttackBehavior = i_pAttackBehavior;
m_pSpeedBehavior = i_pSpeedBehavior;
}
virtual void Display(void) {
printf("I'm NoAttackZombie\n");
}
};
main 函数
int main(int argc, char *argv[])
{
// 僵尸在起跑线应该都不具有攻击方式, 默认正常速度吧
IAttackBehavior *p_objNoAttack = new NoAttack();
ISpeedBehavior *p_objNormalSpeed = new NormalSpeed();
Character *p_objRedHeadZombie = new RedHeadZombie(p_objNoAttack, p_objNormalSpeed);
Character *p_objGreenHeadZombie = new GreenHeadZombie(p_objNoAttack, p_objNormalSpeed);
Character *p_objShortLegZombie = new ShortLegZombie(p_objNoAttack, p_objNormalSpeed);
Character *p_objNoAttackZombie = new NoAttackZombie(p_objNoAttack, p_objNormalSpeed);
p_objRedHeadZombie->Move();
p_objRedHeadZombie->Display();
p_objRedHeadZombie->PerformAttack();
p_objRedHeadZombie->PerformSpeed();
p_objGreenHeadZombie->Move();
p_objGreenHeadZombie->Display();
p_objGreenHeadZombie->PerformAttack();
p_objGreenHeadZombie->PerformSpeed();
p_objShortLegZombie->Move();
p_objShortLegZombie->Display();
p_objShortLegZombie->PerformAttack();
p_objShortLegZombie->PerformSpeed();
p_objNoAttackZombie->Move();
p_objNoAttackZombie->Display();
p_objNoAttackZombie->PerformAttack();
p_objNoAttackZombie->PerformSpeed();
// 动态改变红头僵尸的行为
p_objRedHeadZombie->SetAttackBehavior(new ReinforceAttack());
p_objRedHeadZombie->SetSpeedBehavior(new FastSpeed());
p_objRedHeadZombie->Display();
p_objRedHeadZombie->PerformAttack();
p_objRedHeadZombie->PerformSpeed();
// 动态改变短腿僵尸的行为
p_objShortLegZombie->SetAttackBehavior(new SuperAttack());
p_objShortLegZombie->SetSpeedBehavior(new SlowSpeed());
p_objShortLegZombie->Display();
p_objShortLegZombie->PerformAttack();
p_objShortLegZombie->PerformSpeed();
return 0;
}
运行结果
Move
My head is Red
NoAttack
NormalSpeed
Move
My head is Green
NoAttack
NormalSpeed
Move
I'm ShortLegZombie
NoAttack
NormalSpeed
Move
I'm NoAttackZombie
NoAttack
NormalSpeed
My head is Red
ReinforceAttack
FastSpeed
I'm ShortLegZombie
SuperAttack
SlowSpeed
这下ok了,随便你添加角色,随便你修改速度和攻击行为,我都能随时修改
策略模式定义
现在,我们来说下什么是策略模式?没错,我们上面的实例用的就是策略模式,分别将程序中变化的部分封装起来,让它们之间可以相互替换(速度和攻击行为)。也就是说策略模式可以让算法的变化独立于使用算法的客户
“有一个”可能比“是一个”更好
“有一个”关系相当有趣:每一个僵尸都有一个 IAttackBehavior 和 ISpeedBehavior,好将攻击和速度委托给它们代为处理
当你将两个类结合起来使用,如同本例一样,这就是组合(composition)。这种做法和“继承”不同的地方在于,僵尸的行为不是继承而来的,而是和适当的行为对象“组合”得来的,这是一个很重要的技巧
设计原则:多用组合,少用继承
策略模式的优缺点
无论哪种模式都有其优缺点,当然我们每次在编写代码的时候需要考虑下其利弊
策略模式的优点:
-
提供了快速替换继承关系的办法:本案例中我们能快速的替换僵尸类的速度,攻击类型
-
减少代码中的 if else 等判断语句:我们在更换攻击状态的时候不用去判断,当达到某个条件时直接切换攻击实现类就可以了。比如上文中的 NoAttack 变 SuperAttack
-
实现的选择:策略模式可以提供不同的实现
策略模式的缺点:
-
策略模式会造成很多策略类:上文中的所有速度实现类,攻击实现类……
-
客户必须知道所有的策略实现类,必须自行知道所有的策略实现类有何不同,并决定使用哪一个。客户每次新建一个僵尸类的时候必须要知道所有的速度类及攻击类的不同,此时就会暴露出具体的实现问题。
总结
现在我们回顾下刚开始说的策略模式所用到的三个设计原则:
- 封装变化。我们将变化的速度和攻击行为封装出来,供动态改变调用
- 针对接口、超类编程,不针对实现编程。我们用 ISpeedBehavior 和 IAttackBehavior 接口去代表速度和攻击行为,每种具体的行为类实现其中一个接口
- 多用组合,少用继承。组合最大的好处就是具有弹性,给代码维护和需求修改提供极大的便利。最开始我们使用的就是继承的方式,在添加大量僵尸类的时候,代码的维护及修改量将非常大。后面优化过后我们就使用了速度和攻击类及僵尸类的组合去编写代码
其实策略模式在游戏开发中是经常会使用到的。比如说街机西游记,有不同的角色:猪八戒,白龙马,孙悟空。他们都有武器,武器可能被妖怪打掉,也能自己捡起来,还能发不同的招数…
参考资料
https://blog.csdn.net/qq_32175491/article/details/79465496
Head+First设计模式(中文版).pdf