Head First设计模式——策略设计模式

策略设计模式

说在前面的话

入软件一年啦,平心而论,总算不限于只会钻研些基础的语言语法了,数据结构和算法也恶补的差不多了。所以~趁着现在一边实习一边啃《Head First设计模式》的功夫,怀着一颗敬畏的心决定经营自己的博客,以后呢,也会把这件事坚持下去并作为鞭策自己的一个方法吧O(∩_∩)O~~。24岁的编程巨婴的第一篇博客,在此就立个flag:为了怕以后自己的博客流于平庸,所有的内容只限“有感而发”,仅仅将自己有所思考的东西才会总结成博客,而不是教科书般的全部列出。另外以后的话,如果有引用别人的段落或思想,我也会特别标注出来,愿自己不忘初心喽~

什么是设计模式

记得半年前,我还仅仅限于学了个java的程度的时候,去选了学校朱老师的“设计模式”课程,对软件开发一无所知的我,去试听了第一节课就被处事严谨的朱老师给吓退了,后来当我了解到设计模式有多么重要之后,也是很后悔自己当时的退选。

言归正传:

设计模式是一种编程思想的总结,是一种抽象的、为了提高代码可靠性阅读性等等而被提取并反复使用的规范。

说白了,设计模式就是前人将编程及软件工程中的问题总结成的一套套的解决办法

策略模式解决什么问题

那么,策略设计模式要解决的问题是什么呢?

在代码的维护中,超类中封装了公有方法,在维护的过程中,可能发生如下的问题:
- 需要增加新的方法、实例域。
- 需要对已有的方法或者实例域进行修改。
- 我们可不可以在不改变对象自身的情况下改变对象的行为?

而如果直接在超类中修改,很有可能发生如下问题:所有不具有该方法的子类,都需要重写这个方法。而这个重写带来的可能的恶果是:

  • 可能重写的代价很大,方法很复杂,换句话说,我们要对每个方法的实现细节了如指掌。
  • 大大降低了代码复用性。
  • 方法被写死在超类中,“硬编码”。耦合太严重,牵一发而动全身。

而从另外一个角度,如果定义成接口,把具体需要改变的方法“抽离”出来,需要该方法的子类特定地去实现接口,然后重写接口中的方法。这样做看似解决了问题,但是却带来了更为严重的后果:

  • 由于接口中都是抽象方法,代码无法复用,极大地增大了代码量,每一个实现类都需要重写接口中的方法。

如何解决?

虽然简单地使每个子类特异性地去实现其所需要的功能的接口被证明是不可行的,但是在这里,超类已经不再与特定地功能“硬编码在一起”。这为我们提供了一种思路:解除类与特定“行为”的耦合性

那么,策略设计模式是如何实现解耦的呢?

简单来讲,策略设计模式将类中可变的“行为”高度提取到一个个接口中,对于每一个接口,创建实现该行为的不同实现类。到这里,完成了可变部分的封装。接下来,在超类中,添加“行为”的成员变量,其声明是接口(利用多态性,后面会详述),并添加相应的setter用于动态地改变“行为”,写相应的方法调用接口成员的方法完成具体行为。

举例说明

就以书上的例子,简单说明下。

超类:Duck(鸭子)行为:fly()飞行,display()自我介绍,swim()游泳,quack()嘎嘎叫

    public class Duck{
        public void swim(){...}
        public void fly(){...}
        public void quack(){...}
        public void display(){...}
    }

子类:DecoyDuck诱饵鸭,MallardDuck野鸭,ModelDuck模型鸭,RedHeadDuck红头鸭,RubberDuck橡胶鸭。

1. 提取可变部分

在鸭子的三种行为中,所有鸭子都会游泳swim()属于公共部分(display()方法后面会讲到)。而fly()与quack()对于不同的鸭子有不同的结果,所以我们将二者分别提取到两个接口FlyBehavior和QuackBehavior中,并分别在其中声明了抽象方法fly()和quack(),以FlyBehavior为例:

FlyBehavior .java

    public interface FlyBehavior {
        public void fly();
    }

飞行行为FlyBehavior是对鸭子的飞行行为的高度概括,具体到每一个鸭子,行为会有不同的表现,新建两个实现类:鸭子会飞(FlyWithWings),鸭子不会飞(FlyNoWay):

FlyNoWay.java & FlyWithWings.java

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

2. 耦合进Duck类中

好了,我们已经成功地将飞行行为与嘎嘎叫行为封装到了一个接口的实现关系里,接下来要做的,就是在我们的Duck类中如何耦合进具体的行为。先看看代码吧~

Duck.java:

    public abstract class Duck {
        FlyBehavior flyBehavior;
        QuackBehavior quackBehavior;

        public Duck() {
        }

        public void setFlyBehavior (FlyBehavior fb) {
            flyBehavior = fb;
        }

        public void setQuackBehavior(QuackBehavior qb) {
            quackBehavior = qb;
        }

        abstract void display();

        public void performFly() {
            flyBehavior.fly();
        }

        public void performQuack() {
            quackBehavior.quack();
        }

        public void swim() {
            System.out.println("All ducks float, even decoys!");
        }
    }


首先注意到Duck类是一个抽象类,这一点并非必须的。此处由于每一个Duck的子类都需要进行特异性的“自我介绍”,所以这里选择了在每个子类中覆盖而不是抽象出具体的“自我介绍接口”。从这里我们也可以稍微看出来一些策略设计模式的缺点哈(索性在这说得了O(∩_∩)O~):

  • 如果我们的飞行行为有很多很多种的话,那么我们不得不创建很多很多实现类。
  • 在我们新建一个新的鸭子子类的时候,我们必须知道这个子类它具体使用的是哪个行为。

所以为了避免不得不new 很多“自我介绍接口”的实现类,我们直接选择将display()方法声明为abstract,具体就交由子类去覆盖。可见,设计模式并非万能的,我们要根据需要进行取舍!

回到正题,我们先去找我们耦合进去的行为,不难找到如下两个行为:

        FlyBehavior flyBehavior;
        QuackBehavior quackBehavior;

注意到这里利用多态声明为接口,熟悉java就不难理解其中的好处,不再赘述,详情可见《Head First设计模式》Page 12。

对比之前Duck中直接调用fly()的方式,这里我们调用flyBehavior接口的实现对象的fly()方法,并将其封装进新的方法performFly()中:

        public void performFly() {
            flyBehavior.fly();
        }

这也就意味着!只要我们可以动态地改变flyBehavior,那么绑定在对象上的行为就可以改变。幸运的是,我们可以这么做:

        public void setFlyBehavior (FlyBehavior fb) {
            flyBehavior = fb;
        }

setter方法中再次用到了多态,不再赘述。

看到这里大家可能稍微有点蒙,我们不妨跳出来看一下。

开始,我们给所有的鸭子都绑定了一个方法,fly(),并告诉它们:You all can fly!,然后橡皮鸭子就跳出来了:No,I can’t ! 然而我们说,对不起,这事我管不了,你自己看着办吧,橡皮鸭表示好吧,那我自己整个方法覆盖一下。结果后来鸭子阵容越来越庞大,需要覆盖的越来越多,代码混乱而庞杂。就有鸭子不满了,说,都怪父类,非得在自个身上搞个什么fly()方法,大家变种那么多,能一样吗。不如,我们把这些东西抽离出吧。于是,一个虚拟的概念“飞行行为”(接口)出现了,鸭子们为了区分,定义了两个“飞行行为”的实现类:会飞,不会飞。事情终于简单了,每个鸭子有自己的“飞行行为胸牌”(FlyBehavior成员),橡皮鸭拿了“我不会飞贴纸”(实例化)贴在胸牌上,大家就都知道它不会飞了,别的鸭子也有自己的胸牌和贴纸。突然有一天,橡皮鸭被改造,身上装了一个火箭发射器,OK,橡皮鸭拿了新的“我会用火箭发射器飞贴纸”(调用setter方法)贴在了胸牌上。

综上所述,我们将变化的部分抽离出来,定义了专门的算法族并分别封装起来,让算法的变化独立于算法的客户

3.子类实现

子类覆盖Duck,需初始化FlyBehavior,QuackBehavior两个成员,覆盖display()方法。以红头鸭为例:
RedHeadDuck.java

    public class RedHeadDuck extends Duck {

        public RedHeadDuck() {
            flyBehavior = new FlyWithWings();
            quackBehavior = new Quack();
        }

        public void display() {
            System.out.println("I'm a real Red Headed duck");
        }
    }

最后提一下书中提到的三个设计原则:

  • 封装变化
  • 针对接口编程,不针对实现编程
  • 多用组合,少用继承

回答书中问题

《Head First设计模式》第12页提出了一个问题:

“我不懂你为什么非要把FlyBehavior设计成接口,为何不使用抽象类,这样不就可以使用多态了吗”

开始我也是一脸懵逼,因为我没有读懂书中自带的解释,读了很多遍之后,才明白书在跟我们玩文字游戏:

书中的“接口”并不是我们所理解的接口,而是一种“概念”,代表着可以实现多态的所有supertype(超类型),基本上,按照书中的概念,超类型该如此划分:

超类型————
        |————>普通类
        |————>抽象超类型
                    |————>接口
                    |————>抽象类

所以书中的意思是

  • 根据设计原则二,最好使用接口。
  • 使用抽象类亦可,但最好不要使用有非抽象方法的抽象类,因为万一某个子类没有覆盖,根据多态会自动调用父类。
  • 不推荐使用普通超类,因为最好在使用具体子类对象的时候再实例化,不要在超类中硬编码。

Comparator,Comparable接口与策略设计模式

最后,稍微提一下Comparator、Comparable接口中用到的策略设计模式思想。我们以TreeMap类为例。

首先仅仅看名字,我们就会觉得Comparable应该是个形容词,那么我们就应该理解为“某个对象具有比较的能力”,可以等同于“给Duck的每个子类继承各自的Flyable接口”,也就是说,归根结底,是这个类具有比较的能力。

然后Comparator是个名词“比较器”,这也就意味着,你可以实现我,但是,你本身也必须是个比较器!可以等同于“FlyNoWay实现了FlyBehavior”。

TreeMap底层采用红黑树实现,里面所有的key-value对都是按照key的值排序的。而这个排序的规则来自于其内部成员Comparator,而这个Comparator在外部定义,TreeMap内部无需编写任何比较规则,只需拿外部Comparator对象来用即可。如果要修改TreeMap的比较规则,只需在外部写一个写的Comparator实现类,然后调用构造方法

TreeMap(Comparatorsuper K> comparator)

传入即可。

而如果我们让TreeMap实现Comparable接口,那就意味着TreeMap本身具有比较的能力,我们需要在TreeMap内部重写Comparable接口的compareTo()方法,如果我们要修改我们的比较规则,我们能直接修改TreeMap的底层代码吗?当然不能!

所以如果我们想在不改变对象自身的情况下改变对象的行为,我们就要使用策略设计模式,定义外部类去修改!

另外,Comparator接口中是compare(a,b),因为是外部定义的比较器类,所以要传入两个值到比较器,Comparable接口中是compareTo(a),是直接那对象本身与另外一个值比较,所以只需传入一个值。

另外,Collections类里面的很多方法,如:

    static  super T>> void sort(List list)

我们可以看出一个问题:虽然对于TreeMap,TreeSet,我们需要修改比较规则,使用策略设计模式,但是如果我们对一个List中所有的元素T进行排序的时候,我们只需T是一个内部定义好比较方法的对象即可,所以这里用的是Comparable。

最后

还是希望自己能坚持吧。
2016.9.29

你可能感兴趣的:(设计模式)