首页 经典策略模式VS枚举策略
文章
取消

经典策略模式VS枚举策略

1. 枚举类

1.1. 定义

简单创建一个枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Getter
enum WeekdayEnum {
    Monday(1, "星期一"),
    Tuesday(2, "星期二"),
    Wednesday(3, "星期三"),
    Thursday(4, "星期四"),
    Friday(5, "星期五"),
    Saturday(6, "星期六"),
    Sunday(7, "星期天"),
    ;

    private static final HashMap<Integer, WeekdayEnum> enumHashMap = new HashMap<>();
    private final Integer code;
    private final String desc;
    
    static {
        for (WeekdayEnum value : values()) {
            enumHashMap.put(value.getCode(), value);
        }
    }

    WeekdayEnum(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    
    public static WeekdayEnum getEnumByCode(Integer code){
        return enumHashMap.get(code);
    }

}

该枚举类穷举了一周内的所有天,并建立了一个基于 HashMap 的本地缓存,通过缓存向外界提供了 getEnumByCode(),即通过 code 获取对应的枚举。当类的对象是有限的,该类适合定义成枚举类。

枚举类的特性

反编译 WeekdayEnum.class 能得到下面的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.HashMap;

final class WeekdayEnum extends Enum{

    public static WeekdayEnum[] values(){
        return (WeekdayEnum[])$VALUES.clone();
    }
    public static final WeekdayEnum Monday;
    public static final WeekdayEnum Tuesday;
    public static final WeekdayEnum Wednesday;
    private final Integer code;
    private final String desc;
    private static final WeekdayEnum $VALUES[];

    static {
        Monday = new WeekdayEnum("Monday", 0, Integer.valueOf(1), "\u661F\u671F\u4E00");
        Tuesday = new WeekdayEnum("Tuesday", 1, Integer.valueOf(2), "\u661F\u671F\u4E8C");
        $VALUES = (new WeekdayEnum[] {
            Monday, Tuesday, Wednesday
        });

    }
}

通过这段反编译代码,我们发现编译器为我们做了这些事情:

  1. 枚举类是一个被 final 修饰的普通类,即它无法被继承。
  2. 构造函数新增两个参数,String 类型的 s,和 int 类型的 i,分别是枚举对象的变量名和排序值(从 0 开始)
  3. 新增一个 valueOf(),即通过变量名获取枚举对象
  4. 新定义了一个枚举数组 $VALUES 缓存所有的枚举,以及一个返回数组拷贝的 values()

以上便是编译器为定义的枚举类额外做的事,我们发现枚举类就是一个普通的类,并且如果只有一个枚举对象,那么它其实就是一个单例模式的实现。

1.3. 枚举类 vs 常量类

枚举类和常量类没有可比性,因为枚举是作为对象而存在,并且能使用常量类的地方一定可以用枚举类替换,但使用枚举类的地方不一定能用常量类替换,枚举类作为对象使用时能应用到的场景更多更广,总而言之枚举类可玩性更强。

经典策略模式

策略模式也属于一种行为型模式,在实际的开发中这个模式也用的非常多,最常见的应用场景是利用它来避免冗长的 if-else 或 switch 分支判断。不过,它的作用还不止如此。它也可以像模板模式那样,提供框架的扩展点等等。

定义

策略模式,英文全称是Strategy Design Pattern:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略 模式可以使算法的变化独立于使用它们的客户端(这里的客户端代指使用算法的代码)。其实就是一种解耦方式,们知道,工厂模式是解耦对象的创建和使用,观察者模式是解耦观察者和被观察者,终究是应用的场景不同。

实现

假设有这样一个需求,希望写一个小程序,实现对一个文件进行排序的功能。文件中只包含 整型数,并且,相邻的数字通过逗号来区隔。

很简单:先将文件中的内容读出来,以逗号分隔将其读取成一个个的数字,再以任意一种排序算法对其进行排序,并将排序好的数据重新写入文件当中。

我猜你肯定是这么想的,但是这样存在问题,比如文件 size 非常大,像 10GB,50GB,甚至 100GB,那么内存根本不够,跟别说还在内存中排序了想都别想。这时候我们应该根据文件的 size 合理分析:

  1. 当文件 size < 6GB(现在的手机、电脑,只要稍微好点的内存至少有 6GB 吧),那么直接读取到内存并使用快排。
  2. 10GB <= size < 100GB 这时候没办法加入到内存当中,那么就使用外部排序算法。
  3. 100GB <= size < 1T,为了利用 CPU 多核的优势可以在外部排序的基础之上进行优化,加入多线程并发排序的功能,这就有点类似“单机版”的MapReduce。
  4. size > 1T,这时候即便是单机多线程排序,也算很慢了。这样可以使用真正的 MapReduce 框架,利用多机的处理能力,提高排序的效率。

先用最简单的方式实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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]);
  }
}

这段代码确实很简单,但是类看起来会很臃肿,因为上面的几个算法的内容会很多,同时如果后续增加算法,就需要增加 if/else,同时几个算法函数作为私有函数放在 Sorter 类中,这就没法给其它类复用了。综上可优化重构将这几个算法函数独立出来成为单个类,正好可以用到策略模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
 * 定义和创建策略
 * @see ISortAlg、QuickSort、ExternalSort、ConcurrentExternalSort 和 MapReduceSort
 */
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);
  }
}

经过重构,我们将排序算法设计成独立的类,跟具体的业务逻辑(代码中的 if-else那部分逻辑)解耦,也让排序算法能够复用

实际上上面的代码还可以优化,因为算法是无状态的,跟具体的业务无关,所以没必要每次使用的时候都重新创建一个对象。所以可有使用工厂模式对其对象的创建进行封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
  }
}

通过两次重构,现在的代码已经符合策略模式的代码结构了,不过,Sorter类中的 sortFile()函数还是有一堆if-else逻辑。这里的if-else逻辑分支不多、也不复杂,这样写完全 没问题。但是如果一定要消除 if-else,那么可以依据策略工厂的这种”查表法”,再弄一个工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
    }
  }
}

相应的这里用的是 List 结构遍历查询,而非 Map 的匹配查询。现在当新增策略时,我们只需要新增一个策略类,然后两个工厂类中的静态代码块加上对应的候选项就可了。

当然也许有人会说这样封装的还是不彻底呀,还是需要修改代码,那么我们可以参照 Spring 中的设计,通过为策略类打上注解/配置文件的方式,并通过反射创建策略类对象从而避免对工厂类的修改。

本文由作者按照 CC BY 4.0 进行授权

基于贫血模型的MVC三层架构和基于充血模型的DDD分层架构

如何把粗糙的模糊的需求整理分析得到清晰的可落地的需求描述