"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
类来进行构造,但需要注意的是作用域,防止在跨函数传递时,实例被析构。
总览整个过程,可以总结出一些很经验性的原则:多用组合,少用继承。
继承会导致对类的拓展和修改会牵扯很多东西,相反,利用组合来将大部分方法同类本身分离,来保证在编程甚至在运行时存在很大的弹性空间。
定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
策略模式是很基础的一种设计模式,在面向对象初期便应该考虑到这种模式的应用。
策略模式的根本思想是将静态和动态的部分分离,将可变的部分总结为接口,以一种不变的形式插回原来的类中,保证从类的角度来看是不变的,但实际的实现会根据初始化甚至传入参数的不同来保证区别。
区别于简单的将相同的部分提取出来,策略模式的关键在于将相同的功能提取出来并抽象化,实际使用时通过使用者按需申请来保证可以动态获取。保证整体静态的同时,也满足了具体实现的动态性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。