【代码大全2 选读】看看骨灰级高手消灭 if-else 逻辑的瑞士军刀长啥样

文章目录

    • 1 【写在前面】
    • 2 【心法】这把瑞士军刀长啥样
    • 3 【示例1】确定某个月份的天数(Days-in-Month Example)
    • 4 【示例2】确定保险费率(Insurance Rates Example)
    • 5 【示例3】灵活的消息格式(Flexible-Message-Format Example)
    • 6 【结语】

1 【写在前面】

随手一翻吃灰多年的《代码大全2》,偶然看到一篇讲重构 if-else / case 的三个案例,很有启发,便有了今天这篇分享。

在这里插入图片描述

2 【心法】这把瑞士军刀长啥样

它就是大名鼎鼎的 表驱动方法(table-driven method)。原文是这样说的:

A table-driven method is a scheme that allows you to look up information in a table rather than using logic statements (if and case) to figure it out.

直译:表驱动法是一种 编程模式——从 里面查找信息,而不使用 逻辑语句ifcase

实际开发中,凡是能通过逻辑语句来实现的功能,都可以通过查表来实现(这个语气是不是似曾相识?)。对于简单情况,用逻辑语句更容易也更直白;随着逻辑链越来越复杂,查表法的作用就愈发凸显出来了。例如,要把字符分为 字母标点数字 三类,若用逻辑语句来实现,可能写成:

if ( (('a' <= inputChar) && (inputChar <= 'z')) ||(('A' <= inputChar) && (inputChar <= 'Z'))) {charType = CharacterType.Letter;
} else if ( (inputChar == ' ') || (inputChar == ',') ||(inputChar == '.') || (inputChar == '!') || (inputChar == '(') ||(inputChar == ')') || (inputChar == ':') || (inputChar == ';') ||(inputChar == '?') || (inputChar == '-')) {charType = CharacterType.Punctuation;
} else if ( ('0' <= inputChar) && (inputChar <= '9') ) {charType = CharacterType.Digit;
}

而如果用查表法,上述代码也就一句搞定:

charType = charTypeTable[ inputChar ];

前提是数组 charTypeTable 提前建好。这里的核心在于,把程序中的信息存到 数据 里,而非 逻辑 里;放到表中,而非 if 检测中。(… put your program’s knowledge into its data rather than into its logic — in the table instead of in the if tests.)

使用表驱动法必须回答的两方面问题:

  • 怎样从表中查到数据?推荐方法有三——
    • 直接访问(Direct access)
    • 索引访问(Indexed access)
    • 阶梯访问(Stair-step access)
  • 应该在表里存什么内容?即查出的结果是数据(data)还是操作(action)?
    • 存数据:直接放表里即可;
    • 存操作:要么存一段代码逻辑(想想 Lambda 表达式);要么存某个引用(如 Java8 的方法引用)

心法讲完了,来看三个具体案例。

3 【示例1】确定某个月份的天数(Days-in-Month Example)

先不考虑闰年,逻辑语句法可以写成(Visual Basic 版):

If ( month = 1 ) Thendays = 31
ElseIf ( month = 2 ) Thendays = 28
ElseIf ( month = 3 ) Thendays = 31
ElseIf ( month = 4 ) Thendays = 30
ElseIf ( month = 5 ) Thendays = 31
ElseIf ( month = 6 ) Thendays = 30
ElseIf ( month = 7 ) Thendays = 31
ElseIf ( month = 8 ) Thendays = 31
ElseIf ( month = 9 ) Thendays = 30
ElseIf ( month = 10 ) Thendays = 31
ElseIf ( month = 11 ) Thendays = 30
ElseIf ( month = 12 ) Thendays = 31
End If

这是一段如假包换的经典屎山代码。

再来看看查表法怎么重构的:

' Initialize Table of "Days Per Month" Data
Dim daysPerMonth() As Integer = _{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
days = daysPerMonth( month-1 )

考虑闰年的话,改造查表法也很简单,引入一个标志位变成二维数组就行了(如函数 LeapYearIndex),闰年取 1 否则取 0

days = daysPerMonth( month-1, LeapYearIndex() )

可以想象一下,if-else 逻辑语句法的闰年版长啥样……(此处省略 25,000 字)

4 【示例2】确定保险费率(Insurance Rates Example)

写一个算医保费率的程序,该费率随年龄、性别、婚姻状况及吸烟状况的不同而变化。逻辑语句版实现如下:

// Java Example of a Clumsy Way to Determine an Insurance Rate
if ( gender == Gender.Female ) {if ( maritalStatus == MaritalStatus.Single ) {if ( smokingStatus == SmokingStatus.NonSmoking ) {if ( age < 18 ) {rate = 200.00;} else if ( age == 18 ) {rate = 250.00;} else if ( age == 19 ) {rate = 300.00;}
// ...else if ( 65 < age ) {rate = 450.00;}else {if ( age < 18 ) {rate = 250.00;} else if ( age == 18 ) {rate = 300.00;} else if ( age == 19 ) {rate = 350.00;}
// ...else if ( 65 < age ) {rate = 575.00;}}else if ( maritalStatus == MaritalStatus.Married )
//  ...
}

这还只是其中一小部分情况,还没包含已婚女士、所有男士、或者 18 ~ 65 岁人群。按这个思路写下去,写成屎山中的“珠穆朗玛”也只是时间的问题。

那么,换成查表法呢?

书中还是用 VB 来实现(当时的 VB 正如日中天,也可能是因为 VB 对多维数组的支持更友好吧):

首先构建数据表:

' Visual Basic Example of Declaring Data to Set Up an Insurance Rates Table
Public Enum SmokingStatusSmokingStatus_First = 0SmokingStatus_Smoking = 0SmokingStatus_NonSmoking = 1SmokingStatus_Last = 1
End EnumPublic Enum GenderGender_First = 0Gender_Male = 0Gender_Female = 1Gender_Last = 1
End EnumPublic Enum MaritalStatusMaritalStatus_First = 0MaritalStatus_Single = 0MaritalStatus_Married = 1MaritalStatus_Last = 1
End EnumConst MAX_AGE As Integer = 125
Dim rateTable ( SmokingStatus_Last, Gender_Last, MaritalStatus_Last, _MAX_AGE ) As Double

然后调用该数据表:

' Visual Basic Example of an Elegant Way to Determine an Insurance Rate
rate = rateTable( smokingStatus, gender, maritalStatus, age )

5 【示例3】灵活的消息格式(Flexible-Message-Format Example)

如果前两个示例觉得没啥挑战,那你就太小看写《代码大全2》的这帮大宗师了。下面这个案例要解决的,就是针对查询表中的 key 值(或 index 索引值)复杂到难以用代码完全硬编码的情况:

问题描述

编写一个消息打印程序,所有的消息都存放在某类文件中,通常每个文件大概有 500 条这样的消息,涉及大概 20 种消息类型。这些数据采集自一些水上浮标(buoy),提供水温、漂移、方位等信息。

每条消息都包含一个消息头和一段消息正文。消息头有一个标识消息类型的 ID;消息正文由数量不等的字段构成,每一类消息的格式也不尽相同,如图1、图2所示:

图1
图1 浮标信息没有特定顺序,每条消息用 ID 标识

图2
图2 除了消息 ID 外,每类消息有各自的格式

为了突出重点,具体的输出格式就省略了,书中用伪代码一笔带过,最终的一条关于浮标温度的消息,输出结果大概长这样:

Print "Buoy Temperature Message"Read a floating-point value
Print "Average Temperature"
Print the floating-point valueRead a floating-point value
Print "Temperature Range"
Print the floating-point valueRead an integer value
Print "Number of Samples"
Print the integer valueRead a character string
Print "Location"
Print the character stringRead a time of day
Print "Time of Measurement"
Print the time of day

拿到这样的需求,映入你脑海的第一方案是怎样的?

是不是也像写逻辑分支那样,分 20 多种情况,写到 20 多个 if-elseselect-case 分支,然后每个分支再实现对应的输出逻辑……是这样吗:(伪代码形式)

While more messages to readRead a message headerDecode the message ID from the message headerIf the message header is type 1 thenPrint a type 1 messageElse if the message header is type 2 thenPrint a type 2 message
...Else if the message header is type 19 thenPrint a type 19 messageElse if the message header is type 20 thenPrint a type 20 message
End While

或者用面向对象的思想来解决:(伪代码形式)

While more messages to readRead a message headerDecode the message ID from the message headerIf the message header is type 1 thenInstantiate a type 1 message objectElse if the message header is type 2 thenInstantiate a type 2 message object
// ...Else if the message header is type 19 thenInstantiate a type 19 message objectElse if the message header is type 20 thenInstantiate a type 20 message objectEnd if
End While

作者鄙视道:不管是直接写一堆语句、还是抽成 20 多个子类去分别调用公共接口的方法,本质都一样;而且像这样照搬的面向对象编程,写了还不如不写:写成父级接口+子类实现的多态形式,只会让问题更加复杂,后续扩展看似简单,实则比直接写 if-else 还要麻烦。也要改代码、重新编译打包、重新更新发布……

这种情况下,瑞士军刀怎样证明自己呢?

首先,绕过那 20 多种消息类型,直接看消息正文中每个字段出现过的数据类型,并收集到一起(枚举):

// C++ Example of Defining Message Data Types
enum FieldType {FieldType_FloatingPoint,FieldType_Integer,FieldType_String,FieldType_TimeOfDay,FieldType_Boolean,FieldType_BitField,FieldType_Last = FieldType_BitField
};

然后把每类消息按如下形式抽象出来,变成一组配置:

// Example of Defining a Message Table Entry
Message BeginNumFields 5MessageName "Buoy Temperature Message"Field 1, FloatingPoint, "Average Temperature"Field 2, FloatingPoint, "Temperature Range"Field 3, Integer, "Number of Samples"Field 4, String, "Location"Field 5, TimeOfDay, "Time of Measurement"
Message End

然后,主程序只需要写成下面的伪代码形式即可:

While more messages to readRead a message headerDecode the message ID from the message headerLook up the message description in the message-description tableRead the message fields and print them based on the message description
End While

然后,第 4 行用心法中的那三个方法解决检索的问题。

第 5 行输出模块用下面的伪代码实现:

While more fields to printGet the field type from the message descriptioncase ( field type )of ( floating point )read a floating-point valueprint the field labelprint the floating-point valueof ( integer )read an integer valueprint the field labelprint the integer valueof ( character string )read a character stringprint the field labelprint the character stringof ( time of day )read a time of dayprint the field labelprint the time of dayof ( boolean )read a single flagprint the field labelprint the single flagof ( bit field )read a bit fieldprint the field labelprint the bit fieldEnd Case
End While

这样,20 多个消息类型的实现,就转变成了 6 个基本数据类型的实现,并且后者还无视了消息类型数量的变化——无非是修改配置的活,代码可以一直沿用(只要基本类型一直是这 6 个)。

这样改造后,再写成父接口+子类实现的形式,才是面向对象编程的正确打开方式

所谓好人做到底,送佛送到西,作者还是把大致流程用 C++ 过了一遍(大宗师级别还需要手把手敲代码吗?开什么玩笑):

先声明接口和子类实现:

// C++ Example of Setting Up Object Types
class AbstractField {public:virtual void ReadAndPrint( string, FileStatus & ) = 0;
};
class FloatingPointField : public AbstractField {public:virtual void ReadAndPrint( string, FileStatus & ) {// ...
}
};
class IntegerField // ...
class StringField // ...
// ...

再声明一个容纳所有六种情况的数组并初始化:

// C++ Example of Setting Up a Table to Hold an Object of Each Type
AbstractField* field[ Field_Last+1];// C++ Example of Setting Up a List of Objects
field[ Field_FloatingPoint ] = new FloatingPointField();
field[ Field_Integer ] = new IntegerField();
field[ Field_String ] = new StringField();
field[ Field_TimeOfDay ] = new TimeOfDayField();
field[ Field_Boolean ] = new BooleanField();
field[ Field_BitField ] = new BitFieldField();

最后是放到主程序模块:

// C++ Example of Looking Up Objects and Member Routines in a Table
fieldIdx = 1;
while ( (fieldIdx <= numFieldsInMessage) && (fileStatus == OK) ) {fieldType = fieldDescription[ fieldIdx ].FieldType;fieldName = fieldDescription[ fieldIdx ].FieldName;/* This is the table lookup that calls a routine depending on the type of the field just by looking it up in a table of objects. */field[ fieldType ].ReadAndPrint( fieldName, fileStatus );fieldIdx++;
}

后面的检索实现和瑞士军刀无关,就不介绍了。

6 【结语】

我是看到第三个示例,才下决心写一写这个心得的,因为之前的工作就刚好遇到了类似的情况。当时自认为十分牛逼的写法,只要放到看过这一段、或者真正领悟了表驱动法精髓的开发者面前,也不过是另一堆屎山罢了。拿到需求的第一时间,可能面临的很多因素都是非技术层面的,比如“时间紧、任务重”,或者“立等可取”,“明早出一版”,“这是上面的死命令”……要想甩锅,在职场打拼多年的老油条们谁又不会呢?但我想说的是,自己的代码写成什么样,说到底还是你对自己整个职业生涯的定位决定的。自己写的代码不求流芳百世,但求尽量别遗臭万年、别被后来接盘的某某骂到春秋战国时期就行。

关键的关键,是要明白,这样的问题其实是有瑞士军刀级的解决方案的。从菜鸟成长为高手的过程,往往就是一个不断缩小自我认知领域中“不知道自己不知道”的过程。

P.S.:对《代码大全2》感兴趣的朋友,可以自行感受一下作者那大宗师级别的气场——详见原书第 18 章《表驱动方法》(Chapter 18:Table-Driven Methods

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/369676.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

kylin arm xcb版本异常问题解决

源码编译qt 未生成xcb库&#xff0c;查看源码xcb readme.txt 提示 版本要求 下载 [ANNOUNCE] libxcb 1.14 [ANNOUNCE] xcb-proto 1.14 解压源码编译, 先编译xcb-proto sudo ./configure --prefix/usr/local/xcb-proto make make install 在编译xcb export PKG_CONFIG_PATH…

RH850系列芯片深度剖析 1.8-内存管理之MPU

RH850系列芯片深度剖析 1.8-内存管理之MPU 文章目录 RH850系列芯片深度剖析 1.8-内存管理之MPU一、MPU简介1.1 功能特性1.2 系统保护标识符(SPID)二、保护区域设置2.1 保护区域属性设置2.2 保护区域设置注意事项2.2.1 跨越保护区域边界2.2.2 无效的保护区域设置2.2.3 保护违规…

【Unity】unity学习扫盲知识点

1、建议检查下SystemInfo的引用。这个是什么 Unity的SystemInfo类提供了一种获取关于当前硬件和操作系统的信息的方法。这包括设备类型&#xff0c;操作系统&#xff0c;处理器&#xff0c;内存&#xff0c;显卡&#xff0c;支持的Unity特性等。使用SystemInfo类非常简单。它的…

Java后端每日面试题(day3)

目录 Spring中Bean的作用域有哪些&#xff1f;Spring中Bean的生命周期Bean 是线程安全的吗&#xff1f;了解Spring Boot中的日志组件吗&#xff1f; Spring中Bean的作用域有哪些&#xff1f; Bean的作用域&#xff1a; singleton&#xff1a;单例&#xff0c;Spring中的bean默…

【MySQL备份】Percona XtraBackup总结篇

目录 1.前言 2.问题总结 2.1.为什么在恢复备份前需要准备备份 2.1.1. 保证数据一致性 2.1.2. 完成崩溃恢复过程 2.1.3. 解决非锁定备份的特殊需求 2.1.4. 支持增量和差异备份 2.1.5. 优化恢复性能 2.2.Percona XtraBackup的工作原理 3.注意事项 1.前言 在历经了详尽…

【UE5.3】笔记8 添加碰撞,检测碰撞

添加碰撞 打开BP_Food,添加Box Collision组件&#xff0c;与unity类似&#xff1a; 调整Box Collision的大小到刚好包裹物体&#xff0c;通过调整缩放和盒体范围来控制大小&#xff0c;一般先调整缩放找个大概大小&#xff0c;然后调整盒体范围进行微调。 碰撞检测 添加好碰撞…

window.ai 开启你的内置AI之旅

❝ 成功是得你所想&#xff0c;幸福是享你所得 大家好&#xff0c;我是柒八九。一个专注于前端开发技术/Rust及AI应用知识分享的Coder ❝ 此篇文章所涉及到的技术有 AI( Gemini Nano) Chrome Ollama 因为&#xff0c;行文字数所限&#xff0c;有些概念可能会一带而过亦或者提供…

使用ChatGPT写论文,只需四步突破论文写作瓶颈!

欢迎关注&#xff0c;为大家带来最酷最有效的智能AI学术科研写作攻略。关于使用ChatGPT等AI学术科研的相关问题可以和作者七哥&#xff08;yida985&#xff09;交流 地表最强大的高级学术AI专业版已经开放&#xff0c;拥有全球领先的GPT学术科研应用&#xff0c;有兴趣的朋友可…

如何在 SwiftUI 中熟练使用 sensoryFeedback 修饰符

文章目录 前言背景介绍平台支持仅支持watchOS支持watchOS和iOS 基本用法预定义样式根据触发器值选择样式使用场景当值更改时触发使用条件闭包触发使用反馈闭包触发 可以运行 Demo总结 前言 SwiftUI 引入了新的 sensoryFeedback 视图修饰符&#xff0c;使我们能够在所有 Apple …

【华为战报】5月、6月HCIP考试战报!

华为认证&#xff1a;HCIA-HCIP-HCIE 点击查看&#xff1a; 【华为战报】4月 HCIP考试战报&#xff01; 【华为战报】2月、3月HCIP考试战报&#xff01; 【华为战报】11月份HCIP考试战报&#xff01; 【HCIE喜报】HCIE备考2个月丝滑通关&#xff0c;考试心得分享&#xff…

推荐3款Windows系统的神级软件,免费、轻量、绝对好用!

DiskView DiskView是一款用于管理和查看磁盘空间的工具&#xff0c;它集成了于微软的Windows操作系统资源管理器中&#xff0c;以显示直观的磁盘空间使用情况。该软件通过生成图形化地图&#xff0c;帮助用户组织和管理大量文件和文件夹&#xff0c;从而高效地管理磁盘空间。用…

鸿蒙笔记导航栏,路由,还有axios

1.导航组件 导航栏位置可以调整&#xff0c;导航栏位置 Entry Component struct t1 {build() {Tabs(){TabContent() {Text(qwer)}.tabBar("首页")TabContent() {Text(发现内容)}.tabBar(发现)TabContent() {Text(我的内容)}.tabBar("我的")}// 做平板适配…

Linux_共享内存通信

目录 1、共享内存原理 2、申请共享内存 2.1 ftok 2.2 测试shmget、ftok 2.3 查看系统下的共享内存 3、关联共享内存 3.1 测试shmat 4、释放共享内存 4.1 测试shmctl 5、实现共享内存通信 6、共享内存的特性 结语 前言&#xff1a; 在Linux下&#xff0c;有一…

解决前后端同一个端口跨域问题

前端起了一个代理 如果url是api开头的自动代理访问8080端口&#xff08;解决前后端端口不一致要么是前端代理&#xff0c;要么是后端加过滤器&#xff09; proxy:{/api:{target:http://localhost:8080,changeOrigin : true,// 替换去掉路径上的api// rewrite:(path)>path.r…

【信息学奥赛】CSP-J/S初赛07 排序算法及其他算法在初赛中的考察

本专栏&#x1f449;CSP-J/S初赛内容主要讲解信息学奥赛的初赛内容&#xff0c;包含计算机基础、初赛常考的C程序和算法以及数据结构&#xff0c;并收集了近年真题以作参考。 如果你想参加信息学奥赛&#xff0c;但之前没有太多C基础&#xff0c;请点击&#x1f449;专栏&#…

生产力工具|viso常用常见科学素材包

一、科学插图素材网站 一图胜千言&#xff0c;想要使自己的论文或重要汇报更加引人入胜&#xff1f;不妨考虑利用各类示意图和科学插图来辅助研究工作。特别是对于新手或者繁忙的科研人员而言&#xff0c;利用免费的在线科学插图素材库&#xff0c;能够极大地节省时间和精力。 …

信创-办公软件应用工程师认证

随着国家对信息技术自主创新的战略重视程度不断提升&#xff0c;信创产业迎来前所未有的发展机遇。未来几年内&#xff0c;信创产业将呈现市场规模扩大、技术创新加速、产业链完善和国产化替代加速的趋势。信创人才培养对于推动产业发展具有重要意义。应加强高校教育、建立人才…

适用于 Windows 11/10/8/7/Vista/XP 的最佳免费分区软件

无论您使用的是 SSD、机械磁盘还是任何类型的 RAID 阵列&#xff0c;硬盘驱动器都是 Windows 计算机中不可或缺的组件。在将文件保存到全新磁盘之前&#xff0c;您应该初始化它&#xff0c;创建分区并使用文件系统格式化。在运行计算机一段时间后&#xff0c;您需要收缩、扩展、…

基于深度学习的图像背景剔除

在过去几年的机器学习领域&#xff0c;我一直想打造真正的机器学习产品。 几个月前&#xff0c;在参加了精彩的 Fast.AI 深度学习课程后&#xff0c;似乎一切皆有可能&#xff0c;我有机会&#xff1a;深度学习技术的进步使许多以前不可能实现的事情成为可能&#xff0c;而且开…

Ubuntu DNS服务配置 深度解析

测试方法 resolvectl status dig alidns.com 修改实践 直接用接口配置&#xff0c;没用 /etc/resolv.conf&#xff0c;有效 /etc/netplan/01-network-manager-all.yaml,无效 /etc/systemd/resolved.conf&#xff0c;见link&#xff0c;为全局配置 [Resolve] DNS1.1.1.1 Fa…