第一章 策略模式

在本章将会说到利用其它开发人员的经验和智慧,遭遇过的问题,怎么解决问题的方式进行实例演示。我们会看到设计模式的用途和优点和一些关键的OO设计原则。

模拟鸭子

游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫。UML图如下

现在我们得让鸭子能飞起来
我们只需要在Duck类中加上Fly()方法,然后所有的鸭子都会继承Fly().

出现问题

但是现在遇到一个可怕的问题!
假如需求里面有橡皮鸭子,木头鸭子,是不可能会飞的,但是又继承了Duck基类。
基类加上新的行为,会使得某些并不适合该行为的子类也具有该行动。所以说当我们涉及到维护或者复用目的而使用继承,结局并不完美。

像我们一般写程序或者设计不严谨的情况下,我们首先想的是橡皮鸭子类利用继承关系把Fly()方法覆盖掉,就好像上图覆盖Quack()的做法一样。

假如需求里面要求再加一个木头鸭,不会飞,也不会叫,是不是继承的子类又是什么都不做?

现在缺点显而易见了,代码在多个子类里面重复,很难知道所有鸭子的全部行为,改变基类会牵一发动全身,造成其他鸭子不想要的改变。

利用接口如何?

假如后续需求时不时的要加很多种不同的鸭子,每当有新的鸭子子类出现,他就要被迫检查并可能要覆盖Fly()和Quark() 等基类函数方法…无穷无尽。
所以,他需要一个更清晰的方法,让某些(而不是全部)鸭子类型可飞或可叫。

尝试解决思路一
我们可以把Fly()从基类中取出来,放进一个”Flyble 接口中”。这么一来,只有会飞的鸭子才实现此接口。同样的方式,也可以用设计一个”Quackable接口”,因为不是所有的鸭子都会呱呱叫。

但是这样不知道发现没有,假如说飞行接口类,需要的类就要继承该接口,这么一来重复的代码就会变多(无法复用重写),假如基类有40个子类,稍微修改一下飞行的行为,那将很麻烦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化….
下面开始我们的解决之道:”采用良好的OO软件设计原则”

不变的是变化

不管你在何处工作,构建些什么,用何种编程语言,在软件开发上,一直伴随你的那个不变真理 就是变化!

现在我们知道使用继承并不能很好的解决问题,因为鸭子的行为在子类里面不断的改变,并且让所有的子类都有这些行为是不恰当的。Flyabley与Quackable 接口一开始似乎还可以,解决了问题(只有会飞的鸭子才继承Flyable),但是接口不具备实现代码,所以继承接口无法达到代码复用。
这意味着,无论何时你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它。
接下来我们就先说本文第一个设计原则。
设计原则1:找出应用中可能需要的变化之处,把他独立出来,不要和那些不需要变化的代码混在一起
换句话说,如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要抽出来,和其他稳定的代码有所区分。
下面是这个原则的另一种思考方式:
把会变化的部分取出来并封装起来,以便以后可以轻易的扩充此部分,而不影响不需要变化的其他部分。
这样的概念很简单,几乎是每个设计模式背后的精神所在,所有的模式都提供了一套方法让“系统的某部分改变不会影响其他部分”。
好,现在我们把鸭子的行为从Duck类中取出来!

抽出变化的部分

那么现在据我们现在所知道的,除了Fly()和Quark()的问题以外,Duck类还算一切正常似乎没有特别需要经常变化或者修改的地方。所以,除了某些小改变之外,我们不打算对Duck类做太多处理。
现在,为了要分开“变化和不会变化的部分”,我们准备建立两组类(完全原理Duck类),一个是Fly 相关的,一个是 Quack 相关的,每一组类将实现各自的动作。比方说,我们可能有一个类实现 “呱呱叫”,另一个类实现 “吱吱叫”,还有一个类实现“安静”。

我们知道Duck类内的Fly()和Quark()会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把他们从Duck类中取出来,建立一组新类来代表每个行为。

针对接口编程

设计鸭子的行为

如何设计那组实现飞行和呱呱叫的行为的类呢?

我们希望一切是有弹性的,换句话说就是我们平时经常听说的四个字 降低耦合。毕竟正是因为一开始鸭子行为没有弹性,才让我们走上现在这条路。我们还想 指定 行为到鸭子的实例。比方说,我们想要产生一个新的鸭子(积木鸭子)实例,并指定
特定 “类型”的飞行行为给他。干脆顺便让鸭子的行为可以动态改变好了,换句话说,我们应该让鸭子类中包含设定行为的方法,这样就可以在 “运行时” 动态 “改变” 积木鸭子的飞行行为。

有了这些目标要我们实现,接着看看第二个设计原则:
设计原则2:针对接口编程,而不是针对实现编程。

我们利用接口代表每个行为,比方说,FlyBehavior(飞行为)与QuackBehavior(叫行为),而每个行为的每个实现都将实现其中的一个接口。

所以这次鸭子类不会负责实现Flying和Quacking接口,反而是由我们制造一组其他类专门实现FlyBehavior和QuackBahavior,这就称为 “行为类”。由行为类实现(换句话说就是接口实现)而不是Duck类来实现行为接口。

这样的做法和之前不一样,以前的做法是:行为来自Duck超类的具体实现,或是继承某个接口并由子类自行实现而来。这两种做法都是依赖于“实现”,我们被实现绑的死死的,没办法更改行为(除非写更多代码)。

在我们新设计中,鸭子的子类使用接口(FlyBahavior与QuackBehavior)所表示的行为,所以实际的 实现 不会被绑在鸭子的子类中。(换句话说,特定具体的行为编写在实现了FlyBehavior与QuackBehavior的类中)。

从现在开始,鸭子的行为将会被放在分开的类中,此类专门提供某行为接口的实现。

这样,鸭子类就不需要知道行为的实现细节了

接下来可能就会需要问题了,前面为什么把FlyBehavior设计成接口,为何不使用抽象基类,这样不就可以使用多态了嘛?
其实 “针对接口编程” 的真正意思是 “针对超类型编程(也可以说是基类,每个人叫法不一样)”
这里所谓的 接口 有多个含义,接口是一个概念,也是一种语言的Interface构造。你可以在不涉及Interface情况下,“针对接口编程”,关键就在多态,利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被绑死在超类型的行为上。
针对超类型编程这句话,可以更明确的说 “变量声明的类型应该是超类型”,通常是一个抽象类或者是一个接口,也就是说只要是具体实现超类型的类所产生的对象,都可以指定给这个变量。这也就意味着,声明类时不用理会以后执行时的真正对象类型!

下面举一个简单的多态例子,假设有一个抽象类Animal(动物),有两个具体的实现(Dog和Cat)继承Animal.

针对实现编程
声明变量 d 为Dog类型(是Anmal的具体实现),会造成我们必须针对具体实现

1
2
Dog d=new Dog();
d.back();

但是,针对超类型编程或者接口编程,做法如下:
我们知道该对象是狗,但是我们现在利用Anmal进行多态调用 。
1
2
Anmal anmal=new Anmal();
anmal.MackSound();

还有就是,子类实例化动作不再需要在代码中硬编码(说白了就是不用开始就确实实现具体实例类),例如new Dog(),是在 运行时才指定具体实现的对象。
我们不知道实际的子类型是 什么,我们只关心他知道如何正确的进行MakeSound()的动作就够了。
1
2
a =GetAnmal();
a.MackSound();

实现鸭子的行为

所以现在我们有两个接口,FlyBehavior和QuackBehavior,还有他们对应的类,负责实现具体的行为:

这样一来是不是,有了继承的复用好处,却没有了继承所带来的包袱。

这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经和鸭子类无关了。
而我们可以新增一些行为,不会影响到既有的行为类,也不会影响 使用 到的飞行行为鸭子类。

下面整理一下我们再开发过程中会遇到那些问题
1.我是不是一定先把系统做出来,在看看有那些地方需要变化,然后才回头去把这些地方进行分离封装?
答:这个不一定,通常在你设计系统的时,预先考虑到有那些地方未来可能需要变化,于是提前在代码中加入这些弹性。你会发现,设计原则与设计模式可以应用在软件开发声明周期的任何阶段。

2.用一个类代表一个行为,感觉似乎有点奇怪。类不是应该代表某种 “东西” 吗?类不是应该同时具备状态与行为吗?
答:在OO系统中,是的,类代表的东西一般都是既有状态(实例变量)又有函数方法。只是在本例中 这个东西是个行为。但是即使是行为,也仍然可以有状态和方法,例如,飞行的行为可以具有实例变量,和一些记录行为的一些属性(飞行速度,高度等)。

3.Duck 是不是也该设计成一个接口?
答:在本例中,这么做并不好,目前写的功能 我们已经让一切都整合妥当了,而且让Duck成为一个具体类,这样可以衍生出来其他类(红色鸭子,木头鸭子)具有Duck共同属性的和方法。(提一嘴接口是不能写属性字段的哦,更别说继承了)。我们已经从
Duck的继承结构中删除了变化的部分,原先的问题都已经解决了,所以不需要把Duck设计成接口。

整合鸭子的行为
关键在于,鸭子现在会将飞行和呱呱叫的动作 委托 别处理,而不是使用定义在Duck类(或子类)内的呱呱叫和飞行方法。(这个很重要)

做法是这样的:
1.首先,在Duckl类中 “加入两个实例变量”,分别是 FlyBehavior 与 QuakBehavior,声明为接口类型,(而不是具体类实现类型)每个鸭子对象都会动态的设置这些变量以在运行是的引用正确的行为类型(例如:FlyWithWings Squeak等方法)。
我们也必须将Duck与其所有的子类中的Fly()与Quack() 删除,因为这些行为已经被搬到接口里面去了
我们用两个相似的方法PerformFly()和PerFormQuack取代Duck类中的Fly()与Duck()。下面你就会知道为什么了。

2.现在我们来实现PerformQuack()

1
2
3
4
5
6
7
8
9
10
11
12
13
 public class Duck
{

//没只鸭子都会引用实现QuakBehavior 接口对象
QuakBehavior quakBehavior;

//鸭子对象不亲自处理呱呱叫行为,而是委托给quakBehavior 引用的对象。
public void PerformQuack()
{
quakBehavior.Quack();
}

}

很容易对吧,想进行呱呱叫的动作,Duck对象只要叫quakBehavior 对象去呱呱叫就可以了。在这部分代码中,我们不在乎quakBehavior 接口对象到底是什么,我们只关心该对象知道如何进行呱呱叫就够了。

3.好,现在我们就来设定Flybehavior和QuackBrhavior的实例变量,看看接下来的MallardDuck类:

 
1
2
3
4
5
6
7
8
9
10
public class MallardDuck : Duck 
{
public MallardDuck()
{
//绿头鸭使用Quack处理呱呱叫,所以当performQuack被调用时,叫的职责就被委托给Quack对象,而我们得到了真正的呱呱叫 代码实例在下面
quackBehavior=new Quack();
//使用FlyWithWings 作为其FlyBehavior类型
flyBehavior = new FlyWithWings();
}
}

现在可能我们发现一个问题,之前我们说过 我们将不对具体实现编程,但是我们在构造器里面实现了一个具体要的Quack实现类的实例。
我们目前确实是这么做的,但是这只是暂时的,但是请注意虽然我们把行为设定成具体的类(通过实例化类似Quack或FlyWithWings的行为类,并把它指定到行为引用到变量中)。

测试Duck的代码

1.好,让我们看看以下Duck类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  public abstract class Duck
{
/// <summary>
/// 为行为接口类声明两个引用变量,
/// 所有的鸭子子类都继承他们
/// </summary>
public QuackBehavior quackBehavior;

public FlyBehavior flyBehavior;

public Duck() { }

public abstract void DisPaly();

public void PerformFly()
{
//委托给行为类
flyBehavior.Fly();
}

public void PerformQuack()
{
//委托给行为类
quackBehavior.quack();
}
}

2.接下来看看飞行接口的实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  public interface FlyBehavior
{
public void Fly() { }
}

//-----------------------------------
public class FlyWithWings : FlyBehavior
{
public void Fly()
{
Debug.Log("我是真的会飞");
}
}

//-----------------------------------
public class FlyNoWay : FlyBehavior
{
public void Fly()
{
//橡皮鸭 木头鸭
Debug.Log("我不会飞");
}
}

3.输入并编译QuackBehavior 接口,及其三个实现类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 public interface QuackBehavior
{
public void quack() { }
}
//--------------------------------------
public class Quack : QuackBehavior
{
public void quack()
{
Debug.Log("呱呱叫");
}
}
//--------------------------------------
public class MuteQuack : QuackBehavior
{
public void quack()
{
Debug.Log("不会叫");
}
}
//--------------------------------------
public class Squeak : QuackBehavior
{
public void quack()
{
Debug.Log("吱吱叫");
}
}

4.输入并编译测试类

1
2
3
4
5
6
//这会调用MallardDuck 继承父类共有函数PerformQuack PerformFly
//调用接口QuackBahavior 引用对象Quack(). 飞行也是同理
Duck mallard = new MallardDuck();
mallard.PerformQuack();
mallard.PerformFly();


5.运行代码并输出

动态设定行为

在鸭子里建立一堆动态的功能没有用到,就太可惜了,假设我们想在鸭子子类中通过 “设定方法” 来设定鸭子的行为,而不是在鸭子的构造器内实例化。

1.在Duckl类中,加入两个新方法

从此以后,我们可以 “随时” 调用 这两个方法改变鸭子的行为。

2.创造一个新的鸭子类型:模型鸭(modelDuck)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ModelDuck : Duck
{
public ModelDuck()
{
//一开始我们的模型鸭是不会飞的
flyBehavior = new FlyNoWay();
quackBehavior=new Quack();
}
public override void DisPaly()
{

}
}

  1. 建立一个新的FlyBehavior类型(FlyRocketPowered)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class FlyRocketPowered : FlyBehavior
    {
    //我们建立了一个利用火箭动力飞行的行为。
    public void Fly()
    {
    Debug.Log("火箭动力飞行");
    }
    }

4.改变测试类(MiniDuckSimulator),加上模型鸭,并使模型鸭具有火箭动力

5.运行 在运行时想改变你鸭子的行为,只需要调用鸭子的Set方法就可以了

封装行为的大局观

好,我们已经深入研究了鸭子模拟器的设计,该是将头探出水面,呼吸空气的时候了,现在就来看看整体的格局。
下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck,
飞行行为实现FlyBehavior接口,呱呱叫行为实现QuackBehavior接口。
也请注意,我们描述的方式也有所不同,不再把鸭子的行为说成是一组行为,我们可以把它称作为一组算法。以上面鸭子为例子我们可以再做项目的时会有所启发,下面看整体的关系图,一目了然。

有一个 可能比 是一个更好
有一个的意思就是,没一个鸭子都有一个FlyBehavior和QuackBehavior,好将飞行和呱呱叫委托给他们代为处理。
当将两个类结合起来使用,如同上面例子一样,,这就是组合,这种做法和继承不同的地方在于鸭子的行为不是继承来的,而是和适当行为对象 “组合” 来的。
这是一个很重要的技巧。其实是使用了我们第三个设计原则:

设计原则3:多用组合,少用继承。

如上所见,使用组合建立系统具有很大的弹性,不仅可将算法封装成类,更可以 “再运行时动态的改变行为”,只要组合的行为对象符合正确的接口标准即可。
组合用在许多设计模式中,在本文章中,会看到它的诸多优点和缺点。

其实上面我们用到的就是策略模式

策略模式正式定义了算法群,分别封装起来,让他们之间可以相互转换,此模式让算法的变化独立于使用算法的客户。

我们如何使用设计模式?

我们全部使用别人设计好的库或者框架。我们讨论库与框架,利用他们的API编译成我们的程序,享受运用别人代码所带来的优点。比如 C# java自带的一些API:网络,UI,IO等库。库与框架长久以来
,一直扮演着软件开发过程中的重要角色,我们从中挑选所要的组件,把他们放进合适的地方。
但是库与框架没办法帮助我们将应用组织成容易了解,容易维护,具有弹性的架构,所以需要设计模式。

一般设计模式不会直接进入你的代码中,而是先进入你的大脑中,一旦你现在脑海中装入了许多关于模式的知识,就能够开始在新设计中采用他们,并当你的旧代码变得如同搅和成一团没有弹性的面条一样时,可用他们重做代码。

问:如果设计模式这么棒,为什么没有人建立相关的库那?那样子我们就不必自己动手了。
答:设计模式比库的等级更高,设计模式告诉我们如何组织类和对象以解决某种问题。而且采纳这些设计并使他们适合我们特定的应用,时我们责无旁贷的事。

结语

1.一定要记住一点就是 ,知道抽象,继承,多态这些概念,并不会马上让你变成好的面向对象设计者,我们在做项目的时候注重的时建立弹性的设计,可以维护,可以应付变化。
2.回顾第一章的知识点。
面向对象设计基础:抽象 封装 多态 继承
面向对象设计原则:封装变化 多用组合,少用继承 针对接口编程,不针对实现编程
面向对象设计模式: 策略模式-定义算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户。