设计模式随笔系列:鸭子-策略模式(Strategy)[原]

前言

万事开头难,最近对这句话体会深入!这篇文章是这个系列正式开始介绍设计模式的第一篇,因此肩负着肯定这个系列风格的历史重任,它在我脑壳里默默地酝酿了好多天,却只搜刮出了一点儿不太清晰的轮廓,但是时间不等人,之后再多“迭代”几回吧!在前面的随笔里,我已经提到了,这个系列准备以《Head First Design Patterns》的结构为主线,因此每一个模式的核心故事都是取材于此书,在此再次声明一下。无论怎样,宗旨是为了跟你们一块儿按部就班地去认识设计模式。 html

上一篇:模式和原则,获得不少朋友的支持和鼓励,这里再次深表感谢。这里我仍是想呼吁一下,但愿你们看事后多提宝贵意见,反对意见更好,关键是咱们在互动中能够共同进步,由于经验告诉我讨论(争论更甚)出来的火花,老是印象最深入的。 程序员

其实策略模式是一个很简单的模式,也是一个很经常使用的模式,可谓短小精悍。我在介绍这个模式的同时,为了加深你们对OO的理解,还会反复强调前面讲过的设计原则和GRASP模式。这个系列的文章先后多少会有一些关联的连续性,可是单独一篇文章针对单一模式也必定是独立的,因此不论你们想从前日后连续看也好,仍是挑喜欢的跳着看,都没有问题。 算法

“罗嗦了这么多,太唐僧了吧,快点开始吧(烂西红柿和臭鸡蛋从四面八方飞来) 编程

模拟鸭子

Joe是一名OO程序员,他为一家开发模拟鸭子池塘游戏的公司工做,该公司的主要产品是一种能够模拟展现多种会游泳和呷呷叫的鸭子的游戏。这个游戏是使用标准的面向对象技术开发的,系统里全部鸭子都继承于Duck基类,系统的核心类图以下: 设计模式

如图所示,在 Duck 基类里实现了公共的 quack() swim() 方法,而 MallardDuck RedheadDuck 能够分别覆盖实现本身的 display() 方法,这样即重用了公共的部分,又支持不一样子类的个性化扩展。从目前的状况看,这是一个很好的设计,哈!   

可是,商场如战场,不进则退。Joe的公司最近的日子很差过,盗版泛滥,再加上竞争对手的围追堵劫,已经拖欠好几个月工资了。所以,公司高层在一次集体“腐败”后,决定必定要给系统增长一些超玄的功能,以完全击垮竞争对手。通过董事会讨论,最终以为若是能让鸭子飞起来,那么必定能够给对手致命一击。因而Joe的上司对董事们拍着胸脯说:“这没有问题,Joe是一个OO程序员,这对他来讲太简单了!咱们保证一周内结束战斗。” app

接到任务的Joe丝绝不敢怠慢,研究了上级的指示之后,发现只要在Duck里增长一个fly()方法就能够搞定了,这样全部继承Duck的鸭子就都拥有了会飞的能力,哈!这回奖金有盼头啦!改进后的系统类图以下: 框架


    Joe 的上司很高兴,带着新产品给董事们演示去了 ……  

…… ui

Joe的上司:“我正在给董事们演示你会飞的鸭子,可是怎么有不少橡皮鸭子也在四处乱飞呢?你在耍我吗?你还想不想混啦?!”(此处省略粗话100) spa

Joe被吓坏了,到手的奖金泡汤了!冷静下来的Joe发现,原来在Duck类里增长的方法,也一样被继承于DuckRubberDuck类继承了,因此就有了会飞的橡皮鸭子,这是严重违反该系统“真实模拟各类鸭子”的原则的!那么该怎么办呢?Joe很郁闷!他忽然想到:若是在RubberDuck类里把fly()方法重写一下会如何?在RubberDuck类的fly()里让橡皮鸭子什么都不作,不就一切OK了吗!那之后再增长一个木头鸭子呢?它不会飞也不会叫,那不是要再重写quack()fly()方法,之后再增长其它特殊的鸭子都要这样,这不是太麻烦了,并且也很混乱。 .net

最终,Joe认识到使用继承不是办法,由于他的上司通知他,董事会决定之后每6个月就会升级一次系统,以应对市场竞争,因此将来的变化会很频繁,并且还不可预知。若是之后靠逐个类去判断是否重写了quack()fly()方法来应对变化,显然混不下去!

Joe这时很迷惑,为何屡试不爽的继承,在系统维护升级的时候,没法很好地支持重用呢?)

那么使用接口怎么样?我能够把fly()方法放在接口里,只有那些会飞的鸭子才须要实现这个接口,最好把quack()方法也拿出来放到一个接口里,由于有些鸭子是不会叫的。就像下面这样:



Joe 的上司知道后怒了:“你这样作难道是但愿全部须要 quack() fly() 方法的鸭子都去重复实现这两个方法的功能吗?就这么几个鸭子还好说,可是咱们有几10、上百个鸭子的时候你怎么办?若是某个方法要作一点修改,难道你要重复修改上百遍吗?你是否是疯啦?”

呵呵!若是你是Joe,你该怎么办?

咱们知道,并非全部的鸭子都会飞、会叫,因此继承不是正确的方法。可是虽然上面的使用Flyable接口的方法,能够解决部分问题(再也不有会飞的橡皮鸭子),可是这个解决方案却完全破坏了重用,它带来了另外一个维护的噩梦!并且还有一个问题咱们前面没有提到,难道全部的鸭子的飞行方式、叫声等行为都是如出一辙的吗?不可能吧!

说到这里,为了能帮助Joe摆脱困境,咱们有必要先停下来,从新回顾一些面向对象设计原则。请您告诉我:“什么东西是在软件开发过程当中是恒定不变的?”,您想到了吗?对,那就是变化自己,正所谓“计划没有变化快”,因此直面“变化这个事实”才是正道!Joe面对的问题是,鸭子的行为在子类里持续不断地改变,因此让全部的子类都拥有基类的行为是不适当的,而使用上面的接口的方式,又破坏了代码重用。如今就须要用到咱们的第一个设计原则:

Identify the aspects of your application that vary and separate them from what stays the same.(找到系统中变化的部分,将变化的部分同其它稳定的部分隔开。)

换句话说就是:“找到变化而且把它封装起来,稍后你就能够在不影响其它部分的状况下修改或扩展被封装的变化部分。”尽管这个概念很简单,可是它几乎是全部设计模式的基础,全部模式都提供了使系统里变化的部分独立于其它部分的方法。

OK!如今咱们已经有了一条设计原则,那么Joe的问题怎么办呢?就鸭子的问题来讲,变化的部分就是子类里的行为。因此咱们要把这部分行为封装起来,免得它们老惹麻烦!从目前的状况看,就是fly()quack()行为老是不老实,而swim()行为是很稳定的,这个行为是可使用继承来实现代码重用的,因此,咱们须要作的就是把fly()quack()行为从Duck基类里隔离出来。咱们须要建立两组不一样的行为,一组表示fly()行为,一组表示quack()行为。为何是两组而不是两个呢?由于对于不一样的子类来讲,fly()quack()的表现形式都是不同的,有的鸭子嘎嘎叫,有的却呷呷叫。有了这两组行为,咱们就能够组合出不一样的鸭子,例如:咱们可能想要实例化一个新的MallardDuck(野鸭)实例,而且给它初始化一个特殊类型的飞行行为(野鸭飞行能力比较强)。那么,若是咱们能够这样,更进一步,为何咱们不能够动态地改变一个鸭子的行为呢?换句话说,咱们将在Duck类里包含行为设置方法,因此咱们能够说在运行时改变MallardDuck的飞行行为,这听起来更酷更灵活了!那么咱们到底要怎么作呢?回答这个问题,先要看一下咱们的第二个设计原则:

Program to an interface, not an implementation.(面向接口编程,而不要面向实现编程。)

嘿!对于这个原则,不管是耳朵仍是眼睛,是否是都太熟悉了!“接口”这个词已经被赋予太多的含义,搞的你们一说点儿屁事就满嘴往外蹦“接口”。那么它究竟是什么意思呢?咱们这里说的接口是一个抽象的概念,不局限于语言层面的接口(例如C#里的interface)。一个接口也能够是一个抽象类,或者一个基类也能够看做是一种接口的表现形式,由于基类变量能够用来引用其子类。要点在于,咱们在面向接口编程的时候,可使用多态,那么实际运行的代码只依赖于具体的接口(interface,抽象类,基类),而无论这些接口提供的功能是如何实现的,也就是说,接口将系统的不一样部分隔离开来,同时又将它们链接在一块儿。个人神啊!接口真是太伟大了!(烂西红柿和臭鸡蛋从四面八方飞来)

OK!这回该完全解决Joe的问题了!

根据面向接口编程的设计原则,咱们应该用接口来隔离鸭子问题中变化的部分,也就是鸭子的不稳定的行为(fly()quack())。咱们要用一个FlyBehavior接口表示鸭子的飞行行为,这个接口能够有多种不一样的实现方式,能够“横”着分,也能够“竖”着分,管它呢!这样作的好处就是咱们将鸭子的行为实如今一组独立的类里,具体的鸭子是经过FlyBehavior这个接口来调用这个行为的,由于Duck只依赖FlyBehavior接口,因此不须要管FlyBehavior是如何被实现的。以下面的类图,FlyBehaviorQuackBehavior接口都有不一样的实现方式!

Joe 已经晕了,“你说了这么多,全是大白话,来点代码行不行,我要 C# 的!”。说到这里,咱们也该开始完全改造这个设计了,并会在最后附加部分代码来帮助你们理解。   

第一步:咱们要给Duck类增长两个接口类型的实例变量,分别是flyBehaviorquackBehavior,它们其实就是新的设计里的“飞行”和“叫唤”行为。每一个鸭子对象都将会使用各类方式来设置这些变量,以引用它们指望的运行时的特殊行为类型(使用横着飞,吱吱叫,等等)

第二步:咱们还要把fly()quack()方法从Duck类里移除,由于咱们已经把这些行为移到FlyBehaviorQuackBehavior接口里了。咱们将使用两个类似的PerformFly()PerformQuack()方法来替换fly()qucak()方法,后面你会看到这两个新方法是如何起做用的。

第三步:咱们要考虑何时初始化flyBehaviorquackBehavior变量。最简单的办法就是在Duck类初始化的时候同时初始化他们。可是咱们这里还有更好的办法,就是提供两个能够动态设置变量值的方法SetFlyBehavior()SetQuackBehavior(),那么就能够在运行时动态改变鸭子的行为了。

下面是修改后的Duck类图:

咱们再看看整个设计修改后的类图:


最后你们再看看演示代码,由于代码比较多,就不贴出来了,大 家能够下载后参 考: 。下面是演示代码的执行结果:


这就是策略模式

前面说了那么多,如今终于到了正式介绍咱们今天的主角的时候啦!此刻心情真是好激动啊!其实咱们在前面就是使用Strategy模式帮Joe度过了难过,真不知道他发了奖金后要怎么感谢咱们啊。OK!下面先看看官方的定义:

The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.(策略模式定义了一系列的算法,并将每个算法封装起来,并且使它们还能够相互替换。策略模式让算法独立于使用它的客户而独立变化。)

怎么样,有了前面Joe的经历,这个定义理解起来还不那么太费劲吧?我想凡是认真看到这里的人,应该都能理解的。那么下面再多此一举地罗嗦几句,给那些还不太理解的朋友一个机会吧。J

Context( 应用场景 ):

l         须要使用ConcreteStrategy提供的算法。

l         内部维护一个Strategy的实例。

l         负责动态设置运行时Strategy具体的实现算法。

l         负责跟Strategy之间的交互和数据传递。

Strategy(抽象策略类)

l         定义了一个公共接口,各类不一样的算法以不一样的方式实现这个接口,Context使用这个接口调用不一样的算法,通常使用接口或抽象类实现。

ConcreteStrategy(具体策略类)

l         实现了Strategy定义的接口,提供具体的算法实现。

 

还不理解?!个人神啊!那再看看下面的顺序图吧,这是最后的机会啦!


应用场景和优缺点

上面咱们已经看过了Strategy模式的详细介绍,下面咱们再来简单说说这个模式的优缺点吧!怎么说呢,人无完人,设计模式也不是万能的,每个模式都有它的使命,也就是说只有在特定的场景下才能发挥其功效。咱们要使用好模式,就必须熟知各个模式的应用场景。

对于Strategy模式来讲,主要有这些应用场景:

一、  多个类只区别在表现行为不一样,可使用Strategy模式,在运行时动态选择具体要执行的行为。(例如FlyBehaviorQuackBehavior)

二、  须要在不一样状况下使用不一样的策略(算法),或者策略还可能在将来用其它方式来实现。(例如FlyBehaviorQuackBehavior的具体实现可任意变化或扩充)

三、  对客户(Duck)隐藏具体策略(算法)的实现细节,彼此彻底独立。

 

对于Strategy模式来讲,主要有以下优势:

一、  提供了一种替代继承的方法,并且既保持了继承的优势(代码重用)还比继承更灵活(算法独立,能够任意扩展)

二、  避免程序中使用多重条件转移语句,使系统更灵活,并易于扩展。

三、  遵照大部分GRASP原则和经常使用设计原则,高内聚、低偶合。

对于Strategy模式来讲,主要有以下缺点:

一、  由于每一个具体策略类都会产生一个新类,因此会增长系统须要维护的类的数量。

 

    备注:关于场景和优缺点,上面确定说得不够全面,欢迎你们来补充。

.NET框架里的应用

Strategy模式的应用很是普遍,也许你们有意无心之间一直都在使用。这里举一个.NET框架里使用Strategy模式的例子,象这样的例子其实还有不少,只要你们细心体会就必定会发现的。

若是写过程序,那么ArrayList类确定都会用过吧,那么它的Sort方法想必你们也必定不陌生了。Sort方法的定义以下:

public virtual void Sort (IComparer comparer)

能够看到Sort方法接收一个IComparer类型的参数,那么这个IComparer接口是作什么用的呢?下面咱们看一段程序,下面的代码示例演示如何使用默认比较器和一个反转排序顺序的自定义比较器,对 ArrayList 中的值进行排序。(彻底引自MSDNms-help://MS.MSDNQTR.v80.chs/MS.MSDN.v80/MS.NETDEVFX.v20.chs/cpref2/html/M_System_Collections_ArrayList_Sort_1_a2d90598.htm)

 

 1 using  System;
 2 using  System.Collections;
 3
 4 public   class  SamplesArrayList   {
 5 
 6   public class myReverserClass : IComparer  {
 7
 8      // Calls CaseInsensitiveComparer.Compare with the parameters reversed.
 9      int IComparer.Compare( Object x, Object y )  {
10          return( (new CaseInsensitiveComparer()).Compare( y, x ) );
11      }

12
13   }

14
15   public static void Main()  {
16 
17      // Creates and initializes a new ArrayList.
18      ArrayList myAL = new ArrayList();
19      myAL.Add( "The" );
20      myAL.Add( "quick" );
21      myAL.Add( "brown" );
22      myAL.Add( "fox" );
23      myAL.Add( "jumps" );
24      myAL.Add( "over" );
25      myAL.Add( "the" );
26      myAL.Add( "lazy" );
27      myAL.Add( "dog" );
28 
29      // Displays the values of the ArrayList.
30      Console.WriteLine( "The ArrayList initially contains the following values:" );
31      PrintIndexAndValues( myAL );
32 
33      // Sorts the values of the ArrayList using the default comparer.
34      myAL.Sort();
35      Console.WriteLine( "After sorting with the default comparer:" );
36      PrintIndexAndValues( myAL );
37
38      // Sorts the values of the ArrayList using the reverse case-insensitive comparer.
39      IComparer myComparer = new myReverserClass();
40      myAL.Sort( myComparer );
41      Console.WriteLine( "After sorting with the reverse case-insensitive comparer:" );
42      PrintIndexAndValues( myAL );
43
44   }

45 
46   public static void PrintIndexAndValues( IEnumerable myList )  {
47      int i = 0;
48      foreach ( Object obj in myList )
49         Console.WriteLine( "\t[{0}]:\t{1}", i++, obj );
50      Console.WriteLine();
51   }

52
53}

54
55
56 /* 
57This code produces the following output.
58The ArrayList initially contains the following values:
59        [0]:    The
60        [1]:    quick
61        [2]:    brown
62        [3]:    fox
63        [4]:    jumps
64        [5]:    over
65        [6]:    the
66        [7]:    lazy
67        [8]:    dog
68
69After sorting with the default comparer:
70        [0]:    brown
71        [1]:    dog
72        [2]:    fox
73        [3]:    jumps
74        [4]:    lazy
75        [5]:    over
76        [6]:    quick
77        [7]:    the
78        [8]:    The
79
80After sorting with the reverse case-insensitive comparer:
81        [0]:    the
82        [1]:    The
83        [2]:    quick
84        [3]:    over
85        [4]:    lazy
86        [5]:    jumps
87        [6]:    fox
88        [7]:    dog
89        [8]:    brown 
90*/

 

怎么样,你们看出来了吧,其实在这段代码里,ArrayList至关于Strategy模式中的Context(应用场景)部分,而IComparer至关于Strategy(抽象策略类)部分,myReverserClass至关于ConcreteStrategy(具体策略类)部分。咱们这里抛开myReverserClass类的Compare方法如何具体实现不谈,咱们只要知道这是一个具体策略类,它提供了应用场景须要的具体算法,它实现了抽象策略类接口,而应用场景经过抽象策略类动态调用到了具体策略类中的算法。哈!因此这是一个十分典型的Strategy模式的应用。

基于这个符合Strategy模式的结构,咱们还能够提供不少种自定义的具体策略类的实现,只要这些类实现了IComparer接口,就能够在运行时动态设置给ArrayList类的Sort方法,在Sort方法中会根据具体策略类实现的比较算法规则来对ArrayList中的数据进行排序。

最后一个设计原则

关于Strategy模式的故事讲到这里,应该基本OK啦!下面咱们再聊些更高层次的东西。什么是更高层次的东西?嘿!固然是设计原则了!在前面总结Strategy模式的优势的时候咱们提到过,Strategy模式不只保留了继承的优势,并且还提供了更灵活的扩展能力。为何会这样呢?Strategy模式是怎么作到这一点的呢?哈!这是由于它“上面有人”啊!谁啊?它就是咱们下面要介绍的重量级设计原则:

Favor composition over inheritance.(优先使用对象组合,而非类继承)

关于组合和继承,咱们只要这样来理解便可:组合是一种“HAS-A”关系,而继承是一种“IS-A”关系。很明显“HAS-A”要比“IS-A”更灵活一些。也就是说在建立系统的时候,咱们应该优先使用对象组合,由于它不只能够给你提供更多灵活性和扩展性,并且还使你能够在运行时改变行为(组合不一样的对象),这简直是酷毙了!可是也不是说继承就是不能用,只是说应该把继承应用在相对更稳定,几乎没有变化的地方,例如前面的Duck类里的Swim()方法,由于能够确定全部鸭子必定都会游泳,因此就没有必要给这个行为提供基于Strategy模式的实现方式,由于那样作除了是程序更复杂之外,没有什么意义。

相关文章
相关标签/搜索