0%

【设计模式】策略模式

本篇介绍了策略模式及相关的面向对象设计原则。策略模式定义了一族算法,将它们分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的用户。

1 引例

鸭子模拟游戏中会出现各种鸭子,一边游泳,一边呱呱叫,此系统内部使用了标准的面向对象技术,设计了一个鸭子父类,并让各种鸭子继承此父类。

image-20230228141328616

此时如果希望鸭子能飞起来,最简单的方法就是在父类中加上一个fly()方法:

image-20230228141532173

但并不是所有鸭子都会飞,于是想到可以在子类中重写fly()方法,让其什么也不做,从而覆盖掉父类的fly()方法。同样,也是不所有鸭子都会嘎嘎叫,比如橡皮鸭子是吱吱叫,同样可以通过重写quack()方法覆盖掉父类的方法来实现。

image-20230228141813782

之后,如果我们想新加入一个木头假鸭类型,它既不会飞也不会叫,那么用同样的方式覆盖父类对应的方法就可以实现。但是当我们需要不断地新增不同类型的鸭子,或者不断地修改不同类型鸭子的行为时,就需要不停的新建子类,重写并覆盖原来的方法,显然这样的实现方式使得我们虽然利用了面向对象的“继承”这一特性,但却并没有实现代码的复用。

转变思路,我们可以将fly()方法从鸭子类中分离出来,放进一个Flyable的接口(抽象类)中,只有会飞的鸭子才继承此抽象类,并实现其中的fly()方法,quack()方法同样分离出来放在Quackable的抽象类中,只有会叫的鸭子才继承此抽象类并实现其中的quack()方法。

image-20230228142529567

这样的设计看似更加巧妙,但重复的代码会变得更多,且代码依然无法复用,我们要为每一个会叫的鸭子写一套quack()代码,即使他们叫的方式是相同的。

对于上面这个例子,似乎继承和接口都不能很好的解决问题,而恰好有一个设计模式,可以适用于这种情况。

2 策略模式

设计原则:将变化的部分单独封装起来

找出程序中可能需要变化的地方,把他们独立出来并进行封装,使他们和那些不会变化的代码分开,以便之后可以轻易的修改或扩充这一部分代码,而不影响那些不需要变化的部分。

对于上面的例子,显然fly()quack()就是代码中需要变化的部分,因为他们会随着不同的鸭子类型或者不同的用户需求随时发生变化,所以我们可以将他们从鸭子类中独立出来,分别建立两个抽象类FlyBehaviorQuackBehavior

1
2
3
4
5
6
7
8
9
class FlyBehavior {
public:
virtual void fly() = 0;
};

class QuackBehavior {
public:
virtual void quack() = 0;
};

这两个抽象类代表飞和叫这两种行为,至于怎么飞,怎么叫,通过具体的行为类继承于这两个抽象类来实现,比如飞的行为可以分为用翅膀飞和不会飞这两种:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FlyWithWings : public FlyBehavior {
public:
virtual void fly() {
cout << "用翅膀飞" << endl;
}
};

class FlyNoWay : public FlyBehavior {
public:
virtual void fly() {
cout << "不会飞" << endl;
}
};

叫的行为也类似,比如可以实现嘎嘎叫和不会叫:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Quack : public QuackBehavior {
public:
virtual void quack() {
cout << "嘎嘎叫" << endl;
}
};

class MuteQuack : public QuackBehavior {
public:
virtual void quack() {
cout << "不会叫" << endl;
}
};

之后我们在鸭子类中加入这两种行为类的对象,然后将原本的fly()quack()方法改为PerformFly()PerformQuack()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Duck{
public:
FlyBehavior* flyBehavior;
QuackBehavior* quackBehavior;
//其他成员

void PerformFly(){
flyBehavior->fly();
}

void PerformQuack(){
quackBehavior->quack();
}
}

当我们想要构造一个鸭子子类的时候,只需要继承鸭子父类,并在构造函数中指定该子类的具体行为对象,就可以控制鸭子子类的行为了:

1
2
3
4
5
6
7
class MallardDuck : public Duck{
public:
MallardDuck(){
flyBehavior = new FlyWithWings(); // 绿头鸭会用翅膀飞
quackBehavior = new Quack(); // 绿头鸭会嘎嘎叫
}
};

利用上面的设计方法可以让我们在编程时不需要关注鸭子是什么具体类型,因为我们可以利用多态,比如有一个getDuck()函数会返回一个具体类型的鸭子,至于是什么类型,可能在程序运行时才能确定,那么我们的主函数就可以这样写:

1
2
3
4
5
6
int main(){
Duck* duck = getDuck();
duck->PerformFly();
duck->PerformQuack();
return 0;
}

当然,上面的设计依然不够有弹性,因为每一个子类的鸭子具有什么行为是在构造函数中指定的,如果我们希望同一类鸭子也能自由的设定不同的行为,那么只需要在鸭子父类中加上一个设定行为的方法就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Duck{
public:
FlyBehavior* flyBehavior;
QuackBehavior* quackBehavior;
//其他成员

void PerformFly(){
flyBehavior->fly();
}

void PerformQuack(){
quackBehavior->quack();
}

//动态设定行为
void SetFlyBehavior(FlyBehavior* fb){
flyBehavior = fb;
}
void SetQuackBehavior(QuackBehavior* qb){
quackBehavior = qb;
}
};

这样我们通过调用鸭子的SetFlyBehavior()SetQuackBehavior()方法就可以随时改变鸭子的行为了。

纵观上面的设计过程,我们把鸭子类中经常变化的部分独立出来,封装成了单独的抽象类,也就是一个接口,然后用不同的代码去实现这个接口(抽象类的子类),这些不同的代码就是“一族算法”,而在鸭子类中,只要包含这一族算法的统一接口就可以灵活地使用他们当中的任何一个。

设计原则:面向接口编程,而不是面向实现编程。

能够实现这样灵活的效果的原因在于,所有鸭子的行为并不是从父类继承而来的,而是与不同的行为对象组合而来的,因此恰当的使用类的组合有时要比一味的使用类的继承灵活的多。而进行组合的类也不一定是行为,也可以是物品、属性等等,比如游戏中不同的角色有不同的武器,那么一个角色就可以由角色类和武器类组合而来。

设计原则:多用组合,少用继承。

利用组合构建系统具有很大的弹性,不仅可以将一族算法封装成类,还可以在运行时动态的改变类对象的行为或属性,只要组合的类符合接口标准即可。因此在面向对象程序设计中,简单的使用继承和多态不一定能降低代码量和代码维护成本,灵活使用类的组合也可以达到代码复用的目的。

上面的设计还有一个好处就是,每一族算法或者其中的每一个算法都可以有自己的成员,比如算法要用到的参数,状态和其他辅助函数等等,只要最终实现了接口就可以,这可以让算法的设计更加灵活。

上面的设计模式就是策略模式,策略模式定义了一族算法,将它们分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的用户。

---- 本文结束 知识又增加了亿点点!----

文章版权声明 1、博客名称:LycTechStack
2、博客网址:https://lz328.github.io/LycTechStack.github.io/
3、本博客的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主进行删除处理。
4、本博客所有文章版权归博主所有,如需转载请标明出处。