策略模式—Head First 模式设计

 

软件工程专业今年新开了一门课程:《设计模式》,这门课程的配套用书是O'REILLY的《Head First 设计模式》的中文版,最近我的汇编老师说,中国教育只是教你记住结论,而不是教你起源和目的,不是教你自己去分析。这句话勾起了我有些久远的记忆,以前逛贴吧的时候好像无意之中看到有人说中国的教材让人没有兴趣学习,而外国的计算机教育的教材通俗易懂,生动形象。。。这两句话在我心中激起了层层涟漪,当去图书馆费了些力气借阅到这本《Head First设计模式》之后发现,老师说的问题来源于生活,是用来解决生活中遇到的问题的,脱离了生活的所有算法都是白搭这句话,还有网友所言,学习是兴趣驱动的挺正确的。一下的博客内容将跟进课程进度进行不断的更新,有时,我们需要学习的不是各种先进的框架,不是急于掌握各种不同的编程语言,而是真真正正的从解决生活问题出发,在大脑中存储一些设计模式,这些设计模式能让自己在以后的代码书写中避免很多雷区。从这方面来说,设计模式是重要的,而,大脑驱动的设计模式则更为重要。

下面切入整体,开始跟进各种设计上所产生的疑惑与问题,并通过不断探索和思考,总结出各种软件设计模式,在以后的代码书写中能避免犯一些模板化的错误,踏一些前人的坑。

1.策略机制

相信只要有一些面向对象编程基础的人在进行编程时一定遇到过这样一种情况:它是基于继承而产生的,我们都知道面向对象的四要素:抽象,继承,多态和封装。其中继承更是帮我们解决了IS-A问题中代码复用的一大堆问题,但是继承有没有什么副作用?或者说继承有没有给我们的程序带来一些死板性?如果没有概念,我们就拿书上的例子来说:

鸭子,有很多种类:红头鸭,绿头鸭,橡皮鸭(洗澡用的小玩具),诱饵鸭(用来做诱饵的小木塞),在我们的具体实现中,如果为种类繁多的鸭子建立一个父类,父类中给出鸭子的共有特征和方法,再用不同的子类来继承父类恐怕是很多人一开始就能想到的方法,从总体上来说,各种类型的鸭子和其父类的鸭子之间确实有IS-A关系,这样的做法似乎也是正确的。

世界上有多少种鸭子?会不会添加新的鸭子种类,它是我们之前在设计时没有预料到的?在设计父类时哪些特征(成员变量)应该被提取到鸭子的父类中?哪些方法应该是鸭子们共有的应该被放到父类中去?这些都是我们应该实际思考的问题。

假设我们在父类中添加鸭子的叫行为:quack()和鸭子的游泳行为:swim(),和鸭子的飞行行为fly(),那么问题来了:红头鸭,绿头鸭,橡皮鸭和诱饵鸭都会游泳,红头鸭和绿头鸭都会叫和飞行,但是作为玩具的橡皮鸭的叫声和活生生的鸭子是不一样的而且橡皮鸭并不会飞行,作为诱饵鸭它只是一个木头做的模型,既不会叫也不会飞......这些问题的根源就是这些种类繁多的鸭子虽然是鸭子,但是他们之间存在行为的差异,如果将所有的方法放到鸭子的父类中去,那么在实际和父类行为有差异的鸭子类别中我们不得不通过方法的重写来覆盖掉父类的同名方法。如果采用这种设计方式,虽然是对的,但是应该将哪些方法作为共有方法提取到父类中的确定和子类代码因需要做出的父类同名方法重写将是让人头疼的问题。

                                                       策略模式—Head First 模式设计


 

另一种方式是用接口实现:只有所有鸭子都拥有的方法才将他写到鸭子的父类中去,而将其他鸭子子类不统一 到方法各自设计为接口,让鸭子子类继承鸭子父类,并继承不同的接口不就行了?这样可以解决鸭子子类方法不统一问题,可以叫的鸭子实现quackable接口,可以飞的鸭子实现flyable接口就好了嘛,不能叫的就不实现相应接口,不能飞就不实现飞的接口。在解决了第一个方法的问题的同时,我们知道,接口中声明的都是抽象方法,必须在具体类中给出具体实现,这样对于一些鸭子的行为又会导致代码复用性低的问题,比如:红头鸭,绿头鸭,肉鸭,跑地鸭......这些鸭子叫声一样都会飞,那么他们都实现flyable接口和quackable接口,但是他们的fly()方法和quack()方法都是一样的,要重复写多次。

                                                        策略模式—Head First 模式设计


总结上面两种设计方案,我们能不能找到一种既可以让不同的鸭子子类根据自己的实际情况*的选择自己的方法,不必受限于继承父类带来的死板,又不会降低代码的复用性的更好的设计模式?

我们不妨这样做:宏观表现为,鸭子能根据实际选择自己的行为,比如红头鸭选择呱呱叫,会飞,橡皮鸭选择吱吱叫,不会飞。我们需要构造出两个选择集(算法族),在鸭子题里面这两个选择集是叫声选择集,它具体分为呱呱叫,吱吱叫......还有一个飞行选择集,它具体分为会飞,不会飞......体现在结构上就是实现一个quackable接口,给出不同的实现类,给出flyable接口,也给出具体的不同实现类。接下来,怎么让鸭子可以在这些选择集中动态巡按则自己的行为特征呢?我们特定的鸭子子类中应该有两个成员变量:两个接口类型的成员变量,在不同的鸭子子类中的构造器中在创建鸭子时通过多态性为鸭子附上不同的特定行为?还有一个问题是:怎么组织这些鸭子子类呢?或者说原来的鸭子父类是否还需要呢?答案是肯定的,联想父类的作用我们想到鸭子的不同子类还有相同的方法呢,比如说游泳,将游泳方法放到父类中实现就能让实现它的所有鸭子子类都具有游泳的行为。

                                                    策略模式—Head First 模式设计


总结一下我们发现,策略模式就是将隶属于同一父类的不同子类的不变的东西提取出来放到他们共有的父类中实现,并让这些不同的鸭子子类继承鸭子的父类,而将变化的东西提取出来做成抽象的接口,接口中给出不同的实现类,鸭子父类中拥有这些接口变量作为成员变量,而在鸭子子类的构造器中为他们接口选择一个需要的在该接口下的实现类。

通过将共有的东西放到父类中,我们实现了一定程度发代码复用,而通过将变化的东西做成接口在父类中保持对接口的依赖,往后若是有不同类型的鸭子加入进来(拓展),我们只需在鸭子父类下建立相应的新子类,在接口中增加相应的实现类并在新子类的构造函数中进行选择集的指定,在初始化时就能得到我们需要的鸭子类型。

通过上面的例子我们得出一些设计的原则:
1.将变化的部分与不变的部分相互独立出来,不要让他们混杂在一起。

2.多用组合,少用继承(组合是一种比继承关系更弱的关系,这有利于弹性设计的要求)

并给出官方的策略模式的定义:
策略模式定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。

适用情况
设计父类子类时怎么设计的问题,变化的部分的可拓展性问题。

个人对于涉及模式的名字的理解
我个人对于策略这个词意思的理解是,选择什么方法来解决问题,就类似与我们在上述例子中定义算法族,使用组合关系整合各种相对于子类变化的方法,在具体使用时动态选择需要的具体算法族的实现类的过程。