美文网首页
设计模式之策略(Strategy)模式

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

作者: 纸中圆 | 来源:发表于2019-03-25 20:52 被阅读0次

什么是策略模式?

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

  • 定义了一族算法(业务规则);
  • 封装了每个算法;
  • 这族的算法可互换代替(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设计模式》第一章

相关文章

  • 简说设计模式之策略模式

    前言:对于设计模式基础概念可以去看[简说设计模式之设计模式概述] 一、什么是策略模式 策略(Strategy)模式...

  • 策略模式

    本文参考自: 《JAVA设计模式》之策略模式(Strategy) 1. 作用 策略模式属于对象的行为模式。其用意是...

  • 设计模式-策略模式

    设计模式-策略模式 定义 策略模式(Strategy Pattern)也叫政策模式(Policy Pattern)...

  • 设计模式[13]-策略模式-Strategy Pattern

    1.策略模式简介 策略模式(Strategy Patter)是行为型(Behavioral)设计模式,策略模式封装...

  • Java设计模式之策略模式

    Java设计模式之策略模式 简介 在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行...

  • iOS 设计模式-策略模式

    1.策略模式简介   策略模式(Strategy Pattern),是行为型模式之一(设计模式分类:https:/...

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

    1. 什么是策略模式? 策略模式 定义了算法族,分别封装起来, 让它们之间可以互相替换,此模式让算法的变化独立于使...

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

    概述 写代码时总会出很多的if…else,或者case。如果在一个条件语句中又包含了多个条件语句就会使得代码变得臃...

  • 设计模式之策略模式Strategy

    策略模式(Strategy Pattern) 一个类的行为或其算法可以在运行时更改,这种类型的设计模式属于行为型模...

  • 设计模式之策略模式 - strategy

    典型的案例 在构造PriorityQueue优先级队列时,使用 给优先级队列指定一个元素的排序规则的比较器. 例如...

网友评论

      本文标题:设计模式之策略(Strategy)模式

      本文链接:https://www.haomeiwen.com/subject/fesouqtx.html