日历

April 2011
M T W T F S S
« Mar   May »
 123
45678910
11121314151617
18192021222324
252627282930  

答读者问(3)——再谈抽象

答读者问(3)

以下内容试图回答上位读者在答读者问(2)后的进一步提问。

读者七心葵问:

得蒙先生专门撰文答复,深感荣幸,先生针对我的问题做了详细的解答并给出大量的例证,点出了很多我没有想到过的东西。 需要说明的是我其实是更倾向于OO的,喜欢OO带来的代码的整洁感和易重用,也知道不同的开发方法或语言范型有不同的适用场合,不可一概而论。对于那些驳斥OO的牛人,我不是因为他们是牛人所以怀疑OO,只是觉得他们提出的问题,自己的确得不到解答。上面提出来来的两点,我不认为是足以推翻OO,但是的确认为其有独特的价值(尤其是对比OO来看)所在,因而专门询问这两方面的情况。

关于我提的两个问题:
1 OO的抽象的对立面(“反抽象”的叫法似乎太过了,或者应该说是 “反OO抽象”,或者更进一步“反’静态类型的OO’抽象”?);
2 数据驱动编程;
我仔细想了想先生的话,并陆续读完了先生的整本书, 还是觉得有些地方想再听听先生的看法,这里写出来,请多多指教。

毋庸置疑,抽象是人类处理复杂性的利器。但是,在我看来,“抽象”这个词本身就够抽象,抽象有很多种,有不同角度的抽象,有不同层次的抽象。
在OO里面,我认为“抽象”通常的定义是“抽象就是有选择的忽略”。那么如果脱离开OO的范畴甚至脱离开软件程序设计的范畴来讲,什么是抽象呢? 很难定义,我们不妨从其反面看,什么不是抽象呢?具体的东西不抽象——具象,或叫做“具体事物”。相比于“抽象”,具象对人类来说显然是一种更直观、更可操控,因而显得更自然、更可理解的思维。

从这个角度讲,“everything is file”,这里的“file” 其实是一个概念模型,是一个“隐喻”;对比或与非门、0/1编码,它当然是一种抽象,但更是一种具象,因为Unix/Linux 真的把一切都实现成了文件!
1 你可以所有操作文件的API来操作它,也可以使用用户接口界面CLI通过操作它;你可以直接查看各种真正的文件的内容或属性,也可以通过查看/proc下相应文件来查看进程运行中的内存,还可以通过通过它查看各种I/O数据流。
2 如果以OO的眼光看,“文件”中的诸多子类型,其实存在“接口退化”的现象,例如:你无法删除/proc目录下表示进程运行时内存的文件(即便你有权限)。

这里的要点,我认为,是逻辑模型直接映射到了物理实现,因而显得透明,直观,可显,也就是“浅、平、透”。也就是透明性原则的副标题“设计要可见,以便审查和调试”所强调的要点。《Unix编程艺术》(以下称 TAOUP)中强调的其他原则,如 每个程序做一件事并做好,c语言的薄胶合层,文本化协议,多进程分离功能,数据驱动编程,微语言设计,无不体现着这一点:使逻辑模型直接映射到物理实现,可视可显、可以操作、可以度量、可以把玩可以打磨。

当然,OO设计当然也可以把逻辑设计映射到具体的物理模块,比如分目录的做法,比如分包的做法,比如最终编译为一个动态链接库的做法,都可以将抽象的模块(或层或区) 映射到这样 透明直观可显 的物理实现上,但是我认为这并不等价(尽管分包分库的做法确实已经使得程序的模块性和可理解性变得相对直观很多了)。
譬如,考虑Apache www服务器最早运行CGI动态生成网页的方式:
每一个CGI本身作为一个可执行程序,只是从stdin输入往stdout输出,每一个CGI可以通过C语言编写,也可以通过shell,perl等脚本语言编写,其本身既能和Apache配合,也可以在CLI命令行中直接使用、测试、把玩、打磨。并且,CGI还有其他的好处,例如可以按名字按目录控制权限,可以作为一项资源被引用(RESTful架构)等。
这让人联想到硬件部件具有的长宽高的物理属性,虽然这不是硬件设计的重点,但是一个集成芯片确实可以在关键时候垫桌脚;同样,一个CGI确实可以在必要的时候用作命令行管道中的一个过滤器,而这些都不是那些”由逻辑模块映射成物理模块的动态链接库”等可以做到的(也许能用到一些?比如延迟加载等特性)。

其实,在 TAOUP 一书中,作者有一次把这些 “已经实现的具象” 称为“基本抽象”:
(见 第20章:Unix程序员在30年风雨中学到最有经验的回应,就是回到最初的准则–优先从 字节流,命名空间,进程 等 Unix基本抽象中得到更多的效用,而不是增加新的东西。)

先生一书中曾提到过硬件行业往往因为诸多原因比软件行业更能贯彻OOP的理念,这里的Unix下软件设计这种“保持基本抽象、十分警惕过度纯虚化抽象”的做法似乎更类似于硬件行业的做法。事实上,我认为硬件行业不是能更好的贯彻OOP理念,而是硬件行业的接口标准比较规范(正如书中所述的原因)而不易虚化不易重新制定抽象规范(双刃剑!让人想起来电子垃圾邮件,电子邮件由于其本身的低成本和世界范围内的连通而得以流行,也正是因为同样的原因被发送垃圾邮件的人得以利用并难以预防!)。
试推想,如果所有的操作系统的API都是符合Posix标准,所有的DBMS都遵从SQL标准,软件行业看起来也会像硬件行业一样规范有序而无需经常抽象!——如果具象层面就能解决的问题,为何要抽象呢;换言之,如果抽象的规范为所有应用软件所遵循,又何必强调接口和实现的形式上分离呢。呵呵,当然没有这么理想化的世界(但是好像硬件行业的世界的确是这样的?或者还是有些变动名堂的吧,要不也不会IDE转USB,USB又转SCSI这样的接口了,当然,这些也是规范化标准化了的),这让我想起来《c++沉思录》中提到的一个笑话,说Lisp程序员在处理那些“不能很好的符合s-expression模式的数据时,他们居然认为解决这些问题的惟一办法就是让全世界都只使用Lisp这一种语言!”(从这个角度来说,OO也许好一点,至少OO在想办法把周边非OO的东西先转化为OO再处理:平的数据文件转变成有嵌套层次的对象,关系型数据库中的数据映射为对象,协议内容包装为DTO对象,简单的值包装成 valueObject,分配内存并初始化 转换为 使用工厂生产对象,原本的功能函数 转变为 serviceObject;UI的对象似乎是天然的,真正映射到现实世界的就只剩下实体对象了!这不正是当年 smalltalk 的困境么?——感谢JVM吧,同时也别奇怪为什么C++程序员总是那么OO得不够专业)

在这里,与其说Unix文化所强调的是一种抽象,不如说是一种隐喻。抽象只给你接口,隐喻给你必要的梗概信息,以避免“抽象泄露”(见《Joel说软件》一书)。譬如,windows操作系统下,本地磁盘是一个磁盘,一个可访问的异地远程目录也可以挂载为一个磁盘;当你只是打开小文本文件的时候,这两个磁盘(共同的抽象)表现出来没什么区别,但是当你想分别在两个磁盘上运行大型游戏的时候,可能就会发现加载远程磁盘的游戏内容很慢,因为抽象遗漏了一些你在这种情况下必须注意的信息。

这里我再说我对两个问题的看法:
1 “透明”
说到“透明”这个词,我认为“透明”这个词犹如灰色之余黑色白色,说灰色更近于黑色或者更近于白色,其实还是取决于灰的程度如何,因此说“因为透明而看得见”,或者“因为透明而看不见” 应该都是可以的。而在TAOUP一书中,我认为“透明”一词的含义确实更偏重于“因为透明而看得见”,比如该书第20章有这么一段话:
透明性是重用性的关键,被重用组件不仅要透露给用户“做什么”,还有透露“怎么做”。
OO的做法显然与这里强调的不同,OO强调封装强调黑盒,只告诉用户“做什么”;这里是白盒,会告诉用户“做什么以及怎样做”。

2 “文本化”
text文件比binary文件抽象程度更高更低当然并不一定,但是无结构的文本文件的确是平坦的,和关系型数据库一样,是反OO抽象的(映射成OO时是需要 序列化/反序列化或者ORM 的);但是Unix并不关心于此,Unix关心文本文件内的内容(不论是内容的格式还是字符集)是可显的,透明的,可用来调试,有利于维护,是具象的,直观的,并且可以接合其他过滤器程序处理的,其本身的形式就是元(也许更具体的应该说包括 CSV、TAB键分隔等)。

以上这些,其实与您书中提到的两种抽象“规范抽象,参数抽象”并不矛盾;
我从内心深处喜欢OO,OO带给我更多的整洁感和美的享受。而我也总有一些时候认为,也许Unix文化下的这些准则 只是在更小的局部中解决问题会用到的原则等。
但是数据驱动编程,(我认为其起源于朴素的 参数化思想 和 代码生成技术)则真正的是一种导向与众不同的设计范型。

Booch 书中提到的三类软件设计方法:Top-down structured design、Data-driven design与Object-oriented design,(也是引述别人的),我对其中的 Data-driven design (简称 DDD似容易和 domain-driven design 弄混,既然也有叫做 Data-driven programming,我这里简称 DDP)尤其感兴趣,因为我想知道可以和 结构化设计,OO设计 这样并驾齐驱,号称为众多软件设计方法之祖的这样一门编程语言有何奥妙,为何少听人说道?

这里先说一下我为何愿意相信这三类软件设计方法是众方法之祖,因为:

1 权威作者的认可
如 booch 书中所说“这些方法中的大多数基本上都是类似主题的一些变奏”,“绝大多数方法都可以归为以下三类之一”

2 我个人相信如果从某一个角度看,抓住本质的话,众多的分类可以归并成几类的

3 逻辑的分析:
所谓的“软件设计方法”决定了设计软件的方式(-orinted),我认为这3类方法是完全从不同的角度来看待 软件设计的,
其中 结构化世界 和 OO 不相容在同一个设计中不能并存;而 DDP 则与其他两种方法都各自相容;
众多编程范式可以归入这3类软件设计方法,例如:
DDP 其实就是 机制与策略分离,DDP的起源应该是朴素的 参数化抽象 思想, 包含了以下这些编程范式:
language-orinted programming( declarative programming、function programming、logic-driven、knowledge-driven )
meta-programming( generic programming、aspect-orinted programming、代码生成技术 )
而 event-driven 这种范式似乎划归到 OO 中去更为合适,

TAOUP一书中专门提到了 DDP 和 OO 的对比:
“数据驱动编程有时会跟面向对象混淆起来…他们之间至少有两点不同。第一,在数据驱动编程中,数据不仅仅是某个对象的状态,实际上还定义了程序的控制流;第二,OO首先考虑的是封装,而数据驱动编程看重的是编写尽可能少的固定代码。Unix中 数据驱动编程的传统比OO更深厚。”
Booch书中也提到对DDP的描述
“在这种方法中,系统输入和输出之间的映射关系驱动着软件体系的结构。与 结构化设计 一起,数据驱动设计 已经成功地被应用于一些复杂的领域。”
这里的“映射关系”我理解为 “机制与策略相分离”的 机制的部分,也对应于 LOP 中的 解释器 或 虚拟机,即“程序中固定不变的部分”。
因为booch书引用的相关文献我并没有找到进一步参考学习,所以我只是从个人的理解上进行分析。先生内功深厚,涉猎广博,是否可以予以指点?

按照Tanenbaum在《结构化计算机组成》一书中所言:
机器的指令系统层提供了底层或与非门,0/1数据,电子部件 的一层虚拟机;
操作系统在此之上提供了另一层虚拟机(C语言层紧附于这层虚拟机,所以说是一个“薄胶合层”),
高层语言在此基础上构建更高层的虚拟机(无论是 编译时面对的编译器平台和库 还是 运行时面对的解释器)。

沿用 Tanenbaum 的观点,我认为 数据驱动编程(包括LOP中的解释器)就是在操作系统层之上再构建一层虚拟机 的过程。
为什么这么说? 虚拟机之上 和 虚拟机之下 的部分的对比,其实不是数据和指令的对比,也不是虚拟机的软件指令和机器硬件指令的对比,
而是 相对固定的部分 和 相对易于变动的部分 的对比,是变化率的对比,这正是 数据驱动编程(策略与机制分离) 的本质,是一种朴素的 参数化 的思想。

数据与代码分离,本质上其实是 相对固定不变的部分(机制) 和 相对容易变动的部分(策略) 的分离。
数据驱动编程,本质上是符合分离原则和保变原则的,如 bob大叔所说,变化率 是程序始终应该注意的一个问题。
由此联想到的还有:(数据和代码的分离,微语言和解释器的分离,被生成代码和代码生成器的分离);
更近一步:(微内核插件式体系结构,)

元编程 应该说是更加泛化的 数据驱动编程,元编程 不是新加入一个间接层,而是退居一步,使得当前的层变成一个间接层。
元编程 分为 静态元编程(编译时) 和 动态元编程(运行时),
静态元编程本质上是一种 代码生成技术 或者 编译器技术; 动态元编程一般通过 解释器(或虚拟机)加以实现。

数据驱动编程当然也不应该说是“反抽象的”,但的确与 “OO抽象”的思维方式是迥然不同,泾渭分明的,
如TAOUP一书中所述:“在Unix的模块化传统 和 围绕OO语言发展起来的使用模式之间,存在着紧张的对立关系”
应该说 数据驱动编程的思路与 结构化编程 和 OO 是正交的,更类似一种”跳出三界外,不在五行中”的做法。
或许这是您书中言明的,抽象分为”规范抽象”和”参数化抽象”的精义所在?(我不知道您的这两种抽象思想划分的名字来自于何处,但是我很喜欢这一对词)

考虑 数据驱动编程 与 OO 的不同机制,可以考虑一下 虚拟机 和 类框架 的不同:
虚拟机将逻辑细节(策略)抛给微语言代码;(只提供域原语,更稳定更灵活)——给用户出的是主观题,要求用户具有一定能力
类框架提供所有可能的领域组件,供用户组合;(提供了所有需要的东西,比较僵化)——给用户出的是选择题,对用户要求没有上面那么高

总而言之,在我看来,TAOUP 强调的这两种Unix世界下的传统 对应于 OO所强调的,可以概括如下:
1 不强调 数据抽象,但强调模块化封装,强调浅平透的设计,强调直观简单的具象;
2 不强调 类层次结构,但强调 策略和机制的分离(从而最多2层,策略层再分为 相对固定的部分 和 相对变动的部分 从而形成新的虚拟机层不容易,但不是不可能)
3 多态本来就不是OO所特有的,Linux内核里面 实现 VFS和各种格式的文件系统时就采用了这种很自然的做法;考虑到 duck type,mixin 等动态语言中的多态机制就更不能如此说了。
多态对于人类来说其实是很自然的做法,它给程序设计带来了模糊语义,从而使得代码具有更强的表现力,更简单的形式。

是否可以这么说:有两个世界,OO的世界,和OO以外的世界。
OO的世界对于程序员来说是理想化的,层次分明,抽象整洁的,非常便于处理复杂性的,各司其职共同协作处理问题的,即您书中所言的公民社会;
OO以外的世界,具象横陈的世界,其复杂性的来源正如brooks所说 分为本质复杂性(业务本身的) 和 非本质复杂性(技术上的)。
——但是在我看来,这些两种复杂性的差别并不在于哪些是业务的哪些是技术的哪些是业务的,而在于哪些是已经被固化下来的。

某个领域之所以处理起来还比较复杂,是因为:

1 领域本身的确很复杂:
a 领域内的逻辑本身的确很复杂,很难有确定的算法来处理
b 领域内的逻辑还是在变化中的,
c 领域内逻辑的数量是极大的。

2 领域基础条件的不成熟:
a 还没有探索出好的模型,或者该模型还没有被普遍认识到,
b 还没有制定出好的各个层面的规范并集体遵守,
c 还没有固化足够多的恰当的机制的部分。

3 人类心智的限制,一切的背后都有 人的因素 作为依据:
a 人同时关注的信息数量:7+-2 (所以要分模块)
b 人接收一组新信息的平均时间5s (所以要简单,系统总的模块数不要太多)
c 人思维的直观性(人的视觉能力 和 模糊思维能力),这意味这两点:
A “直”——更善于思考自己能直接接触把玩的东西;(所以要“浅平透”、使用具象的设计),
B “观”——更善于观图而不是推算逻辑;(所以要 表驱动法,数据驱动编程,要UML,要可视化编程——当然MDA是太理想化了)
d 人不能持续集中注意力(人在一定的代码行数中产生的bug数量的比例是一定的,所以语言有具有表现力,要体现表达的经济性)
所以要 机制与策略分离,要 数据和代码分离(数据驱动编程),要 微语言,要 DSL,要LOP,
e 人是有创造欲,有现实利益心的(只要偶可能总是不够遵从规范,或想创造规范谋利——只要成本能承受,在硬件领域就不行)

您点出的一句话 “如果抽象解决的复杂不及其带来的复杂,则说明该抽象是不当的。” 可能正点出了要点所在。
我绝无意批判“抽象”,我只是想说明,的确有另外的思维方式的存在—— 1 直观具象的、浅平透的设计,2 数据驱动编程

小子浅薄,借贵宝地洋洋洒洒说了这么多,愿得先生指点,解后进心中之惑,不胜感谢。

 

作者hui答:

面对如此长篇的评论,作为博主我表示压力很大:)事实上,无论是从评论的篇幅还是内容上看,皆可独立成篇。这里如此大段地引用,窃感颇有掠人之美的嫌疑。不过作为作者,也为能有这么一位认真的读者而欣慰不已。

言归正题。关于抽象,《冒号课堂》中虽有不少阐述,但限于篇幅,未能完全展开。正因意犹未尽,总想找机会续谈,以解心中之痒,你的提问客观上加速了这一进程。

应当说,你对抽象的理解基本上是到位的,只是稍有偏差。比如你提到抽象是一种选择性的忽略(selective ignorance),这无疑是正确的,只可惜你前面加上了“OO”的限制,这就有点画蛇添足了。OO尽管有其侧重的抽象形式,如数据抽象(data abstraction)、类型层级(type hierarchy)、多态抽象(polymorphic abstraction),但它们并未脱离一般抽象的范畴。

你在文中提到:具象对人类来说显然是一种更直观、更可操控,因而是显得更自然、更可理解的思维。听上去不无道理,但似乎有点执着于“抽象”的表面字义了。大多数人对“抽象”一词的第一反应是形容词的抽象(abstract),于是总不自觉地将之与“深奥”、“模糊”、“不直观”、“不具体”等相关联,这固非大谬,然而在编程设计中,人们关注或强调的抽象更多地当是名词的抽象(abstraction)或动词的抽象(abstract,指“抽象化”)。比如著名的针对接口编程(programming to interface)原则、依赖反转(Dependency Inversion)原则,本质上都是提倡针对抽象编程,这样做的好处暂且不提,代码一定会因此变得更不直观、更难操纵、更不自然、更难理解吗?恐怕未必吧。如果说增加的抽象层(abstraction layer)(比如abstract class、interface等)有时会制造一些复杂的话(但最终目的是为了减少复杂),那么普通API的调用则是再常见不过了。相比一个API清晰的接口和明确的规范,它的冗长、复杂的实现代码虽然更加具体,但不是更难以把握和理解吗?

为避免空泛的语言分析,这里顺手拣个例子:不妨注意看上段话中的“形容词”、“名词”、“动词”的字体,我在html源码中特意为它们分别加上了<em>标签,以示强调。为什么不用更简单的<i>标签呢?表面原因是:<i>标签已经过时了,并且只要读者的浏览器支持,此处<em>中的内容会显示比较美观的楷体而不是斜体(根据css中的设置),而深层原因是:标签<em>比<i>的抽象层次更高。看到此处,恐怕许多人会暗生疑惑:两个再简单不过的html标签,竟也扯上了抽象,是不是有些小题大作?再说,<em>与<i>不是一样地直观、具象吗?哪里有一点抽象的影子?其实不然。往大处说,抽象存在于每一段代码之中,哪怕是不入一般程序员法眼的html代码;更广泛地,抽象存在于人们每时每刻的思维之中——如果不有意无意地忽略一些细节,人类的思维寸步难行。往小处说,i是italic(斜体)的缩写,而em是emphasis(强调)的缩写,前者是手段,后者是目的;前者是表象,后者是实质。从这个意义上看,后者当然更抽象。认清这一点,便更能认识到css的价值所在:它把传统的混杂了内容与形式的html进行了抽象分解,让html专注于表达数据(data),css专注于表达样式(style)。这样html标签不再身兼内容与形式的双重职责(如<i>),而仅仅让标签的名称来表达规范的语义,让标签的内容来表达实际的数据(如<em>)。其好处是显而易见的:一方面,在数据不变的情况下,程序员可根据客户喜好定制不同的css;另一方面,相同的css可以应用于不同的数据。于是,形式与内容两方面的可维护性和可重用性都得以增强。与此同时,它们的抽象性也得到了增强,因为每一次分解——或者说,关注点的分离(SoC)——都是被分解者之间彼此无视对方的产物,即所谓的“选择性的忽略”。标签<em>之所以比<i>更抽象,正是由于它忽略了样式的细节(注:尽管标签<i>同样也能用css来定制表现形式,但它没有明确的规范语义)。最后,不要忘记标签<i>本身也蕴涵着抽象:在html规范的保证下,编码者根本不用费心如何让一段文字变成斜体,也不用关心用户使用何种浏览器。饶舌至此,是欲说明一点:设计(不限于编程设计)中的抽象与口语中的抽象并不完全等同,由于其相对性和普适性,看似具体的事物也有抽象的一面,看似抽象的事物也有具体的一面。在提及抽象时,建议少通过形容词(abstract)来联想,多用名词(abstraction)或动词(抽象化)。或者更好地,根据上下文把抽象直接替换为“规范”(specification)、“约定”(convention)、“协议”(protocol)、“概念”(concept)、“语义”(semantic)、“接口”(interface)、“服务”(service)、“职责”(responsibility)之类的名词,或“忽略”、“分离”、“提炼”、“抽取”之类的动词。而抽象的对立面,用“具体”、“具象”、“明晰”、“直观”等就不如“细节”(detail)、“实现”(implementation)等来得更贴切。

你在文中还提到:“everything is file”中的“file” 其实是一个概念模型,是一个“隐喻”…与其说Unix文化所强调的是一种抽象,不如说是一种隐喻。前一句非常正确,后一句值得商榷。正如书中(P181)指出的那样:模型是抽象的结晶,而抽象成功的标志之一是形成引起共识的概念。这些概念在现实世界中通常有自然而朴素的对应,否则难以达成共识。OOP之所以盛行,与它在许多应用中比较符合人类的认知模式是密不可分的。比如,在图形界面的设计中,让Window类对应于窗口,Button类对应于按钮、TextField类对应于文本框;在银行业务中,让Bank类对应于银行、Account类对应于帐户、Transaction类对应于交易,等等。无论是设计者还是使用者,都能迅速地理解程序的设计和编码意图。相反,如果有人把一些关联度不大的数据与函数粘合为一个类,整体上没有形成一个合理有效的概念,那么这个类能称之为真正意义上的抽象吗?当然,有些概念在现实世界中难以找到直接的对应物,这时人们就会借助隐喻(metaphor)了。file把生活中的物理文件与磁盘中的信息块对应起来,二者具有共通性,且形象易记,是一个成功的抽象。总之,建立模型、形成概念、采用隐喻的过程,与抽象不仅毫不矛盾,而且正好合辙。

除了抽象的层次、抽象的角度、概念模型、隐喻等以外,你还涉及了抽象的一些其他关键点,可见你对抽象的思考的确是非常深入的。其中包括:

  • 抽象的扩展(expansion)

实际上,如果追根究底的话,file还不算得真正的隐喻,因为最初计算机中的file指的真是物理的打孔卡片。因此,说是抽象的扩展更加确切。同为抽象的概念,Unix用户脑中的file远比Windows用户的更为抽象。在一般的windows用户的眼中,文件与文件夹(folder)是泾渭分明的。而在Unix中,file不仅包含普通文件,而且包括目录(directory)。不仅如此,在Unix下的链接(link),用户终端(terminal)、硬件设备(device)、网络连接(network connection)以及包括管道(pipe)、共享内存(shared memory)、UNIX域套接字(unix domain socket)等在内的进程间通信(IPC),通通可看成file。顺便提一下,你为了强调file的具象,提到“Unix/Linux 真的把一切都实现成了文件”。可你下面所举的proc文件系统就不是传统意义上真正的文件系统,而是伪文件系统(pseudo-filesystem)。其实这仍是抽象的体现,具体来说,是抽象的扩展——从打孔卡片扩展到磁盘字节块、到I/O字节流(byte stream)、到更广泛的信息资源(resources)。同样,你引用TAOUP中的那句话,“Unix优先从 字节流,命名空间,进程 等 Unix基本抽象中得到更多的效用,而不是增加新的东西”,也反映Unix中的一些抽象并非一蹴而就的,而是逐步扩展的。扩展抽象(而非新建抽象)的一个好处是,减少因新概念的引进而带来的复杂性,并且强化原有的抽象;另一好处是,能让针对该抽象而设计的代码具有更广泛的适用性。

  • 抽象的退化(reduction)

理想的抽象是对某一概念本质特征进行精准的捕捉和描述,不要多也不要少。多则限制该抽象的适用范围,少则不能充分利用该抽象的特性。然而在实际编程中,这几乎是不可能的。当抽象过多时,便会产生退化现象。比如,除非被卸载(unmount),/proc下的文件无法被删除;比如,一个声明为List类型的UnmodifiableList类不支持add、remove等操作;等等。

  • 抽象的泄露(leakage)

“抽象只给你接口,隐喻给你必要的梗概信息,以避免‘抽象泄露’”。如果你该处的抽象泄露来自Joel的The Law of Leaky Abstractions ,那么这种泄露绝非隐喻所能避免的。事实上,这是因为抽象过少之故,即在某些场合下,抽象没有提供足够的信息。为了挖掘必要的信息,人们不得不钻开抽象严密封装的外壳(crust),深入到它声称能够忽略的、此时却不能忽略的内部细节,由此形成了泄露。Joel提到的本地磁盘与远程磁盘的差别是一例,另一个典型的例子是:在Windows下用鼠标拖放文件图标时,将移动相应的文件。这种抽象操作明显来自隐喻,然而当目标文件与源文件处于不同的驱动器时,操作的结果却是文件拷贝,从而增加了用户的学习成本。尽管程序开发者有充分的技术理由区分不同驱动器,但他们没有理由让用户关心其中的细节。程序设计者一定要牢记:你可以嘲笑用户的无知,但必须重视用户的抽象。与前面的例子不同,这种抽象的泄露原本是可以避免的,故而Alan Cooper在《about face》中对此专门作出了批评。此外,在静态类型的语言中,向下转型(downcasting)之所以不受推荐,不仅因为它不够安全,也因为试图深入到更底层类型的代码可能是抽象泄露的征兆。

“如果所有的操作系统的API都是符合Posix标准,所有的DBMS都遵从SQL标准,软件行业看起来也会像硬件行业一样规范有序而无需经常抽象”。有道理,不过如果注意到“规范”本身就是一种抽象,这么说可能更准确:软件行业如果像硬件行业一样规范有序,许多不必要的中间抽象就能省去了。

抽象是对付复杂的有效工具。一方面,通过抽象剔除或屏蔽非本质的部分、提炼并合并本质的部分(即《冒号课堂》中提到的减法与除法),从而降低复杂性;另一方面,通过抽象对复杂分而治之,比如,宏观上水平分层(layer),垂直分区(partition),微观上分模块、分步骤,等等。说到这里,有读者可能会不屑一顾:不就是简化、合并、分层、分块、分步吗?听起来一点也不新鲜,为何一定要拽出“抽象”这个词呢?且慢,除了复杂外,软件设计的另一大敌人是变化。(注:当然,如果把变化作为复杂的一个因素也未尝不可。为方便说明问题,这里特意把变化单列)现实中有这样的程序员,他能独立地设计并开发具有一定难度和规模的软件系统,但他的代码对用户需求的变动具有异常的敏感性,并且很难被其他程序员理解,甚至也很难被以后的自己理解。这是何故?仔细审查,会发现他的代码虽然逻辑正确、格式规范、形式上井井有条,各种设计模式的身影不时出没,甚至还遵循着业界推崇的各种最佳实践,唯独缺乏“抽象”的神韵。例如,虽然遵循了模块化的原则,但每个模块的职责并不清晰,分工也不明确,模块之间的交互耦合不像是前世的约定,更像是今生的邂逅。重要的API没有详细的文档说明,即使有,实现代码也常常不买账。当具体的代码缺失了抽象的外壳,便丧失了必要的缓冲区,一旦变化来临,难免引发代码的强烈震荡。总而言之,把抽象作为设计的出发点,不仅能更合理地组织代码,而且为代码之间划分了明确的概念边界。这种边界的划分越合理、边界的规范越明晰、边界内的代码越遵循规范,系统越稳定、越易维护。

凡事有度,过犹不及。对于软件开发而言,抽象再重要,它也只是手段而非目的。如果待解决的问题并不足够复杂、或者并不足够多变,那么过多的抽象反而成为掣肘。Eric Raymond在TAOUP中对OOP抱怨最大的地方是,OOP语言容易让抽象泛滥。这种指责有一定的道理,相比Unix编程中最常见的C语言,OOP语言增加了多种抽象机制,加之OO界倡导的设计模式、各类流行的框架、企业级架构模式等等,使得OO程序员习惯了为代码裹上层层厚重的抽象(尽管他们可能并未意识到抽象)。一旦抽象层过多、抽象点过密,会造成代码难以理解、优化、调试和维护,运行效率也会大打折扣,这与Unix提倡的“简单、清晰、透明”(即你提到的“浅平透”)格格不入。话说回来,真正有错的不是OOP或抽象本身,而是那些唯对象论者和滥用抽象者的错。

关于Unix中透明(此处指“透明得看得见”)与文本化的问题,我上次的回答过于匆忙,既然你此次再度提起,这里专门补充一下。有一个重要背景不能不提,Unix环境下的编程设计有一个显著的特征:程序经常以进程(process)为单位来模块化,TAOUP中称之为“multiprogramming”。Unix推崇这种设计方式,是有其天然的优势作基础的:相对廉价的进程创建、多种强大的进程间通信(IPC)机制、丰富多样的命令和工具、方便的脚本环境,等等。且不论由此卷入了并发范式,这种设计方式本身就很独特,与人们习惯的以函数或类为基本模块单位的做法大相异趣。你指出了Unix下软件设计透明的一面,这固然在一定意义上与抽象相对,但可能没有意识到这种透明很大程度上仍源自抽象。何出此言呢?方才言及,Unix下有丰富的应用,它们一般都遵从“只做一件事、做好一件事”的哲学,并且每个应用通常都有详尽的文档(man page),这不是绝佳的抽象和规范吗?大多数应用都是二进制文件,这不是不透明(opaque)吗?(即使有公开的源码,恐怕也很少人费事去读)可见,正是底层应用建立了高密度的抽象,才使得上层应用的抽象较为稀疏,从而显得简单、浅显和透明。此外,Unix下大量采用脚本编程,而脚本语言的抽象一般比通用程序语言、尤其是编译型语言少,因为后者为防御变化带来的源码修改而引入的许多抽象,对于前者往往并不那么重要。

说到文本化,与Unix的multiprogramming设计也是密切相关的。由于鼓励通过合作的进程来分解复杂,进程之间的交互便显得尤为重要,而文本流(text stream)是最通用的接口。一旦文本作为接口,哪怕再透明,它也具有了一定的抽象职能。至于为什么是文本而非二进制(binary),我以为并非问题的本质。诚然,文本更简单、直观、易于人工编辑,但如果文本格式不规范,同样会增加代码的复杂度。相反,如果二进制文件有严格而公开的规范,并且有方便的API进行读写操作,那么同样适合作为进程之间的通信接口。只是事实上,Unix下有大量对文本处理的工具,如cut、grep、head、sort等之类过滤器(filter)以及更强大的小语言sed、awk等,乃至大语言perl、python等,而二进制格式的工具相应就少得多。文本的直观透明性倒是带来一个好处,可用于挖掘抽象规范之外信息。举个小例子,要求在Linux下获得硬盘(假设mount在/dev/hda上)的序列号。直接用C语言实现并不困难,但如何用一行命令来实现呢?通过hdparm命令可以获得硬盘的一些参数,但没有专门针对序列号的,故而无法直接获得所需的数据。好在该命令的输出是文本的,经过观察和试验,我们完成了任务:hdparm -i /dev/hda | awk -F= '/SerialNo=/{print $NF}'。不过这并非没有后顾之忧,由于hdparm对输出格式并无明确的规范,上述代码并不能保证对Linux的所有发行版本都适用。如果不追求理论上的完美,该解法倒也差强人意:换台机器如果不灵,大不了再作调整。这很公平,从抽象之外获取的”非法“信息,自然要付出一定的代价——或者是不确定性的代价,或者是额外编码的代价。

上次你提到数据驱动编程与语言导向编程(LOP),当时我只强调了二者的区别,没有仔细思考二者的联系,一时失察,还望见谅。从数据驱动的特点来看,参数抽象可看作其最初级的形式,领域特定语言(DSL)可看作其最高级的形式。具体分析非三言两语所能言明,或许另开专题更好。你的其他思考也非常有价值,我对抽象的论述也远未尽意,只是夜深人乏,恕我暂不能一一展开,下次有机会再讨论吧。

最后,请别再称呼我为“先生”,我会恍惚觉得自己老朽了。

Share
 请您评分1星(很差)2星(不行)3星(一般)4星(不错)5星(很棒) (已有1人评分,平均分为:5.00 / 5)

1 comment to 答读者问(3)——再谈抽象

  • “身兼内容与形式的双重职责”
    空格,tab,回车等也成为’字符’是又一个例子

    SoC和抽象,最近倾向于认为这两者是有必然联系的,理由是一方对其它各方从知情到不知情,便形成了忽略,从而形成了抽象
    这所以说’倾向’是因为..我是不是把语义提升(semantic promotion)纳为抽象的必要特征呢?以及..能不能说知识减少就意味着语义提升呢?我目前只有些倾向而已

Leave a Reply

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

  

  

  

This blog is kept spam free by WP-SpamFree.