中学的时候,学校里开设了电脑课。当时的电脑还是一种比较希罕的东西,学校里的电脑一共就十几台,还专门找了一个大厅摆放这些机器。厅里面铺着厚厚的地毯,整天都拉着重重的窗帘。每次上课前一天,我们需要沐浴更衣,剪好指甲。上课时大家都穿上鞋套,排好队伍,列队进入机房。然后各位同学坐在座位上,在老师的指挥下,拿出一张五英寸的软磁盘,磁盘里安装着DOS操作系统,插入电脑的A驱动器。然后依次打开显示器、主机电源,在一阵吱吱声中,等待着电脑的启动,进入一个充满了幻想的神奇世界。
我就是在那个时候写出了第一行程序。当时我们学的是一种叫做GWBASIC的语言,这是BASIC语言的一个分支。
BASIC是一种非常简单的交互式程序设计语言。编码人员需要为每一行代码编制一个行号。行号是一个自然数,为了日后调试的需要,一般都是在最初编制行号的时候故意留下富余,不是按照1、2、3……的方式编制行号,而是按照10、20、30……的形式。程序输入完毕以后,运行“RUN”命令,编译器按照行号的顺序,解释执行程序。
这样的编程方式太简单了,只要记住几个流程控制、输入输出的关键字,就可以写出一个简单的程序。那段时间我每天都在想着把平时遇到的东西用程序写出来,多元多次的方程式、猜数字的游戏、三角函数曲线……先把编程的思路记在纸上,每个星期上课的时候再在电脑上写出来,调试运行。调试方式也非常简单,就是把数值往屏幕上PRINT,然后运行查看结果。写完了之后运行“SAVE”命令,把代码保存到软盘里。
中学时期应该是一个人的思维最灵活的时期,以后的时期经验逐渐的丰富,但是学习新知识的能力实际上是在走下坡路。除非特地去培养,很多方面的知识也就停留在中学的水平。比如对于一个搞计算机的专业人员来说,他的历史、语文、物理、化学知识很可能就永远的停留在中学时期的水平。那段时间我疯狂的写着程序,最大的愿望就是家里能有一台电脑,安装着GWBASIC的编译器,我可以每天把自己关在屋子里,用GWBASIC写出超级玛丽,或者坦克大战一样的游戏。
GWBASIC使用一种非常原始的方式进行流程控制——GOTO。他也有函数的概念,但是实际上也是GOTO到某段代码上去(使用GOSUB指令),执行完了再GOTO回来(使用RETURN指令)。按照当时流行的观念,程序中使用GOTO并无不妥,这是实现条件循环的一种很正常的方式。例如下面这段代码:
50 LET I = 0
60 I = I + 2
70 PRINT I
80 IF I < 100 THEN GOTO 60
下面是我当时写出的一段代码,这是一个猜数字的游戏,他就是在GOTO来GOTO去的兜圈子,像是进入了一个迷宫。还好这个迷宫的规模不大,很容易就能找出他在干什么。这段代码之所以能够保存到现在,是因为他没有象其他代码一样保存在软盘里,而是随手记录在了书的空白处。
10 PRINT "Guess a number"
20 INPUT A
30 IF A > 5 THEN GOTO 60
40 IF A < 5 THEN GOTO 80
50 IF A = 5 THEN GOTO 100
60 PRINT "Too big"
70 GOTO 20
80 PRINT "Too small"
90 GOTO 20
100 PRINT "Right"
110 END
这样的编程方式就是:面条式代码。这是一种最简单的编码方式,不需要长时间的学习或太多的经验就可以立即掌握。但是他能解决的问题也是有限的,也就只能写一个猜数字的小游戏,或者画出一个Y=A*SIN(X)+B的函数曲线图。如果要真的用他来写一个超级玛丽,那将是很很很很很困难的。
后来进入了结构化编程的时代,GOTO成了破坏程序结构化的罪人,渐渐的被大家抛弃,现在只在的很少的地方仍然发挥着作用(比如在VB或者C语言中进行异常处理)。但是这种GOTO式的编程方法影响了我很久。直到我大学时期,学习FORTRAN77的时候,仍然在采用这种编程方式。这样的方式很简单,不用想的太多就能解决问题,也使我小富即安,懒得去研究别的编程方式。
结构化编程方式是面条式代码的一种改良。他首先把需要处理的问题划分成若干个模块,然后再把每个模块划分成更小的模块,这样一步步细化下去,直到每个模块的功能可以用一个程序语句实现,这就完成了程序的设计。在划分模块的时候,结构化编程的原则是:一个模块只能有一个入口和一个出口,并在要按照基本结构完成模块之间的连接。这就意味着GOTO只能与IF配合使用,以构成分支和循环的结构。分支和循环结构在任何一种编程语言中都有更简单明确的关键字来实现,GOTO自然就失去了存在的意义。
结构化编程是一种面向过程的编程方式。所谓“面向过程编程”是译自“Procedure-oriented programming”,实际上应该理解成“以过程为导向的编程方式”。按照这样的编程方式,人们首先要确定用什么样的流程来实现业务需求,接着就一步步的分解这个流程,分解,再分解……直到可以用一个语句实现,最后就用程序实现这样的流程。下面的流程就实现了这样一个小测验:测试计算11到20的平方,每算对一个数可以得到10分,最后输出总分。
面向过程的编程方式必须在编程之前将解决问题的流程确定下来,这是一种很不灵活的方式。我们解决一个问题的时候,是很难在一开始就把流程定的很合理的。比如说,我们开发一个Email服务器,用户最初要求有这样的功能:收到邮件的时候判断一下是不是垃圾邮件,如果是垃圾邮件,就直接移到垃圾邮件夹里。后来我们在开发的过程中发现,垃圾邮件的识别率并不是很高,难免会把一些正常邮件当作垃圾邮件移走了,造成用户的邮件不能及时收到。于是这个流程必须调整成这样:发现垃圾邮件不能移走,而是在标题上加上一个警告标记。并且这个流程的改变会影响其他的流程:使用POP客户端下载邮件的时候,对垃圾邮件只能下载标题和纯文本的内容,确保客户端的安全。可以看出,业务流程的制定需要在随后的开发过程中不断得到反馈,不断进行调整,最后才能达到一个比较好的效果。但是按照面向过程的编程方式,我们必须先确定一个流程,再开始设计,否则需求就不确定。如果在编程的过程中发现流程不合适,在效率或者准确性上不满足需要,程序就要重新设计了。这样边写边改,又要顾忌对其他流程的影响,程序写起来是很困难的。
后来又发展出来另外一种编程方式:面向对象的编程。面向对象的编程是译自“Object-oriented programming”,简称OOP,其实上也应该理解成“以对象为导向的编程方式”。用这样的编程方式,先不要急于确定用什么样的流程来实现业务需求,而是先看看需求里面包含有哪些“东西”,这些东西是怎样出现的,具有什么样的属性,可以做哪些事情,会发生什么样的事件,这些东西之间是什么样的关系。用程序把这些东西造出来,然后就让这些东西运行起来,实现预想的功能。
比如刚才那个Email服务器的问题,在流程不确定的情况下,我们可以先看看这个Email服务器里面有哪些东西,最明显的,应该有Email类。Email类有三个行为:接收(Receive)、显示(Show)和下载(Download)。垃圾邮件是邮件的一个特例,可以看作Email的一个子类,如下:
JunkMail覆盖了Email的接收、显示和下载方法。如果垃圾邮件应该有一些特别的处理方式,就可以只修改JunkMail类——在接收时移至垃圾邮件夹、或者在显示的时候加上一个警示标志、下载时过滤掉其中的非文本内容——没有其他类的代码需要改动。这样不会对普通邮件的处理流程产生任何影响。
面向过程和面向对象这两种编程思想的区别在于:他们对于需求的理解是不一样的。面向过程的编程人员,他们把需求理解成一条一条的业务流程,他们喜欢问用户“你的业务流程是什么样的?”,然后他们分析这些流程,把这些流程交织组合在一起,最后实现了需求;面向对象的编程人员,他们把需求理解成一个一个的对象,他们喜欢问用户“这个东西叫做什么,他从哪里来,他能做什么事情?”,然后他们制造这些对象,让这些对象互相调用,符合了业务需要。关于这两种编程方式的对比,在后面的章节中会有具体的说明。这里简单的说两点:
1、对象比流程更加稳定。业务流程的制定需要受到很多条件的限制,甚至程序的效率、运行方式都会反过来影响业务流程。有时候用户也会为了更好的实现商业目的,主动的改变业务流程,并且一个流程的变化经常会带来一系列的变化。这就使得按照业务流程设计的程序经常面临变化。
2、对象比流程更加封闭。业务流程从表面上看只有一个入口、一个出口,但是实际上,流程的每一步都可能改变某个数据的内容、改变某个设备的状态,对外界产生影响。而对象则是完全通过接口与外界联系,接口内部的事情与外界无关。
按照面向对象的编程方式,我们要用对象来体现现实世界中出现的事物。如果需求比较复杂,这样会造成程序里出现大量的对象、复杂的关系,修改程序的时候越来越麻烦。其实一些对象之间总是有着比较固定的关系,有的对象之间是包含的关系,有的是依赖的关系,有的对象是别的对象的创建者……有经验的程序员会从这些关系中发现一些规律,寻找出处理这些关系的一些方法,这就形成了设计模式。比如一个通信公司为他的用户设置各种套餐,用户选择不同的套餐,通话就采用不同的计费策略,这种情况可以采用Strategy模式;一家销售自行车的公司同时销售自行车的零件,有的零件是多个零件的组合,又可以和别的零件组合成一个更大的零件,这就可以采用Composite模式。有时候程序员可以从模式中获得设计的灵感,对软件的整体构架产生影响。在以后的章节中我会对设计模式进行介绍。
软件开发思想经过了几十年的发展。最早的面条式的代码,一个中学生很快就能学会,可以立即用它来解决一个多元多次方程;后来发展到结构化编程,把代码分割成了多个模块,增强了代码的复用性,方便了调试和修改,但是结构也复杂了很多。最早的编程方式是面向过程的,非常直观,一个初学者很快就可以理解;后来有了面向对象的方式,问题的解决看上去不再这么直截了当,需要首先开发业务对象,然后才能实现业务流程。随着面向对象编程方式的发展,又出现了设计模式、MVC、ORM、以及不计其数的工具、框架。软件为什么会越来越复杂呢?其实这不是软件本身的原因,而是因为软件需要解决的需求越来越复杂了。
最早的计算机只需要帮助人们做纯粹的计算任务,他用插头和开关作为输入设备,用信号灯做输出设备,没有存储器,也没有程序。当时人们需要计算机,只是想让他帮助人们在人口普查、天气预报这样的事情中担任计算任务。早期的计算机曾经参与了曼哈顿计划,最初军方雇用了大约100名计算员,参与计算原子核裂变的各项数据,但是无法达到满意的进度。后来由于计算机的研制成功,大大加速了原子弹的研制速度,这也加速了第二次世界大战的尽快结束。
稍后一点的计算机采用卡片穿孔的方式作为控制方式。程序员把需要运行的运算用二进制的方式记录在卡片上,穿孔的地方代表1,不穿孔的地方代表0。把卡片塞进计算机的输入设备,计算机就按预定的方式运行,这就是最早的机器语言。随着计算机的运算速度越来越快,人们发现很多时候都是计算机在等待着输入设备,很多时间被浪费掉了。于是人们设计出了一次可以处理多个卡片的计算机。多张卡片可以同时放入计算机,计算机按照一定的规则进行调度,按照某种优先顺序执行他们,这就是中断和任务的调度的概念,最早的操作系统就这样出现了。银行、会计事务所这样的公司每星期把写好的卡片放到计算机中运行,很快就可以完成大量数据的处理工作。这时的计算机仍然是一种非常复杂的设备,维护、使用都十分困难,当时的计算机生产公司必须提供全套的安装、维护、运行解决方案。
此时的计算机编程就是在卡片上穿孔,所使用的语言完全是机器代码。机器代码难以记忆,不容易理解。于是人们发明了汇编语言,汇编语言实际上是机器语言的助记符号,可以用编译器翻译成机器代码卡片。从词法和语法上说,他更接近与自然语言,但是从流程上说,仍然是机器语言的那一套。
这个时候,计算机软件已经成为了一种耗资巨大的工程。当时的计算机硬件是一种十分高档的昂贵设备,以至于一些大公司都没有勇气独立购买,而是采取租用的方式,租借机器的投资可以达到上百万。而软件开发所花的钱比起租机器的钱是只多不少。最为典型的工程是IBM360的操作系统。当时IBM公司研发新的电脑——IBM360,他们的计划是为这种机器单独开发一个操作系统。软件开发耗资巨大,工期一再拖延,直到整机上市一年以后,操作系统才得以发布。后来一位参与操作系统开发的工程师写了一本书,描述了他在项目中得到的一个重要经验:向一个进度缓慢的软件项目中盲目的追加人力,只能让进度更加缓慢。软件开发需要很大的人际交流成本,这使得项目的规模不能简单的用人月数来衡量。
汇编语言这解决了机器代码难写难记的问题,但是不同公司生产的机器、不同型号的机器汇编指令集是不同的,在一种机器上写出来的程序无法移植到另一种机器上执行。于是有人希望解决这个问题,他设计了一种新的程序语言——FORTRAN。FORTRAN的语法更加接近自然语言,并且他设计了适用于多种机型的编译器,可以把FORTRAN的源码编译成可以在多种机器上执行的机器代码。计算机的创始人之一,冯·诺伊曼当时认为搞这种高级语言没有多大的意义,计算机发展的重点不应该在这个地方。冯·诺伊曼肯定是一个汇编高手,写起机器代码来也是相当的娴熟,自然认为搞什么高级语言没有多大的必要,汇编挺好的嘛。这种感觉就应该类似于当今的一些C语言高手面对JAVA和C#时的感想。
FORTRAN的意义在于,他使计算机不再是计算机专业人员才能操纵的工具。数学家、工程师、会计师、学生都有机会亲自使用计算机来解决自己的工作中遇到的问题。高级语言的诞生使得软件业得到突飞猛进的发展。当时的FORTRAN仍然不是一种结构化的编程语言,FORTRAN中的变量都是全局变量,他使用与汇编相似的方式进行流程控制。
随后的几年中,LISP、ALGOL、COBOL等一批高级语言陆续不断的涌现出来。这些语言都有一个共同点:他们都出现了局部变量的概念,都是结构化的编程语言。局部变量有着更加清晰的作用域和生命周期,程序划分为若干个子过程,某个子过程中定义的变量在其他子过程中是不能访问的。有了这样的机制,程序员不用再担心自己定义的变量被意外的修改,这样就有利于更多的程序员合作编写大规模的程序。
接下来的几年出现了越来越多的高级语言,在文字处理、科学计算、数据制表等各种领域发挥着重要的作用。BASIC语言也就是在这一时期出现(BASIC语言并不是结构化语言),他的全称是:初学者通用符号指令代码。BASIC语法简单,易学易用,普通人稍加训练就可以用BASIC进行编程了。
从这一段历史看出,计算机软件技术不断发展,他的目的是使人们使用计算机解决问题的过程越来越简单。人们需要用计算机来解决越来越复杂的需求,从最初的科学计算,逐渐发展到表格处理,会计计算,管理一个大型企业的商业活动,这就迫使软件开发技术和思想不断进步。要创建新的技术平台,让设计人员在这个平台上不用考虑过多的基本技术问题;要创建新的分析方法,让设计人员可以忽略细节上的复杂度,更容易从整体上把握软件的构架。
现在的软件技术发展仍然是在延续这样的方向。软件的构架越来越庞大,从单层的、简单的面向过程的代码,发展到多层的、面向对象的代码,表面上看是变得复杂了,实际上是使得软件的逻辑结构更加清晰,从而不断的满足日益复杂的业务需求。程序员可以从技术细节中腾出力量,去解决更加复杂的业务问题。
学习和运用各种先进的技术也应该注意这一点:技术的发展是为了更容易的解决业务问题,降低开发成本——这也是我们运用他们的目的。本文在后面的章节介绍一些编程的思想和技术,也会遵循着这样一种思路,要看看这些东西是如何使程序员更加迅速准确的解决业务需求。为了能够说明这些思想和技术是如何使得问题的解决逐渐变得简单,本文在举例说明的时候,会尽量的使用接近于实际情况的例子。简单的玩具形式的例子,比如用Cat、Dog和Animal来说明多态的规则,看上去很形象,也是很有趣味,但是事情本身过于简单。使用这样的方式很容易把技术的原理说清楚,但是却让人不易明白他们的使用场景,甚至误以为他们把原本简单的事情搞得复杂了。本文中的事例有的牵涉到一些特定业务领域的知识,看起来难免晦涩枯燥,希望大家能够容忍一下。