表驱动方法(Table-Driven Methods) - winner_0715 - 博客园 https://www.cnblogs.com/winner-0715/p/9382048.html


What

表驱动方法(Table-Driven Methods),在《Unix 编程艺术》中有提到,《代码大全》的第十八章对此进行了详细地讲解。

表驱动法是一种编程模式(Scheme),从表里面查找信息而不使用逻辑语句(if 和case) 它的好处是消除代码里面到处出现的if、else、swith语句,让凌乱代码变得简明和清晰。
对简单情况而言,表驱动方法可能仅仅使逻辑语句更容易和直白,但随着逻辑的越来越复杂,表驱动法就愈发有吸引力。

if...else...比较多的时候就想想表驱动法...

Why

先通过一个简单的例子体验下,在某些情况下,如果不使用表驱动方法,代码会如何地难看。

假设让你实现一个返回每个月天数的函数(为简单起见不考虑闰年)。

初级码农的笨方法是马上摆出 12 副威武雄壮的 if-else 组合拳:

int iGetMonthDays(int iMonth){
int iDays; if(1 == iMonth) {iDays = 31;}
else if(2 == iMonth) {iDays = 28;}
else if(3 == iMonth) {iDays = 31;}
else if(4 == iMonth) {iDays = 30;}
else if(5 == iMonth) {iDays = 31;}
else if(6 == iMonth) {iDays = 30;}
else if(7 == iMonth) {iDays = 31;}
else if(8 == iMonth) {iDays = 31;}
else if(9 == iMonth) {iDays = 30;}
else if(10 == iMonth) {iDays = 31;}
else if(11 == iMonth) {iDays = 30;}
else if(12 == iMonth) {iDays = 31;} return iDays;
}

稍微机灵点的码农发现每月天数无外乎 28、30、31 三种,或许会用 switch-case “裁剪”下:

int iGetMonthDays(int iMonth){
int iDays; switch (iMonth) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:{iDays = 31;break;}
case 2:{iDays = 28;break;}
case 4:
case 6:
case 9:
case 11:{iDays = 30;break;}
} return iDays;
}

这两种方法充斥了大量的逻辑判断,还凭空冒出了一大堆1,2,...,11,12这样的 Magic Number(魔鬼数字公然出现在程序里是很 ugly 的做法),不利于代码的维护与扩展。

表驱动处理起来就赏心悦目得多了:

static int monthDays[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

int iGetMonthDays(int iMonth){
return monthDays[(iMonth - 1)];
}

How

表驱动可以使你的代码更简洁,结构更加灵活,多用于逻辑性不强但是分支多的情况。

如何使用表驱动法?需要明确两个关键问题:

  • 表的形式及表中放什么内容

    • 表形式可以为一维数组、二维数组和结构体数组。
    • 表中可以存放数值、字符串或函数指针等数据。
  • 如何去访问表。

下面介绍表的三种访问方式:

直接访问

直接根据“键”来获得“值”,给定下标 index,然后array[index]就获得数组在相应下标处的数值。例如前面这个根据月份取天数的例子。

索引访问

它适用于这样的情况:假设你经营一家商店,有 100 种商品,每种商品都有一个 ID 号,但很多商品的描述都差不多,所以只有 30 条不同的描述,如何建立建立商品与商品描述的表?

还是同上面做法来一一对应吗?那样描述会扩充到 100 个,会有 70 个描述是重复的!太浪费了。

方法是建立一个 100 长的索引和 30 长的描述,然后这些索引指向相应的描述(不同的索引可以指向相同的描述),这样就解决了表数据冗余的问题啦。

struct product_t {
char * id;
int desc_index;
}; const char * desc[] = {
"description_1",
"description_2",
...
"description_29",
"description_30"
}; const product_t goods [] = {
{"id_1", 3},
{"id_2", 1},
...
{"id_99", 12},
{"id_100", 5}
}; const char* desc_product (const char* id) {
for (const product_t & p : goods) {
if (strcmp(p.id, id) == 0) {
return desc[p.desc_index - 1];
}
} return NULL;
}

阶梯访问

例子:将百分制成绩转成五级分制(我们用的优、良、中、合格、不合格,西方用的 A、B、C、D和F),假定转换关系:

Score Degree
[90-100] A
[80,90) B
[70,80) C
[60,70) D
[0,60) F

如何用表格表示这些范围?你当然可以用第一种直接访问的方法:申请一个 100 长的表,然后在这个表中填充相应的等级。很明显,也会浪费大量空间,有没有更好的方法?

对于这种“某个范围区间内,对应某个值”的逻辑规则,可用阶梯访问的方式。

const char gradeTable[] = {
'A', 'B', 'C', 'D', 'F'
}; const int downLimit[] = {
90, 80, 70, 60
}; int degree(int score)
{
int gradeLevel = 0;
char lowestDegree = gradeTable[sizeof(gradeTable)/sizeof(gradeTable[0]) - 1]; // 这里可用二分查找优化
while (gradeTable[gradeLevel] != lowestDegree) {
if(score < downLimit[gradeLevel]) {
++ gradeLevel;
} else {
break;
}
} return gradeTable[gradeLevel];
}

将来如果等级规则变了(比如 85~100 分为等级 A,或添加 50~60 分为等级 E),只需要修改 gradeTable 和 downLimit 表就行,degree 函数可以保持一行都不改动。

更进一步地,gradeTable 和 downLimit 表还可以配置文件的形式表示,主程序从外部文件 load 进来就行,程序灵活性大大增加。

Review

伟大的 C 语言大师 Rob Pike 有句话说的好:

数据压倒一切。如果选择了正确的数据结构并把一切组织的井井有条,正确的算法就不言自明。编程的核心是数据结构,而不是算法。

对人类来说,数据比编程逻辑更容易驾驭。在复杂数据和复杂代码中选择,宁可选择前者。

更进一步,在设计中,应该主动将代码的复杂度转移到数据中去。

这里谈到了 Unix 哲学之分离原则:

策略同机制分离

机制,即提供的功能。

策略,即如何使用功能。

以百分制转五级分制为例,机制就是 degree 函数:你给一个百分制分数给它,它吐出来一个五级分制给你。策略就是gradeTable 和 downLimit 这两个表,它规定了哪个区间的分数对应哪个等级。

从 degree 的实现可以看出:对机制而言,策略是透明的(degree 完全看不到 gradeTable 和 downLimit 这两个表的内部规则)。

将两者分离,可以使机制(degree)相对保持稳定,而同时支持策略(表)的变化。

Ref:

表驱动

表驱动方法(Table-Driven Methods)的更多相关文章

  1. create table 使用select查询语句创建表的方法分享

    转自:http://www.maomao365.com/?p=6642 摘要:下文讲述使用select查询语句建立新的数据表的方法分享 ---1 mysql create table `新数据表名` ...

  2. MySql清空表的方法介绍 : truncate table 表名

    清空某个mysql表中所有内容 delete from 表名; truncate table 表名; 不带where参数的delete语句可以删除mysql表中所有内容,使用truncate tabl ...

  3. 什么是领域驱动设计(Domain Driven Design)?

    本文是从 What is Domain Driven Design? 这篇文章翻译而来. ”…在很多领域,专家的作用体现在他们的专业知识上而不是智力上.“ -- Don Reinertsen 领域驱动 ...

  4. 领域驱动设计(Domain Driven Design)参考架构详解

    摘要 本文将介绍领域驱动设计(Domain Driven Design)的官方参考架构,该架构分成了Interfaces.Applications和Domain三层以及包含各类基础设施的Infrast ...

  5. Linux Platform驱动模型(二) _驱动方法

    在Linux设备树语法详解和Linux Platform驱动模型(一) _设备信息中我们讨论了设备信息的写法,本文主要讨论平台总线中另外一部分-驱动方法,将试图回答下面几个问题: 如何填充platfo ...

  6. [转载]领域驱动设计(Domain Driven Design)参考架构详解

    摘要 本文将介绍领域驱动设计(Domain Driven Design)的官方参考架构,该架构分成了Interfaces.Applications和Domain三层以及包含各类基础设施的Infrast ...

  7. Linux Platform驱动模型(二) _驱动方法【转】

    转自:http://www.cnblogs.com/xiaojiang1025/archive/2017/02/06/6367910.html 在Linux设备树语法详解和Linux Platform ...

  8. Mysql单表访问方法,索引合并,多表连接原理,基于规则的优化,子查询优化

    参考书籍<mysql是怎样运行的> 非常推荐这本书,通俗易懂,但是没有讲mysql主从等内容 书中还讲解了本文没有提到的子查询优化内容, 本文只总结了常见的子查询是如何优化的 系列文章目录 ...

  9. Berkeley DB的数据存储结构——哈希表(Hash Table)、B树(BTree)、队列(Queue)、记录号(Recno)

    Berkeley DB的数据存储结构 BDB支持四种数据存储结构及相应算法,官方称为访问方法(Access Method),分别是哈希表(Hash Table).B树(BTree).队列(Queue) ...

随机推荐

  1. Pilosa文档翻译(二)入门指南

    目录 开始 Pilosa 简单项目 创建架构(Create the Schema) 从CVS文件导入数据 做一些查询(Queries) 接下来做什么? Pilosa支持默认使用JSON的HTTP接口. ...

  2. H+ 添加(新增)Tab选项卡

    //注:在contabs.js文件中 $(function () { }); 方法外 加入//注: data-name="' + menuName + '" 这句是加入的自定义属性 ...

  3. SNF快速开发平台项目实践介绍

    SNF快速开发平台分如下子平台: 1.CS快速开发平台 2.BS快速开发平台 3.H5移动端快速开发平台 4.软件开发机器人平台(目前是CS版本,后续有规划BS版本) SNF快速开发平台是一个比较成熟 ...

  4. 星云的Linux专用学习手册

    Centos 7 为例 1. 查看操作系统信息 uname -a 执行效果如下: [fairy@localhost ~]$ uname -a Linux localhost.localdomain - ...

  5. 【Java】的四种引用的区别

    强引用:如果一个对象具有强引用,它就不会被垃圾回收器回收.即使当前内存空间不足,JVM 也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止.如果想中断强引用和某个对象之间的 ...

  6. WEB API Filter的使用以及执行顺序

    在WEB Api中,引入了面向切面编程(AOP)的思想,在某些特定的位置可以插入特定的Filter进行过程拦截处理.引入了这一机制可以更好地践行DRY(Don’t Repeat Yourself)思想 ...

  7. HTTP 06 用户认证

    SSL 客户端认证具有高度的安全等级, 但是因为导入及维持费用等问题, 还未普及. 认证的方式有很多种, 多半是 基于表单的认证(即用户名/密码的方式) Session 管理及 Cookie 应用 基 ...

  8. 同步调用异步方法how-would-i-run-an-async-taskt-method-synchronously

    同步调用异步方法帮助类: public static class AsyncHelpers { /// <summary> /// Execute's an async Task<T ...

  9. Orders matters: seq2seq for set 实验

    论文提出了input的顺序影响seq2seq结果 有一些输入本身是无序的怎么去处理呢 作者提出LSTM controller的方式 替代输入的LSTM encode方式         作者实验这种方 ...

  10. Java编码常见的Log日志打印问题

    前言 本文总结了作者在Java代码检视中遇到的一些关于日志打印的问题,并给出修改建议.因能力有限,难免存在错漏,欢迎指正. 一. 不规范的异常打印 使用slf4j日志组件时,logger.error( ...