bboyjing's blog

HeadFirst设计模式一【策略模式】

最近打算巩固下设计模式,顺便记录下学习笔记,供日后查阅以及分享给需要的道友。参考书籍为《Head First设计模式》中文版,读书笔记的内容绝大部分来自书籍,同时会添加一些自己的理解。如涉及侵权行为,会立即停止更新,并删除所有所有文章。至于为什么要学设计模式,我也说不清。当你觉得自己的代码怎么写的那么蠢、那么臃肿,怎么改一个小需求到处都要改,这个时候就需要考虑下设计模式了。这本书的代码示例语言使用的是Java,这没啥好说的了,下面就直接开始学习吧。

场景描述

书中使用了一个构造不同类型的鸭子的场景,一步步由浅入深地来讲解该模式,建议去读一下原著,带入感比较强。这里我们就只简述下该场景,我们要构造不同类型的鸭子,每种鸭子都有一些共有的行为;另外其中某一种鸭子可能会有特殊的行为。比如说:呱呱叫(quack),每种鸭子都会叫,但是叫声可能会不一样;飞(fly),像野鸭子会飞,普通的鸭子就不会。这种时候我们该怎样去设计一个可以扩展的鸭子模型呢。最容易想到的是,继承。写一个鸭子父类,定义两个抽象方法,子类去重写。(或者实现接口,这两种方式在策略模式面前没有本质区别)这样能暂时解决问题,但是可能会带来如下缺点:

  • 代码在多个子类中重复。比如采用继承的方式,很多种类的鸭子都是呱呱叫,那实现这个行为将出现在很多子类中,
  • 运行时的行为不容易改变。可以想象出,一旦鸭子的叫声定成呱呱叫,很难在运行时优雅地改成其他叫声。
  • 很难知道所有鸭子的全部行为。如果没有把行为定义全,如果鸭子子类有很多之后,修改将是个恶梦。
  • 改变会牵一发而动全身,造成其他鸭子不想要的改变。这一点跟上一点有些类似,比如说新增一个行为之后,可能所有的子类都要改一遍。

所以,继承和实现接口并不是适当的解决方案,下面直接看代码,策略模式是如何解决的。

代码示例

  1. 将quack、fly行为从duck中抽出来,单独定义成接口,并且每一种行为都有实现类。

    1
    2
    3
    public interface QuackBehavior {
    public void quack();
    }
    1
    2
    3
    4
    5
    6
    public class Quack implements QuackBehavior {
    @Override
    public void quack() {
    System.out.println("Quack...");
    }
    }
    1
    2
    3
    public interface FlyBehavior {
    public void fly();
    }
    1
    2
    3
    4
    5
    6
    public class FlyWithWings implements FlyBehavior {
    @Override
    public void fly() {
    System.out.println("I'm flying!!!");
    }
    }
  2. 定义鸭子抽象父类,在父类中定义行为接口变量,并且给变量添加setter方法。在这个类中display()方法是所有鸭子共有的,由各个子类去实现。行为接口变量由构造子类的时候传递,并且提供了修改的方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public abstract class Duck {
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;
    public abstract void display();
    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;
    }
    }
  3. 创建一个会呱呱叫的,并且会用翅膀飞的野鸭

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class MallardDuck extends Duck {
    public MallardDuck() {
    quackBehavior = new Quack();
    flyBehavior = new FlyWithWings();
    }
    @Override
    public void display() {
    System.out.println("I'm a real Mallard duck.");
    }
    }
  4. 测试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) {
    Duck mallardDuck = new MallardDuck();
    mallardDuck.performQuack();
    mallardDuck.performFly();
    }
    # 输出
    Quack...
    I'm flying!!!

    运行时需要更改fly的方式,只要调用setFlyBehavior,传递一个新的飞行方式即可。

设计原则

针对上面该模式的实现,总结除了如下设计原则:

  • 找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。在此模式中,就是区分了display与quack、fly,前者是不需要变化的,后者两者是可能需要变化的。把不变的留在duck抽象父类中,把变得单独抽成接口。
  • 针对接口编程,而不是针对实现类编程。在此模式中,就是duck父类的行为变量采用定义成接口而非具体子类型,这样就灵活了。
  • 多用组合,少用继承。在此模式中,就是不采用duck子类实现行为接口的方式,而是将行为接口以成员变量的方式定义在duck抽象父类中,这样的好处是,当新增一种行为时,不会影响到现有的所有子类,可按需修改。

总结

该模式本身不难理解,难的是在实际场景中如何区分哪些是变得,哪些是不变的,并且能够明白当下策略模式是否适合,如果硬套,可能会使情况变得更复杂。在书中模式讲解结束后, 会有一些习题可以练习,这一点挺好的。所以还是推荐买一本看看。