S.l.e!ep.¢%

像打了激速一样,以四倍的速度运转,开心的工作
简单、开放、平等的公司文化;尊重个性、自由与个人价值;
posts - 1098, comments - 335, trackbacks - 0, articles - 1
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

鸭子-策略模式(Strategy) [转]

Posted on 2009-01-27 16:22 S.l.e!ep.¢% 阅读(684) 评论(1)  编辑 收藏 引用 所属分类: Design Pattern

鸭子 - 策略模式( Strategy

前言

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

上一篇:模式和原则,得到很多朋友的支持和鼓励,这里再次深表感谢。这里我还是想呼吁一下,希望大家看过后多提宝贵意见,反对意见更好,关键是我们在互动中可以共同进步,因为经验告诉我讨论 ( 争论更甚 ) 出来的火花,总是印象最深刻的。

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

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

模拟鸭子

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

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

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

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


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

……

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

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

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

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

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



Joe 的上司知道后怒了:“你这样做难道是希望所有需要 quack() fly() 方法的鸭子都去重复实现这两个方法的功能吗?就这么几个鸭子还好说,但是我们有几十、上百个鸭子的时候你怎么办?如果某个方法要做一点修改,难道你要重复修改上百遍吗?你是不是疯啦?”

呵呵!如果你是 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 是如何被实现的。如下面的类图, FlyBehavior QuackBehavior 接口都有不同的实现方式!

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

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

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

第三步:我们要考虑什么时候初始化 flyBehavior quackBehavior 变量。最简单的办法就是在 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 模式来说,主要有这些应用场景:

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

2、  需要在不同情况下使用不同的策略 ( 算法 ) ,或者策略还可能在未来用其它方式来实现。 ( 例如 FlyBehavior QuackBehavior 的具体实现可任意变化或扩充 )

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

 

对于 Strategy 模式来说,主要有如下优点:

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

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

3、  遵守大部分 GRASP 原则和常用设计原则,高内聚、低偶合。

对于 Strategy 模式来说,主要有如下缺点:

1、  因为每个具体策略类都会产生一个新类,所以会增加系统需要维护的类的数量。

 

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

.NET 框架里的应用

Strategy 模式的应用非常广泛,也许大家有意无意之间一直都在使用。这里举一个 .NET 框架里使用 Strategy 模式的例子,象这样的例子其实还有很多,只要大家细心体会就一定会发现的。

如果写过程序,那么 ArrayList 类肯定都会用过吧,那么它的 Sort 方法想必大家也一定不陌生了。 Sort 方法的定义如下:

public virtual void Sort (IComparercomparer)

可以看到 Sort 方法接收一个 IComparer 类型的参数,那么这个 IComparer 接口是做什么用的呢?下面我们看一段程序, 下面的代码示例演示如何使用默认比较器和一个反转排序顺序的自定义比较器,对 ArrayList中的值进行排序。(完全引自MSDN ms-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 /*  
57 This code produces the following output.
58 The 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
69 After 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
80 After 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 模式的实现方式,因为那样做除了是程序更复杂以外,没有什么意义。

BULLET POINTS

l        Knowing the OO basics does not make you a good OO designer.

l        Good OO designs are reusable,extensible and maintainable.

l        Patterns show you how to build systems with good OO design qualities.

l        Patterns are proven object oriented experience.

l        Patterns don’t give you code,they give you general solutions to design problems.You apply them to your specific application.

l        Patterns aren’t invented,they are discovered.

l        Most patterns and principles address issues of change in software.

l        Most patterns allow some part of a system to vary independently of all other parts.

l        We often try to take what varies in a system and encapsulate it.

l        Patterns provide a shared language that can maximize the value of your communication with other developers.

作者

王晓亮 /Justin

MSN:xiaoliang203@hotmail.com

Mail:xiaoliang.justin@gmail.com

参考资料

UML 和模式应用》

《敏捷软件开发—原则、模式与实践》

Head First Design Patterns

李建忠 老师的《 C# 面向对象设计模式纵横谈系列课程》

Feedback

# re: 鸭子-策略模式(Strategy) [转]  回复  更多评论   

2009-01-28 17:31 by 陈梓瀚(vczh)
使用C++的多重继承,则能轻松解决,用不着那么多behavior。

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理