注:本篇博客内容出自Head First设计模式,仅对文章进行整理与总结。
1.引出问题
现在我们有一个鸭子的基类:
public class Duck { public void quack() { System.out.println("Duck quack"); } public void apperence() { System.out.println("Duck apperence"); } }
假设我们需要为每个鸭子的子类添加一个fly,让我们的鸭子会飞,并修改每个子类,实现不同的quack功能,毕竟鸭子也不都是一样叫的。在此,我们看一下两种解决方案:
方案一 :
我们可以像quack功能一样,在Duck基类添加一个fly方法。
public class Duck { public void quack() { System.out.println("Duck quack"); } public void apperence() { System.out.println("Duck apperence"); } public void fly() { System.out.println("Duck fly"); } }
上述的方法实际是通过java的继承特性实现了想要的需求,如果仅仅从需求的角度来看,我们的确实现了所有鸭子的子类均有fly的功能,但实际情况是,某些鸭子是不会飞的,比如浴缸里放着的橡皮鸭,小孩子的玩具鸭,用于当作诱饵的诱饵鸭等。
你可能想说,我在每个子类中覆盖相应的fly方法,实现特定的飞行方式,不会飞的情况什么也不做就行了,那么以后每写一个子类就要覆盖相应的方法,如此一来不仅增加了前期开发工作量,也加大后期添加/修改功能的工作量,并且还得注意别改出bug,这样的话就使得代码的维护成本大幅提高,以上代码只是添加了一个fly功能就如此麻烦,那如果需要添加更多功能呢,你怕不是要天天加班了,过上12+6的幸福生活了。
可见,对代码的局部修改,影响的成面可不是局部的。
方案二:
我们设计一个Flyable并把quack方法拿出来单独放到Quackable的接口:
public interface Flyable { public void fly(); } public interface Quackable { public void quack(); }让有飞行功能的子类实现Flyable接口,类似下面子类:
public class MallardDuck implements Flyable ,Quackable{ @Override public void fly() { // TODO Auto-generated method stub System.out.println("Fly with wings"); } @Override public void quack() { // TODO Auto-generated method stub System.out.println("MallardDuck quack"); } }
这种解决方案相较于方案一,大大减少了我们的工作量,但是还是存在一个问题。我们知道鸭子种类千千万,但飞行的方式不可能千千万,所以有些飞行方式是一样的,这就导致同一个功能我们可以会去写很多遍。
从开发者的角度来说,这无疑造成了代码无法复用。我们程序员的宗旨就是不做重复且毫无意义的编程。
2. 解决问题
有没有即降低工作量,方便后期开发维护,又能实现代码高度复用的方法呢,答案是肯定的,那就是本篇文章即将要介绍的一种设计模式---------策略模式。
先看一下策略模式的定义:
策略模式:定义了算法族,分别封装起来,让他们之间可以相互替换。此模式让算法的变化独立于使用算法的客户。
现在先不对定义做什么解释,稍后再做说明,我们继续往下看。
2.1 问题分析
从上文我们知道使用继承并不能很好的解决问题,因为鸭子的行为在子类里会不断的改变。Flyable和Quackable接口虽然解决了问题,但是java接口不具有实现代码,所以继承接口无法达到代码服用的目的,这就意味着:无论何时你要修改修改某个行为,你就必须得往下追踪并在每一个定义此行为的类中修改他,同时还要避免修改出新的错误。
从上面的分析,我们引出一个新的设计原则,那就是:
找出应用中需要变化之处,把他们独立出来,不要和那些不需要变化的代码混在一起。
另一种更通俗的说法就是:把会变化的部分取出并封装起来,一变以后可以轻易的改动或扩充此部分代码,而不影响不需要变化的其他部分。
首先,我们知道Duck类内部的fly和quack会随着鸭子的不同而改变,为了把这两个行为从Duck类里分开,我们把它们从Duck类中单独拿出来,建立一组新的类来表示不同的行为。比如,建立一个新的类表示“呱呱叫”,另一个新的类表示“嘎嘎叫”等等。
其次,如何设计新的类呢?我们希望一切都能够有弹性,毕竟之前的设计太过于死板,导致工作量居高不下,维护等一系列问题。我们希望能够指定具体的行为到鸭子的实例。比如,设计一个新的鸭子子类,并指定特定的行为给他。最好我还能在程序运行的动态的改变子类的行为,在不同情况下指定不同的行为给指定的子类。
由此,我们引出第二个设计原则,那就是:
针对接口编程,而不是针对实现编程。
针对接口编程真正的意思是针对某个超类型编程。关键就是在java的多态技术,利用多态,程序可以针对超类型编程,执行时会根据实际状态执行到真正的行为,不会被绑死在超类型的行为上。
2.2 代码实现
现在联系代码做进一步说明。
结合上面的分析,我们把变化的部分提取出来,封装在接口里面,并实现特定行为类组,具体如下:
public interface FlyBehavior { public void fly(); } public interface QuackBehavior { public void quack(); }
首先我们将quack和fly方法从Duck中取出,分别封装在QuackBehavior和FlyBehavior中,然后我们实现特性的行为类组:
以下是QuackBehavior的行为类组:
//叫法一 public class Quack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Quack Quack Quack"); } } //叫法二 public class MuteQuack implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("<< Be Slience>>"); } } //叫法三 public class Squeak implements QuackBehavior { @Override public void quack() { // TODO Auto-generated method stub System.out.println("Squeak Squeak Squeak"); } }以下是FlyBehavior的行为类组:
// 飞行方法一 public class FlyWithWings implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("Fly with wings"); } } //飞行方法二 public class FlyNoWay implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("I can't fly"); } }
现在,“工具类”我们都已经准备好了,就差整合一个Duck的行为了。关键在于,Duck现在应该将fly和quack的动作“委托”给“工具类”来处理,而不是自己处理。
下面是经过整合之后的Duck基类:
public abstract class Duck { private FlyBehavior flyBehavior; private QuackBehavior quackBehavior; public Duck() { } public void performFly() { flyBehavior.fly(); } public void performQuack() { quackBehavior.quack(); } public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } public void setQuackBehavior(QuackBehavior quackBehavior) { this.quackBehavior = quackBehavior; } public abstract void apperence(); }
现在对以上代码进行说明:
1. 我们将Duck有具体类变为抽象类,并将appernece方法改为abstract抽象方法,这是因为Duck类是一个基类,我们希望所有的鸭子子类都能继承这个基类,并让子类在实例化的时候通过apperence做一些解释说明。
2. 我们添加了FlyBehavior,QuackBehavior成员变量和setFlyBehavior,setQuackBehavior方法。添加成员变量的根据就是上面提到的针对接口变成,添加方法是为了指定具体的行为到具体的Duck子类。
接下来我们写一些测试代码,为此我们先创建一个鸭子子类:
//绿头鸭 public class MallardDuck extends Duck{ public MallardDuck(FlyBehavior flyBehavior ,QuackBehavior quackBehavior) { // TODO Auto-generated constructor stub this.flyBehavior = flyBehavior; this.quackBehavior = quackBehavior; } @Override public void apperence() { // TODO Auto-generated method stub System.out.println("Mallard duck"); } }测试代码如下:
public static void main(String[] args) { // TODO Auto-generated method stub FlyBehavior flyBehavior = new FlyWithWings(); QuackBehavior quackBehavior = new Squeak(); Duck duck = new MallardDuck(flyBehavior, quackBehavior); duck.performFly(); duck.performQuack(); } //运行结果: Fly with wings Squeak Squeak Squeak
首先,变量的命名都是以接口或者是抽象类的方式,而实际实例化的则是具体的子类对象,这就是针对接口编程而不是针对具体实现编程。其次,调用的fly以及quack方法是通过基类中的函数实现的,没有将具体的实现绑定在子类里,这样做增加了代码的灵活性以及降低了代码之间的耦合性,我们只需要调用基类的perform***方法,之后就会执行到具体的行为。
现在我们尝试在代码运行的过程中改变子类的行为,我们来做一个机器鸭,并设计一种新的飞行方式:
public class RobotDuck extends Duck{ public RobotDuck(FlyBehavior flyBehavior ,QuackBehavior quackBehavior) { // TODO Auto-generated constructor stub this.flyBehavior = flyBehavior; this.quackBehavior = quackBehavior; } @Override public void apperence() { // TODO Auto-generated method stub System.out.println("Robot Duck"); }
public class FlyWithRocket implements FlyBehavior { @Override public void fly() { // TODO Auto-generated method stub System.out.println("Ooooooh......, fly with rocket so cool"); } }
测试类如下:
public static void main(String[] args) { // TODO Auto-generated method stub FlyBehavior flyBehavior = new FlyNoWay(); QuackBehavior quackBehavior = new MuteQuack(); FlyBehavior newFlyBehavior = new FlyWithRocket(); Duck duck = new RobotDuck(flyBehavior, quackBehavior); duck.performFly(); duck.performQuack(); System.out.println("\n now we change to use rocket to fly\n"); duck.setFlyBehavior(newFlyBehavior); duck.performFly(); duck.performQuack(); } //运行结果: I can't fly << Be Slience>> now we change to use rocket to fly Ooooooh......, fly with rocket so cool << Be Slience>>
首先机器鸭不能叫也不能飞,然后我们通过setFlyBehavior的基类方法在程序运行过程中改变了子类的飞行方式,使它使用火箭推进器进行飞行。
现在让我们回头看一下策略模式的定义:
策略模式:定义了算法族,分别封装起来,让他们之间可以相互替换。此模式让算法的变化独立于使用算法的客户。
结合以上例子,这里的算法族就是指实现了fly和quack的接口的各种具体的行为,他们之间可以相互替换且不会影响客户端代码,提高了代码的复用性以及灵活度,降低了代码之间的耦合性以及开发维护的成本,这就是策略模式。
以上例子只是用来简述策略模式的例子,实际开发中遇到的情景不可能完全相同,但是万变不离其宗,打好基础才是最主要的。本篇文章到此结束,谢谢。