第2章 是懒人造就了方法
“僰蘭道有蜀王兵 ,亦有神作大滩江中。其崖崭峻不可破,(冰)乃积薪烧之。”
——《华阳国志》
1. 是懒人造就了方法
战国时期的李冰凿了一座山。
史记中说是“蜀守冰凿离堆”,是说李冰在成都的时候凿出了离堆。一说是李冰将都江堰附近的玉垒山凿了一个大口子,叫宝瓶口,而凿的石头就堆成了离堆。另一说,则是李的确是凿了一座“(溷)崖”,但是是在沫水,亦即是今天的大渡河。
在哪里凿的山,是史学家都说不清楚的事。但的确凿了一座山,而且方法是“(因)其崖崭峻不可破,(冰)乃积薪烧之”。
我们已经看到事物的进化了。同是战国时代,《列子·汤问篇》里的愚公就要“碎石击壤”,而李冰就已经懂得“积薪烧之”了。
会有人说愚公是“碎石”,并没有说他“碎石”的方法究竟是“斧钺以凿之”,还是“积薪以烧之”。但想想那个时代,如果有人懂得了烧石头这个方法,哪能不立即载
文志之,永世传承。
再说了,愚公嘛。愚者怎么会呢?这还需要分析吗?需要吗?
所以愚公会凿,而李冰会烧。那李冰又是为什么会用“烧”这种方法来碎石的呢?如果李冰也象愚公那样日复一日地督促着他的团队凿石开山,那他一定没有时间来学习、寻找或者观察,当然也不会发现“烧”这种方法可以加快工程进度,使得一大座山短时间就被哗啦哗啦地给“碎”掉了。
要知道李冰的团队可是成百上千人,要修堰筑坝,还要“凿离堆”,当然还要吃喝啦撒睡。所以李冰如果忙起来的话,他必然是“受命以来,夙夜忧叹”,必然食难下咽,睡无安枕。反之,李冰一定是个闲人,可以闲到没事去看火能不能把石头烧爆。
这么大个工程里,如果有一个人会闲到看火烧石头,那他一定很懒。那么多事堆着不去做,去看烧石头,你说他不是懒是什么。
正是一个懒人造就了“烧石头”这个“碎石”的方法。愚公太勤快了,勤快得今天可以比昨天多凿一倍的石头。或者在愚公的项目计划案的首页里就写着朱笔大字:“吾今胜昨倍许,明胜今倍许,而山不加增,何苦而不快。”但是越发的勤快,愚公将越发没有机会找到更快的方法,
人的精力终归是有极限的。提出新的“方法”,解决的将是影响做事成效的根本问题。而愚公可以多吃点饭,多加点班,但突破不了人的精力的极限。
记住,在两千年前的某一天,闲极无聊的李冰下厨给夫人炒了一个小菜,他突然发现垒灶的鹅卵石被烧得爆裂开来,遇水尤甚。从此《史记》上记下了“蜀守冰凿离堆”,而《华阳国志》上记下了他做这件事的方法“积薪烧之”。 在差不多同一时间,愚公在山北之塞“碎石击壤”。
2. 一百万行代码是可以写在一个文件里的
早期写程序,都是将代码打在穿孔纸带上,让计算机去读的。要让计算机读的纸带当然是连续的,这无需多讲。其实我也没有那样写过程序,真实的苦楚我也不知道。
后来有了汇编语言,可以写一些代码了。这时的代码是写在文本文件里,然后交给一个编译器去编译,再由一个链接器去链接,这样就出来了程序。
第一个写汇编的人,可能写的是有名的“Hello World”程序,那个程序写在一个文件里就行了。所以后来就成了习惯,大家都把代码写到一个文件里。早期的汇编语言里,
GOTO 语句是用得非常非常频繁的,将一个语句 GOTO到另一个文本文件里去,既不现实也不方便。所以大家习以为常,便统统地把代码写到一个文件里。
再后来出了高级语言,什么 C 呀,Pascal 呀之类的。既然大家已经形成习惯了,所以很自然地会把一个程序写到一个文件里。无论这个程序有多大,多少行代码,写到
一个文件里多方便呀。
直到如今语言发展得更高级了。可是程序员的习惯还是难改,一旦得了机会,还是喜欢把代码写到一个文件里的。
好了,有人说我是想当然尔。En,这当然是有实据的。记得 Delphi 1.0 版发布的时候,全世界一片叫好声。连“不支持双字节”这样的大问题,都不影响他在华语地区的推广。然而不久,爆出了一个大 BUG!什么大 BUG 呢?
Delphi 1.0 的编译器居然不支持超过 64K 的源代码文件!
这被 Fans 们一通好骂。直到我用 Delphi 2.0 时,一个从 VB 阵营转过来的程序员还跑过来问我这件事。好在Delphi 2.0 改了这个 BUG,这让当时我的面子上好一阵风
光。
64k 的文件是什么概念呢?
1 行代码大概(平均)是 30 字节,64k 的源代码是 2184行,如果代码风格好一点,再多一些空行的话,差不多也就是 3000 行上下。
也就是说,在 Delphi 1 的时代(以及其后的很多很多时代),程序员把 3000 行代码写到一个文件里,是司空见惯的事了。如果你不让他这样写,还是会被痛骂的呢。
所以呢,按照这一部分人的逻辑,一百万行代码其实是可以写在一个文件里的。不单可以,而且编译器、编辑器等等也都必须要支持。这才是正统的软件开发。
勤快的愚公创造不了方法。这我已经说过了。对于要把“一百万行代码写到一个文件”,查找一个函数要在编辑器里按五千次 PageDown/PageUp 键的勤快人来说,是
不能指望他们创造出“单元文件(Unit)”这样的开发方法来的。
然而单元文件毕竟还是出现了。这个世界上,有勤快人就必然有懒人,有懒人也就必然有懒人的懒方法。
有了单元文件,也就很快出现了一个新的概念:模块把一个大模块分成小模块,再把小模块分成更细的小小模块,一个模块对应于一个单元。于是我们可以开始分工作了,一部分人写这几个单元的代码,另一部分则写那几个。
很好,终于可以让源代码分散开来。结构化编程的时代终于开始了,新的方法取代了旧的方法,而这一切的功劳,是要归终于那个在按第 5001 次PageDown键时,突然崩溃的程序师。他发自良心地说:不能让这一切继续下去了,我一定要把下一行代码写到第二个文件里去。我发誓,我要在编译器里加入一个Unit关键字。
3. 你桌上的书是乱的吗?
几周之前,在一所电脑培训学校与学生座谈时,一个学员问我:“为什么我学了一年的编程,却还是不知道怎么写程序呢”。
我想了想,问了这个学员一个问题:“你桌上的书是乱的吗?”
他迟疑了一下,不过还是回答我道:“比较整齐。”
我当时便反问他:“你既然知道如何把书分类、归整得整整齐齐地放在书桌,那怎么没想过如何把所学的知道分类一下,归纳一下,整整齐齐地放在脑子里呢?”
如果一个人学了一年的编程,他的脑袋里还是昏乎乎的,不知道从哪里开始,不知道如何做程序。那想来只有一个原因:他学了,也把知识学进去了,就是不知道这些知识是干什么的。或者说,他不知道各种知识都可以用来做什么。
其实结构化编程的基本单位是“过程(Procedure)”,而不是上一小节说到的“单元(Unit)”。然而在我看来,过程及其调用是 CPU 指令集所提供的执行逻辑,而不是普通的开发人员在编程实践中所总结和创生的“方法”。 这里要提及到CPU指令集的产生。产生最初的指令集的方式我已经不可考证,我所知道的是CISC指令集与RISC指令集之争在 1979 年终于爆发。前者被称为复杂指令集,然而经过Patterson等科学家的研究,发现 80%的CISC指令只有在 20%的时间内才会用到;更进一步的研究发现,在最常用的 10 条指令中,包含的流程控制只有“条件分支(IF...THEN...) ”、“跳转(JUMP)” 和“调用返
回(CALL/RET)”
于是 CISC 被 RISC(精简指令集计算机)替代了。动摇CISC 指令集地位的方法,就是分类统计。
正如 CISC 指令集搅乱了一代程序设计师的思路一样,大量的知识和资讯搅乱了上面给我提问的那位学员的思想。他应该尝试一下分类,把既有的知识象桌子上的书一样整理一下,最常用的放在手边,而最不常用的放在书柜里。如果这样的话,我想他已经在九个月前就开始写第一个软件产品了。
你桌上的书还是乱的吗?
4. 我的第一次思考:程序 = 算法 + 结构 + 方法
我的第一次关于程序的本质的思考其实发生在不久前。那是我在 OICQ 上与 Soul 的一次谈话。
Soul(王昊)是DelphiBBS现任的总版主,是我很敬重的一位程序员。那时我们正在做DelphiBBS的一个“B计划II”,也就是出第二本书。他当时在写一篇有关“面向对象(OOP)”的文章。而我正在写《Delphi源代码分析》,在初期的版本里,有“面向对象”这一部分的内容。我们的对话摘要如下 :
Soul:我在写书讨论“面向对象的局限性”
我 :En.这个倒与我的意见一致。哈哈哈。
我 :“绝对可以用面向过程的方法来实现任意复杂的系统。要知道,航天飞机也是在面向过程的时代上的天。但是,为了使一切变得不是那么复杂,还是出现了‘面向对象程序设计’的方法。”
我 :——哈,我那本书里,在“面向对象”一部分前的引文中。就是这样写的。
Soul:现在的程序是按照冯。诺伊曼的第一种方案做的,本来就是顺序的,而不是同步的。CPU怎么说都是一条指令一条指令执行的。
我 :面向过程是对“流程”、“结构”和“编程方法”的高度概括。而面向对象本身只解决了“结构”和“编程方法”的问题,而并没有对“流程”加以改造。
Soul:确实如此。确实如此。
我 :对流程进一步概括的,是“事件驱动”程序模型。而这个模型不是OO提出的,而是Windows的消息系统内置的。所以,现在很多人惑于“对象”和“事件”,试图通过OO来解决一切的想法原本就是很可笑的。
Soul:我先停下来,和你讨论这个问题,顺便补充到书里去。 我 :如果要了解事件驱动的本质,就应该追溯到Windows内核。这样就涉及到线程、进程和窗体消息系统这些与OO无关的内容。所以,整个RAD的编程模型是OO与OS一起构建的。现在很多的开发人员只知其OO的外表,而看不到OS的内核,所以也就总是难以提高。
Soul:OO里面我觉得事件的概念是很牵强的,因为真正的对象之间是相互作用,就好像作用力和反作用力,不会有个“顺序”的延时。
我 :应该留意到,整个的“事件”模型都是以“记录”和“消息”的方式来传递的。也就是说,事件模型停留在“面向过程”编程时代使用的“数据结构”的层面上。因此,也就不难明白,使用/不使用OO都能写Windows程序。
我 :因为流程还是在“面向过程”时代。
Soul:所以所谓的面向对象的事件还是“顺序”的。所以我们经常要考虑一个事件发生后对其他过程的影响,所以面向对象现在而言是牵强的。
我 :如果你深入OS来看SEH,来看Messages,就知道这些东西原本就不是为了“面向对象”而准备的。面向对象封装了这些,却无法改造它们的流程和内核。因为OO的抽象层面并不是这个。
我 :事件的连续性并不是某种编程方法或者程序逻辑结构所决定的。正如你前面所说的,那是CPU决定的事。
Soul:比如条件选择,其实也可以用一种对象来实现,而事实没有。这个是因为cpu的特性和面向对象太麻烦。
我 :可能,将CPU做成面向对象的可能还是比较难于想象和理解。所以MS才启动.NET Framework。我不认为.NET在面向对象方法上有什么超越,也不认为它的FCL库会有什么奇特的地方。——除了它们足够庞大。但是我认为,如果有一天OS也是用.NETFramework来编写的,OS一级的消息系统、异常机制、线程机制等等都是.NET的,都是面向对象的。那么,在这个基础上,将“事件驱动”并入OO层面的模型,才有可能。
Soul:所以我发觉面向对象的思维第一不可能彻底,第二只能用在总体分析层上。在很多时候,实质上我们只是把一个顺序的流程折叠成对象。
我 :倒也不是不可能彻底。有绝对OO的模型,这样的模型我见过。哈哈~~但说实在的,我觉得小应用用“绝对OO”的方式来编写,有失“应用”的本意。我们做东西只是要“用”,而不是研究它用的是什么模型。所以,“Hello World”也用OO方式实现,原本就只是出现在教科书中的Sample罢了。哈哈。
Soul:还有不可能用彻底的面向对象方法来表达世界。 因为这个世界不是面向对象的。 是关系网络图,面向对象只是树,只能片面的表达世界。所以很多时候面向对象去解决问题会非常痛苦。所以编程退到数据结构更合理,哈哈。
我 :如果内存是“层状存取”的,那么我们的“数据结构”就可以基于多层来形成“多层数据结构”体系。如果内存是“树状存取”的,那么我们当然可以用“树”的方式来存取。——可惜我们只有顺序存取的内存。
我 :程序=数据+算法 ——这个是面向过程时代的事。 程序=数据+算法+方法 ——在OO时代,我们看到了事件驱动和模型驱动,所以出现了“方法”问题。
Soul:我的经验是:总体结构->面向对象,关系->数据结构,实现->算法
Soul:看来我们对面向对象的认识还是比较一致的。
我第一次提到我对程序的理解是“程序=数据+算法+方法”,便是在这一次与 Soul 的交谈之中。在这次的交谈
中的思考仍有些不成熟的地方,例如我完全忽略了在面向过程时代的“方法”问题。实际上面向过程开发也是有相关的“方法”的。
所谓“面向过程开发”,其实是对“结构化程序设计”在代码阶段的一个习惯性的说法。而我忽略了这个阶段的“方法”的根本原因,是即使没有任何“方法”的存在,只需要有了“单元(Unit)”和“模块(Module)”的概念,在面向过程时代,一样可以做出任意大型的程序。在那个时代,“方法”问题并不会象鼻子一样凸显在每一个程序员的面前。
面向过程开发中,“过程(procedure)”是 CPU 提供的,“单元(unit)”则是编译器提供的(机制)。程序员不需要(至少是不必须)再造就什么“方法”,就可以进行愚公式的开发工作了。
如果不出现面向对象的话,这样伟大的工程可能还要再干一百年??
而与“面向对象”是否出现完全无关的一个东西,却因为“过程”和“单元”的出现而出现了。这就是“工程(engineering)”。
评论