什么是策略模式?
策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法。
- 定义了一族算法(业务规则);
- 封装了每个算法;
- 这族的算法可互换代替(interchangeable)。
直接用的书中的例子,感觉还可以,可能会重构此文。
我们通过一个鸭子应用来了解一下这个模式。。。
模拟鸭子应用中的问题
假设现在你的公司做了一套模拟鸭子游戏:SimDuck。游戏中出现了各种鸭子,一边游泳嬉水,一边呱呱叫。该系统内部设计使用了标准的OO技术,设计了一个鸭子超类(SuperClass),并让各种鸭子继承此超类。
过了几个月,行业间其他公司冒出了很多鸭子游戏,公司为了将竞争对手抛在后头,需要将该游戏加个功能:让鸭子飞起来。
你
:这还不简单,我在Duck
类加个fly()
方法,然后所有的鸭子都会继承fly()
,不就搞定了?
但是,可怕的问题发生了!!!
你忽略了一件事,并非所有种类的鸭子都会飞,比如橡皮鸭。当我们为Duck
超类加上了新的行为,会使某些子类也具有这个不恰当的行为。我们对代码做的局部修改,影响层面可能不只是局部。
继承不是可以重写吗?我们将fly()
方法覆盖重写不就行了?可是,如果又有个木头鸭呢,它不会飞也不会叫,我们又要覆盖重写?出现更多的其他鸭子的,别的鸭子可能嘎嘎叫呢?还继续覆盖重写?从这里可以看出,利用继承来提供Duck的行为,会出现下列问题:
- 代码在多个子类中重复;
- 难以得知所有鸭子的全部行为;
- 运行时的行为不容易改变;
- 改一发而动全身,造成其他鸭子不想要的改变。
利用接口如何?
我们可以把fly()
取出来,放进一个Flyable接口
中,这样一来,只有会飞的鸭子才实现此接口。同样我们也可以设置一个Quackable接口
让会叫的鸭子实现该接口。
虽然接口可以解决一部分问题(不会飞的橡皮鸭和不会叫的木头鸭),但是却造成代码无法复用,这只是治标却不治本。
现在我们知道使用继承有一些缺失,因为改变鸭子的行为会影响所有种类的鸭子,这行不通。用接口一开始还可以,解决了问题,但接口没有具体的代码实现,所以继承接口的方式无法使代码能复用。这意味着:无论何时你需要修改某个行为,你必须得往下追踪并修改每一个定义此行为的类,一不小心,可能造成新的错误。
幸运地,有一个
设计原则
,正适用于此状况:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。换句话说,如果每次新的需求一来,都会变化到某方面的代码,那么你就可以确定,这部分的代码需要被抽出来,和其他闻风不动的代码有所区隔。
下面是这个原则的另一个思考方式:「把会变化的部分取出并封装起来,以便以后可以轻易地扩充此部分,而不影响不需要变化的其他部分」。
这样的概念很简单,几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让「系统中的某部分改变不会影响其他部分」。代码变化之后,出其不意的部分变得很少,系统变得更有弹性。
分开变化和不会变化的部分
现在,为了要分开「变化和不会变化的部分」,我们准备建立两组类(完全远离 D u c k类),一个是fly
相关的,一个是quack
相关的,每一组类将实现各自的动作。
我们知道Duck类内的fly ( )和quack( )会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把它们自 Duck类中取出,建立一组新类代表每个行为。
如何设计类实现飞行和呱呱叫的行为?
我们希望一切能有弹性,毕竟,正是因为一开始的鸭子行为没有弹性,才让我们走上现在这条路。我们还想能够「指定」行为到鸭子的实例,比方说,想要产生绿头鸭实例,并指定特定「类型」的飞行行为给它。干脆顺便让鸭子的行为可以动态地改变好了。换句话说,我们应该在鸭子类中包含设定行为的方法,就可以在「运行时」动态地「改变」绿头鸭的飞行行为。
有了这些目标要达成,接着看看第二个设计原则
:针对接口编程,而不是针对实现编程。
我 们 利 用 接 口 代 表 每 个 行 为 , 比 方 说 ,FlyBehavior
与QuackBehavior
,而行为的每个实现都必须实现这些接口之一。
所以这次鸭子类不会负责实现 Flying
与Quacking
接口,反而是由其他类专门实现FlyBehavior
与QuackBehavior
,这就称为
「行为」类。由行为类实现行为接口,而不是由Duck类实现行为接口。
这样的作法迥异于以往,以前的作法是:行为是继承 D u c k超类的具体实现而来,或是继承某个接口并由子类自行实现而来。这两种作法都是依赖于「实现」,我们被实现绑得死死的,没办法更改行为(除非写更多代码)。
在我们的新设计中,鸭子的子类将使用接口( FlyBehavior
与QuackBehavior
)所表示的行为,所以实际的「实现」不会被绑死在鸭子的子类中。(换句话说,特定的实现代码位于实现FlyBehavior
与QuackBehavior
的特定类中)。
实现鸭子的行为
整合鸭子的行为
关键在于,鸭子现在会将飞行和呱呱叫的动作,「委托」(delegate
)别人处理,而不是使用定义在自己类(或子类)内的方法。
作法是这样的:
①首 先 , 在 鸭 子 中 「 加 入 两 个 实 例 变 量 」 , 分 别 为 FlyBehavior
与QuackBehavior
,声明为接口类型(而不是具体类实现类型),每个变量会利用多态的方式在运行时引用正确的行为类型(例如:FlyWithWings)。我们也必须将 D u c k类与其所有子类中的 f l y ( )与 q u a c k ( )移除,因为这些行为已经被搬移到FlyBehavior
与QuackBehavior
类中了。
我们用 performFly()
和 performQuack()
取代 Duck
类中的fly()
与quack()
。
稍后你就知道为什么。
②父类鸭子的代码:
public class Duck{
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public void performFly(){
flyBehavior.fly();
}
public void performQuack(){
quackBehavior.quack();
}
}
③现 在 来 关 心 「 如 何 设 定flyBehavior
与quackBehavior
的实例变量」。看看 RedDuck
类:
public class RedDuck extends Duck{
public RedDuck(){
flyBehavior = new GaGaQuack();
quackBehavior = new FlyWithWings();
}
}
所以,红头鸭会嘎嘎叫,而不是吱吱叫,或叫不出声,红头鸭还会用翅膀飞。这是怎么做到的呢?当RedDuck
实例化时,它的构造器会把继承来的flyBehavior
与quackBehavior
实例变量初始化为相应接口的具体实现类。
可是这样还是有个问题
:红头鸭创建时就被定义了飞和叫的行为,这是不是太过于死板,不够emmm,灵活?是的,如果红头鸭病了呢,嗓子叫不出来,变成了哑巴呢。。。它不就不会叫了呀。。。却是会发生这种情况的呀!没事,我们还有解决办法:动态设定行为
动态设定行为
①在Duck
类中,加入下面的方法:
public void setFlyBehavior(FlyBehavior flyBehavior) {
this.flyBehavior = flyBehavior;
}
public void setQuackBehavior(QuackBehavior quackBehavior) {
this.quackBehavior = quackBehavior;
}
从此以后,我们就可以随时调用这两个方法改变鸭子的行为。比如将前面变成哑巴的鸭子的叫声变成不会叫。。。
②现在我们制造一个新的鸭子模型鸭(一开始它是不会飞的):
public class ModelDuck extends Duck {
public ModelDuck() {
flyBehavior = new FlyNoWay();
quackBehavior = new Quack();
}
public void display() {
System.out.println("我是一个模型鸭");
}
}
③建立一个新的FlyBehavior
的实现类:
public class FlyRocketPowered implements FlyBehavior {
public void fly() {
System.out.println("我用火箭飞了起来");
}
}
//FlyBehavior 接口
public interface FlyBehavior {
public void fly();
}
③创建模型鸭,并设置其飞行行为带上火箭:
Duck model = new ModelDuck();
model.performFly();
model.setFlyBehavior(new FlyRocketPowered());
model.performFly();
封装行为的大局观
好,我们已经深入鸭子模拟器的设计,该是将头探出水面,呼吸空气的时候了。现在就来看看整体的格局。
下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck
,
飞行行为实现FlyBehavior
接口,呱呱叫行为实现QuackBehavior
接口。
也 请 注 意 , 我 们 描 述 事 情 的 方 式 也 稍 有 改 变 。 不 再 把 鸭 子 的 行 为 说成「一组行为」,我们开始把行为想成是「一族算法」。想想看,在游戏设计中,算法代表鸭子能做的事(不同的叫法和飞行法),这样的作法也能用于用一群类计算不同国家的销售税金。
请特别注意类之间的『关 系』。拿一枝笔 ,把下面图形中的每个箭头标上适当的关系,关系可以是is-a(是一个)
、has - a(有一个)
、implements(实现)
。
『有一个』可能比『是一个』更好
『 有 一 个 』 关 系 相 当 有 趣 : 每 一 鸭 子 都 有 一 个飞的行为和叫的行为,让鸭子将飞行和呱呱叫委托它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合(composition
)。这种作法和『继承』不同的地方在于,鸭子的行为不是继承而来,而是和适当的行为对象『组合』而来。
这是一个很重要的技巧。其实是使用了我们的第三个设计原则
:多用组合,少用继承。
如你所见,使用组合建立系统具有很大的弹性,不仅可将 算 法 族 封 装 成 类 , 更 可 以 『 在 运 行 时 动 态 地 改 变 行为』,只要组合的行为对象,符合正确的接口标准即可。
组合用在『许多』设计模式中,它有优点也有缺点。
没错,这就是策略(Strategy)模式。
本章使用到了3个设计原则:
- 分离程序中变与不变的部分
- 针对接口编程,不针对实现编程
- 多用组合,少用继承
参考资料
《HeadFirst设计模式》第一章