Java 策略设计模式

欢迎来到设计模式的设计(第一篇)

在本章,你将学到为何(以及如何)利用开发人员的经验与智慧。本章结束前,我们会看到设计模式的用途与优点。再看一些关键的的OO设计原则,并通过一个实例来了解模式的用途与优点,并通过实列来了解设计模式是如何运作的。使用模式最好的方式是:把设计模式装到脑子里,在你的设计和应用中,寻找何处使用它们。以往是代码复用,现在是经验复用。

先从简单的模拟鸭子的应用做起

我们来做一款模拟鸭子的游戏给小朋友们玩,游戏中会出现各种鸭子,一边游戏戏水,一边嘎嘎叫。游戏中的技术使用了标准的OO,设计了一个鸭子超类,并让各种鸭子继承此超类。



我经常去西溪湿地国家公园跑步,有时候突然飞速的跑到鸭子跟前,吓唬它们一下。其实,鸭子每次发现我就跟兔子发现狐狸,吓得半死。没错,你猜猜鸭子怎么跑掉的,野外的鸭子都会飞,它们竟然学会了水上漂,在水上连飞带跑,我接着吼一声,它们竟然飞起来了。我要加一个飞行功能,你觉得加再哪里号?

但是,可怕的问题发生了。。。。。。
并非所有的鸭子都能够飞行,在Duck超类中加上新的行为,橡皮鸭飞了起来,没有生命的橡皮鸭飞行或许是一种特色,给它加入飞行行为显然不合适。使得某些并不适合该行为的子类也具有该行为,对代码所作的局部修改,影响层面可不止局部。

从中我们体会到为了复用(reuse)目的而使用继承,结局并不完美。

不久之后,加入了诱饵鸭(DecoyDuck),又会如何?诱饵鸭是木头假鸭,不会飞也不会叫。


在以后的维护中,每当由新的鸭子类出现,就要被迫检查并尽可能覆盖fly()和quard().....这真是无穷无尽的恶梦。

所以,需要一个更清晰的方法,让某些鸭子类型可飞或者可叫。我可以把fly从超类中取出来,放进一个Flyable接口中。这么一来,只有会飞的鸭子才实现此接口。同样的方式,也可以用来设计一个Quackable接口,因为不是所有的鸭子都叫叫。

你觉得这个设计如何?

如果你要设计,你会如何设计?



我们知道,并非所有的子类都有飞行和嘎嘎叫的方法,所以继承并不是适当的解决办法。虽然Flyable与Quackable 可以解决一部分问题,不会再有会飞的橡皮鸭,但是却造成代码无法复用,这只能算是从一个恶梦跳进另一个恶梦。甚至,在会飞的鸭子中,飞行的动作可能还有多种变化......

此时,你可能正期盼着设计模式能骑着白马来解救你离开苦难的一天。但是,如果直接告诉你答案,这又有什么乐趣呢?我们会用老方法找出一个解决之道:采用良好的OO软件设计原则。


如果有一种对代码影响最小的方式来修改软件该有多好,我们就可以花较少的事件重构代码,花更多的时间去做更酷的事情。。。。。

不管当初软件设计得多好,一段时间过后,总是需要成长和改变。否则,软件就会死亡。

驱动改变的因素有很多,找出你的应用中需要该改变代码的原因,一 一列出来。

比如:我们的用户需要新功能
我们在程序中写了个Bug,需要修复
还有很多,等等


设计原则

找出应用中可能需要变化之处,把它独立出来,不要和那些不需要变化的代码混合在一起。这是我们的第一个设计原则,以后其它的设计原则会陆续出现。

换句话说,每次新的需求以来,某方面的代码就会发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其它稳定的代码有所区分。

下面是这个原则的另一种思考方式:把会变化的部分取出并封装起来,以便以后可以轻易的改变或扩充此部分,而不影响不需要变化的部分。


现在我们知道使用继承并不能很好的解决问题,因为鸭子的行为在子类里不断的改变,并且让所有的子类都有这些行为是不恰当的。Flyable和Quackable接口一开始挺不错的,解决了问题,但是Java接口不具有实现代码,所以继承接口无法达到代码复用。

好,该是把鸭子的行为从Duck类中取出的时候了!

分开变化和不会变化的部分

从哪里开始呢?就我们目前所知,除了fly()和quack()的问题之外,Duck类还算一切正常,似乎没有特别需要经常变化和修改的地方。所以,除了某些小改变之外,我们不打算对Duck类做太多处理。

现在,为了要分开变化和不会变化的部分,我们准备建立两组类(完全远离鸭子类),一个是fly相关的,一个是quack相关的,每一组类都将实现各自的动作。比方说,我们可能有一个类实现嘎嘎叫,另一个类实现吱吱叫,还有一个类是实现安静。

我们知道Duck类内的fly()和quack()会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把它们从Duck类中取出来,建立一组新类来代表每种行为(一种行为代表一个类)

如何实现那组飞行和呱呱叫的行为的类呢?

我们希望一切能够弹性,毕竟,正式因为一开始鸭子行为没有弹性,才让我们走到现在这条路。我们还想指定行为到鸭子的实列。比方说,我们想要产生一个新的绿头鸭实列,并指定特定类型的行为实例给它。干脆让鸭子的行为可以动态改变好了。

有了这些目标要实现,接着看看第二个设计原则:

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

我们利用接口代表每个行为,比方说,FlyBehavior 与 QuackBechavior ,而行为的每个实现都将实现其中一个接口。

所以这次鸭子类不会负责实现Flying与Quacking接口,反而是由我们制造一组其它类专门实现FlyBehavior 与 QuackBechavior,这就称为 行为类。由行为类而不是Duck类来实现行为接口。

这样的作法不同以往,以前的做法是:行为来自Duck超类的具体实现,我们被实现绑的死死的,没办法更改行为。


针对接口编程,真正的意思是针对超类型(supertype)编程

针对超类型编程这句话,可以更明确的说成 变量的类型声明应该是超类型,通常是一个抽象类或者一个接口。

这里所谓的接口有多个含义,接口是一个概念,针对接口编程,关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际情况执行到真正的行为,不会绑死在超类型行为上。只要实现具体超类型的类所产生的对象,都可以指定给这个变量。这也意味着,声明类时不用理会以后执行时的真正对象类型。

实现鸭子的行为


问:用一个类代表一个行为,感觉似乎有点奇怪。类不是应该代表某种东西吗?类不是同时具备状态和行为吗?
答:再OO系统中,是的,类代表的事务一般都是既有状态(实列变量),又有方法。只是在本列中,碰巧事务是行为。但是,即使是行为,也仍然有状态和方法,例如,飞行的行为可以具有实列变量,记录飞行行为的属性(每秒翅膀拍动击下,最大飞行高度和飞行速度等)。
问:Duck是不是也该设计成一个接口?
答:在本例中,这么做并不恰当。如你所见,我们已经让一切都整合妥当,而且让Duck称为一个具体类,这样可以让衍生的特定类(例如绿头鸭)具有Duck共同的属性和方法。我们已经从Duck的继承结构中删除了变化的部分,原先的问题都已经解决了,所以不需要把Duck类设计成接口。

整合鸭子的行为

关键在于,鸭子现在会将飞行和呱呱叫的动作委托别人处理,而不是使用定义在Duck类(或子类)的呱呱叫和飞行方法。

做法是这样:
首先,在Duck类中加入两个实列变量,分别为FlyBehavior 与 QuackBehavior,声明为接口类型,每个鸭子对象都会动态设置这些变量在运行时引用正确的类型。
我们也必须将Duck类与其所有子类中的fly()与quack()删除,因为这些行为已经搬到FlyBehavior与QuackBehavior类中了。
我们用两个相似的方法performFly()和performQuack()取代Duck类中的fly()和quack()。稍后你就会知道为什么。


现在我们来实现performQuack()
package head.first.strategy;

public class Duck {

    QuackBehavior quackBehavior;
    
    public void perfomQuack() {
        quackBehavior.quack();
    }

}

很容易,是吧?想进行呱呱叫的动作,Duck对象只要叫quackBehavior对象去呱呱叫就可以了。在这部分代码中,我们不在乎quackBehavior接口的对象到底是什么,我们只关心该对象如何进行呱呱叫就够了。

package head.first.strategy;

public class MallardDuck extends Duck {
    
    public MallardDuck() {
        quackBehavior = new Quack();
        flyBehavior = new FlyWithWings();
    }

    @Override
    public void display() {
        System.out.println("我是一只绿头鸭");
    }

}

所以,绿头鸭会真的 呱呱叫 ,而不是 吱吱叫,或叫不出声。这是怎么办到的?当MallardDuck实例化时,它的构造器会把继承来的quackBehavior实列变量初始化成Quack类型的新实列。

同样的处理方式也可以用在飞行行为上:MallardDuck的构造器将flyBehavior实列变量初始化成FlyWithWings类型的实列。

测试Duck的代码

package head.first.strategy;

public abstract class Duck {

    QuackBehavior quackBehavior;
    FlyBehavior flyBehavior;
    
    public Duck() {
    }
    
    public void perfomQuack() {
        quackBehavior.quack();
    }
    
    public void perfomFly() {
        flyBehavior.fly();
    }
    
    public abstract void display();
    
    public void swim() {
        System.out.println("所有的鸭子都会游泳,包括假鸭!");
    }

}

FlyBehavior接口与两个行为实现类

package head.first.strategy;

public interface FlyBehavior {
    void fly();
}
package head.first.strategy;

public class FlyWithWings implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("煽动翅膀飞行");
    }

}

package head.first.strategy;

public class FlyNoWay implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("不会飞");

    }

}

QuackBehavior 接口及其三个实现类

package head.first.strategy;

public interface QuackBehavior {

    void quack();
}

package head.first.strategy;

public class Quack implements QuackBehavior{

    @Override
    public void quack() {
        System.out.println("呱呱叫");
    }

}
Squeak
package head.first.strategy;

public class MuteQuack implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("不出声");
    }

}

package head.first.strategy;

public class Squeak implements QuackBehavior {

    @Override
    public void quack() {
        System.out.println("吱吱叫");
    }

}

编译测试类

package head.first.strategy;

public class MiniDucksSimulator {
    
    public static void main(String[] args) {
        Duck duck=new MallardDuck();
        //这会调用MallardDuck继承来的performQuack()方法,进而委托quackBehavior对象处理。
        duck.perfomFly();
        duck.perfomQuack();
        
    }

}

运行代码:

煽动翅膀飞行
呱呱叫

动态设定行为

在Duck类中,动态增加两个新方法

//我们可以随时调用这两个方法改变鸭子的行为
    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }

从此以后,我们可以 随时 调用这两个方法改变鸭子的行为。

制造一个新的鸭子类型

package head.first.strategy;

public class ModelDuck extends Duck {

    public ModelDuck() {
        //一开始,我们的模型鸭是不会飞的
        flyBehavior = new FlyNoWay();
        quackBehavior = new MuteQuack();
    }
    
    @Override
    public void display() {
        System.out.println("我是一只模型鸭子");
    }

}

建立一个新的飞行行为

package head.first.strategy;

/**
 * 我们建立一个火箭动力飞行的行为
 */
public class FlyRocketPowered implements FlyBehavior {

    @Override
    public void fly() {
        System.out.println("火箭动力飞行");
    }

}

改变测试类

package head.first.strategy;

public class MiniDuckSimplator {

    public static void main(String[] args) {
        Duck mallard = new MallardDuck();
        mallard.perfomQuack();
        mallard.perfomFly();
        
        System.out.println("-----------------------------------");
        
        Duck model=new ModelDuck();
        model.perfomFly(); //调用此方法会被委托给flyBehavior对象,也就时FlyNoWay实例,该对象是在模型鸭
        //构造器中设置的
        model.perfomQuack();
        model.setFlyBehavior(new FlyRocketPowered()); //这回调用继承来的setter方法,把火箭动力飞行行为设定到模型鸭中。
        //哇!模型鸭突然具有了火箭动力飞行行为!
        
        model.perfomFly(); //如果成功了,就意味着可以动态改变它的飞行行为。如果把行为的实现绑死在鸭子中,就无法做到这样了。
    }

}

运行:

呱呱叫
煽动翅膀飞行
-----------------------------------
不会飞
不出声
火箭动力飞行

封装行为的大局观

我们已经深入研究了鸭子模拟器的设计,该是将头探出水面,呼吸空气的时候了。现在就来看看整体的格局。

下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck,飞行行为实现FlyBehavior接口,呱呱叫行为实现QuackBehavior接口。

也请注意,我们描述事情的方式也有所改变。不要把鸭子的行为说成是一组行为,我们开始把行为想象成一簇算法。想想看,在MiniDuckSimplator的设计中,算法代表鸭子能做的事,这样的做法也能很容易地用一群类计算不同地区的销售金额。

请特别注意类之间的关系。拿起笔,把下面图像中的每个箭头标上适当的关系,关系可以实IS-A(是一个),HAS-A(有一个)或IMPLEMENTS(实现)。

有一个可能比是一个更好

有一个关系相当有趣,每一个鸭子都有一个FlyBehavior和QuackBehavior,好将飞行和呱呱叫委托给它们代为处理。

当你将两个类结合起来使用,如同本例一般,这就是组合。这中做法和“继承”不同的地方在于,鸭子的行为不是继承而来的,而是和适当的行为对象组合来的。

这是一个很重要的技巧,其实是使用了我们的第三个设计原则:

多用组合,少用继承

如你所见,使用组合建立系统有很大的弹性,不仅可见算法族封装成类,更可以"在运行时动态改变行为",只要组合的行为对象符合正确的接口标志即可。

恭喜你学会了第一个设计模式,为了介绍这个设计模式,我们走了很长一段路。下面是此模式的正式定义:

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

模式让我们用更少的词汇做更充分的沟通。
当你用模式描述的时候,其它开发人员很容易理解你对设计的想法。

共享词汇可以帮助你的团队快速充电
对于设计模式有深入了解的团队,彼此之间对于设计模式的看法不容易产生误解。

如果设计模式这么棒,怎么没人建立相关的库呢?那样我们就不必自己动手了。

答:设计模式比库的等级更高。设计模式告诉我们如何组织类和对象以解决某种问题,而且采纳这些设计并使它们适合我们特定的应用,是我们的责任。

库和框架不也是设计模式吗?

答:库和框架提供了特定功能的实现,让我们的代码可以轻易引用和实现某种功能,但是这并不是设计模式。有些时候,库和框架用到了设计模式,这样很好,一旦你了解设计模式,能够更好的使用API.

记住,知道抽象,封装,继承和多态这些概念,并不会马上让你变成好的面向对象设计者。设计大师关心的是建立弹性的设计,可以维护,可以应付变化

你几乎快要读完了,你已经在你的设计工具箱内放进了几样工具,在我们进入第二章之前,先将这些工具一一列出。


项目地址:https://github.com/githubwwj/DesignPattern

文章参考自 Head Firest 设计模式一书

我们第二篇设计模式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350