IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    设计模式之美-课程笔记38-策略模式

    10k发表于 2023-09-17 00:00:00
    love 0

    策略模式

    如何避免冗长的if-else、switch分支判断代码?

    原理与实现

    1. 定义一族算法类,将每个算法分别封装起来,让他们可以相互替换。策略模式可以使算法独立于使用它们的客户端(使用算法的代码)。

      Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use them.

    2. 工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者。策略模式也是解耦,他解耦的是策略的定义,创建和使用这三部分。

    策略的定义
    1. 策略的定义包括一个策略接口和一组实现这个接口的策略类。
    2. 客户端基于接口而非实现编程, 可以灵活替换策略。
    public interface Strategy {
      void algorithmInterface();
    }
    
    public class ConcreteStrategyA implements Strategy {
      @Override
      public void  algorithmInterface() {
        //具体的算法...
      }
    }
    
    public class ConcreteStrategyB implements Strategy {
      @Override
      public void  algorithmInterface() {
        //具体的算法...
      }
    }
    
    策略的创建
    1. 一般通过类型来判断创建哪个策略。
    2. 这个地方可以将创建逻辑封装->工厂模式。
    public class StrategyFactory {
      private static final Map<String, Strategy> strategies = new HashMap<>();
    
      static {
        strategies.put("A", new ConcreteStrategyA());
        strategies.put("B", new ConcreteStrategyB());
      }
    
      public static Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
          throw new IllegalArgumentException("type should not be empty.");
        }
        return strategies.get(type);
      }
    }
    
    1. 一般来说策略类如果是无状态的,不包含成员变量,只是单纯的算法实现,这样的策略对象是可以被共享的。
    2. 相反如果是有状态的,可以根据业务的需要,每次调用时都是创建新的对象。
    public class StrategyFactory {
      public static Strategy getStrategy(String type) {
        if (type == null || type.isEmpty()) {
          throw new IllegalArgumentException("type should not be empty.");
        }
    
        if (type.equals("A")) {
          return new ConcreteStrategyA();
        } else if (type.equals("B")) {
          return new ConcreteStrategyB();
        }
    
        return null;
      }
    }
    
    策略的使用
    1. 典型的场景就是运行时动态确定使用哪种策略。
    2. 动态是指我们在程序运行期间根据配置、用户输入、计算结果等不确定因素来确定使用哪种策略。
    // 策略接口:EvictionStrategy
    // 策略类:LruEvictionStrategy、FifoEvictionStrategy、LfuEvictionStrategy...
    // 策略工厂:EvictionStrategyFactory
    
    public class UserCache {
      private Map<String, User> cacheData = new HashMap<>();
      private EvictionStrategy eviction;
    
      public UserCache(EvictionStrategy eviction) {
        this.eviction = eviction;
      }
    
      //...
    }
    
    // 运行时动态确定,根据配置文件的配置决定使用哪种策略
    public class Application {
      public static void main(String[] args) throws Exception {
        EvictionStrategy evictionStrategy = null;
        Properties props = new Properties();
        props.load(new FileInputStream("./config.properties"));
        String type = props.getProperty("eviction_type");
        evictionStrategy = EvictionStrategyFactory.getEvictionStrategy(type);
        UserCache userCache = new UserCache(evictionStrategy);
        //...
      }
    }
    
    // 非运行时动态确定,在代码中指定使用哪种策略
    public class Application {
      public static void main(String[] args) {
        //...
        EvictionStrategy evictionStrategy = new LruEvictionStrategy();
        UserCache userCache = new UserCache(evictionStrategy);
        //...
      }
    }
    
    1. 如果不是在运行时判断确定策略,这样就没有发挥策略模式的优势,而是利用了基于接口编程。

    如何避免分支判断代码?

    public class OrderService {
      public double discount(Order order) {
        double discount = 0.0;
        OrderType type = order.getType();
        if (type.equals(OrderType.NORMAL)) { // 普通订单
          //...省略折扣计算算法代码
        } else if (type.equals(OrderType.GROUPON)) { // 团购订单
          //...省略折扣计算算法代码
        } else if (type.equals(OrderType.PROMOTION)) { // 促销订单
          //...省略折扣计算算法代码
        }
        return discount;
      }
    }
    

    上面代码可以利用策略模式优化,将不同类型的订单的计算算法和类型通过工厂类封装。

    // 策略的定义
    public interface DiscountStrategy {
      double calDiscount(Order order);
    }
    // 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...
    
    
    // 策略的创建
    public class DiscountStrategyFactory {
      private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();
    
      static {
        strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
        strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
        strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
      }
    
      public static DiscountStrategy getDiscountStrategy(OrderType type) {
        return strategies.get(type);
      }
    }
    
    
    // 策略的使用
    public class OrderService {
      public double discount(Order order) {
        OrderType type = order.getType();
        DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
        return discountStrategy.calDiscount(order);
      }
    }
    

    重构之后就没有if else了。本质上是借助了工厂类中的map, 通过数据结构本身的表的查询替换了我们手动的判断分支查询。

    如果业务场景需要每次创建不同的策略对象,我们就需要另外一种工厂类的实现:

    public class DiscountStrategyFactory {
      public static DiscountStrategy getDiscountStrategy(OrderType type) {
        if (type == null) {
          throw new IllegalArgumentException("Type should not be null.");
        }
        if (type.equals(OrderType.NORMAL)) {
          return new NormalDiscountStrategy();
        } else if (type.equals(OrderType.GROUPON)) {
          return new GrouponDiscountStrategy();
        } else if (type.equals(OrderType.PROMOTION)) {
          return new PromotionDiscountStrategy();
        }
        return null;
      }
    }
    

    这里只是把分支挪进了工厂类。并没有真正移除。

    结合例子:实现给不同文件大小文件排序的小程序

    问题和解决思路

    1. 写一个程序实现对一个文件中内容进行排序的功能。文件中只包含整型数,通过逗号相连。
    2. 简单思路:确定文件大小,
      1. 如果相对较小,读取内容不需要特别多时间,可以简单的使用内存解决。将数字读取到内存并且使用排序算法排序。
      2. 如果相对较大,内存有限没办法一次性加载。就可能需要一些外部排序算法。
      3. 如果文件更大,可以利用多核的优势,在外排序的基础上优化加入多线程并发排序。
      4. 如果文件超大,单机多线程也很慢的时候,引入多机器,使用例如MapReduce框架解决。

    代码实现和分析

    public class Sorter {
      private static final long GB = 1000 * 1000 * 1000;
    
      public void sortFile(String filePath) {
        // 省略校验逻辑
        File file = new File(filePath);
        long fileSize = file.length();
        if (fileSize < 6 * GB) { // [0, 6GB)
          quickSort(filePath);
        } else if (fileSize < 10 * GB) { // [6GB, 10GB)
          externalSort(filePath);
        } else if (fileSize < 100 * GB) { // [10GB, 100GB)
          concurrentExternalSort(filePath);
        } else { // [100GB, ~)
          mapreduceSort(filePath);
        }
      }
    
      private void quickSort(String filePath) {
        // 快速排序
      }
    
      private void externalSort(String filePath) {
        // 外部排序
      }
    
      private void concurrentExternalSort(String filePath) {
        // 多线程外部排序
      }
    
      private void mapreduceSort(String filePath) {
        // 利用MapReduce多机排序
      }
    }
    
    public class SortingTool {
      public static void main(String[] args) {
        Sorter sorter = new Sorter();
        sorter.sortFile(args[0]);
      }
    }
    

    每种排序算法实现逻辑都比较复杂实际上,代码也比较多。这样可读性和可维护性都有点差。所有的排序算法设计成Sorter的私有函数也会影响代码的可复用性。

    优化的重构

    两个点:第一个点是排序算法可以被拆分出来一个新的类,易于维护和复用;另外一个就是前文说的,多个分支可以通过基于工厂类创建的策略模式来优化,让排序策略的定义、创建和使用解耦。

    public interface ISortAlg {
      void sort(String filePath);
    }
    
    public class QuickSort implements ISortAlg {
      @Override
      public void sort(String filePath) {
        //...
      }
    }
    
    public class ExternalSort implements ISortAlg {
      @Override
      public void sort(String filePath) {
        //...
      }
    }
    
    public class ConcurrentExternalSort implements ISortAlg {
      @Override
      public void sort(String filePath) {
        //...
      }
    }
    
    public class MapReduceSort implements ISortAlg {
      @Override
      public void sort(String filePath) {
        //...
      }
    }
    
    public class Sorter {
      private static final long GB = 1000 * 1000 * 1000;
    
      public void sortFile(String filePath) {
        // 省略校验逻辑
        File file = new File(filePath);
        long fileSize = file.length();
        ISortAlg sortAlg;
        if (fileSize < 6 * GB) { // [0, 6GB)
          sortAlg = new QuickSort();
        } else if (fileSize < 10 * GB) { // [6GB, 10GB)
          sortAlg = new ExternalSort();
        } else if (fileSize < 100 * GB) { // [10GB, 100GB)
          sortAlg = new ConcurrentExternalSort();
        } else { // [100GB, ~)
          sortAlg = new MapReduceSort();
        }
        sortAlg.sort(filePath);
      }
    }
    

    然后Sorter中的sortFile可以进一步使用策略模式优化。因为他们是无状态的排序算法,可以对每个算法策略类只创建一个对象重复使用。

    public class SortAlgFactory {
      private static final Map<String, ISortAlg> algs = new HashMap<>();
    
      static {
        algs.put("QuickSort", new QuickSort());
        algs.put("ExternalSort", new ExternalSort());
        algs.put("ConcurrentExternalSort", new ConcurrentExternalSort());
        algs.put("MapReduceSort", new MapReduceSort());
      }
    
      public static ISortAlg getSortAlg(String type) {
        if (type == null || type.isEmpty()) {
          throw new IllegalArgumentException("type should not be empty.");
        }
        return algs.get(type);
      }
    }
    
    public class Sorter {
      private static final long GB = 1000 * 1000 * 1000;
    
      public void sortFile(String filePath) {
        // 省略校验逻辑
        File file = new File(filePath);
        long fileSize = file.length();
        ISortAlg sortAlg;
        if (fileSize < 6 * GB) { // [0, 6GB)
          sortAlg = SortAlgFactory.getSortAlg("QuickSort");
        } else if (fileSize < 10 * GB) { // [6GB, 10GB)
          sortAlg = SortAlgFactory.getSortAlg("ExternalSort");
        } else if (fileSize < 100 * GB) { // [10GB, 100GB)
          sortAlg = SortAlgFactory.getSortAlg("ConcurrentExternalSort");
        } else { // [100GB, ~)
          sortAlg = SortAlgFactory.getSortAlg("MapReduceSort");
        }
        sortAlg.sort(filePath);
      }
    }
    

    最后如果精益求精,sortFile中的if判断也可以去掉,通用可以借助策略模式中根据type表查找的思想,优化:

    public class Sorter {
      private static final long GB = 1000 * 1000 * 1000;
      private static final List<AlgRange> algs = new ArrayList<>();
      static {
        algs.add(new AlgRange(0, 6*GB, SortAlgFactory.getSortAlg("QuickSort")));
        algs.add(new AlgRange(6*GB, 10*GB, SortAlgFactory.getSortAlg("ExternalSort")));
        algs.add(new AlgRange(10*GB, 100*GB, SortAlgFactory.getSortAlg("ConcurrentExternalSort")));
        algs.add(new AlgRange(100*GB, Long.MAX_VALUE, SortAlgFactory.getSortAlg("MapReduceSort")));
      }
    
      public void sortFile(String filePath) {
        // 省略校验逻辑
        File file = new File(filePath);
        long fileSize = file.length();
        ISortAlg sortAlg = null;
        for (AlgRange algRange : algs) {
          if (algRange.inRange(fileSize)) {
            sortAlg = algRange.getAlg();
            break;
          }
        }
        sortAlg.sort(filePath);
      }
    
      private static class AlgRange {
        private long start;
        private long end;
        private ISortAlg alg;
    
        public AlgRange(long start, long end, ISortAlg alg) {
          this.start = start;
          this.end = end;
          this.alg = alg;
        }
    
        public ISortAlg getAlg() {
          return alg;
        }
    
        public boolean inRange(long size) {
          return size >= start && size < end;
        }
      }
    }
    

    但是这里还有问题就是每次新加代码都要回去修改策略工厂类,违反OCP开闭原则。

    对于 Java 语言来说,我们可以通过反射来避免对策略工厂类的修改。具体是这么做的:我们通过一个配置文件或者自定义的 annotation 来标注都有哪些策略类;策略工厂类读取配置文件或者搜索被 annotation 标注的策略类,然后通过反射动态地加载这些策略类、创建策略对象;当我们新添加一个策略的时候,只需要将这个新添加的策略类添加到配置文件或者用 annotation 标注即可。还记得上一节课的课堂讨论题吗?我们也可以用这种方法来解决。

    对于Sorter,通过将文件大小区间和算法之间的对应关系放到配置文件中。当添加新的排序算法时,我们只需要改动配置文件即可,不需要改动代码。



沪ICP备19023445号-2号
友情链接