策略模式

56 阅读7分钟

策略,古时也称“计”,指为了达成某个目标而提前策划好的方案。但计划往往不如变化快,当目标突变或者周遭情况不允许实施某方案的时候,我们就得临时变更方案。策略模式(Strategy)强调的是行为的灵活切换,比如一个类的多个方法有着类似的行为接口,可以将它们抽离出来作为一系列策略类,在运行时灵活对接,变更其算法策略,以适应不同的场景。

例如我们经常在电影中看到,特工在执行任务时总要准备好几套方案以应对突如其来的变化。实施过程中由于情况突变而导致预案无法继续实施A计划时,马上更换为B计划,以另一种行为方式达成目标。所以说提前策划非常重要,而随机应变的能力更是不可或缺,系统需要时刻确保灵活性、机动性才能立于不败之地。

“顽固不化”的系统

一个设计优秀的系统,绝不能来回更改底层代码,而是要站在高层抽象的角度构筑一套相对固化的模式,并能使新加入的代码以实现类的方式接入系统,让系统功能得到无限的算法扩展,以适应用户需求的多样性。

我们先从一个反例开始,了解一个有设计缺陷的系统。流行于20世纪80年代的便携式掌上游戏机的系统设计非常简单,最常见的是“俄罗斯方块”游戏机。这种游戏机只能玩一款游戏,所以玩家逐渐减少,最终退出了市场。这是一种嵌入式系统设计,主机不包含任何操作系统。制造商只是简单地将软件固化在游戏机芯片中,造成游戏(软件)与游戏机(硬件)的强耦合。玩家要想换个游戏就得再购买一台游戏机,严重缺乏可扩展性。

与这种耦合性极高的系统设计类似的还有计算器,它只能用于简单的数学运算,算法功能到此为止,没有后续扩展的可能性。我们就以计算器为例,探讨一下这种设计存在的问题。假设计算器可以进行加减法运算

package strategy;

public class Calculator {
   public int add(int a,int b){
      return a+b;
   }
   public int sub(int a,int b){
      return a-b;
   }
}

我们分别为计算器类定义了加减法,看上去简单易懂。然而随着算法的不断增加,如乘法、除法、乘方、开方等,我们不得不把机器拆开,然后对代码进行修改。当然,对计算器这种嵌入式系统来说,这么做也无可厚非,毕竟其功能有限且相对固定,但若换作一个庞大的系统,反复的代码修改会让系统维护变成灾难,最终大量的方法被堆积在同一个类中,臃肿不堪。

游戏卡带

通过分析和对比代码中的计算器类,我们不难发现,不管是何种算法(加、减、乘、除等),都属于运算。从外部来看,它们都是基于对两个数字型入参的运算接口,并能返回数字型的运算结果。既然如此,不如把这些算法抽离出来,使它们独立于计算器,并各自封装,让一种算法对应一个类,要使用哪种算法时将其接入即可,如此算法扩展便得到了保证。这种设计上的演变不正类似于从嵌入式掌上游戏机到可插卡式游戏机的演变吗?不同种类的游戏卡带就像各种独立的策略类,只要为游戏机更换不同的卡带就能带来全新体验,这也是这种设计思想可以一直延续至今的原因(想象一下操作系统与应用软件的关系)。

策略与系统分离的设计看起来非常灵活,基于这种设计思想,我们对计算器类进行重构。首先要对一系列的算法进行接口抽象,也就是为所有的算法(加法、减法或者即将加入的其他算法)定义一个统一的算法策略接口

package strategy;

public interface Strategy {
   public int calculate(int a,int b);//操作数a,操作数b
}

为保持简单,我们假设算法策略接口的参数与返回结果都是整数,接收参数为操作数a与被操作数b,通过运算后返回结果。算法策略接口定义完毕,顺理成章,我们接着分别定义加法策略、减法策略对应的实现类

package strategy;

public class Addition implements Strategy {
   @Override
   public int calculate(int a, int b) {
      return a+b;
   }
}
package strategy;

public class Subtraction implements Strategy{
   @Override
   public int calculate(int a, int b) {
      return a-b;
   }
}
package strategy;

public class Calculator {
   private Strategy strategy;//算法策略接口
   public void setStrategy(Strategy strategy){
      //注入算法策略
      this.strategy = strategy;
   }
   public int getResult(int a,int b){
      return this.strategy.calculate(a,b);//返回具体策略的运算结果
   }
}

计算器类里已经不存在具体的加减法运算实现了,取而代之的是对算法策略接口strategy的计算方法calculate()的调用,而具体使用的是哪种算法策略则完全取决于第5行的setStrategy()方法。它可以将具体的算法策略注入进来,所以对于获取结果方法getResult(),注入不同的算法策略将会得到不同的响应结果

package strategy;

public class Client {
   public static void main(String[] args) {
      Calculator calculator = new Calculator();//实例化计算器
      calculator.setStrategy(new Addition());//注入加法策略
      System.out.println(calculator.getResult(1,1));
      calculator.setStrategy(new Subtraction());
      System.out.println(calculator.getResult(1,1));
   }
}

显而易见,通过重构的计算器类变得非常灵活,不管进行哪种运算,我们只需注入相应的算法策略即可得到结果。此外,今后若要进行功能扩展,只需要新增兼容策略接口的算法策略类(如乘法、除法等),这与插卡式游戏机的策略如出一辙,我们不必再对系统做任何修改便可实现功能的无限扩展。

总结:即插即用

策略模式让策略与系统环境彻底解耦,通过对算法策略的抽象、拆分,再拼装、接入外设,使系统行为的可塑性得到了增强。策略接口的引入也让各种策略实现彻底解放,最终实现算法分立,即插即用。

策略模式的各角色定义如下。

trategy(策略接口):定义通用的策略规范标准,包含在系统环境中并声明策略接口标准。

ConcreteStrategyA、ConcreteStrategyB、ConcreteStrategyC……(策略实现):实现了策略接口的策略实现类,可以有多种不同的策略实现,但都得符合策略接口定义的规范。

Context(系统环境):包含策略接口的系统环境,对外提供更换策略实现的方法setStrategy()以及执行策略的方法executeStrategy(),其本身并不关心执行的是哪种策略实现。对应本章例程中的计算机主机类Computer。

变化是世界的常态,唯一不变的就是变化本身。拥有顺势而为、随机应变的能力才能立于不败之地。策略模式的运用能让系统的应变能力得到提升,适应随时变化的需求。接口的巧妙运用让一系列的策略可以脱离系统而单独存在,使系统拥有更灵活、更强大的“可插拔”扩展功能。

GO版本代码

package strategy

type strategy interface {
    Calculate(a, b int) int
}

type Addition struct {
}

func (s Addition) Calculate(a, b int) int {
    return a + b
}

type Subtraction struct {
}

func (s Subtraction) Calculate(a, b int) int {
    return a - b
}

type Calculator struct {
    strategy
}

func (c *Calculator) SetStrategy(s strategy) {
    c.strategy = s
}

func (c Calculator) GetResult(a, b int) int {
    return c.Calculate(a, b)
}
package strategy

import (
    "fmt"
    "testing"
)

func TestStrategy(t *testing.T) {
    calculator := Calculator{}
    calculator.SetStrategy(Addition{})
    fmt.Println(calculator.GetResult(1, 1))
    calculator.SetStrategy(Subtraction{})
    fmt.Println(calculator.GetResult(1, 1))
}