策略(Strategy)模式

策略模式属于对象行为型模式,定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。本文详细介绍了策略模式的意图、动机、适用性、结构、参与者、协作、效果以及实现,通过Java代码展示了如何使用策略模式实现不同算法,如换行策略。同时,文章提到了策略模式与桥接模式的区别,并引用了相关设计原则和实际应用案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

策略(Strategy)模式

隶属类别——对象行为型模式


1. 意图

定义一系列算法(即算法组),分别封装起来,让他们之间可以互相替换,此模式可以让算法独立于它的客户变化

2. 别名

Policy(政策)

3. 动机

有许多算法可对一个正文流进行分析,将这些算法硬编(硬编码在计算机程序或文本编辑中,硬编码是指将可变变量用一个固定值来代替的方法。 用这种方法编译后,如果以后需要更改此变量就非常困难了。 大部分程序语言里,可以将一个固定数值定义为一个标记,然后用这个特殊标记来取代变量名称.软编码是一种设计,而硬编码不过一种具体的实现)进入使用他们的类是不可取的其原因如下:

  • 需要换行的功能的客户程序如果直接包含换行算法代码的话将会变得复杂,这使得客户程序庞大而且难以维护,尤其当需要支持多种换行算法时问题会更加严重。
  • 不同的时候需要不同的算法,我们不想支持我们并不适用的换行算法
  • 当换行功能是客户程序的一个难以分割的成分是,增加新的换行算法或者改变现有算法将十分困难。

我们可以定义一个类来封装不同的换行算法,从而避免这些问题。一个以这种算法封装的算法成为策略(strategy)如下图所示:

在这里插入图片描述

假设一个Comoisiton(means 排版)类负责维护和更新一个正文浏览程序中显示的中文换行。换行策略不是Compostior类实现的,而是抽象的Compositor类的子类独立实现的,Compositor各个子类实现不同的换行策略:

  • SimpleCompositor实现一个简单的策略。它一次决定一个换行位置。

  • TeXCompositor实现查找换行位置的TEX算法。这个策略尽量全局地优化换行,也就是,一次处理一段文字的换行。

  • ArrayCompositor实现一个策略,该策略使得每一行都含有一个固定数目的项例如,用于对一系列的图标进行分析

    Composition持有对Compositor的一个引用,一旦Composition重新格式化它的正文,它就将这个职责转发给它的Compositor对象.Composition的客户指定应该使用哪一种Compositor的方式是直接将它想要的Compositor装入Composition中。

4. 适用性

当存在以下情况时使用Strategy模式

  • 许多相关的类仅仅是行为有异。“策略”提供了一种用多个行为中的一个行为来配置一个类的方法。
  • 需要使用一个算法的不通辩题。例如,你可能会定义一些反映不通的鸣叫的方式或者飞行方式,当这些变体实现为一个算法的类层次时,可以使用策略模式
  • 算法使用客户不应该知道的数据,可使用策略模式以避免暴露复杂的,与算法相关的数据结构
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,将相关的条件分支移入它们各自的strategy类以代替这些条件语句。

5. 结构

在这里插入图片描述

6. 参与者

  • Strategy(策略,如FlyBehavior,QuackBehavior)

    ——定义所有支持的算法的公共接口。Context使用这个接口来调用某ConcreteStrategy定义的算法

  • ConcreteStrategy(具体策略,如FlyWithWing,FlyNoWay,Quack,Squeak,MuteSqueak)

    ——以Strategy接口实现某具体算法

  • Context(上下文,如Duck)

    • 用一个ConcreteStrategy对象来完成具体操作
    • 维持一个队Strategy对象的引用
    • 定义一个接口(这个接口不一定是真正的接口,可理解为入口把)让Strategy访问它的数据

7. 协作

  • Strategy和Context相互作用以实现指定的算法。当算法被调用时,Context可以将该算法所需要的所有数据都传递给Strategy(例如在ConcreteStrategy使用Context中定义的方法比如getData(){}去获取所需要的数据)。或者,Context可以将自身作为一个参数传递给Strategy(如Duck鸭子中的构造方法,有点将自身传递给Strategy的意思),总之让Context的必要的数据传递给Strategy比如ConcreteStrategyA()为加法,ConcreteStrategyB为减法,ConcreteStrategyC为乘法,将两个数据传递给Strategy,总是就是让State和Context互相作用以决定具体的算法。
  • Context将他的客户请求转发给它的Strategy(委托).客户通常创建并传递一个ConcreteStrategy对象给该Context(就像上述的Duck构造进行Behavior的初始化和SetFlyBehavior()和SetQuackBehavior());这样客户仅与Context交互。通常有一系列的ConcreteStrategy类可供客户选择。

8. 效果

Strategy模式有下面一些优点和缺点

优点:

  • 1.相关算法系列 Strategy类层级为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法的公共功能。

  • 2.一个代替继承的方法 继承提供了另一种支持多种算法或行为的方法。你可以直接生成一个Context类的子类,从而给它以不同的行为。但这会将行为硬行编制到Context中,而将算法的实现和Context的实现混合起来,从而使Context难以理解、难以维护和难以扩展,而且还不能动态地改变算法。最后你得到一堆相关的类,它们之间的唯一差别是它们所使用的算法或者行为。将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。

  • 3.消除了一些条件语句 Strategy模式提供了用条件语句选择所需要的行为以外的另一种选择。当不同的行为堆砌在一个类中是,很难避免使用条件语句来选择合适的行为。将行为封装在一个个独立的Strategy类中消除了这些条件语句

    例如,不用Strategy,正文换行的代码可能是像下面这样

    // 可能这么写有问题,Java中好像不能这么判断对象,要使用instanceof关键字。
    void performFly(Duck duck) {
        switch (duck) {
            case MillardDuck: millarDuck.fly(); break;
            case RedHeadDuck: redHeadDuck.fly(); break;
            //... 
        }
    }
    

    采用Strategy模式将换行的任务委托给一个Strategy对象从而消除了这些case语句:

    void performFly() {
        flyBehavior.fly();
    }
    
  • 4.实现的选择 Strategy模式可以提供相同行为的不同实现。客户可以根据不同时间/空间权衡取舍要求从不同的策略中进行选择。

缺点:

  • 1.客户必须了解不同的Strategy 一个客户要选择一个合适的Strategy就必须知道这些Strategy到底有何不同。此时可能不得不向客户暴露具体的实现问题。因此,仅当这些不同的行为变体与客户相关的行为是,才需要使用Strategy模式。
  • 2.Strategy个Context之间的通信开销 无论各个ConcreteStrategy实现的算法是简单还是复杂,它们都共享Strategy定义的接口.简单的ConcreteStrategy不会用到所有通过这个接口传递给它们的信息;这就意味着有事Context会创建和初始化一些它们不会用到的参数。如果存在这样的问题,那么将需要在Strategy和Context之间紧密的耦合。
  • 3.增加对象的数目 Strategy增加了一个应用中的对象的数目。有时你可以将Strategy实现为可供各Context共享的无状态对象来减少这一开销。其余任何的状态都由Context维护。Context在每一次对Strategy对象的请求中中将这些状态传递过去。共享的Strategy不应在各次调用之间维护状态。Flyweight模式将更详细地描述这种方法。

9. 实现

考虑下面的实现问题:

  • 1.定义Strategy和Context访问入口(接口泛指包含抽象方法的类或者接口) Strategy和Context访问入口必须使得ConcreteStrategy可以访问它所需要的Context中的任何数据,反之亦然。

    • 一种办法是让Context将数据放在参数中传递给Strategy操作——也就是说,将数据发送给Strategy.这使得Strategy和Context解耦。但另一方面,Context可能发送一些Strategy不需要的的数据。
    • 另一种办法是让Context将自身作为参数传递给Strategy,该Strategy再显式地向该Context请求数据。或者,Strategy可以存储一个它的Context的一个引用,这样根本不需要传递任何东西。这两种情况下,Strategy都可以请求到它所需要的数据。但现在Context必须对它的数据定义一个更为精细的接口,这将Strategy和Context更紧密的耦合在一起。
  • 2 将Strategy作为模板参数 GOF的《Deign Pattern》中原话是,在C++中可以利用模板机制制一个Strategy来配置一个Strategy来配置一个类。然而这种技术仅当下面条件满足下面条件时才可以使用:

      1. 可以在编译时选择Strategy。
      1. 它不需在运行时改变。

      使用模板不在需要定义给Strategy定义接口的抽象类,把Strategy作为一个模板参数也使得可以将一个Strategy和它的Context静态地绑定在一起,从而提高效率。

    ——我查阅了部分资料,翻译了一下C++代码,大抵这个意思相当于Java中的多态特性,就是持有父类引用,但是可以实例化为子类。就像(flyBehavior = new FlyWithWing();),好像就是Duck类那样的操作持有Strategy引用。

  • 3.使Strategy对象成为可选的 如果即使在不使用额外的Strategy对象的情况下,Context也还有意义的话,那么它还可以被简化。Context在访问某Strategy前先检查它是否存在,如果有,那么有,那么就先使用它;如果没有,那么Context执行缺省的行为,这种方法的好处是客户根本不需要处理Strategy对象,除非它们不喜欢缺省的行为。

10. 代码示例

首先是两个策略接口:

Stategy——FlyBehavior.java & QuackBehavior.java

FlyBehavior接口:FlyBehavior.java

public interface FlyBehavior {
	void fly();
}

QuackBehavior接口: QuackBehavior.java

public interface QuackBehavior {
	void quack();
}

接下来是它们具体实现子类:

ConcreteStrategy——FlyWithWings.java & FlyNoWay.java & FlyRocketPowered.java & Quack.java & Mute.java & Squeak.java

飞行行为子类:

FlyWithWings.java

public class FlyWithWings implements FlyBehavior{
	
	@Override
	public void fly() {
		System.out.println("I'm flying");
	}
}

FlyNoWay.java

public class FlyNoWay implements FlyBehavior{
	
	@Override
	public void fly() {
		System.out.println("I can't fly");
	}
}

FlyRocketPowered.java

public class FlyRocketPowered implements FlyBehavior{
	
	@Override
	public void fly() {
		System.out.println("I'm flying with a rocket");
	}
}

嘎嘎叫行为子类:

Quack.java

public class Quack implements QuackBehavior{
	
	@Override
	public void quack() {
		System.out.println("Quack");
	}
}

Squeak.java

public class Squeak implements QuackBehavior{
	
	@Override
	public void quack() {
		System.out.println("Squeak");
	}
}

MuteQuack.java

public class MuteQuack implements QuackBehavior{
	
	@Override
	public void quack() {
		System.out.println("<<Silence>>");
	}
}

接下是Context——Duck.java

Duck.java

public abstract class Duck { 
   
	FlyBehavior flyBehavior;
	QuackBehavior quackBehavior;
		
	public Duck() {
		flyBehavior = new FlyWithWings();
		quackBehavior = new Quack();
	}
	 
	public void performQuack() {
		quackBehavior.quack();
	}
	
	public void performFly() {
		flyBehavior.fly();
	}
	
	public void swim() {
		System.out.println("All ducks float,even decoys");
	}
	
	public abstract void display();
	
	public void setFlyBehavior(FlyBehavior flyBehavior) {
		this.flyBehavior = flyBehavior;
	}
	
	public void setQuackBehavior(QuackBehavior quackBehavior) {
		this.quackBehavior = quackBehavior;
	}
	
}

这里我在Duck构造方法中默认初始化了flyBehavior和quackBehavior行为,当然你可以不初始化就像上述实现中提到使Strategy对象成为可选的那样让Context执行缺省操作。并且也把Duck弄成了抽象类,因为可能子类中存在display()不同的情况,严格意义上讲,标准的策略模式这里Duck可以设置为具体类。

以下是四个实现子类:

MallardDuck.java

public class MailardDuck extends Duck{

	@Override
	public void display() {
		System.out.println("I'm a real Mailard duck");
	}
}

RedHeadDuck.java

public class RedHeadDuck extends Duck{
	
	@Override
	public void display() {
		System.out.println("I'm a read head duck!");
	}
}

RubberDuck.java

public class RubberDuck extends Duck{

	@Override
	public void display() {
		System.out.println("I'm a  rubber duck");
	}
}

DecoyDuck.java

最后一个子类呢,我在其构造器中对flyBehavior和quackBehavior重新进行了初始化,这样其实更符合逻辑,这样也行。

public class DecoyDuck extends Duck {
	
	public DecoyDuck() {
		flyBehavior = new FlyNoWay();
		quackBehavior = new MuteQuack();
	}
	
	@Override
	public void display() {
		System.out.println("I'm a duck Decoy");
	}
}

最后是测试类Client:

MiniDuckSImulator.java

public class MiniDuckSImulator {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Duck mailard = new MailardDuck();
		mailard.performFly();
		mailard.performQuack();

		System.out.println("--------------------");
		Duck decoy = new DecoyDuck();
		decoy.performFly();
		decoy.performQuack();
		decoy.setFlyBehavior(new FlyRocketPowered());
		decoy.performFly();
	}
}

以及对应的结果:

I'm flying
Quack
--------------------
I can't fly
<<Silence>>
I'm flying with a rocket


Decoy的行为在动态替换改变,之前无法飞行,之后却可以用火箭飞翔!

Context类(如Duck类)的方法需要经过仔细设计,以支持子类可能实现的所有Strategy方法.你不希望在生成一个新的子类不得不修改父类方法。因为修改父类这个方法会影响到其它已有的子类。一般来说,Strategy和Context的接口决定了该模式能在多大程度上达到既定的目的。

其实这里这个四个子类Duck完全可以不要, 这样容易让人和桥接模式产生混淆!!!Duck中已经完全实现了功能。但也可以要,正好体现Strategy的优点之一——相关算法系列,子类也的确是可以直接重用这些算法!

11. 已知应用

ET++[WGM88]和InterViews都使用Strategy来封装不同的换行算法。

在用于编译器代码优化的RTK系统[JML92中],Strategy定义了不同的寄存器分配方案(RegisterAllocator)和指令调度策略(RISCscheduler,CISCscheduler).这就为在不同的目标机器结构上实现优化程序提供了所需要的灵活性。

12. 相关模式

桥接(Brige)模式: 个人觉得觉得桥接模式和策略模式很相似,相同点是都是利用委托,不同点事桥接是利用构造器传入具体的引用,而策略是其它其他方法且利用两个接口去创建一个“桥”,而且桥那边实现的是一组行为,委托也是一组行为。

13. 设计原则口袋

  • 找出应用中可能需要变化的地方,把它们和那些不需要变化的代码独立开来。
  • 针对接口编程,不是针对实现编程
  • 多用组合,少用继承

14. 参考文献

《HeadFirst设计模式》

《设计模式:可复用面向对象软件的基础》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值