Skip to content

策略模式

Williams_Z edited this page Aug 5, 2018 · 2 revisions

俗话说:条条大路通罗马。在很多情况下,实现某个目标的途径不止一条,例如我们在外出旅游时可以选择多种不同的出行方式,如骑自行车、坐汽车、坐火车或者坐飞机,可根据实际情况(目的地、旅游预算、旅游时间等)来选择一种最适合的出行方式。即怎么来不重要,人到达目的地就行了!

在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。即为了适应算法灵活性而产生的设计模式——策略模式 

在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。

策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可


常规思路:

常规思路下我们是将一种行为写成一个类方法,比如计算器类中有加、减、乘、除四种方法,而策略模式则是将每一种算法都写成一个类,然后动态的选择使用哪一个算法。

实际代码很简单,具体如下:

  • Calculate 实现类--加法和减法:

    public class Calculator {
        public int add(int a, int b){
            return  a+b;
        }
    
        public int subtract(int a,int b){
            return a-b;
        }
    }
  • main 方法:

    public static void main(String[] args) {
            Calculator calculator = new Calculator();
            int result =calculator.add(1,2);
            System.out.println(result);
        }

策略模式:

策略模式

从上图可以看到,我们将操作封装到类中,他们实现了同一个接口,然后在 Context 中调用.

add() 方法封装到 OperationA类中,subtract() 方法封装到 OperationB() 类中.即为加法和减法分别创建了一个类,实现一个共同的接口.

**类比交通出行就是:我们可以选择不同的运算规则去得出最终结果,可以加,可以减,可以乘,可以除,(例如最终结果为5,那么可以是 3+2=5,也可以是8-3=5)选用什么运算规则不重要,重要的是得到我们需要的最终结果。**我们可以根据自己的喜好决定使用什么运算规则,选用不同的运算规则就相当于选择了不同的策略,所以这个模式叫策略模式。

实际代码也很简单,具体如下:

  • Operation 接口:

    public interface Operation {
        public int doOperation(int a,int b);
    }
  • 两个实现类——加法和减法

    public class OperationAdd implements Operation{
        @Override
        public int doOperation(int a, int b) {
            return a+b;
        }
    }
    public class OperationSubtract implements Operation {
        @Override
        public int doOperation(int a, int b) {
            return a-b;
        }
    }
  • 计算类

    public class Calculator {
        private Operation operation;
    
        public void setOperation(Operation operation) {
            this.operation = operation;
        }
    
        public int doOperation(int a, int b){
            return operation.doOperation(a,b);
    
        }
    }
  • main 方法

    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        calculator.setOperation(new OperationAdd());
        int result = calculator.doOperation(3,4);
        System.out.println(result);
    }

用策略模式使用计算器类时,如果要进行加法运算,就 new 一个加法类传入,减法也是同理。因此要改动的代码永远只有一行,即:calculator.setOperation(new OperationAdd());,只要改变setOperation里传入的参数即可。

看到这里,相信大家一定会有疑惑,为什么要把加、减、乘、除四则运算分别封装到类中?直接使用常规思路在 Calculator 中写 add()、subtract() 等方法不是更方便吗?

甚至如果要添加其他的运算方法,每次都要创建一个类,反而更麻烦。

的确用了策略模式之后代码比普通写法多了一些,但是这里假设一种场景:

假设把写好的计算器代码打包好作为一个库发布出去给其他人用,其他人发现你的计算器中只有加、减、乘、除四个方法,而他想增加平方、开方等功能,咋办?

如果是用普通写法写的计算器,想要增加功能唯一的办法就是修改你写好的 Calculator,增加平方和开方两个 method。

可是你提供的是一个 jar 包啊,jar 包,jar...jar...jar...jar...包……

就算你提供的是源码,你希望其他人可以随意的修改你写好的代码吗?一般我们发布出去的开源框架或库都是经过千锤百炼,经过测试的代码,其他人随意修改我们的源码很容易产生不可预知的错误。

如果你用的是策略模式,那么其他人想要增加平方或开平方功能,只需要自己定义一个类实现你的 Operation 接口,然后调用 calculator.setOperation(new 平方类()); 即可。

看到这里相信你已经对策略模式有了一定的好感,甚至惊叹一声:哇,还有这种操作?

顺便提一嘴,这里很好的体现了一个设计模式的基本原则:开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。


策略模式的模型抽象

抽象模型

Strategy模式中的登场角色,在设计策略模式时要找到并区分这些角色:

  • Strategy(策略):策略(算法)的抽象类,定义统一的接口,规定每一个子类必须实现的方法。在示例程序中就是 Operation 接口
  • ConcreteStrategy(具体的策略):策略的具体实现者,可以有多个不同的(算法或规则)实现。在示例程序中就是 OperationAdd 和 OperationSubtract 这两个实现类。
  • Context(具体的策略):起着承上启下的封装作用,屏蔽上层应用对策略(算法)的直接访问,封装可能存在的变化。在示例程序中就是 Calculator 类。

下面再介绍 2个经典案例:

  • 案例1:

    实现上文的交通出行

    • Vehicle (车辆)接口:

      public interface Vehicle {
          public void arrival();
      }
    • 两个实现类--骑共享单车和开私家车

      public class SharedBicycle implements Vehicle{
          @Override
          public void arrival() {
              System.out.println("我骑共享单车去");;
          }
      }
      public class Car implements Vehicle {
      
          @Override
          public void arrival() {
              System.out.println("我开车去");
          }
      }
    • Travel 旅游类

      public class Travel {
          private Vehicle vehicle;
      
          public void setVehicle(Vehicle vehicle) {
              this.vehicle = vehicle;
          }
      
          public void advent(){
              vehicle.arrival();
          }
      }
    • main 方法

      public class test {
          public static void main(String[] args) {
              Travel travel = new Travel();
              travel.setVehicle(new Car());
              travel.advent();
          }
      }

    示例类图如下:

    示例类图

  • 案例2:

    电影票打折方案:

    Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

    ​ (1) 学生凭学生证可享受票价8折优惠;

    ​ (2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);

    ​ (3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。

    ​ (4)该系统在将来可能还要根据需要引入新的打折方式。

    常规方案:

    • MovicTicket 类

      public class MovieTicket {
          private double price;
          private String type;
      
          public MovieTicket() {
          }
      
          public MovieTicket(double price, String type) {
              this.price = price;
              this.type = type;
          }
      
          public double getPrice() {
              return price;
          }
      
          public void setPrice(double price) {
              this.price = price;
          }
      
          public String getType() {
              return type;
          }
      
          public void setType(String type) {
              this.type = type;
          }
      
          public double calculatePrice(){
              if("students".equals(type)){
                  System.out.println("学生票");
                  price = price*0.8;
      
              }
      
              if("children".equals(type) && price>=20){
                  System.out.println("儿童票");
                  price = price-10;
      
              }
      
              if("vip".equals(type)){
                  System.out.println("vip,增加积分!");
                  price = price*0.5;
      
              }
              return price;
          }
      
      }
    • main 方法

      public static void main(String[] args) {
          MovieTicket movieTicket = new MovieTicket(80,"students");
          double price = movieTicket.calculatePrice();
          System.out.println(price);
      }

    常规思路至少至少存在如下三个问题:

    (1) MovieTicket类的calculatePrice()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护

    (2) 增加新的打折算法或者对原有打折算法进行修改时必须修改MovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。

    (3) 算法的复用性差,如果在另一个系统(如商场销售管理系统)中需要重用某些打折算法,只能通过对源代码进行复制粘贴来重用,无法单独重用其中的某个或某些算法(重用较为麻烦)。

    如何解决这三个问题?导致产生这些问题的主要原因在于MovieTicket类职责过重,它将各种打折算法都定义在一个类中,这既不便于算法的重用,也不便于算法的扩展。因此我们需要对MovieTicket类进行重构,将原本庞大的MovieTicket类的职责进行分解,将算法的定义和使用分离,这就是策略模式所要解决的问题。

    策略模式:

    • Discount 接口:

      public interface Discount {
          public double calculate(double price);
      }
    • 三个实现类——ChildrenDiscount,StudentDiscount,VipDiscount

      public class ChildrenDiscount implements Discount{
      
          @Override
          public double calculate(double price) {
              System.out.println("儿童票");
              price = price - 10;
              return price;
          }
      }
      public class StudentDiscount implements Discount{
      
          @Override
          public double calculate(double price) {
              System.out.println("学生票");
              price = price * 0.8;
              return price;
          }
      }
      public class VipDiscount implements Discount{
      
          @Override
          public double calculate(double price) {
              System.out.println("Vip票");
              price = price*0.5;
              return price;
          }
      }
    • Context 类

      public class MovieTicket {
          private Discount discount;
      
          public void setDiscount(Discount discount) {
              this.discount = discount;
          }
      
          public double calculatePrice(double price){
              return this.discount.calculate(price);
      
          }
      }
    • main 方法

      public static void main(String[] args) {
          MovieTicket movieTicket = new MovieTicket();
          movieTicket.setDiscount(new StudentDiscount());
          double price = movieTicket.calculatePrice(30);
          System.out.println(price);
      }

示例类图如下:

示例类图

看完这两个案例代码,你会发现跟计算器惊人的相似,没错,所谓设计模式就是前人总结出来的武功套路,经常可以直接套用。当然也要灵活的根据实际情况进行修改,设计模式想要传达给我们的更多的是一种编程思想。策略模式的模板可于参考链接获取!


这里还有一个小窍门:

movieTicket.setDiscount(new StudentDiscount()); 在这里 new 一个学生折扣对象,如果系统在将来升级引入新的打折方式,那么其他开发者引入新的打折方式还要修改这行代码,new 开发者自定义的打折方式对象。根据开闭原则,我们不希望其他人修改我们的任何一行代码,否则拔刀相见。因此可以将打折方式的包名和类名写到配置文件中,利用 java 的反射机制动态生成打折方式对象,因此更换打折方式也只要修改配置文件即可。详情参考链接!


策略模式的优缺点:

策略模式的优点:

  • 算法(规则)可自由地切换。
  • 避免使用多重条件判断。
  • 方便拓展和增加新的算法(规则)。
  • 遵循了开闭原则,扩展性良好

策略模式的缺点:所有策略类都需要对外暴露。如果你在实际开发中使用了策略模式,一定要记得写好文档让你的伙伴们知道已有哪些策略,否则根本不知道如何使用。


参考链接: