数据驱动编程与防御式编程
Symbol 与 Tagged Data
Tagged Data,一言以蔽之,就是为每种数据结构增加一个唯一地标识符。有了唯一标识符,写程序时就可以根据标识符来判断需要完成的相应处理,在数据结构的使用与实现解耦的基础上,增加了代码整体的易读性。
举例:复数
复数在笛卡尔坐标系上可以拆分成实部 (Real Part) 和虚部 (Imaginary Part),在极坐标系上可以拆分成模 (Magnititue) 和辐角 (Angle)。复数的操作有些在笛卡尔坐标系上比较方便,如复数的加法,只需要把两个复数的实部和虚部分别相加;而有些则在极坐标系上比较方便,如复数的乘法,只需要把两个复数的模相乘,辐角相加。假设“复数”这个数据结构的实现已经存在,我们便可以实现复数加法和乘法:
(define (+c z1 z2)
(make-complex-from-rect (+ (real z1) (real z2))
(+ (imag z1) (imag z2))))
(define (*c z2 z2)
(make-complex-from-polar (* (mag z1) (mag z2))
(+ (angle z1) (angle z2))))那么,数据结构“复数”会如何实现?
小明的实现
小明是传统的极简主义者,他选择用 list 来存储复数的实部和虚部。因此他的复数构造器实现如下:
; constructors
(define (make-complex-from-rect rl im) (list rl im))
(define (make-complex-from-polar mg an)
(list (* mg (cos an))
(* mg (sin an))))由于小明的复数内部利用实部和虚部表示,当构造器接受模和辐角时,构造器需要将其转化成实部和虚部。然后是选择器的实现,只要保证实现和使用质检的契约不变即可:
小红的实现
小明的同学小红,是个喜欢北极的极简主义者,她选择用 list 来存储复数的模和辐角。她的实现如下:
问题来了
老师说,小明和小红需要共同完成一个关于复数应用的项目,小明或者小红遇到了(list a b)时,a和b分别表示什么?是实部和虚部还是模和辐角?显而易见,我们需要某种方式去判断这个复数来自于小明的实现还是小红的实现;或者从根本上,我们需要知道这个复数内部是用笛卡尔坐标系表示还是极坐标系表示。办法很简单,利用原始类型 symbol 为两种表示法打上标签,即 tagged complex number。此时,构造器改造如下:
数据驱动编程与防御式编程
从上面的实例可以看出,Tagged Data 有两个特点
为每种复杂数据添加标识符
对复杂数据操作时,根据它的标识符来判断使用哪种方法
使用 Tagged Data 变成可以让我们做两件事情:Data Directed Programming 和 Defensive Programming
Data Directed Programming
数据定向编程其实就是上文实现 “复数” 时所使用的方法,每种复杂数据自带标签,程序通过读取其身上的标签来判断应该对它做怎样的操作。这种编程方式使得模块化变得十分自然,而代码整体也变得更容易扩展和维护。
Defensive Programming
防御性编程,一言以蔽之,就是:
It's much better to give an error message than to return garbage
换句话说,就是我们只对系统的输入中符合前提假设的进行处理,一旦遇到与假设不合的情况,立即优雅地抛错,而不是让错误的结果继续在程序里传播。
例:Arithmetic Expressions
在本例中,我们将构建一个可以创建、评价数学表达式的系统,这个系统不仅可以评价简单的数学表达式,如:
它还能够简化区间和精确范围,如
我们从简单的情况开始:
It is almost always easier to extend a base system, than to try to do the whole thing at once
此时,我们的第一版 eval 如下
然后考虑扩展到区间的 ADT
让我们的 eval 兼容区间的加法
然而,以上的实现有两个问题:
如果 eval 数值与区间的和,系统会抛错,系统本身并未考虑数值与区间相加的情况
如果我们把精确度的数据作为参数传入,按照目前的逻辑,只要不是数值就认为它是区间,因此这种实现无法做到 defensive programming
以上的例子恰好展示了我们在构建复杂系统时出现函数传入错误参数的成因
写代码手滑打错
逻辑有缺陷
改变了系统的一部分代码,但没有修改与之关联的其余代码
以及后果
Garbage in garbage out
没有 defensive programming 导致错误在程序中传播
但究其根本原因,在于我们依赖于数据的内部结构来判断这个数据的类型,但这违背了抽象的原则,使得数据的实现和使用耦合,且数据的内部结构并不能唯一代表该数据的类型 — 因此我们需要引入Tagged Data, 同时引入Data Directed Programming与Defensive Programming。
引入 Tagged Data 来解决上述问题
从 eval-3 与 eval-4 我们可以总结出
带标签的 ADT 的一般模式通常由以下几点构成
一个储存标签的变量
在构造器 (constructor) 中将标签打在每个数据的 car 上
写一个函数 (predicate) 来判断数据类型是否与标签一致
任意操作 (operations) 拿到数据后,先摘下标签,再操作数据,最后将便签打上
使用带标签的 ADT 时
要尽可能使用数据标签来判断作何操作
返回时要返回带标签的数据
加入区间 ADT
如此一来,利用 Tagged Data 我们成功利用 tag 来引导程序运行以及在遇到预期之外的输入时抛错,做到了简单、安全。
加入精确度 ADT
从 eval-5 到 eval-7 我们可以总结出:
Data Directed Programming 可以简化逻辑层次较高的代码
程序在每次操作前都检查了标签才能真正做到 Defensive Programming
通常情况下,ADT 内部不会检查标签,原因在于性能考虑
参考
Last updated