设计形式之一:策略模式(Strategy pattern)

设计模式之一:策略模式(Strategy pattern)

设计模式之一:策略模式(Strategy pattern)

定义:

定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

The strategy pattern definesa family of algorithms, encapsulates each one, and makes them interchangeable.Strategy lets the algorithm vary independently from clients that use it.

要点:

1.       知道OO基础,并不足以让你设计出良好的OO系统。

2.       良好的OO设计必须具备可复用、可扩充、可维护三个特性。

3.       模式可以让我们建造出具有良好OO设计质量的系统。

4.       模式被认为是历经验证的OO设计经验。

5.       模式不是代码,而是针对设计问题的通用解决方案。你可以把它们应用到特定的应用中。

6.       模式不是被发明,而是被发现。

7.       大多数的模式和原则,都着眼于软件变化的主题。

8.       大多数的模式都允许系统局部改变独立于其他部分。

9.       我们常把系统中会变化的部分抽出来封装。

10.  模式让开发人员之间有共享的语言,能够最大化沟通的价值。

模式介绍

策略模式是一个很简单的模式,也是一个很常用的模式,可谓短小精悍。废话不多说了,下面开始介绍策略模式。

Joe的公司做了一套相当成功的模拟鸭子游戏:SimUDuck.游戏中会出现各种鸭子,一边游泳戏水,一遍呱呱叫。系统的核心类图如下所示:

设计形式之一:策略模式(Strategy pattern)设计形式之一:策略模式(Strategy pattern)

如图所示,在Duck基类里实现了公共的quack()和swim()方法,而MallardDuck和RedheadDuck可以分别覆盖实现自己的display()方法,这样既重用了公共的部分,又支持了不同子类的个性化扩展。

随着公司竞争压力的加剧,公司主管决定设计出会飞的鸭子来将竞争对手抛在后头。Joe拍着胸脯保证一个星期就可以解决。Joe发现只要在Duck类中加上fly()方法,然后所有鸭子都会继承fly().

设计形式之一:策略模式(Strategy pattern)

设计形式之一:策略模式(Strategy pattern)

Joe很高兴的带着自己的产品到股东会议上去展示,有很多“橡皮鸭子”飞来飞去。这是怎么回事?原来Joe忽略了一件事:并非Duck所有的子类都会飞。Joe在Duck超类中加上新的行为,会使得某些并不适合该行为的子类 也具有该行为。现在可好了!SimUDuck程序中有了一个无生命的会飞的东西。他意会到了一件事:当涉及“维护”时,为了“复用”目的而使用继承,结局并不完美。Joe很郁闷!他突然想到:如果在RubberDuck类里把fly()方法重写一下会如何?在RubberDuck类的fly()里让橡皮鸭子什么都不做,不就一切OK了吗!那以后再增加一个木头鸭子呢?它不会飞也不会叫,那不是要再重写quack()和fly()方法,以后再增加其它特殊的鸭子都要这样,这不是太麻烦了,而且也很混乱。

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

那么用接口能不能解决这个问题吗?把fly()从超类中取出来,放进一个“Flyable接口”中。这么一来只有会飞的鸭子才实习该接口。同样的方式,也可以用来设计一个“Quackable接口”,因为不是所以的鸭子都会叫。

设计形式之一:策略模式(Strategy pattern)

设计形式之一:策略模式(Strategy pattern)

但是这种方法会出现代码无法重用的问题,如果鸭子的类特别多的话,就这么几个鸭子还好说,但是我们有几十、上百个鸭子的时候你怎么办?如果某个方法要做一点修改,就需要重复修改上百遍。

呵呵!如果你是Joe,你该怎么办?

我们知道,并不是所有的鸭子都会飞、会叫,所以继承不是正确的方法。但是虽然上面的使用Flyable接口的方法,可以解决部分问题(不再有会飞的橡皮鸭子),但是这个解决方案却彻底破坏了重用,它带来了另一个维护的噩梦!而且还有一个问题我们前面没有提到,难道所有的鸭子的飞行方式、叫声等行为都是一模一样的吗?不可能吧!

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

找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

换句话说,如果每次心的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他文档的代码有所区分。这个原则的另一种思考方式是:把会变化的部分取出并封装起来,以便以后可以轻易地盖栋或扩充此部分,而不影响不需要变化的其他部分。

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

面向接口编程,而不是针对实现编程。

针对接口编程真正的意思是针对超类型编程。“针对接口编程”关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为,不会被蚌寺在超类型的行为上。“针对超类型编程”这句话,可以更明确地说成“变量的生命类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型”

根据面向接口编程的设计原则,我们应该用接口来隔离鸭子问题中变化的部分,也就是鸭子的不稳定的行为(fly()、quack())。

设计形式之一:策略模式(Strategy pattern)设计形式之一:策略模式(Strategy pattern)

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

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

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

修改后的Duck类如下图所示:

设计形式之一:策略模式(Strategy pattern)

设计形式之一:策略模式(Strategy pattern)


测试代码:

public abstract class Duck {
	FlyBehavior flyBehavior;
	QuackBehavior quackBehavior;
	public Duck(){}
	public void performFly()
	{
		flyBehavior.fly();
	}
	
	public void performQuack()
	{
		quackBehavior.quack();
	}
	
	public void swim()
	{
		System.out.println("All ducks float,even decoys!");
	}
	
	public void setFlyBehavior(FlyBehavior fb)
	{
		flyBehavior = fb;
	}
	
	public void setQuackBehavior(QuackBehavior qb)
	{
		quackBehavior = qb;
	}
}

public class ModelDuck extends Duck{

	public ModelDuck()
	{
		flyBehavior = new FlyNoWay();
		quackBehavior = new Quack();
	}
	
	public void display()
	{
		System.out.println("I'm a model duck");
	}

}

public class MallardDuck extends Duck{
	public MallardDuck()
	{
		quackBehavior = new Quack();
		flyBehavior = new FlyWithWings();
	}
	
	public void display()
	{
		System.out.println("I'm a real Mallard duck");
	}
}

public class MuteQuack implements QuackBehavior {

	@Override
	public void quack() {
		// TODO Auto-generated method stub
		System.out.println("<<Silence>>");

	}

}

public interface FlyBehavior {
	public void fly();
}

public class FlyNoWay implements FlyBehavior {
	public void fly(){
		System.out.println("I can't fly");
	}
}

public class FlyRocketPowered implements FlyBehavior {

	@Override
	public void fly() {
		// TODO Auto-generated method stub
		System.out.println("I'm flying with a rocket!");
	}

}

public class FlyWithWings implements FlyBehavior {
	public void fly(){
		System.out.println("I'm flying");
	}
}

public interface QuackBehavior {
	public void quack();
}

public class Quack implements QuackBehavior {

	@Override
	public void quack() {
		// TODO Auto-generated method stub
		System.out.println("Quack");
	}

}

public class Squeak implements QuackBehavior {

	@Override
	public void quack() {
		// TODO Auto-generated method stub

		System.out.println("Squeak");
	}

}

public class MiniDuckSimulator {

	public static void main(String[] args){
		Duck mallard = new MallardDuck();
		
		mallard.performQuack();
		mallard.performFly();
		
		Duck model = new ModelDuck();
		model.performQuack();
		model.performFly();
		model.setFlyBehavior(new FlyRocketPowered());
		model.performFly();
	}
}

程序运行结果如下:

设计形式之一:策略模式(Strategy pattern)设计形式之一:策略模式(Strategy pattern)

应用场景和优缺点

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

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

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

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

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

 

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

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

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

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

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

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

封装行为的大局观

我们已经深入研究了鸭子模拟器的设计,概述将头探出水面,呼吸空气的时候了,先来就来看看整体的格局。
下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck,飞行行为实现FlyBehavior接口,呱呱叫行为实现QuackBehavior接口。类图如下图所示:
设计形式之一:策略模式(Strategy pattern)
最后一个设计原则:多用组合,少用继承。
将两个类结合起来使用,如同本例一般,这就是组合。这种红作法和“集成”不同的地方在于,鸭子的行为不是继承来的,而是和适当的行为对象“组合”来的。使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以“在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准即可。


1楼cjr15233661143昨天 20:54
模式就是前人经验的传承,是经过总结形成的一套某一类问题的一般性解决方案,加油