设计模式_第二篇_策略模式

本文是我通过学习《Head First 设计模式》而写。

作为我要描述的第一个模式,首先要说什么是设计模式,然后,用一个实例,并对这个实例不断的改进,引出策略模式。

与其空泛地给出一堆描述,倒不如给出通过一个实例、一个情景,来引出你要说的东西。因为,人们对于事物的理解,越是具体、形象,就越容易,而但凡理论性、抽象性的东西,你无论怎样描述它,也只是用一个概念去解释另一个概念。对于一个没有多少项目经验的人来说,着实不易。《Head First设计模式》这本书做得就很好。

上面这句话,有三个关键词:情境、问题和解决方案。所谓“情境”是应用某个设计模式的情况;所谓“问题”是你在某个情境下达到的目标,但也可以是某个情境下的约束;而“解决方案”是你所追求的,一个通用的设计,用来解决约束,达到目标。

需要明确几点:

  • 项目不一定非要使用设计模式。使用设计模式只是为了让程序更加灵活、更容易维护,尤其是当需求变化的时候。但模式会无形中增加程序的复杂性,这是我们所不期望的。我们期望,用简单、清晰的方法来解决复杂的问题,使程序更容易让人理解,而不是“机器”——简单的想1一样。任何一个程序员都能写出让机器理解的代码,但只有一个熟练的程序员才能写出让绝大多数人都能理解的程序。
  • 设计模式初学者的误区,包括我自己,总是希望为自己的程序找到一个模式(虽然这个初衷是想练习这些模式)。有一句经典的话:“我要为 'Hello World' 找一个设计模式”。
  • 你可以不会使用设计模式,但你绝对不能不知道它的存在。
  • 模式本身是不存在的,它只是在长期的项目实践中的一种经验。你可以有自己的模式,并发布出去。但你的模式必须有三次成功的案例,并经过其他人的评价后才可能被列入模式目录。

下面,通过一个实例,来说明策略模式。

假如,你所在公司要求你制作一个模拟各种鸭子飞行的程序。你立刻就会想到,所有的鸭子都会游泳、都会叫,那么,可以设计一个鸭子的抽象类(Duck),然后让所有的鸭子,如绿头鸭(MallardDuck)或红头鸭(RedHeadDuck)都继承这个抽象类。如图1所示:

01其中,

  • Duck 类是一个抽象类。MallardDuck 类和 RedHeadDuck 继承 Duck 类。
  • 所有的鸭子都会游泳、都会叫,因此,由 Duck 类来实现 quack() 和 swim() 方法。
  • 每个鸭子都有自己的 display() 方法,因此,Duck 类中的 display() 是抽象abstract方法。

这个设计看似不错,但有什么缺点?现在,公司要求——鸭子要能飞。你很容易想到,只要在 Duck 类中,加入并实现 fly() 方法,那么,Duck 类的子类都可以继承这个方法。如图2所示:

02

但问题也来了——不会飞的橡皮鸭 RubberDuck 到处飞。因此,在抽象类 Duck 中加入 fly() 方法后,其所有子类也就都具备了 fly() 方法,连不该具备 fly() 方法的子类也无法避免。解决的方法也很容易想到——覆盖子类的 fly() 方法。

但这样做也有问题啊!以后,要是再加入诱饵鸭 DecoyDuck?诱饵鸭即不会飞,也不会叫。这样,除了要覆盖 fly() 方法,还要覆盖 quack() 方法。一个公司的产品都会定期更新,加入其他种类的鸭子。这样,你不得不每次都要检查 fly() 和 quack() 方法。因此,这也不是一个好的解决方案。

采用接口总可以了吧!如图3所示:

03其中,

  • Duck 类仍然是个抽象类。所有种类的鸭子,如绿头鸭(MallardDuck)、红头鸭(RadHeadDuck)、橡皮鸭(RubberDuck)和诱饵鸭(DecoyDuck),都要继承 Duck 类。
  • Duck 类的子类必须继承 Flyable 和 Quackable 接口,并实现 fly() 和 quack() 方法。

对于这个设计,虽然不会出现,橡皮鸭(RubberDuck)到处飞的情况,但代码无法复用。因为,显然绿头鸭和红头鸭都会飞都会叫,即它们的 fly() 和 quack() 方法的实现一样,而橡皮鸭和诱饵鸭都不会飞,即 fly() 方法的实现一样等等——这只是从一个“恶梦”跳到另一个“恶梦”而已。

那么究竟应该怎么做?答案是:找出应用中可能需要变化的地方,把它们独立出来,不要和那些不需要变化的代码混在一起。在本情景中,将鸭子的“飞”和“叫”的行为从抽象类 Duck 中分离出来。如图4所示:

04 其中,

  • Duck 类还是一个抽象类。FlyBehavior 和 QuackBehavior 接口聚合到 Duck 类,作为 Duck 类的属性。
  • ModelDuck 类继承 Duck 类。该类重构的 display() 方法是所有鸭子都具备的共同需求。因为,无论什么种类的鸭子,最终都是要显示出来,或是一边飞,一边叫;或是不飞(也许会飞,也许不会飞),只叫;或是一边游泳,一边叫……等等。我在开发时,一般将像 ModelDuck 这样的类,命名为 BaseDuck。
  • 类 FlyNoWay、FlyWithWings、FlyRocketPowered 分别继承接口 FlyBehavior,实现里边的方法——“不会飞”、“用翅膀飞”和“坐火箭飞”。继承 QuackBehavior 接口的三个类同理。

这个设计究竟好在哪里?我觉得有如下几点:

  • 将鸭子“飞”和“叫”的行为(方法)从抽象类Duck中分离出来,这样,Duck类以及其子类就不需要再知道“飞”和“叫”是如何实现的,有利于加入新的鸭子子类,而完全不会影响现有的代码;
  • 当需要添加新的鸭子“飞”的行为或是新的“叫”的行为时,只要继承相应的接口即可,完全不会影响现有的代码;
  • 为了使程序能在运行时改变鸭子“飞行”和“叫”的状态,让程序更加灵活,在Duck类中添加两个set方法和perform方法,分别设置鸭子“飞行”和鸭子“叫”的状态,然后再让perform方法执行这些鸭子的行为;
  • 另外,通常情况下,在实际的项目中,当我们需要添加新类型的鸭子时,不会直接继承Duck,而是用一个基类先继承这个抽象类,比如用ModleDuck类继承Duck类,再让新的鸭子类继承这个基类,这样,会使程序变得更加灵活。

从以上在对策略模式的分析中,可以得到如下经验和结论:

  • 如果为了代码复用而使用继承,结局往往并不完美;
  • 针对接口编程;
  • 将程序变化的部分和不变化的部分分离。

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

原文地址:https://www.cnblogs.com/liuning8023/p/2153738.html