zl程序教程

您现在的位置是:首页 >  后端

当前栏目

策略模式

模式 策略
2023-06-13 09:15:14 时间

策略模式

"Head + First"书中为策略模式设计的实际场景是为鸭子类添加功能。

场景分析

class Duck{
public:
    void quack(void){
        // 鸭子叫
    }
    virtual void display(void);
    void swim(void){
        // 鸭子游泳
    }
}
class MallardDuck : public Duck{
public:
    virtual void display(void){
        // 外观是绿头
    }
}
class RedHeadDuck : public Duck{
public:
    virtual void display(void){
        // 外观是红头
    }
}
class RubberDuck : public Duck{
public:
    virtual void display(void){
        // 外观是橡皮鸭
    }
    void quack(void){
        // 覆盖为吱吱叫
    }
}

一个很直接的鸭子类,并且除了真实的鸭子子类,还存在橡皮鸭这种有点小区别的子类。新的一个需求是要求鸭子能飞。

直接在Duck类中加上fly()方法,会导致所有继承了Duck的子类都会飞,那么橡皮鸭在空中飞的场景将会发生。为了避免这种情况,需要在RubberDuck子类中将fly()方法重写。当前情况下这么修改是可以接受的。

不同的子类对功能的需求是不同的,假如简单的在父类中添加功能,在子类中通过重写来将功能覆盖,从开发角度来看简直是灾难。

另一种方法是利用接口,将不同的功能设计为不同的接口,具有功能的子类手动调用该接口实现功能。但这太不抽象了,完全没有面向对象之魂。而且时间久了,接口多了,开发新的子类将是无比麻烦的,最主要的是重复代码将会非常多。

设计原则

封装变化原则

假如把所有功能全部放在父类中,那么子类必须频繁的重写函数来保证某些功能能符合自身需求。为了避免这种情况,提出了一种封装变化的设计原则:

找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

于是我们把不变的功能放在父类中实现,变化的功能放在需要他们的子类中实现,那些不需要这些功能的子类同样也不需要为这种行为付出代码。

针对接口编程原则

现在和场景分析的第二种情况一样,一些不同的子类可能那些变化的功能也是相同的,所以还是有很大的代码重复率。

为了避免这种情况,提出了一种针对接口编程原则:针对接口编程,而不是针对实现编程。

用一个形象的例子表示。

class Animal {
public:
    virtual void makeSound();
}

class Dog : public Animal {
public:
    void bark(){
        //狗叫声
    }
    virtual void makrSound(){
        bark();
    }
}

//针对实现编程
Dog d = new Dog();
d.dark();

//针对接口编程
Animal animal = new Dog();
animal.makeSound();

//更进一步的针对接口编程
Animal a = getAnimal();    //在使用时动态获取动物类型
a.makeSound();

可以很明显的看出,针对接口的编程更多得保障代码的灵活性,提高代码的复用性,减少修改代码的频率。

以鸭子为例,将fly()行为单独设计一个接口类FlyBehavior(),并且实现了不同的子类:

FlyBehavior

fly()

并不做具体实现

FlyWithWings

fly()

用翅膀飞

FlyNoWay

fly()

不飞

这样,飞这个行为和鸭子这个生物已经无关了,其他动物也可以使用这个接口来实现飞的行为。

新的鸭子类

由于定义了FlyBehavior()接口,所以鸭子类也需要进行针对性的变化,同时还要兼顾到针对接口编程的原则。

class FlyBehavior{
public:
    virtual void fly(void);
}
class FlyWithWings : public FlyBehavior{
public:
    virtual void fly(void){
        // 用翅膀飞
    }
}
class FlyNoWay : public FlyBehavior{
public:
    virtual void fly(void){
        // 不飞
    }
}

class Duck{
    FlyBehavior *pFlyBehavior;
public:
    void fly(){
        pFlyBehavior->fly();
    }
    ~Duck(){
        delete pFlyBehavior;
    }
    // ... 
}
class MallarDuck : public Duck{
public:
    MallarDuck(){
        pFlyBehavior = new FlyWithWings;
    }
    // ...
}
class RedHeadDuck : public Duck{
public:
    RedHeadDuck(){
        pFlyBehavior = new FlyWithWings;
    }
    // ...
}
class RubberDuck : public Duck{
public:
    RubberDuck(){
        pFlyBehavior = new FlyNoWay;
    }
    // ...
}

在构造函数中,不同的子类获得了对应的FlyBehavior,实现了不同的飞行行为。与此类似的,可以将quack()方法也提取出来作为接口,使得鸭子的叫声也可以很好的进行拓展。

同样的,也可以更进一步通过传入FlyBehavior类来进行构造,但需要注意的是作用域,防止在跨函数传递时,实例被析构。

组合继承原则

总览整个过程,可以总结出一些很经验性的原则:多用组合,少用继承。

继承会导致对类的拓展和修改会牵扯很多东西,相反,利用组合来将大部分方法同类本身分离,来保证在编程甚至在运行时存在很大的弹性空间。

策略模式

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

策略模式是很基础的一种设计模式,在面向对象初期便应该考虑到这种模式的应用。

策略模式的根本思想是将静态和动态的部分分离,将可变的部分总结为接口,以一种不变的形式插回原来的类中,保证从类的角度来看是不变的,但实际的实现会根据初始化甚至传入参数的不同来保证区别。

区别于简单的将相同的部分提取出来,策略模式的关键在于将相同的功能提取出来并抽象化,实际使用时通过使用者按需申请来保证可以动态获取。保证整体静态的同时,也满足了具体实现的动态性。