提示:本文只是想教会大家策略模式,案例代码用的是c++,如果你已经掌握了策略模式,请跳过。内容是模仿有关设计模式的一书《Head first Design Patterns》,如有差错请在评论区指出。
从SimDuck应用设计中学习策略模式
- 1.SimUDuck介绍
- 2.需要鸭子会飞——Duck中添加fly方法
- 3.代码复用概念
- 4.选择继承fly和quack
- 5.接口如何?
- 6.解决方案——抽离变化部分封装,针对接口编程
- 7.使用组合而不是继承
- 8.总结
- 9.思考题
1.SimUDuck介绍
SimUDuck应用:有一款鸭塘模拟游戏SimUDuck,游戏中会出现各种鸭子,一边戏水,一边嘎嘎叫,系统最初的设计师用的是标准的OO技巧,创建了一个Duck抽象基类,然后所有鸭子继承它。
class Duck{
public:
void quack();//鸭子都会嘎嘎叫
//每个鸭子长相都不同,因此将display设置为纯虚函数
virtual void display()=0;
//其他鸭子的方法
};
2.需要鸭子会飞——Duck中添加fly方法
现在管理层突然决定需要鸭子会飞。自然而然我们就会想到:只需在Duck中添加一个fly()方法,然后所有鸭子也继承Duck,就可实现鸭子会飞,同时也满足代码复用原则
class Duck{
public:
void quack();//鸭子都会嘎嘎叫
//每个鸭子长相都不同,因此将display设置为纯虚函数
virtual void display()=0;
void fly();//Ok,现在鸭子会飞了
//其他鸭子的方法
};
3.代码复用概念
什么是代码复用?
代码复用是指在软件开发过程中,利用已有的代码模代码复用是指在程序设计中尽量利用已有的代码,以便在不必重新编写相同或类似功能的情况下实现特定功能。代码复用可以通过将一段代码封装成函数、模块、类等形式来实现,然后在需要的地方调用这些可复用的代码。代码复用可以提高代码的可维护性、可读性和可扩展性,同时也可以减少代码的重复性,提高开发效率。
4.选择继承fly和quack
是不是感觉这样设计还行,如果你也这样想就大错特错了!
原因是并不是所有的鸭子都应该会飞,当给Duck类添加飞的行为的同时让不应该会飞的也会飞了,在游戏中出现了飞行的橡皮鸭,多荒唐!
接下来你可能会想:把Duck中的fly()设置成virtual,然后选择性的覆盖fly,只要在向类似于橡皮鸭这种不会飞的鸭子重写fly,然后什么也不做就好了
class Duck{
public:
void quack();//鸭子都会嘎嘎叫
//每个鸭子长相都不同,因此将display设置为纯虚函数
virtual void display()=0;
virtual void fly();//设置成虚函数
//其他鸭子的方法
};
class RubberDuck:public Duck{
public:
void display() override{//橡皮鸭}
void fly() override{//什么也不做}
};
现在问题解决了,不会出现不应该飞的鸭子到处飞来飞去了。但是当我们给程序添加木质诱饵鸭时,又会发生什么?诱饵鸭不应该飞也不应该嘎嘎叫,于是再把Duck中的quack设置虚函数,再让诱饵鸭覆盖Duck中的quack使之啥也不干
class Duck{
public:
virtual void quack();//设置成虚函数
//每个鸭子长相都不同,因此将display设置为纯虚函数
virtual void display()=0;
virtual void fly();//设置成虚函数
//其他鸭子的方法
};
class DecoyDuck:public Duck{
public:
void displsy() override{//诱饵鸭}
void fly() override{//什么也不做}
void quack() override{//什么也不做}
};
5.接口如何?
这样设计接口如何?
若是产品需要持续更新(大概率),每一次约束都会变化,每一次在程序中添加新的Duck子类,你都需要被迫检查,有可能还要覆盖fly和quack,直到永远。
因此,我们需要用更干净的方式来处理这些问题,只让某些鸭子飞或嘎嘎叫。于是我们可以把Duck中的fly和quack抽离出来分别成为单独的类,只让会飞或会嘎嘎叫的鸭子继承这些类。问题似乎就解决了。
但是代码复用就被摧毁了,当你考虑有72种鸭子的时候,做这些改变只会让你的代码变得难以维护,更别论飞行方式和教的声音有多种的时候,这简直就是噩梦。
那么应该怎么办?
6.解决方案——抽离变化部分封装,针对接口编程
我们需要把会变化的部分抽离出来,并将其封装,这样以后你修改或者扩展这个部分时,就不会影响其他不需要变化的部分。于是我们抽离出鸭子飞和嘎嘎叫的行为,创建两组类,一组类是flyBehavior,一组类是quackBehavior。那么问题来了,我们如何设计者两组类?
我们希望保持各种东西的弹性,为了使系统变得易于维护和更新,我们需要针对接口编程,而不是针对实现编程,即需要给flyBehavior和quackBehavior这两组类设计统一的接口。
class FlyBehavior{
public:
virtual void fly() = 0;
};
class FlyWithWings:public FlyBehavior{
public:
void fly() override{//实现鸭子飞}
};
class FlyNoWay:public FlyBehavior{
public:
void fly() override{//什么都不做,即不会飞}
};
class QuackBehavior{
public:
virtual void quack() = 0;
};
class Quack:public QuackBehavior{
public:
void quack() override{//实现鸭子嘎嘎叫}
};
class Squeak:public QuackBehavior{
public:
void quack() override{//橡皮鸭吱吱叫}
};
class MuteQuack: public QuackBehavior{
public:
void quack() override{//诱饵鸭不出声}
};
通过这种设计,其他类型的对象可以复用飞行和嘎嘎叫的行为,我们也可以增加新的行为,而不用修改任何已有行为类或任何使用了飞行行为类的Duck类。简直就是完美!
接下来重新实现Duck及其子类即可
class Duck{
public:
Duck(FlyBehavior *fly=nullptr,QuackBehavior *quack=nullptr):fly_behavior(fly),quack_behavior(quack){}
virtual void display() = 0;
void performFly(){fly_behavior->fly();}
void performQuack(){quack_behavior->quack();}
protected:
FlyBehavior *fly_behavior;
QuackBehavior *quack_behavior;
};
class MallardDuck:public Duck{
public:
MallarDuck(FlyBehavior *fly =new FlyWithWings(),QuackBehavior *quack=new Quack()):Duck(fly,quack){}
void display() override{std::cout<<"我是绿头鸭"<<std::endl;}
};
class RubberDuck:public Duck{
public:
RubberDuck(FlyBehavior *fly=new FlyNoway(),QuackBehavior *quack=new Squeak()):Duck(fly,quack){}
void display() override{std::cout<<"我是橡皮鸭"<<std::endl;}
};
class DecoyDuck:public Duck{
public:
void display() override{std::cout<<"我是诱饵鸭"<<std::endl;}
DecoyDuck(FlyBehavior *fly=new FlyNoWay(),QuackBehavior *quack=new MuteQuack()):Duck(fly,quack);
};
此外,我们还可以为Duck类增加一个可以在运行时改变鸭子飞行或嘎嘎叫的方法,使Duck类更加灵活
class Duck{
public:
void setFlyBehavior(FlyBehavior *fly){delete fly_behavior;fly_behavior = fly;}
void setQuackBehavior(QuackBehavior *quack){delete quack_behavior;quack_behavior = quack;}
//其他方法...
};
7.使用组合而不是继承
OK,现在我们已经深入研究了鸭子模拟器的设计,是时候浮出水面看看全貌。以下是重新设计后的整个类结构:鸭子扩展Duck,飞行行为类FlyBehavior,嘎嘎叫行为类QuackBehavior。我们现在把鸭子的行为看做一个算法家族,分别封装起来,注意,这里是Duck类组合了FlyBehavior和QuackBehavior,即每个鸭子都有一个FlyBehavior和QuackBehavior,以便委托飞行和嘎嘎叫,这里又用到了一个设计原则:
优先使用组合而不是继承,即HAS-A比IS-A更好。正如你看到的,使用组合创建系统给于我们更大的弹性,不仅是把一个算法家族封装进它们自己的一组类,而且可以在运行时改变行为,只要组合的对象实现正确的行为接口。
8.总结
到这里,就已经本文主题中的设计模式–策略模式
现在正式给出策略模式的定义:策略模式定义了一个算法家族,分别封装起来,使得他们之间可以互相变换,策略让算法的变化独立于使用他们的客户。
虽然使用策略模式不仅可以提高代码的灵活性和可扩展性(策略模式将每个算法封装成一个类,可以随时切换算法而不影响代码的其他部分),而且可以提高代码的复用性,同时可以简化复杂的条件判断,避免了使用大量的if-else语句判断不同的情况。但是使用策略模式也有其缺点:首先是增加了代码的复杂度。其次需要创建大量的策略类。如果策略较多,可能会导致创建大量的策略类,进而增加代码量和维护成本。此外客户端需要了解所有的策略类。客户端需要了解每个策略类的作用和使用方式,很可能会增加客户端的复杂度。
9.思考题
最后这里给出一个思考题:
鸭鸣器是一种猎人用来模拟鸭子嘎嘎叫的装置,你如何实现自己的鸭鸣器,而不用从Duck类继承?(提示:组合)