设计模式之策略(Strategy)模式

什么是策略模式?

  策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法。

  • 定义了一族算法(业务规则);
  • 封装了每个算法;
  • 这族的算法可互换代替(interchangeable)。

  直接用的书中的例子,感觉还可以,可能会重构此文。
  我们通过一个鸭子应用来了解一下这个模式。。。


模拟鸭子应用中的问题

  假设现在你的公司做了一套模拟鸭子游戏:SimDuck。游戏中出现了各种鸭子,一边游泳嬉水,一边呱呱叫。该系统内部设计使用了标准的OO技术,设计了一个鸭子超类(SuperClass),并让各种鸭子继承此超类。


  过了几个月,行业间其他公司冒出了很多鸭子游戏,公司为了将竞争对手抛在后头,需要将该游戏加个功能:让鸭子飞起来。
:这还不简单,我在Duck类加个fly()方法,然后所有的鸭子都会继承fly(),不就搞定了?

但是,可怕的问题发生了!!!

  你忽略了一件事,并非所有种类的鸭子都会飞,比如橡皮鸭。当我们为Duck超类加上了新的行为,会使某些子类也具有这个不恰当的行为。我们对代码做的局部修改,影响层面可能不只是局部。
  继承不是可以重写吗?我们将fly()方法覆盖重写不就行了?可是,如果又有个木头鸭呢,它不会飞也不会叫,我们又要覆盖重写?出现更多的其他鸭子的,别的鸭子可能嘎嘎叫呢?还继续覆盖重写?从这里可以看出,利用继承来提供Duck的行为,会出现下列问题:

  • 代码在多个子类中重复;
  • 难以得知所有鸭子的全部行为;
  • 运行时的行为不容易改变;
  • 改一发而动全身,造成其他鸭子不想要的改变。

利用接口如何?

  我们可以把fly()取出来,放进一个Flyable接口中,这样一来,只有会飞的鸭子才实现此接口。同样我们也可以设置一个Quackable接口让会叫的鸭子实现该接口。


  虽然接口可以解决一部分问题(不会飞的橡皮鸭和不会叫的木头鸭),但是却造成代码无法复用,这只是治标却不治本。
  现在我们知道使用继承有一些缺失,因为改变鸭子的行为会影响所有种类的鸭子,这行不通。用接口一开始还可以,解决了问题,但接口没有具体的代码实现,所以继承接口的方式无法使代码能复用。这意味着:无论何时你需要修改某个行为,你必须得往下追踪并修改每一个定义此行为的类,一不小心,可能造成新的错误。
  幸运地,有一个设计原则,正适用于此状况:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
  换句话说,如果每次新的需求一来,都会变化到某方面的代码,那么你就可以确定,这部分的代码需要被抽出来,和其他闻风不动的代码有所区隔。
  下面是这个原则的另一个思考方式:「把会变化的部分取出并封装起来,以便以后可以轻易地扩充此部分,而不影响不需要变化的其他部分」。
  这样的概念很简单,几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让「系统中的某部分改变不会影响其他部分」。代码变化之后,出其不意的部分变得很少,系统变得更有弹性。

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

  现在,为了要分开「变化和不会变化的部分」,我们准备建立两组类(完全远离 D u c k类),一个是fly相关的,一个是quack相关的,每一组类将实现各自的动作。
  我们知道Duck类内的fly ( )和quack( )会随着鸭子的不同而改变。
为了要把这两个行为从Duck类中分开,我们将把它们自 Duck类中取出,建立一组新类代表每个行为。

如何设计类实现飞行和呱呱叫的行为?

  我们希望一切能有弹性,毕竟,正是因为一开始的鸭子行为没有弹性,才让我们走上现在这条路。我们还想能够「指定」行为到鸭子的实例,比方说,想要产生绿头鸭实例,并指定特定「类型」的飞行行为给它。干脆顺便让鸭子的行为可以动态地改变好了。换句话说,我们应该在鸭子类中包含设定行为的方法,就可以在「运行时」动态地「改变」绿头鸭的飞行行为。
  有了这些目标要达成,接着看看第二个设计原则针对接口编程,而不是针对实现编程。
  我 们 利 用 接 口 代 表 每 个 行 为 , 比 方 说 ,FlyBehaviorQuackBehavior,而行为的每个实现都必须实现这些接口之一。
  所以这次鸭子类不会负责实现 FlyingQuacking 接口,反而是由其他类专门实现FlyBehaviorQuackBehavior,这就称为
「行为」类。由行为类实现行为接口,而不是由Duck类实现行为接口。
  这样的作法迥异于以往,以前的作法是:行为是继承 D u c k超类的具体实现而来,或是继承某个接口并由子类自行实现而来。这两种作法都是依赖于「实现」,我们被实现绑得死死的,没办法更改行为(除非写更多代码)。
  在我们的新设计中,鸭子的子类将使用接口( FlyBehaviorQuackBehavior)所表示的行为,所以实际的「实现」不会被绑死在鸭子的子类中。(换句话说,特定的实现代码位于实现FlyBehaviorQuackBehavior的特定类中)。

实现鸭子的行为

整合鸭子的行为

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

  作法是这样的:

①首 先 , 在 鸭 子 中 「 加 入 两 个 实 例 变 量 」 , 分 别 为 FlyBehaviorQuackBehavior,声明为接口类型(而不是具体类实现类型),每个变量会利用多态的方式在运行时引用正确的行为类型(例如:FlyWithWings)。我们也必须将 D u c k类与其所有子类中的 f l y ( )与 q u a c k ( )移除,因为这些行为已经被搬移到FlyBehaviorQuackBehavior类中了。
我们用 performFly()performQuack()取代 Duck类中的fly()quack()
稍后你就知道为什么。

②父类鸭子的代码:

public class Duck{
    FlyBehavior flyBehavior;
    QuackBehavior quackBehavior;

    public void performFly(){
        flyBehavior.fly();
    }
    public void performQuack(){
        quackBehavior.quack();
    }
}

③现 在 来 关 心 「 如 何 设 定flyBehaviorquackBehavior的实例变量」。看看 RedDuck类:

public class RedDuck extends Duck{
     public RedDuck(){
     flyBehavior = new GaGaQuack();
     quackBehavior = new FlyWithWings();
     }
}

  所以,红头鸭会嘎嘎叫,而不是吱吱叫,或叫不出声,红头鸭还会用翅膀飞。这是怎么做到的呢?当RedDuck实例化时,它的构造器会把继承来的flyBehaviorquackBehavior实例变量初始化为相应接口的具体实现类。
  可是这样还是有个问题:红头鸭创建时就被定义了飞和叫的行为,这是不是太过于死板,不够emmm,灵活?是的,如果红头鸭病了呢,嗓子叫不出来,变成了哑巴呢。。。它不就不会叫了呀。。。却是会发生这种情况的呀!没事,我们还有解决办法:动态设定行为

动态设定行为

①在Duck类中,加入下面的方法:

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

    public void setQuackBehavior(QuackBehavior quackBehavior) {
        this.quackBehavior = quackBehavior;
    }

  从此以后,我们就可以随时调用这两个方法改变鸭子的行为。比如将前面变成哑巴的鸭子的叫声变成不会叫。。。
②现在我们制造一个新的鸭子模型鸭(一开始它是不会飞的):

public class ModelDuck extends Duck {
    public ModelDuck() {
        flyBehavior = new FlyNoWay();
        quackBehavior = new Quack();
    }
    public void display() {
        System.out.println("我是一个模型鸭");
    }
}

③建立一个新的FlyBehavior的实现类:

public class FlyRocketPowered implements FlyBehavior {
    public void fly() {
        System.out.println("我用火箭飞了起来");
    }
}

//FlyBehavior 接口
public interface FlyBehavior {
    public void fly();
}

③创建模型鸭,并设置其飞行行为带上火箭:

        Duck model = new ModelDuck();
        model.performFly();
        model.setFlyBehavior(new FlyRocketPowered());
        model.performFly(); 

封装行为的大局观

  好,我们已经深入鸭子模拟器的设计,该是将头探出水面,呼吸空气的时候了。现在就来看看整体的格局。
  下面是整个重新设计后的类结构,你所期望的一切都有:鸭子继承Duck
  飞行行为实现FlyBehavior 接口,呱呱叫行为实现QuackBehavior接口。
也 请 注 意 , 我 们 描 述 事 情 的 方 式 也 稍 有 改 变 。 不 再 把 鸭 子 的 行 为 说成「一组行为」,我们开始把行为想成是「一族算法」。想想看,在游戏设计中,算法代表鸭子能做的事(不同的叫法和飞行法),这样的作法也能用于用一群类计算不同国家的销售税金。
请特别注意类之间的『关 系』。拿一枝笔 ,把下面图形中的每个箭头标上适当的关系,关系可以是is-a(是一个)has - a(有一个)implements(实现)

『有一个』可能比『是一个』更好

  『 有 一 个 』 关 系 相 当 有 趣 : 每 一 鸭 子 都 有 一 个飞的行为和叫的行为,让鸭子将飞行和呱呱叫委托它们代为处理。
当你将两个类结合起来使用,如同本例一般,这就是组合(composition )。这种作法和『继承』不同的地方在于,鸭子的行为不是继承而来,而是和适当的行为对象『组合』而来。
这是一个很重要的技巧。其实是使用了我们的第三个设计原则:多用组合,少用继承。

  如你所见,使用组合建立系统具有很大的弹性,不仅可将 算 法 族 封 装 成 类 , 更 可 以 『 在 运 行 时 动 态 地 改 变 行为』,只要组合的行为对象,符合正确的接口标准即可。
组合用在『许多』设计模式中,它有优点也有缺点。

没错,这就是策略(Strategy)模式

  本章使用到了3个设计原则:

  • 分离程序中变与不变的部分
  • 针对接口编程,不针对实现编程
  • 多用组合,少用继承

参考资料

《HeadFirst设计模式》第一章

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

推荐阅读更多精彩内容