• 【阅读笔记】理解表驱动设计


    前言

    《代码大全2》中单独罗列了一章描述表驱动法,旨在告诉读者,面对复杂的多分支结构(if 或者是 case),可以想着使用表驱动法实现或者改造。面向对象的多态也是用来改善多 if 或者多 case 的代码结构。诚然,多if 或者多 case 的代码并非十恶不赦,但是也需要看到多态实现和表驱动实现的好处,用于评估改造的成本和收益。工作中曾经也用过类似的思想,这里记录下来。


    问题定义

    • 存在多分支结构
    // 判断一个月有多少天
    if (month = 1) {
    	days = 31
    } else if (month = 2) {
    	days = 28
    } else if (month = 3) {
    	days = 31
    } ..... else {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 嵌套 if 结构,用于支撑复杂逻辑
    if (女性) {
    	if (单身) {
    		if (不抽烟) {
    			if (年龄 < 18) {
    				保险费率 = 200
    			} else if (年龄 < 38) {
    				保险费率 = 300
    			} ... 
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 解析多种数据格式(规则)。这里也可以使用多态改造,后续会交代表驱动的实现为什么比多态更好
    if (规则1) {
    	规则1解析逻辑块 
    } else if (规则2) {
    	规则2解析逻辑块 
    } else if ...  {
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    需求定义

    改造代码属于非功能性需求,是优化程序的,让复杂逻辑保持迭代健康的手段。主要的目的是

    • 为新需求奠定良好的实现基础
    • 明确程序职责,把变化的东西归集到表中,提高拓展性

    改造过程

    • 存在多分支结构
    // 判断一个月有多少天
    if (month = 1) {
    	days = 31
    } else if (month = 2) {
    	days = 28
    } else if (month = 3) {
    	days = 31
    } ..... else {
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个场景很简单,用一个数组维护即可

    int daysPerMonth[] = {0, 31, 28,31, ....}
    days = daysPerMonth[input]
    
    • 1
    • 2
    • 嵌套 if 结构
    if (女性) {
    	if (单身) {
    		if (不抽烟) {
    			if (年龄 < 18) {
    				保险费率 = 200
    			} else if (年龄 < 38) {
    				保险费率 = 300
    			} ... 
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    不管逻辑再怎么复杂,都能把嵌套的逻辑扁平化,如:

    性别 - 是否单身 - 是否抽烟 - 年龄 - 费率

    形如数据库中的一条表字段,那么就能启示我们这么做:

    保险费率 = rateTable(性别, 是否单身, 是否抽烟, 年龄)
    
    • 1
    • 解析多种数据格式(规则)
    if (规则1) {
    	规则1解析逻辑块 
    } else if (规则2) {
    	规则2解析逻辑块 
    } else if ...  {
    	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    规则是一种较为复杂的实现,不同的规则可能需要读取不同的字段。简单来说,不同规则所关注的信息类型、范围可能不同。那么针对这些容易变化的场景,表驱动大有用处。

    // 规则定义Eg
    规则1 
    	总字段数 3
    		 [FiledType]	[FiledValue]
    	字段1 角色 			admin
    	字段2 姓名 			james
    	字段3 参数 			{a: hello}
    规则1结束
    
    // 有多个规则,都放到数组中
    fieldDescription[]
    
    // 规则字段对象表定义 AbstractFiled 是一个抽象类 (这里引入了多态)
    AbstractFiled field[]
    field[角色] = new 角色解析器 extends AbstractFiled ();
    field[姓名] = new 姓名解析器 extends AbstractFiled ();
    field[参数] = new 参数解析器 extends AbstractFiled ();
    
    
    // 规则解析核心代码
    while (有待消费的规则事件) {
    	规则k = fieldDescription[规则事件.规则编号]
    	n = 1
    	while (未遍历完规则k的所有字段) {
    		fieldType = 规则k[字段n].FiledType
    		fieldValue = 规则k[字段n].FiledValue
    		解析器 = field[fieldType]
    		解析器.解析(fieldType, fieldValue)
    		n++
    	}
    }
    
    • 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

    可以看到,用了表驱动设计,核心代码块很好看懂,变化的部分都被封装到了不同的类去了。
    其中fieldDescription[规则事件.规则编号] 规则k[字段n] field[fieldType] 都是查表的思想


    表驱动设计中数据范围的处理

    上文提到的查询都可以视为 索引表, 但是索引是一个区间的时候该如何映射到入参呢,《代码大全2》把以下这种解决方案成为 阶梯访问表

    • 一个简单的例子
    [分数 / 总分 定义]		[等级]
    >= 90% 					A
    < 90% 					B
    < 75% 					C
    < 65%					D
    < 50%					F
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    根据分数查等级的SQL

    select 
    	等级 
    fromwhere 
    	自己的分数 / 总分 >= [分数 / 总分 定义]
    limit 1	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 同理,更加复杂的例子也可以用这个方法
    概率			保险索赔额度
    0.45232			0
    0.54723			254
    0.5323			43535
    
    • 1
    • 2
    • 3
    • 4

    后记

    曾经在学习 Spock 测试框架的时候就了解到了表驱动设计。遗留的问题今天终于理解了。包括工作中遇到的异常节点的规则配置、lookup大宽表、角色权限表查询、多条件计算器实现。举的例子跟公司业务相关就不太好展开描述。简单来说,印证了表驱动设计有足够多的应用场景。使用表驱动设计的时候,想一想能不能用多态,如果代价合理,引入多态是个不错的选择。

  • 相关阅读:
    ML XGBoost详细原理及公式推导讲解+面试必考知识点
    C# 多线程一: Thread 的简单理解与运用
    万字肝完nodejs入门教程,详解入口,建议收藏(更新中)
    Java反射机制
    网络爬虫-----http和https的请求与响应原理
    线性表的链式存储的基本
    端口映射的几种常见应用场景
    使用vite搭建前端项目
    【Mac】VSCode 更新1.73版本后JS&TS代码跳转异常
    java设计模式---策略模式
  • 原文地址:https://blog.csdn.net/chenghan_yang/article/details/127718364