读headFirst设计模式 - 策略模式
有些人已经解决你的问题了
什么是设计模式?我们为什么要使用设计模式?怎样使用?按照书上的说法和我自己的理解,我认为是这样的:我们遇到的问题其他开发人员也遇到过,他们利用他们的智慧和经验将问题解决了,把他们解决问题的方法提炼总结出来使其能解决其他同类问题。使用设计模式是为了更方便快捷的解决问题。把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用这些模式,以往是代码复用,现在是经验复用。
先把书上的例子过一遍,简单的鸭子模拟应用
Joe所在的公司决定开发一款模拟鸭子的应用。系统中有各种鸭子,鸭子可以游泳,可以呱呱叫,各种鸭子有不同的外观。此系统建立了一个鸭子的基类,其中有游泳的方法swim,有呱呱叫的方法quack,有显示鸭子外貌的方法display。每种鸭子的外貌不同,必须在其子类中重写display方法。就像这样:
由于竞争加剧,公司决定搞定不一样的东西:“嘿,Joe,我想鸭子应该能飞!”,“嗯, 这个听起来很容易”,在Duck中加一个fly方法就行了。过了几天,公司开会,“Joe,怎么会有一只橡皮鸭(RubberDuck,不会飞,吱吱叫)在屏幕里面飞来飞去?”,好吧,这是Joe疏忽了,只有真正的鸭子能飞,橡皮鸭会叫,会游泳但是不会飞,马上修复(覆盖RubberDuck中的fly方法, 让它什么也不做)。但是如果我需要一只诱饵鸭(DecoyDuck,不会飞也不会叫)呢,也在子类中重写quack和fly方法?Joe还收到通知,此系统还会不定时更新,至于怎么更新还没有想到,于是Joe意识到继承不是一个好方法,因为每添加一个鸭子的子类,他就要被迫检查该子类的quack和fly方法并可能需要重写他们,如果直接修改父类中的方法,但有些子类并不想修改,那么这些子类就都要重写这些方法。
继承所采用的代码
Duck
public abstract class Duck { public void quack(){ System.out.println("呱呱叫"); } public void swim(){ System.out.println("游泳"); } //每个鸭子的外观不同, 在子类中实现 public abstract void display(); //鸭子飞行的方法 public void fly(){ System.out.println("飞行"); } }
MallardDuck
/** * 外观是绿色的鸭子 */ public class MallardDuck extends Duck { @Override public void display() { System.out.println("绿头鸭"); } }
RubberDuck
/** * 橡皮鸭 * 橡皮鸭不会呱呱叫(quack), 而是吱吱叫(squeak) */ public class RubberDuck extends Duck { @Override public void display() { System.out.println("可爱的黄色橡皮鸭"); } //橡皮鸭不会呱呱叫(quack), 而是吱吱叫(squeak) @Override public void quack() { System.out.println("橡皮鸭吱吱叫"); } //橡皮鸭不会飞 @Override public void fly() { } }
DecoyDuck
/** * 诱饵鸭, 不会飞也不会叫 */ public class DecoyDuck extends Duck { @Override public void display() { System.out.println("诱饵鸭"); } @Override public void quack() { System.out.println("什么都不会做, 不会叫"); } @Override public void fly() { System.out.println("什么都不做, 不会飞"); } }
采用接口呢
将行为抽离出来,比如将fly方法抽离出来放到Flyable接口中,只有会飞的Duck的子类才实现该接口,同样的也可以将quack方法抽离到Quackable接口中。就像这样:
这样解决了一部分问题,至少橡皮鸭不会到处飞了。但是你有没有想过另外的一个问题,就是这样代码无法复用,因为接口中的方法只能是抽象的,这样导致在每个子类中都需要重写方法。这样无疑是从一个噩梦跳入了另一个噩梦。
变化与不变分离
好吧,在软件开发上,有什么是你深信不疑的,那就是——change,不变的是变化。
虽然使用接口也不妥,但思想是值得借鉴的: 把变化的和不变的离,那哪些是变化的呢?“真正的”鸭子会游泳swim,会呱呱叫quack,会飞fly,而橡皮鸭会游泳swim,会吱吱叫squeak,不会飞。所以说叫可以是呱呱叫,可以是吱吱叫,也可以不叫,飞可以沿直线飞,可以沿曲线飞,也可以不会飞,所以叫和飞是变化的。游泳都是在水面上无规则的游泳,可以看作是不变的,当然,你要认为游泳有多种方式,也可以认为它是变化的,我在这里把游泳认为是不变的,所以说判断变化的和不变的要根据实际情况来定。
软件设计原则: 变化与不变分离
找出应用中可能会变化的部分,把它们独立出来,不要把他们和那些不变的混在一起。即把应用中可能会变化的抽离出来并封装起来。
继续设计
基于变化和不变分离的原则,我们可以把变化的(飞和叫)抽离出来并封装起来。比如将飞行的行为抽离出来设计成一个接口FlyBehavior,将叫的行为设计成接口QuackBehavior,然后具体的行为实现接口并重写方法,如:
针对接口编程
我们把鸭子的行为从鸭子类Duck中分离出来,和以往不同,以往的做法是:行为是从Duck中的具体实现继承过来,或是实现接口重写方法而来,这两种方式都依赖实现,我们被实现绑得死死的。在我们新的设计中,我们把行为分离出来,所以行为不会绑死在鸭子的子类中,换句话说现实行为的代码位于特定类QuackBehavior和FlyBehavior中,可以在运行时动态的改变行为。这就是针对接口编程的一种体现,针对接口编程指的是针对超类型编程,不一定是interface,可以是abstract class。
软件设计原则:针对接口编程
针对接口编程,而不是针对实现编程
整合实现代码
首先是两个行为接口QuackBehavior和FlyBehavior
QuackBehavior
public interface QuackBehavior { void quack(); }
FlyBehavior
/** * 飞行行为接口 */ public interface FlyBehavior { void fly(); }
QuackBehavior具体行为实现,呱呱叫Quack,吱吱叫Squeak,不会叫MuteQuack
Quack
/** * 呱呱叫 */ public class Quack implements QuackBehavior { @Override public void quack() { System.out.println("呱呱叫"); } }
Squeak
/** * 吱吱叫 */ public class Squeak implements QuackBehavior { @Override public void quack() { System.out.println("吱吱叫"); } }
MuteQuack
/** * 什么都不做, 不会叫 */ public class MuteQuack implements QuackBehavior { @Override public void quack() { System.out.println("什么都不做, 不会叫"); } }
FlyBehavior的具体实现,用翅膀飞FlyWithWings,不会飞FlyNoWay
FlyWithWings
/** * 用翅膀飞 */ public class FlyWithWings implements FlyBehavior { @Override public void fly() { System.out.println("用翅膀飞行"); } }
FlyNoWay
/** * 不会飞 */ public class FlyNoWay implements FlyBehavior { @Override public void fly() { System.out.println("什么都不做, 不能飞"); } }
然后是基类Duck,将行为接口当做实例变量放入Duck中,需要在运行时动态的改变行为,可以提供setter方法
Duck
public abstract class Duck { //针对接口编程的体现 private FlyBehavior flyBehavior; private QuackBehavior quackBehavior; //设置一个默认的行为 public Duck() { flyBehavior = new FlyWithWings(); quackBehavior = new Quack(); } public void swim(){ System.out.println("游泳"); } //每个鸭子的外观不同, 在子类中实现 public abstract void display(); //执行呱呱叫的方法 public void performQuack(){ quackBehavior.quack(); } //执行飞行的方法 public void performFly(){ flyBehavior.fly(); } public void setFlyBehavior(FlyBehavior flyBehavior) { this.flyBehavior = flyBehavior; } public void setQuackBehavior(QuackBehavior quackBehavior) { this.quackBehavior = quackBehavior; } }
注意,抽象类也是有构造方法的,不过不能创建对象,抽象类的构造方法是用来初始化变量的
两个子类MallardDuck和RubberDuck
MallardDuck
/** * 外观是绿色的鸭子 */ public class MallardDuck extends Duck { @Override public void display() { System.out.println("绿头鸭"); } @Test public void test1() throws Exception { MallardDuck mallardDuck = new MallardDuck(); mallardDuck.performQuack(); mallardDuck.performFly(); display(); } }
RubberDuck
/** * 橡皮鸭 * 橡皮鸭不会呱呱叫(quack), 而是吱吱叫(squeak) */ public class RubberDuck extends Duck { @Override public void display() { System.out.println("可爱的黄色橡皮鸭"); } @Test public void test1() throws Exception { Duck rubberDuck = new RubberDuck(); //运行时动态改变行为 rubberDuck.setFlyBehavior(new FlyNoWay()); rubberDuck.setQuackBehavior(new Squeak()); rubberDuck.performFly(); rubberDuck.performQuack(); rubberDuck.display(); } }
封装行为
好了,我们来看看整体的格局:飞行行为实现了FlyBehavior接口,呱呱叫行为实现了QuackBehavior接口,也请注意,我们描述事务的方式也有所改变,我们把行为看成一组算法,飞行行为是算法,呱呱叫行为也是算法,就像这样:
我们把QuackBehavior和FlyBehavior类型的变量放到了Duck中,这其实就用到了组合。用组合创建的系统更具有弹性,不会如继承一般一处改可能需要多处改
软件设计原则:
多用组合,少用继承
策略模式
好了,到这里我们终于学到了第一个模式:策略模式(strategy pattern),介绍一下策略模式的概念
定义了算法族,并分别封装起来,让他们之间可以互相替换,此模式让算法的变化独立于使用算法的客户