程序员思维
起因
首先简单说一下,为什么我会想到这个话题。主要有这么几方面的原因。
当我试图回过头去总结大学在计算机专业所学习的一些理论和知识的时候。发现,在学校里面学习的一些东西,走了两个极端。 一个极端是偏向了细节。比如我们学习的那些《***程序设计》的课程。看这几门课的名称的我们能够很明显的看出,***是一个形容词定语,用来修饰主题“程序设计”。但是,你却非常意外的意识到《C++面向对象程序设计》和面向对象程序设计貌似关系不大,整门课程主要讲了一个更好用的C,比C好用的地方是在于人家有对象。学习了这一系列《***程序设计》的课程之后的结果是:你知道了汇编的语法,知道了C的语法,知道了C++的语法;但是用他们能做些什么却不知道。 另外一个极端是偏向了宏观。在我们对工程是个什么东西,项目是个什么东西,软件又是个什么东东还没有构建起最基本概念的时候。我们上了《软件工程》这样的课程。劈头盖脸的理论砸下来了,没有消化也没有吸收。你甚至找不到,学这些课程对于你的编程实践有什么样的版主。等你写代码的时候,你真真切切的发现软件工程这个东西对于我这个函数怎么命名,模块怎么划分真的帮助不是很大啊。 其实,随着时间一点点的流逝,工作年限的增加。你能够发现,这两个极端的东西还是有作用的。只不过,学校里面的课程少了一些能够把它们融会贯通,串联起来的东西。而这个中间起到粘连作用的东西就是我所思考的。
另外一个原因是,当与其他同学或同事去写同一个程序的时候。他们有些时候看到你的代码,然后就会评论:你这代码耦合性太紧,内聚性太差,不符合高内聚低耦合的概念啊。你当然,有点气不过啊。你凭什么说我代码没高内聚低耦合啊。于是你就问他为什么这么说。结果绝大多数时候,对方只是说”我感觉“。即使有些时候他们根据他们多年的编程经验说出了一些什么东西。但是你还是感觉说服不了你。你心里会暗自嘀咕,不就是你感觉嘛。你感觉的东西也不一定对。 同样的问题,也会发生在当你去评论别人的代码质量差的时候。要想让别人认识到问题所在是异常困难的。果然世界上最难的事情有两个:把别人的钱放进自己的口袋里面,把自己的思想放进别人的脑袋里面。 于是,你就会去思考,有没有一种理论或者评价的方法,是大家都认可的。而且的确能够衡量出一个设计耦合性和内聚性的强弱来呢?
还有一个原因。和上面的原因有点类似,在编程中接触到的很多同事。或者同样是编程的人。我发现他们的能力参差不齐。而且,有些时候这种能力的参差不齐不止是因为经验造成的。当然,一个有十年多编程经验的人肯定比一个刚刚入门的小孩写的程序靠谱。但是,你会惊奇的发现有些人写了四五年程序后,程序的质量甚至比十几年经验的人要好很多。与他们聊天的时候,就发现导致这种现象的原因在于他们思维方式的不同。有很多十几年编程经验的同事,很多时候只是在用感觉编程。在长期编程时间中形成的某些思维定势帮助他们能够快速的完成编码工作,但是仅限于此。而那些年少却有能力的同事,虽然年轻但是能够编写高质量的代码。因为他们不是通过经验积累获得编码能力;而是通过理论学习,并且快速的消化掌握了和十年多经验同事一样的思维定势。不一样的是,他们还能通过理论触类旁通。写出更高质量的代码。 以前有个同事(他不是干程序员的,而是产品经理),他和其他同龄人最大的不同就是思维方式。他总喜欢干一件事情就是找规律。后来,他想自己做点东西,一时又没有拉到程序员一起干,就自己操刀学习JS。你看他的代码,才一两个月的时间,代码质量和以JS为职业的人已经有的一拼了。每当想到这,我就想:我们还能说对于编程这件事情经验是最重要的吗?
总结一下上面的原因就是,在我们的编程实践中,我们需要找到一种思维框架来帮助我们设计和解释我们的程序。这个东西就是要讨论的程序员思维。
从哪里来,是什么
先讲一个笑话,说是一个外国的哲学家来中国做客。中途去一个小区找一个朋友,然后就发现原来中国的小区保安都是哲学家。他进门的时候,保安问了哲学家三个问题: 你从哪里来? 你是谁? 你到哪里去? 哲学家就感慨这是终极问题啊,自己穷其一生也没能解答,没想到这么个小地方还有人关心哲学的终极问题。其实人家保安就是想搞明白这个“哲学家”嘛。因为你要弄明白一个概念,只要能够回答好这三个问题,基本上就比较OK了。同样我们要弄明白“程序员思维”,也要问三个问题?
- 程序员思维是什么?
- 程序员思维从哪里来?
- 程序员思维到哪里去?
好吧,这是三个终极的问题。没有标准答案,也没有什么不标准的答案。我只是试图给出自己的一个思考的总结。
类比法的一个描述
对于程序员思维这个东西现在还给不出一个大家都能够接受的描述来定义它。当我们无法非常准确的定义一个东西的时候,我们回去找这个东西的类似的东西来描述它。就像字典里面用同义词来解释某一个词语一样。我们先来看看程序员思维像个什么东西。说的具体一点就是我们编程敲代码这个事情像是什么? 这里有不同的说法。 有些人说像是做数学证明题。你看到了一道数学题,然后就开始在你脑袋里面搜罗能够用来证明这个题目的定理和方法。然后,按照一定的顺序把这些定理和方法组织起来,一步接一步的,就证明了。而写程序,有些时候就是产品来了一个需求,我们就开始在脑袋里搜罗能够帮助我们实现这个需求的方法和工具,很多时候是一些算法或者程序的组织形式,然后把这些东西一个字符一个字符的敲下来,不出意外的情况下,产品的需求就实现了。 有些人说,编程序就像是在操作机器。和司机了没有多大的区别。你往左打方向盘,车就往左开;你往右打方向盘,车就往右开。你给机器下达一条进行加法的命令,机器即开始进行加法运算;你给机器下达一条进行减法的命令,机器就开始进行减法运算。 有些人说,程序设计或者编程这个东西。就像是作家写作,都是在操作字符。都是敲敲打打的写了一大堆字符,然后竟然还能够通过这些字符赚钱。 有些人说,整个软件的制作过程和盖房子差不多。都有终端的用户,一个是买房,一个是软件用户。有了需求然后就有了公司,房地产公司和软件公司。公司就开始招人了。房地产公司找了建筑设计师,软件公司找了架构师。有了设计,就得动工啊。房地产公司找了包工头把项目包给了报共同,软件公司招了一批项目经理来负责控制进度。包工头找了几个能挑头的熟练工,然后每个熟练工后面跟着一批农民工。项目经理开始给高级程序员布置任务,每个高级程序员手下都有几个码农。然后开始没日没夜的干活。最后把东西交付到终端用户手上。 。。。。。 如果我们继续这样类比下去,可能这是个无穷无尽的大列表。咱们适可而止。 不知道通过上面的这些类别有没有发现一些什么东西。上面说的这些事情,都是在用工具来解决问题或者通过工具来达到一些目的。编程或者程序设计也没有逃出这个框架,我们用程序这个工具来完成需求。 马克思*韦伯在《新教理论与资本主义》中首次提出了一个概念能够很好的解释上面列举的事情的共性——工具理性。所谓“工具理性”,就是通过实践的途径确认工具(手段)的有用性,从而追求事物的最大功效,为人的某种功利的实现服务。工具理性是通过精确计算功利的方法最有效达至目的的理性,是一种以工具崇拜和技术主义为生存目标的价值观,所以“工具理性”又叫“功效理性”或者说“效率理性”。 仔细一想,程序员思维就是在工具理性这个大的思维框架下面。所以一,程序员思维势必符合工具理性的一些特征。这有点想父类的子类的关系。工具理性是父类,程序员思维是子类。既然如此我们要了解程序员思维,那么我们先来看看工具理性。
工具理性
虽然工具理性是在19世纪提出来的一个概念。但是像绝大部分的概念和定理一样,虽然我们没有认知到它,但是它已经在那里。哥白尼之前人们没有认识到地球是围绕着太阳转的,但是太阳在那之前也是围绕着太阳转的。虽然我们在很长一段时间内没有明确的提出工具理性的概念,但是我们确实一直在实践着工具理性,而且是从一开始。 人从灵长类的猿开始进化的时候,与猿最大的不同就是我们开始使用工具。WIKI上关于工具的定义是这样的:工具(英语:Tool)或装备、器材(英语:Equipment)是指能够方便人们完成工作的器具,它的好处可以是机械性,也可以是智能性的。大部分工具都是简单机械;例如一根铁棍可以当作杠杆使用,力点离开支点越远,杠杆传递的力就越大。 虽然绝大部分的篇幅是在描述机械性的工具,但是其中很短的一句“也可以是智能性的”貌似说出了些什么。有很多人类学家认为人与动物的最大却在于使用工具,后来又有一部分人对这句话做出了修正:人与动物最大的却别是在于能够使用智力工具。试想一下,对于使用石头这个工具来砸核桃这个事情来说,是这个工具石头重要,还是知道用什么样的石头,怎样的区砸重要呢? 或许从砸核桃这个事情上,你就能够看到后来我们称之为“算法”的东西的影子。
- 把核桃放在硬质的地上。
- 找到一块非常坚硬的石头。
- 抬高石头,用力砸核桃。
- 监察核桃是否碎裂,如果碎裂执行5,没有碎裂执行3
- 吃掉核桃
而在这个过程中,是哪块石头并不重要,重要的是它能够帮助我们完成上面的步骤。真正的核心还是步骤。 古希腊有个叫泰勒斯的数学家和哲学家。生活穷困潦倒,竟然靠借贷度日。但是,他醉心研究哲学和数学。周围的邻居就嘲笑他,说他净整些没用的。思考的那些东西又不能当饭吃。刚开始,泰勒斯没太当回事。后来周围的人都以此为笑柄,甚至教育孩子不要学他。于是,在某一年的春季。那年的春天比较干旱,人们都预计这年的橄榄的收成会很不好。但是,泰勒斯借钱,用极低的价格买断了当地榨汁机的使用权。当秋天到来,结果橄榄丰收。但是榨汁机的使用权却在泰勒斯手上,泰勒斯高价出售了榨汁机的使用权。大赚!原来泰勒斯,在很早之前就通过自己的思考和分析,预见到今年橄榄会丰收。漂亮的给了嘲笑他的人响亮的回击。 这个过程中,橄榄重要吗?榨油机重要吗?都重要,但是没有泰勒斯的思维重要。是他思维的工具帮助他赚到了这一笔钱。同样思维方式或者思维工具起到决定性作用的例子数不胜数。比如以少胜多的赤壁之战之类。 在工具这个层面理解的话,思维工具要比机械性的工具要重要的多。而仔细分析,其实机械性的工具只不过是把我们的思维工具固化成了实物——把我们对锋利的认知、理解与分析物化成了刀子;把我们对色彩的认知理解分支物化成了笔;把我们对水流动性的认知物化成了杯子。。。。。。不一而足。的确思维工具要想对现实产生作用,必须经过物化这一层。思维工具只有变成一个实物了才能够真正的对现实世界产生作用。于是机械性工具的作用,不过就是承载我们思维工具的嘛,不就是我们思维工具的物化嘛! 而程序呢,通过计算机这个机械性的工具物化了,然后又链接到了各式各样的物化的工具上面。而神奇的是,我们在有了计算机之后创造工具的方式与以前有天壤之别,我们不需要直接去创造机械性的工具了。程序让我们有了间接操作机械性工具的能力。我们只需要负责思考就好了。而这思考的内容是我们目前为止掌握的所有的知识。 程序这个东西实在物化我们现在几乎所有能够掌握的知识。我们通过程序协作,通过程序作图,通过程序喂猪,通过程序通信,通过程序定饭店。。。 于是程序设计就是利用计算机将知识物化,并利用知识的可复现性来对现实产生作用。程序员所要做的事情就是用理性的工具来完成这一过程。而程序员思维就是帮助我们来完成这个事情的那一整套的思维框架。
一些历史
大家比较公认的世界上第一个程序员是个女程序员。阿达·洛芙莱斯(Ada Lovelace)的名字是否使你想起什么呢?没有?它应该在电脑、平板或手机上出现过。这位维多利亚时代的女士、三个孩子的母亲出生于1815年,是世界上第一个计算机程序员。 他是查尔斯·巴贝齐(Charles Babbage)的朋友,巴贝奇发明了一个很奇怪的机器(可以把它看作是第一台计算机)——差分机,洛芙莱斯用巴贝齐的数学机器翻译意大利数学家路易吉·蒙博(Luigi Menabrea)里的笔记。洛芙莱斯进行的很顺利,用她自己丰富的数学知识扩展了蒙博里的笔记,她用一种算法使计算机能够识别一系列数字,有效的发明了第一个计算机程序。 她最要的贡献就是提出了类似于软件或者程序的概念,将思维工具与具体的机械属性剥离。在单一的机械工具上你能够物化不同的思维工具。这就是程序的魅力所在,它让思维工具减少了对机械的依赖。 她和巴贝奇所处的时代是工业革命爆发的时代,或者话句话说他们所处的时代是一个工具大爆炸的时代。通常认为人类生产力突飞猛进式的跃进有三个时期: 第一个是农业革命,我们发明了农业,将天文学、数学。。。。那个时候我们能够掌握的绝大部分知识应用在了耕作这个事情上。产生了生产力的跃进。 第二个是工业革命,正是巴贝奇所处的时代。工业化生产出现,人们使用大型工具,组织起工厂来生产。文艺复兴和航海大发现以来,所做的知识储备在工业革命得到直接物化。 第三个是信息革命,即现在。我们在使用信息技术,当然主要就是编写程序来间接物化知识。让知识能够不直接变成机械性的物体也能够作用于现实。 其实这三次生产力跃进时间附近都有人类的知识大爆发: 农业革命:在国外对应着古希腊文明,在中国对应着三皇五帝到春秋战国。 工业革命之前是文艺复兴和航海大发现。 信息革命,现在时近现代科学体系的完善。 而有些人把文艺复兴称之为工具理性的复兴。文艺复兴的本意是指古希腊文明的复兴。古希腊文明也是工具理性的一次兴盛。即使到了现在,现代科学体系下面的,工具理性依然是核心骨架。在整个人类文明中,工具理性起到了举足轻重的作用。每一次革命都是知识大爆发的时代,也是我们把知识物化成工具的时代。 到上面为止,我简单的说了一下自己对于程序员思维父类——工具理性的认知。我自己在构建起工程理性这个概念之后。带着这些认知,看一下软件(程序员思维)的历史,准确说应该是编程的历史。 关于编程的历史推荐大家看一本书《信息简史》,其中第四章到第七章从信息的角度切入讲解了程序的一个起源。包括硬件层面和软件层面。当然作为Code Monkey我们比较关心的是软件层面的问题。 上面说到了Ada Lovelace,第一个提出了程序的概念,减小了思维工具对机械工具依赖性的人。而她背后的那个人——实现她的程序硬件的巴贝奇,也是非常重要的。巴贝奇的差分机被认为是第一台计算机。一个能够生产数的机器。但是从巴贝奇到第一台现代计算机ENAIC诞生的一百多年的时间里面。人们可能更多的是关注,直接用机械来物化思维工具。但是他们在做着为软件能够真正意义上的诞生做着基础性工作——他们创造了信息论。 信息论的诞生,为我们物化思维工具,严格意义上说是间接的物化思维工具提供了一整套的理论依据和方法论。我们不再需要直接去制作机械性的工具就能够改变现实。我们在信息论的基础上。软件或者程序设计开始突飞猛进。在短短的60年间,已经完成了几个量级的跨越。为了更好的展现程序设计的历史,我们从两个方面来看。一个是软件的发展;一个是计算机程序语言的发展历史。
软件历史
计算机软件技术发展很快。50年前,计算机只能被高素质的专家使用,今天,计算机的使用非常普遍,甚至没有上学的小孩都可以灵活操作;40年前,文件不能方便地在两台计算机之间进行交换,甚至在同一台计算机的两个不同的应用程序之间进行交换也很困难,今天,网络在两个平台和应用程序之间提供了无损的文件传输;30年前,多个应用程序不能方便地共享相同的数据,今天,数据库技术使得多个用户、多个应用程序可以互相覆盖地共享数据。了解计算机软件的进化过程,对理解计算机软件在计算机系统中的作用至关重要。
第一代软件(1946-1953)
第一代软件是用机器语言编写的,机器语言是内置在计算机电路中的指令,由0和1组成。例如计算2+6在某种计算机上的机器语言指令如下: 10110000 00000110 00000100 00000010 10100010 01010000 第一条指令表示将“6”送到寄存器AL中,第二条指令表示将“2”与寄存器AL中的内容相加,结果仍在寄存器AL中,第三条指令表示将AL中的内容送到地址为5的单元中。 不同的计算机使用不同的机器语言,程序员必须记住每条及其语言指令的二进制数字组合,因此,只有少数专业人员能够为计算机编写程序,这就大大限制了计算机的推广和使用。用机器语言进行程序设计不仅枯燥费时,而且容易出错。想一想如何在一页全是0和1的纸上找一个打错的字符! 在这个时代的末期出现了汇编语言,它使用助记符(一种辅助记忆方法,采用字母的缩写来表示指令)表示每条机器语言指令,例如ADD表示加,SUB表示减,MOV表示移动数据。相对于机器语言,用汇编语言编写程序就容易多了。例如计算2+6的汇编语言指令如下: MOV AL,6 ADD AL,2 MOV #5,AL 由于程序最终在计算机上执行时采用的都是机器语言,所以需要用一种称为汇编器的翻译程序,把用汇编语言编写的程序翻译成机器代码。编写汇编器的程序员简化了他人的程序设计,是最初的系统程序员。
第二代软件(1954-1964)
当硬件变得更强大时,就需要更强大的软件工具使计算机得到更有效地使用。汇编语言向正确的方向前进了一大步,但是程序员还是必须记住很多汇编指令。第二代软件开始使用高级程序设计语言(简称高级语言,相应地,机器语言和汇编语言称为低级语言)编写,高级语言的指令形式类似于自然语言和数学语言(例如计算2+6的高级语言指令就是2+6),不仅容易学习,方便编程,也提高了程序的可读性。 IBM公司从1954年开始研制高级语言,同年发明了第一个用于科学与工程计算的FORTRAN语言。1958年,麻省理工学院的麦卡锡(John Macarthy)发明了第一个用于人工智能的LISP语言。1959年,宾州大学的霍普(Grace Hopper)发明了第一个用于商业应用程序设计的COBOL语言。1964年达特茅斯学院的凯梅尼(John Kemeny)和卡茨(Thomas Kurtz)发明了BASIC语言。 高级语言的出现产生了在多台计算机上运行同一个程序的模式,每种高级语言都有配套的翻译程序(称为编译器),编译器可以把高级语言编写的语句翻译成等价的机器指令。系统程序员的角色变得更加明显,系统程序员编写诸如编译器这样的辅助工具,使用这些工具编写应用程序的人,称为应用程序员。随着包围硬件的软件变得越来越复杂,应用程序员离计算机硬件越来越远了。那些仅仅使用高级语言编程的人不需要懂得机器语言和汇编语言,这就降低了对应用程序员在硬件及机器指令方面的要求。因此,这个时期有更多的计算机应用领域的人员参与程序设计。 由于高级语言程序需要转换为机器语言程序来执行,因此,高级语言对软硬件资源的消耗就更多,运行效率也较低。由于汇编语言和机器语言可以利用计算机的所有硬件特性并直接控制硬件,同时,汇编语言和机器语言的运行效率较高,因此,在实时控制、实时检测等领域的许多应用程序仍然使用汇编语言和机器语言来编写。 在第一代和第二代软件时期,计算机软件实际上就是规模较小的程序,程序的编写者和使用者往往是同一个(或同一组)人。由于程序规模小,程序编写起来比较容易,也没有什么系统化的方法,对软件的开发过程更没有进行任何管理。这种个体化的软件开发环境使得软件设计往往只是在人们头脑中隐含进行的一个模糊过程,除了程序清单之外,没有其他文档资料。
第三代软件(1965-1970)
在这个时期,由于用集成电路取代了晶体管,处理器的运算速度得到了大幅度的提高,处理器在等待运算器准备下一个作业时,无所事事。因此需要编写一种程序,使所有计算机资源处于计算机的控制中,这种程序就是操作系统。 用作输入/输出设备的计算机终端的出现,使用户能够直接访问计算机,而不断发展的系统软件则使计算机运转得更快。但是,从键盘和屏幕输入输出数据是个很慢的过程,比在内存中执行指令慢得多,这就导致了如何利用机器越来越强大的能力和速度的问题。解决方法就是分时,即许多用户用各自的终端同时与一台计算机进行通信。控制这一进程的是分时操作系统,它负责组织和安排各个作业。 1967年,塞缪尔(A.L.Samuel)发明了第一个下棋程序,开始了人工智能的研究。1968年荷兰计算机科学家狄杰斯特拉(Edsgar W.Dijkstra)发表了论文《GOTO语句的害处》,指出调试和修改程序的困难与程序中包含GOTO语句的数量成正比,从此,各种结构化程序设计理念逐渐确立起来。 20世纪60年代以来,计算机用于管理的数据规模更为庞大,应用越来越广泛,同时,多种应用、多种语言互相覆盖地共享数据集合的要求越来越强烈。为解决多用户、多应用共享数据的需求,使数据为尽可能多的应用程序服务,出现了数据库技术,以及统一管理数据的软件系统——数据库管理系统DBMS。 随着计算机应用的日益普及,软件数量急剧膨胀,在计算机软件的开发和维护过程中出现了一系列严重问题,例如:在程序运行时发现的问题必须设法改正;用户有了新的需求必须相应地修改程序;硬件或操作系统更新时,通常需要修改程序以适应新的环境。上述种种软件维护工作,以令人吃惊的比例消耗资源,更严重的是,许多程序的个体化特性使得他们最终成为不可维护的,“软件危机”就这样开始出现了。1968年,北大西洋公约组织的计算机科学家在联邦德国召开国际会议,讨论软件危机问题,在这次会议上正式提出并使用了“软件工程”这个名词。
第四代软件(1971-1989)
20世纪70年代出现了结构化程序设计技术,Pascal语言和Modula-2语言都是采用结构化程序设计规则制定的,Basic这种为第三代计算机设计的语言也被升级为具有结构化的版本,此外,还出现了灵活且功能强大的C语言。 更好用、更强大的操作系统被开发了出来。为IBM PC开发的PC-DOS和为兼容机开发的MS-DOS都成了微型计算机的标准操作系统,Macintosh机的操作系统引入了鼠标的概念和点击式的图形界面,彻底改变了人机交互的方式。 20世纪80年代,随着微电子和数字化声像技术的发展,在计算机应用程序中开始使用图像、声音等多媒体信息,出现了多媒体计算机。多媒体技术的发展使计算机的应用进入了一个新阶段。 这个时期出现了多用途的应用程序,这些应用程序面向没有任何计算机经验的用户。典型的应用程序是电子制表软件、文字处理软件和数据库管理软件。Lotus1-2-3是第一个商用电子制表软件,WordPerfect是第一个商用文字处理软件,dBase III是第一个实用的数据库管理软件。
第五代软件(1990-今)
第五代软件中有三个著名事件:在计算机软件业具有主导地位的Microsoft公司的崛起、面向对象的程序设计方法的出现以及万维网(World Wide Web)的普及。 在这个时期,Microsoft公司的Windows操作系统在PC机市场占有显著优势,尽管WordPerfect仍在继续改进,但Microsoft公司的Word成了最常用的文字处理软件。20世纪90年代中期,Microsoft公司将文字处理软件Word、电子制表软件Excel、数据库管理软件Access和其他应用程序绑定在一个程序包中,称为办公自动化软件。 面向对象的程序设计方法最早是在20世纪70年代开始使用的,当时主要是用在Smalltalk语言中。20世纪90年代,面向对象的程序设计逐步代替了结构化程序设计,成为目前最流行的程序设计技术。面向对象程序设计尤其适用于规模较大、具有高度交互性、反映现实世界中动态内容的应用程序。Java、C++、C#等都是面向对象程序设计语言。 1990年,英国研究员提姆·柏纳李(Tim Berners-Lee)创建了一个全球Internet文档中心,并创建了一套技术规则和创建格式化文档的HTML语言,以及能让用户访问全世界站点上信息的浏览器,此时的浏览器还很不成熟,只能显示文本。 软件体系结构从集中式的主机模式转变为分布式的客户机/服务器模式(C/S)或浏览器/服务器模式(B/S),专家系统和人工智能软件从实验室走出来进入了实际应用,完善的系统软件、丰富的系统开发工具和商品化的应用程序的大量出现,以及通信技术和计算机网络的飞速发展,使得计算机进入了一个大发展的阶段。 在计算机软件的发展史上,需要注意“计算机用户”这个概念的变化。起初,计算机用户和程序员是一体的,程序员编写程序来解决自己或他人的问题,程序的编写者和使用者是同一个(或同一组)人;在第一代软件末期,编写汇编器等辅助工具的程序员的出现带来了系统程序员和应用程序员的区分,但是,计算机用户仍然是程序员;20世纪70年代早期,应用程序员使用复杂的软件开发工具编写应用程序,这些应用程序由没有计算机背景的从业人员使用,计算机用户不仅是程序员,还包括使用这些应用软件的非专业人员;随着微型计算机、计算机游戏、教育软件以及各种界面友好的软件包的出现,许多人成为计算机用户;万维网的出现,使网上冲浪成为一种娱乐方式,更多的人成为计算机的用户。今天,计算机用户可以是在学习阅读的学龄前儿童,可以是在下载音乐的青少年,可以是在准备毕业论文的大学生,可以是在制定预算的家庭主妇,可以是在安度晚年的退休人员,所有使用计算机的人都是计算机用户。
计算机语言发展历史
(PS:摘自WIKI)
1940之前
第一个编程语言比现代的计算机还早诞生。首先,这种语言是种编码(en:code)。 于1801年发明的提花织布机(或称甲卡提花织布机,英文:en:Jacquard loom),运用打孔卡上的坑洞来代表缝纫织布机的手臂动作,以便自动化产生装饰的图案。 Ada Lovelace在1842年至1843年间花费了九个月,将意大利数学家Luigi Menabrea关于查尔斯·巴贝奇新发表机器分析机的回忆录翻译完成。她于那篇文章后面附加了一个用分析机计算伯努利数方法的细节,被部分历史学家认为是世界上第一个电脑程序。这个故事我们上面也说过。 Herman Hollerith在观察列车长对乘客票根在特定位置打洞的方式后,意识到他可以把资讯编码记载到打孔卡上,随后根据这项发现使用打孔卡来编码并纪录1890年的人口统计资料。 第一个严格意义上的计算机程式码是针对他们的应用面设计的。在20世纪的前十年主要是用十进制来算数,后来人们发现不只是用文字,也可以用数字来表现逻辑。举例来说,阿隆佐·邱奇曾以公式化(formulaic)的方式表达λ演算。图灵机是一种纸带标记(tape-marking)机器(就像电话公司用的那种)操作方法抽象化后的集合。图灵机这种透过有限数字(finite number)呈现机器的方式,奠定了程式如同冯·诺伊曼结构计算机中的资料一样地储存的基础。但不同于λ演算,图灵机的程式码并没有办法成为高阶编程语言的基石,这是是因为它主要的用途是分析算法的复杂度。 就像许多历史上的”第一次”一样,第一个现代编程语言也很难界定。最一开始是因为硬件限制而限定了语言,打孔卡允许80行(column)的长度,但某几行必须用来记录卡片的顺序。FORTRAN则纳入了一些与英文字词相同的关键字,像是”IF”、”GOTO”(原字词为go to),以及”CONTINUE”。之后采用磁鼓(magnetic drum)作为内存使用,也代表计算机程式也必须插入(interleave)到磁鼓的转动(rotation)中。和现今比较起来,这也让编程语言必须更加依赖硬件(hardware-dependent)。 对部分的人认为必须在”编程语言”的状态确立之前,根据能力(power)以及可读性(human-readability)的程度来决定历史上第一个编程语言是什么语言。提花织布机和查尔斯·巴贝奇所制作的差分机(en:Difference Engine)都具备在大量限制下,简单描述机器应执行行为的语言。也有种并非设计给人类运用的受限特定领域语言(en:domain-specific language),是将打孔卡运用到自动演奏钢琴(en:player piano)上。
1940年代
最早被确认的现代化、电力启动(electrically powered)的计算机约在1940年代被创造出来。程式设计师在有限的速度及内存容量限制之下,撰写人工调整(hand tuned)过的组合语言程式。而且很快就发现到使用组合语言的这种撰写方式需要花费大量的脑力(intellectual effort)而且很容易出错(error-prone)。 Konrad Zuse于1948年发表了他所设计的Plankalkül编程语言的论文[1]。但是在他有生之年却未能将该语言实作,而他原本的贡献也被其他的发展所孤立。 在这段期间被开发出来的重要语言包括有:
- 1943 – Plankalkül (Konrad Zuse)
- 1943 – ENIAC coding system
- 1949 – C-10
1950与1960年代
有三个现代编程语言于1950年代被设计出来,这三者所衍生的语言直到今日仍旧广泛地被采用:
- Fortran (1955),名称取自”FORmula TRANslator”(公式翻译器),由约翰·巴科斯等人所发明;
- LISP,名称取自”LISt Processor”(列举处理器),由约翰·麦卡锡等人所发明;
- COBOL,名称取自”COmmon Business Oriented Language”(通用商业导向语言),由被葛丽丝·霍普深刻影响的Short Range Committee所发明。
另一个1950年代晚期的里程碑是由美国与欧洲计算机学者针对”算法的新语言”所组成的委员会出版的ALGOL 60报告(名称取自”ALGOrithmic Language”(算法语言))。这份报告强化了当时许多关于计算的想法,并提出了两个语言上的创新功能:
- 巢状区块结构:可以将有意义的程式码片段群组成一个区块(block),而非转成分散且特定命名的程序。也就是我们所熟悉的模块化设计。
- 词汇范围(lexical scoping):区块可以有区块外部无法透过名称存取,属于区块本身的变量、程序以及函式。就是我们所熟悉的作用域。
另一个创新则是关于语言的描述方式:一种名为巴科斯-诺尔范式 (BNF)的数学化精确符号被用于描述语言的语法。之后的编程语言几乎全部都采用类似BNF的方式来描述程式语法中上下文无关的部份。BNF主要使用在了Algol 60的设计上面。而Algol 60对之后语言的设计上带来了特殊的影响,在其他部分的语言设计中这种设计思想很快的就被广泛采用。并且后续为了开发Algol的扩充子集合,设计了一个名为Burroughs(en:Burroughs large systems)的大型系统。而延续Algol的关键构想所产生的成果就是ALGOL 68:
- 语法跟语意变的更加正交(orthogonal)
- 采用匿名的历程(routines)
- 采用高阶(higher-order)功能的递回式输入(typing)系统等等。
整个语言及语意的部分都透过为了描述语言而特别设计的Van Wijngaarden grammar来进行正式的定义,而不仅止于上下文无关的部份。Algol 68一些较少被使用到的语言功能(如同步与并列区块)、语法捷径的复杂系统,以及型态自动强制转换(coercions),使得实作者兴趣缺缺,也让Algol 68获得了很难用(diffcult)的名声。尼克劳斯·维尔特就干脆离开该设计委员会,另外在开发出更简单的Pascal语言。 在这段期间被开发出来的重要语言包括有:
- 1951 – Regional Assembly Language
- 1952 – Autocode
- 1954 – FORTRAN
- 1954 – IPL (LISP的先驱)
- 1955 – FLOW-MATIC (COBOL的先驱)
- 1957 – COMTRAN (COBOL的先驱)
- 1958 – LISP
- 1958 – ALGOL 58
- 1959 – FACT (COBOL的先驱)
- 1959 – COBOL
- 1962 – APL
- 1962 – Simula
- 1962 – SNOBOL
- 1963 – CPL (C的先驱)
- 1964 – BASIC
- 1964 – PL/I
- 1967 – BCPL (C的先驱)
1967-1978:确立了基础范式
1960年代晚期至1970年代晚期的期间中,编程语言的发展也有了重大的成果。大多数现在所使用的主要语言范式都是在这段期间中发明的:
- Simula,于1960年代晚期由奈加特与Dahl以Algol 60超集合的方式发展,同时也是第一个设计支援面向对象进行开发的编程语言。
- C,于1969至1973年间由贝尔实验室的研究人员丹尼斯·里奇与肯·汤普逊所开发,是一种早期的系统程式设计(en:system programming)语言。
- Smalltalk,于1970年代中期所开发,是一个完全从零开始(ground-up)设计的面向对象编程语言。
- Prolog,于1972年由Colmerauer、Roussel,以及Kowalski所设计,是第一个逻辑程式语言。
- ML,于1973年由罗宾·米尔纳所发明,是一个基于Lisp所建构的多型(polymorphic)型态系统,同时也是静态型别函数编程语言的先驱。
这些语言都各自演展出自己的家族分支,现今多数现代编程语言的祖先都可以追朔他们其中至少一个以上。 在1960年代以及1970年代中结构化程式设计的优点也带来许多的争议,特别是在程式开发的过程中完全不使用GOTO。这项争议跟语言本身的设计非常有关系:某些语言并没有包含GOTO,这也强迫程式设计师必须结构化地编写程式。尽管这个争议在当时吵翻了天,但几乎所有的程式设计师都同意就算语言本身有提供GOTO的功能,在除了少数罕见的情况下去使用GOTO是种不良的程序风格。结果是之后世代的编程语言设计者发觉到结构化编程语言的争议实在既乏味又令人眼花撩乱。 在这段期间被开发出来的重要语言包括有:
- 1968 – Logo
- 1970 – Pascal
- 1970 – Forth
- 1972 – C语言
- 1972 – Smalltalk
- 1972 – Prolog
- 1973 – ML
- 1975 – Scheme
- 1978 – SQL (起先只是一种查询语言,扩充之后也具备了程式结构)
1980年代:增强、模组、效能
1980年代的编程语言与之前相较显得更为强大。C++合并了面向对象以及系统程式设计。美国政府标准化一种名为Ada的系统编程语言并提供给国防承包商使用。日本以及其他地方运用了大量的资金对采用逻辑编程语言结构的第五代语言进行研究。函数编程语言社群则把焦点转移到标准化ML及Lisp身上。这些活动都不是在开发新的范式,而是在将上个世代发明的构想进一步发扬光大。 然而,在语言设计上有个重大的新趋势,就是研究运用模组或大型组织化的程式单元来进行大型系统的开发。Modula、Ada,以及ML都在1980年代发展出值得注意的模组化系统。模组化系统常拘泥于采用泛型程式设计结构:
- 泛型存在(generics being)
- 本质(essence)
- 参数化模组(parameterized modules)
尽管没有出现新的主要编程语言范式,许多研究人员仍就扩充之前语言的构想并将它们运用到新的内容上。举例来说,Argus以及Emerald系统的语言配合面向对象语言运用到分散式系统上。 1980年代的编程语言实际情况也有所进展。计算机系统结构中RISC假定硬件应当为编译器设计,而并非为人类设计。借由中央处理器速度增快的帮助,编译技术也越来越进展神速,RISC的进展对高阶语言编译技术发展来不小的贡献。 在这段期间被开发出来的重要语言包括有:
- 1980 – Ada
- 1983 – C++ (就像有类别的C)
- 1984 – Common Lisp
- 1985 – Eiffel
- 1986 – Erlang
- 1987 – Perl
- 1988 – Tcl
- 1989 – FL (Backus)
1990年代:互联网时代
1990年代未见到有什么重大的创新,大多都是以前构想的重组或变化。这段期间主要在推动的哲学思想是提升程式设计师的生产力。许多”快速应用程式开发” (RAD) 语言也应运而生,这些语言大多都有相应的集成开发环境、垃圾回收等机制,且大多是先前语言的衍生语言。这类型的语言也大多是面向对象的编程语言,包含有Object Pascal、Visual Basic,以及C#。Java则是更加保守的语言,也具备垃圾回收机制。与其他类似语言相比,也受到更多的观注。新的脚本语言则比RAD语言更新更好。这种语言并非直接从其他语言衍生,而且新的语法更加开放地(liberal)与功能契合。虽然脚本语言比RAD语言来的更有生产力,但大多会有因为小程式较为简单,但是大型程式则难以使用脚本语言撰写并维护的顾虑[来源请求]。尽管如此,脚本语言还是网络层面的应用上大放异彩。 在这段期间被开发出来的重要语言包括有:
- 1990 – Haskell
- 1991 – Python
- 1991 – Visual Basic
- 1993 – Ruby
- 1993 – Lua
- 1994 – CLOS (part of ANSI Common Lisp)
- 1995 – Java
- 1995 – Delphi (Object Pascal)
- 1995 – JavaScript
- 1995 – PHP
- 1997 – REBOL
- 1999 – D
现今的趋势
编程语言持续在学术及企业两个层面中发展进化,目前的一些趋势包含有:
- 在语言中增加安全性与可靠性验证机制:额外的堆栈检查、资讯流(information flow)控制,以及静态执行绪安全。
- 提供模组化的替代机制:混入(en:mixin)、委派(en:delegates),以及观点导向。
- 元件导向(component-oriented)软件开发
- 元编程、反射或是存取抽象语法树(en:Abstract syntax tree)
- 更重视分散式及移动式的应用。
- 与数据库的整合,包含XML及关联式数据库。
- 支援使用Unicode编写程式,所以源代码不会受到ASCII字符集的限制,而可以使用像是非拉丁语系的脚本或延伸标点符号。
- 图形化使用者接口所使用的XML(XUL、XAML)。
在这段期间被开发出来的重要语言包括有:
- 2001 – C#
- 2001 – Visual Basic .NET
- 2002 – F#
- 2003 – Scala
- 2003 – Factor
- 2006 – Windows PowerShell
- 2007 – Clojure
- 2009 – Go
需求
在回顾这些历史的时候发现,我们无论是创造程序语言还是计算机,或者软件也好,最终的目的都是为了两个字——需求。我们遵循着工具理性的框架,追寻着完成需求的目标。而在计算机发展的过程的过程中,主要的需求有哪些?
- 创造工具来满足人们现实生活中的需求,比如金融工具、QQ、微信
- 不断创造更加好用的硬件基础。并且创造响应的软件来适应更快的硬件。
- 随着硬件规模和软件规模的不断扩大,发展相应的理论去控制规模扩大带来的影响,即控制复杂性。
当然,我们必须先解决的第一个需求就是我们创造计算机的原始需求:创造工具来满足人们现实生活中的需求。但是像绝大部分工具一样,一旦我们穿凿了它,它本省也会衍生出来很多需求。工具本身也需要演化。而工具本身的需求就是后两条。 满足第一条需求的方式,千奇百怪!基本上会涉及到人类现已掌握的知识的各个层面。比如一个图书分享网站,最起码要涉及:管理学、心理学、营销、产品设计、美术设计、交互设计、图书馆管理等等。而这些知识通过软件设计物化在了网站这个东西上面。然后我们就能通过网站这个工具,来满足我们图书分享的需求。 但是,我们能够发现,其实程序设计(程序员思维的最主要的展现形式)虽然与第一条需求有关,但又关系不是很大。的确,我们是通过软件设计这件事情,物化了我们的知识。但是在这个过程中,软件设计并不关系人们的需求具体是什么,是图书网站,还是聊天软件和软件设计并没有直接的关系。相对来说,软件设计比较关心的是后面两个需求:来自工具本身的需求。用一句话说就是:Make it work, Keep it simple。 首先你必须让这个工具能够工作,其次你必须让这个工具能够持续稳定的工具的工作(不会因为规模扩大,复杂性增长而招致灾难)。而这就是程序员思维中在工具理性下面最为核心的两个具体的概念:Make it work, keep it simple。 明白了这一点,我们再回过头去看一下刚刚所述的软件和程序设计的历史。刚开始人们需要有一个机器替代人进行计算于是有了差分机和ENIAC,有了硬件之后,自然就需要一种驱动机制能够让这些机器能够运转起来,于是我们发明了程序语言和软件。而随着硬件的不断发展和软件规模的不断扩大,人们发现最原始的计算机语言(机器语言)不使用于快速开发。于是就有了汇编这样的低级语言。后来低级语言也被证明在开发速度上存在缺陷,也不太适合快速开发,于是我们有了高级语言,比如Lisp、C、fortran。刚开始的时候这些高级语言,的确能够满足快速开发的需求。但是,随着软件规模的不断扩大。我们开始发现:靠,我们创造了软件却控制不了软件。计算机和软件这个工具的规模已经超出了人类能够认知的规模,它的复杂性已经开始变得不可控了。这怎么能行呢,于是我们开始创造了面向对象和软件工程等理论工具来帮助我们控制这种软件复杂性。 而到了今天,我们看一下我们日常Coding中常见的那些理论的方法,基本上也都是围绕着Make it work, keep simple展开的。其实这两个问题是不能够割裂开阐述的,为了理解上方面,我们就先单独说吧,不过中间会有概念的穿插。
Make it work(编程范式,程序语言的世界观)
如何让计算机按照我们制定的方式工作,如何让软件能够按照我们假象的方式运行?我们把完成这两个需求的过程叫做程序设计。就是我们在前文中所说:程序设计就是利用计算机将知识物化,并利用知识的可复现性来对现实产生作用。但是,这只是一个概念啊。如何才能把它落实到实践上呢?这就要说一下编程范式了,我们对于程序语言是什么的世界观。 编程范型或编程范式(英语:Programming paradigm),(范即模范之意,范式即模式、方法),是一类典型的编程风格,是指从事软件工程的一类典型的风格(可以对照方法学)。如:函数式编程、程序编程、面向对象编程、指令式编程等等为不同的编程范型。 编程范型提供了(同时决定了)程序员对程序执行的看法。例如,在面向对象编程中,程序员认为程序是一系列相互作用的对象,而在函数式编程中一个程序会被看作是一个无状态的函数计算的串行。编程范式就是我们程序设计的世界观,他决定了程序在我们眼中是个什么样,而我们又怎样去操作或者使用程序语言。 正如软件工程中不同的群体会提倡不同的“方法学”一样,不同的编程语言也会提倡不同的“编程范型”。一些语言是专门为某个特定的范型设计的(如Smalltalk和Java支持面向对象编程,而Haskell和Scheme则支持函数式编程),同时还有另一些语言支持多种范型(如Ruby、Common Lisp、Python和Oz)。 很多编程范型已经被熟知他们会禁止使用哪些技术,同时又允许使用哪些技术。 例如,纯粹的函数式编程不允许有副作用;结构化编程不允许使用goto。可能是因为这个原因,新的范型常常被那些惯于较早的风格的人认为是教条主义或过分严格。然而,这样避免某些技术反而更加证明了关于程序正确性——或仅仅是理解它的行为——的法则,而不用限制程序语言的一般性。 编程范型和编程语言之间的关系可能十分复杂,由于一个编程语言可以支持多种范型。例如,C++设计时,支持过程化编程、面向对象编程以及泛型编程。然而,设计师和程序员们要考虑如何使用这些范型元素来构建一个程序。一个人可以用C++写出一个完全过程化的程序,另一个人也可以用C++写出一个纯粹的面向对象程序,甚至还有人可以写出杂揉了两种范型的程序。 下面是我们比较常见的几种“程序语言的世界观”:
名称 | 代表语言 | 核心概念 | 运行机制 | 关键突破 | 实现原理 | 主要目的 | 常见应用 |
---|---|---|---|---|---|---|---|
命令式/过程式(Impreative/procedural) | Fortran/Pascal/C | 命令、过程 | 命令执行 | 突破单一主程序和非结构化程序的限制 | 引入逻辑控制和子程序 | 模拟机器思维,实现自顶向下的模块设计 | 交互式、事件驱动型系统、数值计算等 |
函数式、应用式(Functional、Applicative) | Scheme/Haskell | 函数 | 表达式计算 | 突破机器思维的限制引入高阶函数、将函数作为数据处理 | 模拟数学思维,简化代码,减少副作用 | 微积分计算、数学逻辑、博弈等 | |
逻辑式 | Prolog/Mercury | 断言 | 逻辑推理 | 突破逻辑与控制粘合的限制 | 利用推理引擎在已知的事实和规则的基础上进行逻辑推断 | 专注逻辑分析、减少代码 | 机器证明、专家系统、自然语言处理、语义网、决策分析、业务规则管理等 |
对象式(Object-Oriented) | Smaltalk/Java/Objc | 对象 | 对象间信息交互 | 突破数据与代码分隔的限制 | 引入封装、继承和多态机制 | 迎合人类认知模式,提高软件的易用性和重用性 | 大型复杂交互系统等 |
并发式/并行式(Concurrent/Parallel) | Erlang/Oz | 进程、线程 | 进程、线程、协程间通信与同步 | 突破串行的限制 | 引入并行的线程模块以及模块间的通信与同步机制 | 充分利用资源、提供运行效率、提高软件的响应能力 | 图形界面,IO处理,多任务系统,计算密集型系统 |
泛型式(Generic) | Ada/Eiffel/C++ | 算法 | 算法实例化(多发生于编译期) | 突破静态类型语言的限制 | 利用模板推迟类型指定 | 提高算法的普适性 | 普适性算法如排序、搜索等,集合类容器等 |
元编程(Meta programming) | Lisp/Ruby/JavaScript | 元程序 | 动态生成代码或者自动修改执行指令 | 突破语言的常规语法限制 | 利用代码生成或语言内奸的反射、动态等机制,将程序语言作为数据处理 | 减少手工编码、提高语言级别 | 自动代码生成、定义结构化配置文件,IDE、编译器,解释器,人工智能、领域特定语言等 |
切面式(Aspect-Oriented) | AspectJ/AspectC++ | 切面 | 在接入点处执行建议 | 突破横切关注点无法模块化的限制 | 通过编制将附加行为嵌入主题程序 | 实现横切关注点分割 | 日志输出、代码耿宗、性能监控、异常处理、安全检查等 |
时间驱动(Event-Driven) | C#/VB.NET | 事件 | 监听器收到事件后做出响应 | 突破顺序、同步的流程限制 | 引入控制反转和异步机制 | 调用者与被调用者在代码和时间上双重解耦 | 图形界面、网络应用、服务器、异步输入等、DOM等 |
为了能够深入的理解每一种编程范式对我们编程实践的影响,我们来看一下用上面提到的不同的编程范式的代表语言去写一个我们常见的快速排序算法:
网址: http://zh.wikipedia.org/wiki/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F
同样是一个快速排序算法,每一种编程范式下他们实现的思路真的是千差万别。最主要的是各有千秋。我们很难说某一种编程范式比另外一种编程范式优秀,也很难说某一种编程范式比另外一种要坏。我们只能说某一种编程范式在某些特殊的情境下或者需求下要比其他的合适。因为他们就是为这些需求而生的。而其他的编程范式是为另外的编程范式。如果剪子砸不了核桃,不是剪刀的问题,而是你没有使用对工具的问题。
而在我们看了这么多的编程范式,接受了这么多的对于程序的世界观之后。就会问,这些编程范式之间有没有一些内在的联系呢?这个问题一个明显的答案是:他们都是为了解决问题而生的。但是这个答案似乎有点欠妥。因为,我们是说的内在联系。而不是问他们的目标。 Pascal之父尼克劳斯·沃思曾经给出过程序的一个定义:算法+数据结构=程序,后来他又对算法进行了定义:逻辑+控制=算法,最后一个完整的表述是:程序=(逻辑+控制)+数据结构。虽然他提出这个概念的时候是针对命令式或者过程式的程序的。但是我们在很多编程范式中依稀能够看到:逻辑+控制+数据结构=程序的影子。 我们能够看到无论是在哪一种编程范式指导下的语言。我们都能够看到相同的“控制”结构——顺序结构、分支结构、循环结构。我们也都能够看到是与非的逻辑判断。数据结构更不用说。没有任何语言不去操作数据的吧。所以命令式是一种基础范式。同样为基础范式的还有函数式和逻辑式。而比较有意思的是,这几种基础编程范式各有所重。命令式编程着重控制和数据结构,函数式编程着重逻辑、控制和数据结构,逻辑式着重逻辑和数据结构。这几种编程范式很好的构建起了,程序的基础组成部分的概念。命令式可以告诉你控制这个东西是怎么回事,怎么去用。逻辑式,像数学思维一样的精巧告诉你逻辑的魅力。函数式,告诉你怎样组合起来使用逻辑和控制。而他们又共同在讲着数据结构的概念。其实,逻辑+控制+数据结构的世界观来自于我们对于计算机这个机械器件的认知。 其他的编程范式我们称之为高级范式,因为他们在基础范式概念基础之上增加了自己对于程序的额外的一些认知。这里着重说一下,面向对象编程范式。面向对象认为程序就是信息交互。他把数据结构放到了一个非常重要的位置。突破了数据域代码隔离的限制。让数据能够与逻辑和控制紧密的结合在一起。这种思维方式,非常符合人类的认知模式。面向对象,通过人类思维中常用的手段——抽象,提供了一种新的组织程序的方式。而这种方式,极大的提高了软件的易用性和重用性,同时非常好的控制了整个软件的复杂性。 如果想要了解更多关于编程范式的东西,推荐看一下《冒号学堂》。
Keep it simple(复杂性控制)
首先我们尝试先构建起复杂性的概念。什么是复杂性? 复杂,字面意思理解,就是一个东西极其庞大承载的东西太多而我们理解或者认知起来已经很困难的时候,我们就说这个东西很复杂。而《现代汉语词典》对于复杂的解释也符合我们的直觉:
<code>(事物的种类、头绪等)多而杂:颜色~ㄧ~的问题ㄧ~的人际关系 </code>
复杂性,就是在把复杂作为一种特性了而已。就是说一件事物具有了一种“多而杂”的特性。而现代的复杂性科学认为复杂性可分为:情景复杂性(事物固有属性)和认知复杂性,而且两者互为因果。 那么软件为什么会有复杂性呢?或者说程序设计为什么会带来复杂性呢?这和程序设计本身有关系。我们回顾一下前面我们对于程序设计的定义:程序设计就是利用计算机将知识物化,并利用知识的可复现性来对现实产生作用。 那么说,程序设计就是在为整个软件系统增加知识。在增加整个系统的信息总量。通过信息论我们知道,当一个系统中的信息越多的时候,其信息熵值就越大。熵值越大,我们理解难度就越大,势必会带来复杂性增加。于是,复杂性一个比较浅显的数学定义就是信息熵。
上述公式是香农信息熵的公式,现在也可以理解成软件复杂性的公式。我们容易发现,软件设计这个事情,本事就是在增加复杂性的。他在不断的往整个软件系统中,增加信息,信息越多,我们越难去理解这个软件。这个软件就越复杂,当这种复杂到了一定的量级的时候,就爆发了软件危机。于是我们需要扩充一下我们对于程序设计的定义:程序设计在完成思维工具物化的同时,还需要尽可能的控制信息熵的增长,来降低系统的复杂性。 但是,人们总是聪明的,有了问题,我们就去发明新的工具啊。于是我们就有了一系列复杂性控制的理论。这里我们不去探讨更普遍层面的复杂性,我们先只看一下,程序设计,这一个我们比较关心的实物的复杂性控制的问题(要想对复杂性有一个浅显的认识可以看一下《探索复杂性》和《信息简史》)。 在程序设计层面上,我们没有很好理论化的东西去控制复杂性。但是我们创造了一系列的方法来控制复杂性。我们把这一些列的方法叫做“最佳实践“。如果你去看一些软件工程或者面向对象设计的书的时候,这个词可能经常遇到。而我们的最佳实践有哪些呢? 结构化程序设计、面向对象程序设计、软件工程、敏捷开发、UML、领域建模…….不一而足。 关于“最佳实现”的具体细节的东西,有很多写的非常好的书中都有描述我们这里就不再赘述了。我们着重关心一点,如何去评价一个设计或者软件的复杂性呢?因为你不知道,如何评价或者衡量自己设计的复杂性的话,势必也不知道如何改进。 而评价的方式比较靠谱的有两个:
- 高内聚低耦合
- 是否满足设计模式的六大基本原则
高内聚低耦合
这个是我们刚开始提出的问题之一。内聚和耦合是一对相对的概念。相辅相成。两者虽然有联系,但不是必然联系。高内聚的设计不一定低耦合,低耦合的东西也不一定高内聚。那么内聚性和耦合性是什么,如何去评价呢?
内聚性
在程序设计中,内聚性是指机能相关的程序组合成一模块的程度。应用在面向对象程序设计中,若服务特定类型的方法在许多方面都很类似,则此类型即有高内聚性。在一个高内聚性的系统中,代码可读性及复用的可能性都会提高,程序虽然复杂,但可被管理。 以下的情形会降低程序的内聚性:
- 许多机能封装在一类型内,可以借由方法供外界使用,但机能彼此类似之处不多。
- 在方法中进行许多不同的机能,使用的是相关性低或不相关的数据。
低内聚性的缺点如下:
- 增加理解模块的困难度。
- 增加维护系统的困难度,因为一个逻辑修改会影响许多模块,而一个模块的修改会使得一些相关模块也要修改。
- 增加模块复用困难度,因为大部份的应用程序无法复用一个由许多不一定相关的机能组成的模块。
内聚性的衡量
内聚性是一种非量化的量测,可利用评量规准来确认待确认源代码的内聚性的分类。内聚性的分类如下,由低到高排列:
- 偶然内聚性(Coincidental cohesion,最低)偶然内聚性是指模块中的机能只是刚好放在一起,模块中各机能之间唯一的关系是其位在同一个模块中(例如:“工具”模块)。
- 逻辑内聚性(Logical cohesion)逻辑内聚性是只要机能只要在逻辑上分为同一类,不论各机能的本质是否有很大差异,就将这些机能放在同一模块中(例如将所有的鼠标和键盘都放在输入处理副程序中)。
- 时间性内聚性(Temporal cohesion)时间性内聚性是指将相近时间点运行的程序,放在同一个模块中(例如在捕捉到一个异常后调用一函数,在函数中关闭已打开的文件、产生错误日志、并告知用户)。
- 程序内聚性(Procedural cohesion)程序内聚性是指依一组会依照固定顺序运行的程序放在同一个模块中(例如一个函数检查文件的权限,之后打开文件)。
- 联络内聚性(Communicational cohesion)联络内聚性是指模块中的机能因为处理相同的数据,因此放在同一个模块中(例如一个模块中的许多机能都访问同一个记录)。
- 依序内聚性(Sequential cohesion)依序内聚性是指模块中的各机能彼此的输入及输出数据相关,一模块的输出数据是另一个模块的输入,类似工厂的生产线(例如一个模块先读取文件中的数据,之后再处理数据)。
- 功能内聚性(Functional cohesion,最高)功能内聚性是指模块中的各机能是因为它们都对模块中单一明确定义的任务有贡献(例如XML字符串的词法分析)。
由赖瑞·康斯坦丁、爱德华·尤登及史蒂夫·麦康奈尔等人的研究都提出偶然内聚性和逻辑内聚性是不好的,联络内聚性和依序内聚性是好的,而功能内聚性是最理想的状态。 当我们拿到一个设计的时候,完全可以通过上述的几个层面去衡量这个设计的内聚性。
耦合性
内聚性是一个和耦合性相对的概念,一般而言低耦合性代表高内聚性,反之亦然。耦合性和内聚性都是由提出结构化设计概念的赖瑞·康斯坦丁所提出[1]。低耦合性是结构良好程序的特性,低耦合性程序的可读性及可维护性会比较好。 耦合性可以是低耦合性(或称为松散耦合),也可以是高耦合性(或称为紧密耦合)。以下列出一些耦合性的分类,从高到低依序排列:
- 内容耦合(content coupling,耦合度最高)也称为病态耦合(pathological coupling)是指一个模块依赖另一个模块的内部作业(例如,访问另一个模块的局域变量),因此修改第二个模块处理的数据(位置、形态、时序)也就影响了第一个模块。
- 共用耦合(common coupling)也称为全局耦合(global coupling.) 是指二个模块分享同一个全局变量,因此修改这个共享的资源也就要更动所有用到此资源的模块。
- 外部耦合(external coupling)发生在二个模块共用一个外加的数据格式、通信协议或是设备界面,基本上和模块和外部工具及设备的沟通有关。
- 控制耦合(control coupling)是指一个模块借由传递“要做什么”的信息,控制另一个模块的流程(例如传递相关的旗标)。
- 特征耦合(stamp coupling)也称为数据结构耦合,是指几个模块共享一个复杂的数据结构,而每个模块只用其中的一部份,甚至各模块用到的部份不同(例如传递一笔记录给一个函数,而函数只需要其中的一个字段。
- 数据耦合(data coupling)是指模块借由传入值共享数据,每一个数据都是最基本的数据,而且只分享这些数据(例如传递一个整数给计算平方根的函数)。
- 信息耦合(message coupling,是无耦合之外,耦合度最低的耦合)可以借由以下二个方式达成:状态的去中心化(例如在对象中),组件间利用传入值或信息传递 (计算机科学)来通信。
- 无耦合模块完全不和其他模块交换信息。
耦合性和内聚性二个名词常一起出现,用来表示一个理想模块需要有的特点,也就是低耦合性及高内聚性。耦合性着重于不同模块之间的相依性,而内聚性着重于一模块中不同功能之间的关系性。低内聚性表示一个模块中的各机能之间没什么关系,当模块扩充时常常会出现问题。 相对于内聚性而言耦合性是一个相对比较好度量的概念。已经有比较好的数学公式来比较精准的衡量耦合性。 以下是一种计算模块耦合性的方法[: 对于数据和控制流的耦合:
- di: 输入数据参数的个数
- ci: 输入控制参数的个数
- do: 输出数据参数的个数
- co: 输出控制参数的个数
全局耦合:
- gd: 用来存储数据的全局变量
- gc: 用来控制的全局变量
环境耦合:
- w: 此模块调用的模块个数(扇出)
- r: 调用此模块的模块个数(扇入)
若Coupling(C)数值越大,表示模块耦合的情形越严重,数值一般会界于0.67(低度耦合)到1.0(高度耦合)之间。 到此,我们已经构建起了高内聚和低耦合的概念。并且知道了如何去使用这两个概念去控制复杂性。
设计模式的六大原则
设计模式这个东西就不细说了,大家都知道。但是如何去评价一个设计模式呢?或者是什么知道我们设计“设计模式”的,那就是六大原则:
- 单一职责原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则
- 开闭原则
这里偷点懒不详细赘述了,可以参考六大原则。
总结
在上面的叙述中,我们讲了工具理性,之后从工具理性衍生出了程序员思维的定义。通过回顾历史我们定义了程序设计,并且指出了程序设计中的两个主要的问题:Make it work, keep it simple。之后我们针对这两个问题,分别阐述了不同的方法论。以编程范式为主的make it work,和以复杂性控制为主的Keep it simple。同时提到了,在这个方法论之下的一些最佳实践。至此我们构建起了一个程序员的思维框架。
PS:个人见解,仅供参考。