表格驱动的常见形式

开发中,常常会用到各种分支逻辑,if else switch case 霸屏,如下代码。

1
2
3
4
5
6
7
if (state == 1) {
process1();
} else if (state == 2){
process2();
} else {
processByDefault();
}

在编程的世界,只有两种元素组成,那就是数据与代码。像上面的代码,估计大家一看,除了 state{1, 2...} 是数据的概念,其他的诸如 if else process1() process2() processByDefault() 自然也就化为代码的部分。

而代码和数据的划分,不是泾渭分明的,你写的任何代码,在编译器的角度,又成为了数据。

因此如果我们换一个角度思考上述代码,重新定义数据,可能就会得到另一个结果。 如下

1
2
3
4
5
6
7
Map<Integer, Runnable> stateProcessMap = new HashMap<>{{
put(1, this::process1);
put(2, this::process1);
}};

stateProcessMap.getOrDefault(state, this::processByDefault).run();

提出了 stateProcessMap 的数据,表示 state 与业务的映射关系。

那数据的提出有什么好处呢?

显而易见的是,以后同类结构的修改,你不再是改 if else 代码的部分,而只需要修改 stateProcessMap 数据部分。

可不要小看这一点点小改动,虽然现在 stateProcessMap 概念是数据,而载体还是在代码上,但可是动静分离的基础。

阶梯式的表格驱动

在常见形式中,key 一般为静态的数据,能够用在 if (x == xx) 等于的部分,而如果面对如下的代码,可能又得继续思考了。

1
2
3
4
5
6
7
8
9
if (distance > 3) {
return this.avg3();
} else if (distance > 2) {
return this.avg2();
} else if (distance > 1) {
return this.avg1();
} else {
return this.avg();
}

上述逻辑,由于是阶梯式的逻辑,必须先从最大的数开始判断,所以无法使用常规的形式,你可以这样重新定义数据,如下:

1
2
3
4
5
6
7
8
9
10
int[] distanceLevels = {3, 2, 1};
Supplier[] avgLevels = {this::avg3, this.avg2, this.avg1};


for (int i = 0; i < distanceLevels.length; i++) {
if (distanceLevels[i] > state) {
return avgLevels[i].get();
}
return this.avg();
}

上述形式,我们称为阶梯访问表,虽然不如常见形式简介,但也达到了分离了数据与代码的效果,取得了更高的扩展性。

在上述的阶梯访问表中,也存在几个问题:

  • 大量数据时,存在线性访问,查找成本较大
  • 需要手工保证 key -> value 的映射

应运而生的,我们想到可以使用 TreeMap,即是 key -> value 的结构,又是有序的结构。

1
2
3
4
5
6
7
NavigableMap<Integer, Supplier> distanceAvgMap = new TreeMap<> {{
put(1, this::avg1);
put(2, this::avg2);
put(3, this::avg3);
}};

distanceAvgMap.lowerEntry(distance).getValue().get();

NavigableMap 是一个可导航的 Map 接口, 上层接口为 SortedMap,顶层接口为 MapTreeMapNavigableMap 的具体实现,是一个平衡二叉树的实现,主流的实现是八股文中的红黑树,其中 lowerEntry(key) 是以二分查找法查找一个小于或等于 keyEntry

这样,我们阶梯式的逻辑,就用一个二叉树的 Map 数据结构表示成了数据形式,与阶梯访问表相比,更优雅的达到了数据与代码分离的效果。