前言
万 事开头难,最近对这句话体会深刻!这篇文章是这个系列正式开始介绍设计模式的第一篇,所以肩负着确定这个系列风格的历史重任,它在我脑袋里默默地酝酿了好 多天,却只搜刮出了一点儿不太清晰的轮廓,可是时间不等人,以后再多“迭代”几次吧!在前面的随笔里,我已经提到了,这个系列准备以《 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 (IComparer comparer )
可以看到 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 )
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 模式的实现方式,因为那样做除了是程序更复杂以外,没有什么意义。
转载自 : http://www.cnblogs.com/justinw/archive/2007/02/06/641414.html
相关推荐
- **策略模式**:定义一系列的算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。 - **模板方法模式**:定义一个操作中的算法骨架,而将一些步骤延迟到子类中。使得...
GOF(Gang of Four)设计模式,由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides四位专家在他们的著作《设计模式:可复用面向对象软件的基础》中提出,被誉为设计模式的经典之作。本资源包含了GOF设计...
20. **策略模式 (Strategy)**:定义一系列的算法,并将每一个算法封装起来,使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。 21. **模版方法模式 (Template Method)**:在一个方法中定义一个算法...
21. **策略模式**:定义一系列算法,并将每个算法封装起来,使它们可以互相替换,策略对象可以独立变化。 22. **模板方法模式**:定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法...
GoF(Gang of Four)所提出的23种设计模式被视为面向对象设计的核心内容之一。本文旨在深入解析这些设计模式,并通过C++实现来帮助读者更好地理解和应用这些模式。 #### 1. 创建型模式 创建型模式关注的是对象的...
- 策略模式(Strategy):定义一系列的算法,把它们一个个封装起来,并且使它们可以互相替换。 - 模板方法模式(Template Method):定义一个操作中的算法骨架,而将一些步骤延迟到子类中。 - 访问者模式...
《GOF设计模式》是软件...总结,《GOF设计模式》是每个程序员的必读之作,它提供了面向对象设计的宝贵经验,帮助我们解决复杂问题,提升代码质量。深入理解并熟练运用设计模式,是成为一名优秀软件工程师的关键步骤。
GoF(Gang of Four)所提出的23种设计模式,被认为是面向对象编程中最核心的设计原则之一。这些模式可以帮助开发者解决常见的编程问题,并提高代码的可复用性和可维护性。 #### 创建型模式 创建型模式关注的是对象...
- 策略模式:定义一组相关的操作算法,使它们可以互相替换,从而使算法的变化独立于使用它的客户。 - 模板方法模式:定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可...
行为型模式则关注对象之间的责任分配和交互,如观察者模式(Observer)、策略模式(Strategy)、职责链模式(Chain of Responsibility)等。 设计模式的运用不仅限于某一特定编程语言,而是普遍适用于面向对象设计...
包括策略模式、模板方法模式、观察者模式、迭代器模式、责任链模式、命令模式、备忘录模式、解释器模式、状态模式和访问者模式。如,策略模式定义了一系列算法,并将每个算法封装起来,使得它们可以互相替换,让算法...
- 策略模式(Strategy):定义一系列算法,并将每一个算法封装起来,使它们可以相互替换。 - 模板方法模式(Template Method):定义一个操作中的算法骨架,而将一些步骤延迟到子类中。 - 访问者模式(Visitor)...
### c++设计模式GoF23 #### 0. 引言 - **设计模式的重要性**:“Next to My Life, Software Is My Passion”——Robert C. Martin 的名言揭示了软件设计的重要地位。掌握设计模式意味着理解面向对象分析与设计...
3. **行为型模式**:包括策略模式、模板方法模式、观察者模式、命令模式、迭代器模式、访问者模式、中介者模式、备忘录模式、状态模式、解释器模式和责任链模式。 这些设计模式为软件开发者提供了解决常见问题的...
- 策略模式(Strategy):定义了一系列的算法,并将每一个算法封装起来,使它们可以互相替换,算法的变化不会影响使用算法的客户。 - 模板方法模式(Template Method):在一个方法中定义一个算法的骨架,而将一些...
2.3.3 策略模式 29 2.4 修饰用户界面 29 2.4.1 透明围栏 29 2.4.2 Monoglyph 30 2.4.3 Decorator 模式 32 2.5 支持多种视感标准 32 2.5.1 对象创建的抽象 32 2.5.2 工厂类和产品类 33 2.5.3 Abstract Factory模式 35...
22. **策略模式**:定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换,让算法独立于使用它的客户。 23. **模板方法模式**:在抽象类中定义一个操作中的算法骨架,而将一些步骤延迟到子类中。使得子类...
9. **策略模式**:定义了一系列算法,并将每一个算法封装起来,使它们可以相互替换,让算法独立于使用它的客户。 10. **模板方法模式**:定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变一...