第3章 面向对象编程

难难难,

编程难,

面向对象谈一谈,

对了对象谈几句,

不对对象枉费口舌尖!

找对象难,找个对的对象更难,跟对象过日子是男上加男。但是凭良心说,在编程中,面向对象是真不算太难。

本章,且听我从头考证,细细说来。考证中,我发现面向对象的发展过程中,至少6个科学家因此获得图灵奖。更神奇的是,这些科学家对面向对象的看法并不统一。但是,都不同程度的受到了第一门面向对象Simula的影响。幸好,有很多的论文可以读,我发现,几乎所有的面向对象的概念,都已经在Simula中包含了,只是后来的语言,侧重点不同。Java也只是其中之一。Simula语言的故事,我觉得还挺有趣的,也是本章的重点。

但是写书,就要多写点,详细点。本章的计划是通过研究面向对象的历史来学习面向对象编程,具体的技术细节在第9章再讲,如果没有其它面向对象编程语言的经验,这一章中碰到不懂的问题直接跳过即可。

对C++或者Python语言来说,可以使用面向对象编程,也可以不使用面向对象编程。但是对Java来说,没有面向对象就没法编程。从这个意义上来说,Java是一种非常纯粹的面向对象编程。那面向对象有什么作用呢?James Gosling在接受采访时说:“面向对象可以使你把一个系统分解开,系统的每个部分都能够被分解,这样对更新、调试等很多事情都有帮助。

既然面向对象此的重要,我们不妨来研究一下面向对象的历史。

1 第一门面向对象编程语言——Simula

2001年,美国计算机学会(ACM)将代表计算机界最高奖的图灵奖颁发给了两位挪威计算机科学家,一位是Ole-Johan Dahl,另一位是Kristen Nygaard,以表彰他们“通过设计编程语言Simula 1和Simula 67,创造了面向对象编程的基本概念”这一伟大成就。

Kristen Nygaard和Ole-Johan Dahl

Kristen Nygaard和Ole-Johan Dahl两人对面向对象的贡献太多了。

奥斯陆大学是挪威最好的大学,也是世界最好的大学之一,在这个大学里,有两栋隔路相望的楼,一个名为Kristen Nygaards楼,一个名为Ole-Johan Dahl楼。

这是纪念奥斯陆大学两位杰出的校友,这两个年轻人在编程进入危机的黑暗年代,用努力的工作和杰出的才能,让人们看到了一丝曙光,这丝曙光,开启了编程的新时代——面向对象编程的时代。

在共同获得图灵奖一年后,2002年,两人先后离世。编程很难,感谢他们照亮了我们前进的路。

上世纪60年代,Simula语言从诞生之初,就具备了现在大部分面向对象语言所具备的功能,包括但不限于类、继承、动态绑定、垃圾回收等功能。虽然那个时候还没有“面向对象”这个名称,但是在这个名称出现之前,面向对象的概念已经存在了。

让我们看看当年面向对象语言是如何发展而来的吧。

1.1 面向对象发明人

1952年,二战结束不久,挪威政府决定成立一个叫NCC(Norwegian Computing Centre)的组织,把全国零散的计算机资源整合起来。说起来容易,做起来难,因为这些计算机资源都分散在全国各个部门,比如有的在NDRE(Norwegian Defense Research Establishment)这样的军方部门,有的在奥斯陆这样的大学,还有的在工业界。之前的挪威,还没有一个组织可以协调所有的部门,NCC为此而成立。

在二战的时候,所有国家都知道了计算机的威力,挪威也不例外。毕竟挪威是在二战中与纳粹德国抗争时间坚持第二长的欧洲国家,仅次于俄国。战后,1954年,他们研发了第一台计算机,名字叫NUSSE(Numerical Universal Automatic Sequential Electronic Computer),这是一台真空管计算机。该真空管计算机制作完成以后,就被运到了上面所说的新成立的NCC里。

NCC刚成立的几年,除了这台NUSSE真空管计算机,没有其它的计算机。拥有最多计算机资源的是挪威军方NDRE,在当时战争的影响尚未完全消退的大环境下,任何国家都是如此,军方优先拥有最多的计算机资源。军方要制造自己的核反应堆,根本没有多余的计算机给别人。NCC和NDRE这两个组织同时存在,计算机资源都很贫瘠,地主家也没有余粮,多一个组织,多一个僧,这两个组织反而加剧了僧多粥少的竞争关系。

军方要增加自己的计算机资源,就从英国的Ferranti公司买一台名为 Mercury的电脑。该项目的负责人叫Jan Garwick,之前在奥斯陆大学当教授,他招了两个人来当助手。

其中一个助手是1952年加入的新兵,他就是Ole-Johan Dahl,他的任务是给Mercury那台电脑写一个编译器,这个编译器叫MAC(Mercury Automatic Coding)。

另一个助手也是个士兵,是1948年入伍的Kristen Nygaard,他被安排了另外一个项目,该项目是研究一个开放性的问题:如果有一天挪威要制造核武器了,应该如何提前模拟核武器的爆炸威力?

Kristen Nygaard以模拟核武器为题写了一篇名为《Theoretical Aspects of Monte Carlo Methods》的论文,随后在军方成了专职的核武器研究员。虽然他的专职工作是研究核武器,但是他兴趣广泛,他的研究范围从单纯的核武器扩大到世间万物,尤其是人力资源能不能也用计算机来建模呢?他对此很有兴趣。

他希望他的方法可以管理社会的组成单位:人。他想通过计算机模拟,使用统计学的方法来解决人工作效率不高的问题。通过计算机来模拟现实世界,这样想法听起来非常的诱人,尤其引起了苏联的注意,当时苏联是计划体制,如果人也可以被计算被管理,那就再好不过了。

这是一种将社会工程学和管理学相结合的产物,Kristen Nygaard研究人的行为并加以预测和控制。苏联对此十分热衷,一直跟进他的研究,一旦有成果,苏联就把成果在自己的乌拉尔大型机上实现出来。

后来Kristen Nygaard与军方发生了一些摩擦,他于1960年离开军方并加入前文提到的NCC。相比于军方,NCC更多倾向于民用研究,在这里,他有更大的空间可以自由的发挥。在此,他思考这样一个问题,能不能将他在军用上做的工作转成民用呢?

一封当时保留下来的信如实记录了当时的情况,1962年,Kristen Nygaard给法国计算机科学家Charles Salzmann写了一封信,在信中他透露说他已经有了完整的模拟现实世界的概念,还没动手写语言的编译器,他想等语言先设计好了再动手做这个工作。他认识一位写程序的天才,两人都表示很乐观。在这封信里他提到的那位天才就是他未来的合作伙伴Ole-Johan Dahl。

Tip

这封信可以在这个网址查看:http://cs-exhibitions.uni-klu.ac.at/index.php?id=37

这两个年轻人在1962年终于开始共同工作了。

1.2 Simula的研究成果

1963年,他们两人开始研究并实现这个创意,两年后,于1965年完成第一阶段工作。这个阶段的成果在当时被称为Simula,为了区分,后被约定俗成称之为Simula I。

随后的两年,两人继续研究,于1967年发表了第二版本的语言,也就是后来的Simula 67,Simula 67已经有了面向对象的雏形,几个与面向对象相关的概念已经被提出。我们看看这几个重要的概念吧。

1.2.1 错误检查

由于Simula语言最初研究的是核爆炸,安全性显得特别重要,与其它同期的语言不同,Simula格外重视安全,毕竟核爆炸可不是只让电脑死机那么简单了。Simula在设计之初对错误十分重视,Simula设计了两种错误检查,一种是编译时检查,一种是运行时检查。

现在我们把这种机制叫做类型安全(type safety)。

程序员在讲到类型安全的时候,不同语言的程序员,对这个概念的理解十分不同,讲出来的意义也就十分的宽泛。

在本书中,我所指的类型安全不仅是对一个A类型的变量赋值了一个B类型的值,[^ 2] 更多的是考虑到类的层面,比如每个对象创建后都要初始化,外部对类的访问要受相关的限制,对象抛出异常之前要先将自身重置到合法状态等等。类型安全的思想贯穿于整个编程中,而不仅仅在变量赋值这种技术细节。

不止Java借鉴了Simula的错误检查,目前像Haskell语言中的inspect方法或多或少都是从这里学来的。

1.2.2 继承

Ole-Johan Dahl曾经这样写过:“增量的抽象,对已经抽象过可以加上一个前缀C,只要有这个前缀C,就可以使用其所有的属性。这就是继承的雏形,虽然还没有正式叫继承。”

其实对任何语言来说,都可以复用代码,至少可以通过简单的“复制粘贴”来完成代码的重复使用,但是这样复用的代码并不优美,而且还很难维护。如果能够直接使用别人已经完成的代码,或者自己先前抽象好的代码,而不是自己再重新开始,那么将会有效的降低工作量。Simula语言在这个方面进行了探索。

Java当然也借鉴了这个思路,Java围绕类的概念做了很多工作,其中最重要的概念之一就是:“继承”。在以后的章节里,我会详细介绍继承的概念。

望文生义,继承的表面意思就是从“长辈”那里得来一些东西,在Java中,基本也是这样的,采用已有的类,无需改动这些类,就能获得相应的功能,这种方式在Java中也叫“继承”。

1.2.3 内存回收机制

Ole-Johan Dahl和C.A.R. Hoare合作写过一篇论文《Hierarchical Program Structures》 ,在这篇论文中,他们介绍了当时的想法:“在实现Simula的时候,借鉴了Algol 60这个编程语言的实现方法,并且对Algol 60的block进行了必要的改进,将block视为数据,其它的程序可以使用这些block,这后来演化成了类的使用方法。”

Simula语言的两位作者写的《The Development of the Simula Language》里, 也提到了这一点:“改进了存储机制,引入了内存垃圾回收机制。也许很多语言都意识到了Algol block的威力,但是第一个意识到并且做出垃圾回收的语言是Simula,这在当时是一件了不起的创举。虽然内存垃圾回收很难,还有可能对语言的运行速度产生影响,但是为了以后程序员的方便,这点性能缺失不算什么。Simula还独创了一种名为二维空闲列表的方式来负责内存垃圾回收。”

在计算机科学中,内存泄漏(Memory leak)是一种常见的bug,由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

内存泄漏会因为减少可用内存的数量从而降低计算机的性能。最终,在最糟糕的情况下,过多的可用内存被泄露掉从而导致全部或部分设备停止正常工作,最终导致应用程序崩溃。

现在主流的编程语言如Java就提供了内存回收机制,这对保证软件的可靠性和安全性非常有好处。如果大家有C/C++编程经验的话,很可能为了一个内存泄露花费数小时甚至数周来查找。有了内存回收机制以后,将节省大量的编程与调试时间。

1.2.4 动态绑定

Simula开创性的使用了动态绑定技术,虽然当时的名字不叫dynamic binding,而是叫virtual。刚开始的时候,Simula所有的属性都是静态的。Ole-Johan Dahl是在最后一分钟才决定做成动态的,如果对属性定义为virtual,那么就可以动态绑定了。后来的语言如Smalltalk更纯粹,直接把所有的属性和方法都定义为virtual,在C++中也用相同的关键字virtual。

Note

以上讲的这部分内容也在《The Development of the Simula Language》这篇文章中。大家可以自行搜索,或者到xueban.app上下载,如果能少刷两小时抖音看美女,看看这篇文章我觉得是值得的!

Java语言同样借鉴了Simula语言,在默认情况下是可以动态绑定的,如果使用了final这个关键字,就是静态绑定从而阻止被覆盖。

1.2.5 小结

如果用Peter Wegner在1987年对面向对象下的定义:object-oriented = objects + classes + inheritance来衡量的话,至此,面向对象最重要的几个要素在Simula语言中都已经有了雏形。

Peter Wegner介绍

Peter Wegner是一位出生于1932年的英国科学家,对面向对象编程有很大的贡献。

在1999年,他被奥地利授予奥地利科学与艺术荣誉奖,在去伦敦领奖的路上,出了车祸,昏迷了好久之后才苏醒,但是有了严重的后遗症。

如果只有技术,没有推广,Simula可能仍然会像世界上绝大部分的编程语言一样无闻。著名的科技史作家、宾夕法尼亚大学历史学教授Thomas Parker Hughes在爱迪生的传记《Networks of Power: Electrification in Western Society》里这样评价:“像爱迪生这样伟大的发明家有这样的特征,为了达成目标,他们不仅有超越普通人和科学家的认知,还能够综合利用自己的社会关系,政治资源,商业手段。”

我觉得强有力的推广这种优秀的品质,在Simula两位创始人身上体现的也很明显,两位创始人不只是可以埋头搞科研的人,同样和爱迪生一样是政治和商业上的天才。他们不仅有雄心壮志,同样也有政治动员能力、商业运作水平,这两位创始人靠着自己无与伦比的谈判技巧和推广能力,把Simula语言从挪威推广到了整个英国、法国、美国、苏联,最后影响了全世界。

虽然这是一本编程的书,我还是希望大家能学到比编程更多的东西。要记住,酒香也怕巷子深。能够把一门编程语言推广起来,可不是一件容易的事情。

当你写出一款优秀的软件,或者创造了一个编程语言,只是成功走完了第一步。如何推广软件或者语言,让别人用你的软件或者语言,是更重要也是更困难的一步。接下来学习一下Kristen Nygaard和Ole-Johan Dahl是如何推广Simula的吧。

1.3 推广Simula

挪威不是计算机强国,如果要推广自己的Simula语言,就要先在计算机上运行。在当时,计算机是非常昂贵的,NCC有意从英国购买KDF-9这台大型计算机,但是这台计算机的价格实在太贵了,远远超出了NCC的预算。当时美国已经制造出了UNIVAC这样一台机器,NCC想购买这台电脑。

Tip

再宣传一下我做的名字叫《软件那些事儿》的podcast,在这个电台的200期到203期里,我讲过UNIVAC这台计算机。可以通过泛podcast客户端搜索收听。

Kristen Nygaard表现出了天才般的谈判技巧,他找到了UNIVAC在欧洲的负责人James W. Nickitas,经过了一次谈判,他说服了对方,对方同意把UNIVAC打5折。还敲定了下一次会谈,要和UNIVAC软件的灵魂人物Robert Bemer——前IBM计算机的核心之一——坐下来谈谈软件和编程语言的事情。

UNIVAC的软件核心Robert Bemer在和Kristen Nygaard谈过以后,UNIVAC不仅可以以半价卖给NCC,而且还给Simula项目带来了一笔赞助,还有,Kristen Nygaard成了北欧UNIVAC的销售代理。这样的谈判水平,已经可以说是高手中的高手了。

这种谈判并不是只发生了一次,在Kristen Nygaard推广Simula的时候,这种事情屡次上演,他总是能把最难谈的谈判像谈天一样搞定,很快Simula就可以在UNIVAC,IBM360/370, CDC 6000, DEC System-10等一系列当时最主流的机器上运行了。

Kristen Nygaard不仅对编程和计算机有兴趣,他一生还投入了大量精力去做政治运动,始终站在劳动者一边,争取劳动者的权力。

Simula影响了工业界和学术界,也培养出了一大批Simula的拥趸,比如Smalltalk的作者Alan Kay,C++的作者Bjarne Stroustrup都曾声称自己的语言深受Simula的影响。

2 Java中的一切皆对象

写这个小标题的时候我有点犹豫,因为很多语言都这样宣传自己。Java是,Python是,Ruby也是。但是如果仔细的钻起牛角尖来,每门语言又都有一些不是“面向对象”的部分。对Java来说,如果不用面向对象的方法来写,根本就没法运行,所以我还是起了这个标题。

支持面向对象的语言有很多,这些语言都是按照设计者的意图来开发的,我们学Java,那么James Gosling的意见是最值得参考的。幸好,James Gosling为了推广Java,给我们留下了大量的参考资料,他本人写过很多本书,比如《The Java Language Specification》、《The Java Language Environment》等。除了文本资料,还接受了大量的访谈。这些资料,可以解释Java中几乎所有的为什么?

前文讲过,作为先驱的Simula语言,有两个最重要的“学生”,一个是SmallTalk,一个是C++。这两个语言对Simula的继承与推广,影响了后来几乎所有的面向对象编程语言。这两个语言,像两座山峰,其它的语言,大部分都是在这两个山峰之间寻找自己的位置,要么离SmallTalk近一点,要么离C++近一点,Java也不例外。

前面讲了好多Simula语言的历史,但是“面向对象”这个词并不是Simula提出的,而是SmallTalk的作者Alan Kay提出的。对面向对象,Alan Kay的理解是:“互相传递消息的程序设计就是面向对象。”对先驱Simula语言,Alan Kay有自己的看法:“我不喜欢Simula中继承的做法,我并不是不喜欢类,但是我还没有见过不让我感到痛苦的包含类的编程语言。”

Alan Kay介绍

Alan Kay博士是计算机科学领域的重要人物,以其在面向对象编程和图形用户界面的开发方面的贡献而闻名。他于1970年代在施乐帕洛阿尔托研究中心(Xerox PARC)工作期间,提出了SmallTalk编程语言的概念,这是首批面向对象编程语言之一。Kay还提出了“Dynabook”的构想,这是一种类似于今天平板电脑的个人计算设备,旨在通过计算技术来促进教育和创作。他被认为是计算机领域的先驱之一,并获得了多项重要奖项。

Alan Kay博士在回答编程的问题时,对面向对象有自己的看法。并且是与C++、Java完全不同的看法。一般来说,咱们要同时在脑子里装上几种不同的看法,不要当成纸片人。关于面向对象,想看完整的问答,可以参考:http://www.purl.org/stefan_ram/pub/doc_kay_oop_en

他还说过一句特别出名的话,我在短视频中听到过多次:预测未来最好的方法就是创造未来。(The best way to predict the future is to invent it.)

C++语言则是有不同的看法。在C++作者Bjarne Stroustrup的著作《The Design and Evolution if C++》中,他认为“Simula的继承机制是解决问题的关键”,“类是一种创建用户自定义类型的功能”,“面向对象程序设计是使用了用户定义和继承的程序设计”。我觉得这可能与C++作者的经历有关,他在发明C++之前,一直用C和Simula语言,C语言不支持类,Simula又太慢。他最初的想法是实现一个速度很快的Simula,最好像C语言一样快,C++最初的名字是C with Class语言。

Java的位置处于两者之间。接下来,我们来看看Java是如何权衡利弊,被设计成这样的。

2.1 什么是对象

1995年,James Gosling写了第一份Java白皮书《The Java Language Environment》,在这本白皮书的第三章,详细的介绍了什么是对象(object)。

当时是1995年,人们对“面向对象”有很多争论,现在20多年过去了,争论的声音越来越少。就像现在谈起“面向过程”编程,几乎没有什么争论一样,“面向对象”也要经历同样的过程,尘埃会慢慢落定。

生活中充满了对象:车、咖啡机、鸭子、树都是对象。软件也由对象组成:按钮、菜单、图标也都是对象。无论是生活中,还是软件中的对象,都有自己的状态和行为。如何把现实中的“对象”建立在计算机中,是“面向对象”要解决的问题。

我们可以对一辆现实中的汽车进行建模,让其对应于计算机中。一辆汽车有其状态(车速、油耗、颜色、手动档还是自动档等等)和行为(启动、转向、停车等)。

现实中的对象与编程中的对象

当我们开着这辆车去上班后,可能会在办公室里查查自己买的股票,看看是不是又融断了。股票也是对象,也可以映射在电脑里,也有自己的状态(最高价、最低价、开盘价、收盘价等)和行为(股价波动、退市等等)。

看完十天融断四次的股票,你头晕眼花,想去冲一杯咖啡缓一缓。咖啡机也有自己的状态(水温、咖啡种类等等)和行为(加热、搅拌、流出一杯Java咖啡等等)。

所有这一切都可以映射为对象。

2.2 对象怎么工作

简单来说,人是对象的一种,人怎么工作,对象就是怎么工作。以牛马为例:牛马.拉磨(),牛马.吃(苦),牛马.吃(饭),牛马.喝(水)。

这个“.”号就是Java中让对象工作的方法。

3 面向对象为什么这么难?

面向对象目前来说是最流行的软件开发技术,但是却比较难理解。要掌握这项技术,需要很多的锻炼,就算现在有了很多辅助工具,如果不能从内涵中理解面向对象编程,也无法熟练使用。在这一小节中,我想来讨论一下为什么面向对象这么难以理解,到底难在什么地方?

3.1 名词搅拌器

用2000多年前孔子的话来讲:“道不远人”。道并不远离人的日常生活,一个人修道的时候如果远离人的日常生活,那他修的就不是道了,用武侠小说中的话来说,可能已经走火入魔了。面向对象编程也是如此,不管这个技术多好,都不能远离程序员太多,如果这种技术已经让大部分程序员搞不懂了,那就不是好的编程技术。

如果面向对象编程让程序员有亲近感,首先在语言上要平易近人。显然,面向对象编程在这一点上做的不好,甚至可以说很差。面向对象有大量的名词,这些名词像把一本新华字典丢进了一个搅拌器,随机搅拌了一堆词语出来,比如下面的词汇:

泛化、特化、父类、多态、属性、委托、注入、构造函数、异常、框架、类库、组件、模式、用例、建模、重构、敏捷、重写、集合、关联……

这只是举了一部分中文的,还有大量英文缩写的没有列出来。这种名词搅拌器一样的编程语言,确实让人很头疼。如果是初学者,一看到这些名词,就已经吓的不敢深入学习了。

存在大量的术语有多方面的原因,如果了解了背后的原因,就不会如此纠结了。

一部分原因是广告的需要,公司推广一项技术,就要写的云山雾罩的,这样显得比较有技术含量。这个不仅是技术行业,在任何行业都是这样,以化妆品为例,我经常盯着老婆化妆品上那些诸如“活泉精华”“抗氧精粹油”发呆,每个字都认识,连起来就是不懂这是什么黑科技。

还有一部分原因是技术人员故意避开以前存在的名字,以便显示自己的独创性,其实没没什么独创性,人类总是重复发明旧的科技,然后新瓶装旧酒,用新的名字包装旧的技术。比如Java中的多态(polymorphism),这个技术无论是内涵与外延,都和动态绑定(dynamic binding)是一样的,后来又来了一个后期绑定(late binding)和一个运行时绑定(run-time binding)。同一个类似的技术,一下子有了四个名字。这种情况在面向对象的技术发展过程中实在是太普遍了,大家一定要一眼看穿这种鬼把戏。

3.2 滥用隐喻

在著名的《代码大全》这本书里,第二章讲了隐喻对理解软件开发的影响。如果选择错了隐喻,那么就会对软件开发有很大的误解。

同样,在面向对象中,如果乱用隐喻,也会让人对面向对象产生误解。

前文3.2小节举的例子是参考James Gosling写的Java白皮书《The Java Language Environment》的第三章。把现实中的对象映射为编程中的类,有助于理解面向对象编程,但是同时也有副作用,会让读者误以为现实事件的物体都可以映射为类。实际情况并不是这样的。

现实中的物体不是由类创建的,而且和面向对象编程中的类大相径庭。现实中的人是父母生的,不是类创建出来的实例。除此之外,现实中的人会根据场景的不同有多种角色,比如我在公司是“员工”,在家里是“父亲”和“丈夫”,对父母则是“儿子”。

但是在Java面向对象编程中,一旦根据类创建了实例,那么这个实例就只属于唯一的类,无论时间空间怎么改变,都无法改变这个实例的类型与行为。现实中的我,随着时间的变化,已经从“后浪”成了“前浪”,从“少年”变成了“大叔”。

软件只能涵盖人类工作的一小部分,并不因为引入了面向对象编程而让软件有质的变化。虽然在在推广和宣传面向对象编程的过程中,有意无意的夸大了该方法的优点,但是我们程序员切不可认为面向对象真能模拟现实世界。

在面向对象编程以后,还曾经兴起过一个叫“面向代理”的技术浪潮,但是这个技术很快销声匿迹了。“面向代理”的技术在宣传的过程中,有点类似“人工智能”,能够主动的创造软件,自主的响应人类的需求。很可惜,目前的技术实现不了所宣传的目标。

有了计算机,大量的工作仍然要用人来实现。目前的计算机架构决定了无法完全顶替人类来完成现实世界的工作。引用《Code: The Hidden Language of Computer Hardware and Software》的作者Charles Petzold的话来说:“人类有很多的交流形式不能用非此即彼的可能的选择来表示,但是这些交流形式对我们人类的生存又非常重要。这就是人类为何没有与计算机建立起浪漫关系的原因(无论如何,我们都不希望这种情况会发生)。如果你无法用图画或者声音来表达某种事物的时候,你就无法将这个信息用比特的形式来编码。”

是啊,像林俊杰的歌中唱的:“确认过眼神,我遇上对的人。”如果你相信面向对象可以对世间万物进行建模,那么,我们如何能用面向对象的方法对眼神进行建模呢?

隐喻能帮助我们理解面向对象编程,但是也会让干扰我们对面向对象编程的理解。这也是面向对象编程难以理解的又一个因素:滥用隐喻。

3.3 过度宣传

其实,比起现在的宣传,如果说编程的宣传有点捕风捉影,现在的宣传简直就是平地扣饼,编程已经是已经很内敛了。

面向对象编程是正确程序的替代品。 (object oriented programs are offered as alternatives to correct ones.)—— Edsger W. Dijkstra

Dijkstra的这句话提醒我们不要教条,过度宣传会给用户不切实际的幻想,经过30多年坚持不懈的宣传,让程序员误以为就没有面向对象解决不了的问题,我把这称之为“面向对象”综合症。

通过前面介绍Simula 67的历史,我们会知道Simula 67这个面向对象语言的先驱,当时并没有“发明”出面向对象这个概念,而是后来Smalltalk语言的作者提出的。在Simula 67中,引入class的作用仅仅是汇总子程序和变量的结构,到了Smalltalk中才开始使用继承结构来组织类库,所有的类都来自于Object类。

由于Smalltalk和C++的流行,面向对象的概念已经不是某个人能左右的了。这种现象已经不能用技术来解释了,让我想起了郭德刚讲的一段相声,当一个人听到一个段子的时候,会添油加醋的渲染一番,然后再传给下一个人,下一个人也是如此这般,等传到十个人的时候,事情已经面目全非了。

以我工作的经历,那些完全不懂技术的领导也知道面向对象的好处,而且很有主见。领导或者项目经理在给程序员提意见的时候,经常说:“不要想得那么难,用面向对象的方法,对这些东西建个模,很容易的。”然后,我还要装作恍然大悟的样子夸领导几句。

这都是过度宣传导致的结果,面向对象编程被包装成了无所不能的银弹。虽然现实世界是由一个又一个的对象组成的,但是想把现实世界映射在程序中,并不容易。

Note

IBM大型机之父佛瑞德·布鲁克斯写过一篇论文《没有银弹:软件工程的本质性与附属性工作》。

在这篇论文中他强调由于软件的复杂性本质,而使真正的银弹并不存在;所谓的没有银弹是指没有任何一项技术或方法可使软件工程的生产力在十年内提高十倍。

其中银弹(Silver Bullet)的来历是:在欧洲民间传说及19世纪以来哥特小说风潮的影响下,银弹往往被描绘成具有驱魔功效的武器,是针对狼人等超自然怪物的特效武器。后来也被比喻为具有极端有效性的解决方法,作为杀手锏、最强杀招、王牌等的代称。

两界普利策得主约翰·卡雷鲁写过一本书叫《坏血:一个硅谷巨头的秘密与谎言》,在书里他这样写道:“雾件(Vaporware)反映了计算机行业的一种倾向,在涉及市场营销时,做法非常轻率散漫。微软、苹果和甲骨文都曾被谴责某些时候都有类似的做法,过度承诺是硅谷的标志性特征之一。”雾件(Vaporware)这个词现在已经不太出现在媒体上了,但是这种过度吹嘘的行为至今仍然存在,甚至愈演愈烈。

以上就是面向对象为什么这么难的原因。

很多的程序员并不清楚为什么要用面向对象编程,也不了解面向对象编程的历史,反正大家都在用,有关面向对象编程的工具有很多,有好用的IDE,有UML工具,那我也跟着用用就好了。在这种心态下,面向对象编程就成了一种很神秘的东西,“虽然不理解,但是大家都在用”。

本书要做的事情就是尽量把Java和面向对象的知识点讲清楚。为尽量避免上面讲到的三种缺点,本书会使用如下的原则:

1. 为了避免名词搅拌器,尽量不过多引入术语。

2. 会使用隐喻,但是不会到处使用隐喻,最终还是会落实到代码上。

3. 不过度的宣传Java以及面向对象的强大,而是从为什么引入这种技术以及这些技术能解决什么问题的角度来讲解。

4 面向对象可以很简单

在大体了解了什么是对象,如何操控对象以后,我们再用例子讲解一个为什么面向对象编程有优点?

无论写什么软件,都要先进行设计,设计有简单有复杂,复杂的可能要采用很多文档图表来描述其设计过程,简单的可能只需要想一下,但是,都要有设计的过程。在设计的时候,第一原则是:要尽量让好事发生,让坏事不要发生。

设计阶段,人自然而然的是用面向对象的方法来思考问题。比如考虑一间房子时,不会去想门、窗户和玻璃的具体组成。如果房子很多,我们的视角会放大到小区。如果小区很多,我们的视角会再次发生改变,抽象到城市的层面。城市再多,会抽象成一个国家。

一层又一层的抽象之中,我们实际上是用面向对象的方法隐藏了很多细节信息,这主要是为了让我们的大脑有足够的处理空间。

这是有科学依据的,著名的计算机科学家Dijkstra曾经说过:“没有人的大脑可以容纳下一个现代的计算机软件”。目前的软件越来越庞大,数百万数千万行的软件早已经不是什么新鲜事,没有人可以了解一个大型软件的方方面面。我们如何确保在不知道其它功能的前提下,安心的做我们这一块功能?又如何确保我们做完了这个功能,能保证和其它部分和谐运行呢?面向对象就是来解决这个问题的。

软件危机的主要原因,很不客气地说:在没有机器的时候,编程根本不是问题;当我们有了电脑,编程开始变成问题;而现在我们有巨大的电脑,编程就成为了一个同样巨大的问题。

– Dijkstra

目前,人们找到的一个解决方法是将整个系统分解为子系统,然后再将子系统分解为类,再将类分解为子程序,最后再把子程序变成可以运行的代码。

如何设计类是一门大学问,本书将会用一整本书来反复研究类和面向对象这件事。下面就先以一个足球游戏来简要的说明一下,先有个大体的概念。

4.1 FIFA足球游戏

足球是世界第一大运动,上一届世界杯说有22亿人观看,这有可能是真的么?在游戏行业,有两个最著名的足球游戏,一个是KONAMI的《实况足球》,一个是EA出的《FIFA》。以FIFA 19来举例吧,假设EA把我们请去设计FIFA游戏,我们应该怎么做?

想象一下,如果我们要去设计FIFA这款足球游戏。这个游戏里,有数百支球队,数百位教练,数千名球员,看台边上举着相机的记者,看台上数万球迷……想想头就大了。幸好,我们可以用面向对象的方法把所涉及的对象抽象出来。

也许有些人没有玩过FIFA足球,为了讲编程概念比较容易,我先用文字描述一下吧,如果可能,自己找个视频看看,或者自己玩一下就更有直观的体验了。

大家在电视里看过足球吧,FIFA游戏尽量模拟真实的足球,只是把控制权交给了玩家。玩家可以用手柄或者键盘控制场上踢球的球员。现实足球场上发生的一切,都尽量的在游戏中加以模拟。当然有所取舍,因为完全模拟是没办法做到的,如果仔细看得话,会看到场边有举着相机的记者,他们的动作是一模一样的,看台上的球迷也都雷同,场边的第四官员和教练动作也比较僵硬。原因就计算力有限,FIFA使用的寒霜引擎非常耗费资源。好钢用在刀刃上,好运算力用在渲染球员上。

对这个游戏来说,最重要的部分是球员。让我们来看看如何抽象游戏中最重要的球员吧。

4.2 对球员进行抽象

下面是FIFA 19中的截图,在FIFA中,球员有很多的属性,我挑一小部分说一下。

FIFA 19中球员属性截图

每个球员都有名字,图里面用的是Messi,还有球员在场上的位置,Messi在FIFA 19是右边锋(RW)。再就是94这个能力值,这个能力值是FIFA游戏开发组给每个球员的综合评价,梅西和C罗是最高的94分。目前来说我知道是最低的能力值是天津权健的吴磊,能力值仅有48分,现在中超的射手王武磊能力值是76分。

还有一些属性是国家:阿根廷(显示的国旗),所属的球队(巴塞罗纳)。还有下面数字分别代表的单项能力值(满分是100):

PAC(速度):87

SHO(射门):92

PAS(过人):92

DRI(盘带):96

DEF(防守):39

PHY(体能):66

以上列出了球员的状态,除了属性,还要列出球员的行为。

当设计类的时候,主要考虑类的两个方面:
  1. 这个类有什么属性?

  2. 这个类有什么行为?

球员在球场的行为有很多种,像FIFA游戏,场上的每个球员可以响应86种动作1,目前只需要列出其中的几种:停球,传球,射门,防守。根据这些属性和行为,可以画出如下球员类所示:

球员的类图

经过这样的抽象,我们把球员的信息得以大幅简化。编程首要的任务是控制复杂性,用这种抽象的方法能有效的简化编程过程,这样,我们可以忽略掉次要的属性,而将主要的精力放在重要的属性上。在我们设计的球员类上,对外部程序来说,类名是可见的,但是类的内部信息比如如何实现停球,传球则是要隐藏的。学术一点叫数据封装与信息隐藏。

现代只要号称支持面向对象的语言都会提供数据封装与信息隐藏的机制,包括Java。只是不同的编程语言支持的方式有些许不同,但是总体来说,都没有超过上一小节中讲的Simula语言所规定的范围。接下来,把数据封装和信息隐藏分别再讲一下。

4.2.1 数据封装

在讲数据封装以前,我们先考虑这样一件事,如果没有数据封装,我们应该如何写软件?比如游戏中的一个球员,在处理的时候,我们要分别处理每一项属性,比如身高、国籍、球衣号码还有各种球员的动作等等。

数据封装的意思就是把这些乱七八糟的东西,封装好了,只传递一个对象过来。

目标是明确的,但是具体到实现上,就有各种各样的问题。不同的语言针对这些问题分别给出了自己的解决方案,本书在后面的章节中将会详细的介绍Java对这些不同问题所采用的取舍。

在Java中采用的是和C++语言相似的方式来定义类,都是用的class。但是在对象的分配与引用,是否回收上有些许的不同。

4.2.2 信息隐藏

当数据都被封装起来以后,为了提高可靠性,不管是有意还是无意,我们都不能让用户通过随意的方式修改里面的内容。如果要修改,只能通过我们允许的方式来修改,这就是信息隐藏。

黑箱操作在现实中一般不是好事,但是在面向对象编程中,把对象包装成黑箱是非常好的方法。这样的好处非常多,比如可以限制变动的影响范围,可以在黑箱内部修改数据结构或者方法,而无需修改调用程序本身。还可以“使用我们允许的方式”监视数据的使用,这样会促使我们思考类中的数据是否应该是全局的?在Java编程之中,我们会经常发现,“全局数据”其实是某个对象的数据。

具体到我们的球员类,我们不会想让外部的程序随意更改对象的名字,比如把Messi改成Ronaldo。同时,我们又想让一些属性可以通过我们允许的方式被修改,比如更改场上的位置,有些球员可以踢前锋,也可以踢中场,这是可以修改的。基于这种要求,Java也提供了相应的机制,字段是私有的(private)还是公有的。

4.3 继承

每个球队中都有一个场上队长,像梅西目前担任球队的队长,布教授是球队的副队长。从编程角度来看,队长和普通球员绝大部分属性是一样的,在球场上,队长会戴个队长的袖标。当把队长换下场的时候,有时候会出现一个过场动画,会有球员把队长袖标戴给副队长戴上,在玩游戏的时候,我会为这种细节感动。

队长类和球员类几乎是一样的,虽然直接把球员类拿过来是不行的,但是只要把球员类稍微的做一下修改,那就可以了。这时候又产生了一个问题,我们刚刚讲过,类要保证数据封装和数据隐藏,如果直接修改源码的话,修改人员要了解球员类的代码,才能做出修改,这很困难。为此,Java引入了继承的机制来解决这个问题。

在编程中,如果新的类(比如队长类)可以继承已有的类(比如球员类)的数据和功能,而且新的类可以允许对原有的类增加或者修改一些内容,那么复用将会变得更加便利。用这种方法,程序员就可以从已经完成的类开始,修改得到其子类型,满足新的要求。这还能理顺类之间的关系,队长也是球员,两个类之间是父子关系,球员类是父类,队长类是子类。

我们要给队长加个行为:挑边。挑边的意思是开场前,主裁判会扔个硬币,两队队长会根据硬币的正反来选择进攻的方向。按照前面的理论,只要有个机制确保能继承球员类即可。示意图如下:

在这里,先有个概念即可,在实际的编程中,继承并不是这么单纯,实现继承的策略也不像这个例子中这么清晰。目前先不用担心这些细节,在第9章开始,我们再来一起学习Java提供的继承机制。

从理论上来说,继承可以从多个父类中继承,这种叫多继承;也可以从单个父类中继承,这种叫单继承。不同的语言在这个地方有所取舍,Java采用了一种较为简单的方式,只支持单继承。这样虽然简单,同时也失去了多重继承的优点,为了弥补这种缺点,Java语言实际上可以借助接口(Interface)来实现多重继承的功能,这个在以后的章节我们一起学习。

4.3.1 继承的缺点

继承的好处我们讲了好多了,再讲点继承的缺点。

前面讲了类的优点,可以把数据封装起来,还可以隐藏信息。但是过度的使用继承,会导致有多层的继承树,这样编程风格的代码,处理起来非常困难。假设我们有这样一个类,由多层继承而来,该类中有一个方法。如何找到这个方法最初的定义位置呢?没有好办法,只能先查父类,如果没有,再查父类的父类……就算查到了,也不敢贸然修改,因为不确定这个类影响的范围有多广,如果修改了这种类,一定要用一种叫回归测试的方法进行广泛测试。

回归测试是什么?

回归测试是指修改了旧代码后,重新进行测试以确认修改没有引入新的错误或导致其他代码产生错误。自动回归测试将大幅降低系统测试、维护升级等阶段的成本。

总之,凡事要有个取舍,我在工作中读过10来层的继承代码,理解起来非常难。尽量兼顾代码的复用度和代码的清晰度。

5 虚构的访谈

《FIFA首席设计师》

以下故事纯属虚构。帕克休斯是一个虚构的记者,栋哥是一个虚构的FIFA首席设计师。

帕克休斯:你好,栋哥,我们都知道你已经成了FIFA的首席设计师,你能谈谈这次设计的目标么?

栋哥:这次设计我只有一个小目标,先赚他一个亿!

帕克休斯:呃……不是从经济收入上,能从程序设计的角度谈一下么,比如,听说你在设计这款足球游戏的时候用到了面向对象的编程方法,我对面向对象也有所了解,我在大学里的时候老师就讲过,面向对象就是类,包括封装,继承好像还有多态什么的。我有一个问题,听老师讲了这么多,还是有点不清楚,不知道这些具体怎么用,你能给我讲一下么?

栋哥:学校里主要是书本知识,书本知识是一种提炼与抽象,套用一句常用的话来说就是源于编程,又高于编程。在学校中,绝大部分学生最关注的是考试成绩,在现实的编程中,你认为编程最重要的是什么?

帕克休斯:是如期的发布产品?

栋哥:也可以这么说吧,如期发布高质量的产品是软件开发最困难的事情。像FIFA每年都是雷打不动的9月末发布,去年9月28,今年还是9月28。而每年的FIFA都要进行不少的改动,用软件开发大师爱德华·贝拉德话来说就是:“Walking on water and developing software from a specification are easy if both are frozen.”翻译成中文是:“一边走在水上一边开发软件是非常容易的,只要两者都冻结了。”但是现实社会不停的变化,软件需求也要不停的变化,比如在2019年7月份,我得知我们的竞争对手KONAMI在7月3日宣布和曼联成为合作伙伴,7月12日宣布与拜仁慕尼黑达成协议,7月16日拥有C罗的尤文图斯也加入了他们的阵营。为了应对这些变化,在9月末发布的《FIFA 20》中将不能采用这些球队的队名、队徽、球衣和球场,只能用虚拟的模型替换,侵犯了版权要赔钱的,还有两个月时间,对我们来说,变化是持续的。

但是,我们不能因为这种变化就延期游戏的发布。

帕克休斯:那么,你们是如何将这种影响减少到最低的呢?

栋哥:这其中最主要的关键是设计,一个优雅的软件,像FIFA这样的软件,是可以很容易应对改变的,也非常容易进行扩展和复用。这种设计可以称之为“面向对象设计”,优雅软件设计的关键之一。

帕克休斯:你说的“面向对象设计”和“面向对象编程”是一回事么?

栋哥:面向对象设计不仅仅从微观上着眼于编码,还要从宏观上着眼于产品。所以这两者不是一回事,但是又相互联系。 面向对象设计需要面向对象编程,但是又不仅包含面向对象编程这一个概念,还要更多内涵,主要有这四个方面:

1.       面向对象编程的方法

2.       代码复用

3.       只改变极少量代码就可以改应对需求变化

4.       不改变任何代码就能扩展软件的功能

我分别来讲一下这四个方面,面向对象编程方法已经讲了太多了,这里就先不讲了。讲第二个代码复用,FIFA已经做成了一年发一版的年货游戏,不可能每次都从头做起,代码的复用显得特别重要。实际上FIFA的引擎不仅是FIFA自己在用,EA的很多游戏都在用,目前使用的是寒霜引擎,EA公司的通用引擎,还用在《战地》、《极品飞车》等其它十二款游戏上。2

第三点是只改变极少量代码就可以应对需求的变化。像前面说的,7月得知公司丢了几个球队的版权,9月游戏发布,这种调整要非常迅速。

第四点是不改变任何代码就能扩展软件的功能。这一点,FIFA做的也非常优秀,像FIFA 19刚发布的时候,并没有女足世界杯,但是在女足世界杯前夕,官方发布了补丁,该补丁升级了22支女足世界杯国家队,球衣队徽,官方比赛用球,真实的球场等等,这些扩展没有修改任何代码就完成了。

帕克休斯:你说的这些听起来不错,是你自己悟出来的么?

栋哥:当然不是我自己悟出来的。计算机行业中有很多人花费大量时间来研究“面向对象设计”,并且总结出了很多行之有效的原则,形成了不少设计模式供我们借鉴。像SOLID原则就是其中之一。

帕克休斯:SOLID原则?

栋哥:是的,SOLID是面向对象设计和面向对象编程中几个重要的原则,SOLID是这几个原则的首字母的缩写,总共用有五个原则。分别是:

1.       单一责任原则(The Single Responsibility Principle)

2.       开放封闭原则(The Open Closed Principle)

3.       里氏替换原则(The Liskov Substitution Principle)

4.       接口分离原则(The Interface Segregation Principle)

5.       依赖倒置原则(The Dependency Inversion Principle)

分别取上面各条原则黑体部分的首字母,就组成了SOLID这个单词。因为这几条原则太重要了,不光在面试时候经常被考官问,在实际的编程中,也要用到这五条原则。

帕克休斯:这样说太笼统了,你能详细讲一下么?

栋哥:好的,先来看第一个原则,单一责任原则。这个原则是说一个类只有一种类型责任。就拿球员类来说吧,十一名上场的球员有一名是守门员,守门员的职责和普通球员有所不同,所以,守门员和球员要分成两个类。

帕克休斯:也就是说在划分类的时候要根据类的责任了?

栋哥:可以这么说,像我前面所举的例子,普通球员和守门员大部分情况下都是一样的,甚至可以认为普通球员能做的,守门员都可以做。像德国的诺伊尔就经常跑到禁区外,简直就是个后卫,所以人们称之为门卫。在禁区之外,门将和普通球员一样,他们的区别在禁区之内,在禁区内,门将是可以用手的。所以,在设计的过程中可以这样来设计:

1.       Player类:普通球员类中只要做正常的设定即可。

2.       Goalkeeper类:门将类可以继承Player类,然后再添加在禁区内用手的操作。

帕克休斯:好吧,我就假装听懂了,能继续介绍一下这个开放封闭原则么?

栋哥:开放封闭原则也非常的重要,用一句话来解释可以称之为一个类要易于被扩展,但要难于被修改。通俗来说就是核心的类,如果要增加或者改变其功能,最好的方法是扩展这个类,除非万不得已,不要修改这个类,因为一旦修改,众多依赖于这个类的其它类都会受到影响。开放封闭原则的开放是对扩展开放,封闭则是对修改封闭。

帕克休斯:你这个原则让我想到了我们自己也是易于扩展而难于修改。如果我想社会一点,我可以贴个纹身贴,来个爬行动物贴在身上。如果我想显得潮一点,可以来个锡纸烫。这些都没有对我的身体造成影响,如果要动手术整容那可就比较麻烦了。所以,我觉得人类也是易于扩展,而难于修改的。

栋哥:你这个想法非常的好,我还没想到呢。对开放封闭原则来说,最重要的是抽象,把最重要的特征与功能抽象出来,如果抽象做的不好,这个类就设计的不够有扩展性。

帕克休斯:那你能给举个设计的比较有好的例子呢?

栋哥:这种扩展性设计的很好的软件有很多,比如浏览器上网,很多编辑器都有很好的扩展性。拿浏览器来说,这几大主流的浏览器在写软件的时候,都是对一个抽象的服务器来写,至于这个服务器是Apache还是NGINX,还是微软的IIS,都没关系。所以,这样就减少了对具体服务器的依赖。对NGINX服务器来说,如果要扩展功能,是可以完全不用改变原有代码时行扩展的。所以,这种设计又开放又封闭。

帕克休斯:那还有第三个原则叫里氏替换原则,这是什么意思呢?里氏这个名字好奇怪啊。

栋哥:和牛顿定律是牛顿提出来的一样,里氏替换原则也是一位叫Barbara Liskov的女士提出的。Liskov女士发表了一篇名为《数据的抽象与层次》的论文,在论文中她提出了这样一个观点:“如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。”

Note

这篇论文我放在xueban.app中,在论文《Data Abstraction and Hierarchy》中的原话是:If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

确实比较抽象,我也看不出来这就是里氏置换原则,但是,别人都说是,所以,我也只能跟大流了……

但是这个表述实在是太科学严谨了,于是有人把这个描述通俗化了:“派生类(子类)对象可以在程序中代替其基类(超类)对象。”

帕克休斯:这个说法还是太严谨了,能再举个通俗的例子么?这些原则是为了科研而提出的么?

栋哥: 这些原则都是长期的经验总结,并不是为了科研而科研。我们在做设计的时候,最重要的目标是简化编程的难度,如果设计得当,可以让我们在写一个类的时候,安全的忽略其它的类。我们设计类的时候,也是冲着这个目标去的。

我们在使用类的继承时,不能给自己添麻烦。设想一下,如果我们继承的子类将父类已经实现的方法给重写了,会给我们写程序带来相当的惊喜。为了不让我们惊喜,使用继承,要遵守的原则是,只要出现父类的地方,都可以用子类代替。

比如说前面我举的例子,门将类继承自球员类,只要出现球员的地方,都可以用门将来代替。在现在中也是如此,只要教练愿意,是可以派上11个门将的,虽然没人这么做过。球员能拥有的属性和动作,因为门将是子类,所以门将都有,但是在程序中,反之并不如此,在程序中,门将比球员多了一些属性和动作,比如用手扑球,如果用父类来取代子类,那么这个门将就不会扑球了。

帕克休斯:我明白了,也就是说,在做测试的时候,所有针对父类的测试,在子类上都要通过,是么?

栋哥:是的,就是这个意思。所有在父类上执行的动作,在子类上都要执行的一模一样,这就是替换的原则,子类可以代替父类。

帕克休斯:那继续说一下第四个原则,接口分离原则吧。

栋哥:我举个例子吧,你应该去过火车站飞机场这种场合,那些地方有那种免费给手机充电的接口,长的样子就像下面这样。

现在每个人都有手机,你会用这样的手机充电线么?

帕克休斯: 当然不会用了,我的手机只有一种接口,有那么多,根本没用啊。

栋哥:是的,在编程中也是如此。如果接口被设计的大而全,就像上面的充电器一样支持所有手机,会让程序显的非常杂乱,从而难以维护。最好的设计是只用自己有用的接口,将大而全的接口分离。当然了,在特殊的情况下,像机场,大而全的设计也有一定的存在价值,但是总体来说,在编程中,这样的设计基本上是没有好处的。如果有些类用不到大而全的接口,一定要记得分离。

帕克休斯:你这样说我就清楚了,我可不想拿着那么恐怖的充电器。那再讲一下最后一个设计原则吧,叫依赖倒置原则吧。

栋哥:先要理解什么叫依赖,在编程中的依赖和现实中的依赖有些不同。我用依赖造几个句子,比如“她不喜欢依赖别人,自己的事情总是自己做”。再比如“老婆和老公在生活中互相依赖”。在现实中,当我们说某个东西依赖另一个东西的时候,往往有些弱者依赖强者的味道在里面,或者互相依赖。在现实中,可以你依赖我,我依赖他,他依赖你,我们就像一个团结有爱的一家人。

但是,在软件中,你要是设计出了一个A类依赖B类,B类依赖C类,C类又依赖A类的死循环,那就有你受的了!

当我们说A类依赖B类的时候,是指B类是以局部变量的形式存在于A类之中。

帕克休斯:还是举个例子吧,这样说有点摸不着头脑。

栋哥:比如说我要去旅行,假设一个怀着“世界这么大,我想去看看”的旅行者类好了,这个旅行者有一个方法是旅出行,参数是某种交通工具。当我们出去的时候,是无法得知自己用哪种交通工具的,可能是走路,可能是坐三蹦子、出租车、高铁、飞机、轮船……

我来举个软件的例子,我开一下电脑,空口无凭,只要稍微看下代码就知道了。你现在还写代码么?

帕克休斯: 略懂一些,你讲吧。

栋哥:懂一点就好,看看下面的代码,这里面的Traveller旅行者类依赖巴士车类。

    class Bus {  
        public String start(){  
            return "我是巴士车,来不急了,快上车!"; 
       }  
  }  
      
    class Traveller {  
        public void travel(Bus bus) {  
            System.out.println("世界这么大,我想去看看");  
            System.out.println(bus.start());  
        }  
    }  
      
    public class TravellerTest{  
        public static void main(String[] args){  
            Traveller traveller = new Traveller();  
            traveller.travel(new Bus());  
        }  
  }

帕克休斯:这个我理解了,只要是巴士车,我们的旅行者就能远行。

栋哥:是的,但是还有个问题,如果出行的路上,没有巴士车呢?

帕克休斯:那可以坐其它交通工具啊,交通工具又不止一种。

栋哥:但是,我们看看上面的旅行者类,里面的travel有个参数是Bus类,如果不是Bus类,那就没办法了。只能一种交通工具准备一种调用方法。

帕克休斯:这也太不合理了吧!

栋哥:非常的不合理,仅仅是换一种交通工具,就要不停的修改旅行者类,这不是好的设计。原因是Traveller类和Bus类之间的耦合性太高了。必须降低他们之间的耦合度才行。

帕克休斯:那应该怎么做呢?

栋哥:可以引入一个抽象的接口IStart。只要是交通工具,都可以跑起来。看看下面的代码:

  interface IStart{  
      public String start();  
  }  
    
  class Bus implements IStart{  
      public String start(){  
          return "我是巴士车,来不急了,快上车!";  
      }  
  }  
   
 class Tank implements IStart{  
     public String start(){  
         return "我是坦克,上车吧!";  
     }  
 }  
   
 class Traveller {  
     public void travel(IStart vehicle) {  
         System.out.println("世界这么大,我想去看看");  
         System.out.println(vehicle.start());  
     }  
 }  
   
 public class TravellerTest{  
     public static void main(String[] args){  
         Traveller traveller = new Traveller();  
         traveller.travel(new Bus());  
         traveller.travel(new Tank());  
     }  
 }  

帕克休斯:我知道了,这样无论怎么扩展,都不用再修改旅行者类了。

栋哥:是的,这就是依赖倒置原则。代表高层的旅行者类一般负责完成更主要的业务,一旦对它进行修改,引入错误的风险极大,使用依赖倒置原则就可以不对这个类进行修改。依赖倒置原则的核心就是要面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

帕克休斯:是不是只要掌握这几个设计原则就能设计出完美的类呢?

栋哥:当然不能这么说,只能说一般情况下设计类的时候都要考虑这几种原则,基于这几种原则,人们还总结了很多经验,这些经验有个名字叫设计模式。

帕克休斯:我知道设计模式,没想到设计模式是建立在这些原则之上呢。

栋哥:其实设计模式并不神秘,算是一些长期以来的经验总结吧,有了设计模式,能让我们少走不少弯路呢。

帕克休斯:今天的采访就到这里吧,下次有机会再来谈谈设计模式的话题。

栋哥:好的,再见。

6 程序员故事

《潘闻来到公司》

部署网络的工作又有趣又繁琐,有时候繁忙的工作会让我的睡眠严重不足,但是我喜欢这个工作,所以,我每天都会早早的赶到公司,去查一下今天又有什么工作要做。

早上我赶到公司的时候,老板已经在公司了,我照例去问他有什么安排。他说:“今天你去接我弟弟吧,他大学二年级,要从外地赶来,他学的是计算机专业,这是他的电话,你给他打电话,问问什么情况。”

“没问题。”

“对了,他叫潘闻。”

我打了那个电话号码,在接通电话的那一刻,我以为把电话打给了老板,电话那头传来了一个和老板一模一样的声音。我赶忙仔细确认了一下电话号码,没打错。我问:“你是潘闻么?你听起来和你哥一模一样。”

电话那头传来了爽朗的笑声:“是吧,大家都这么说,我上午11点半就到了。我在火车站等你。”

我在车站接到他,简单的寒暄后,我认定潘闻是个阳光大男孩。他坐在副驾驶上,很自然的玩起了手机,一路上把他在手机上看到的新闻读给我听。东方的天空已经有些阴云,我问他:“你查一下手机上今天会下雨么?”。

“今天会有雷阵雨。”

我没话找话的说了一句:“你性格和你哥一点也不一样。”

潘闻沉默的片刻时间里,我意识到我说错了话,他悠然的回答:“都说哥哥是上帝送给弟弟的天使,我哥是上帝送给我的考试。”我着急把话题引开,听到他说考试,就连忙问道:“你现在刚刚考完期末考试吧?”

“是的,刚刚考完一些副科,还有两门主课《计算机组成原理》和《Java语言程序设计》。”

“这两门课程都非常有用,你哥的公司里,最主要的软件就是Java写的,这下你有用武之地了。”一路上闲聊了很多,乌云越来越黑,虽然还是中午时分,路上的车已经开了大灯,一阵阵的大风吹着路边的尘土和塑料袋漫天飞扬。偶尔有两颗顽皮的雨滴从空中落下,落在挡风玻璃上,积攒好久,我才开一下雨刷把它们擦掉。

一个多小时后回到公司,潘新已经在公司等着我们吃午饭了。在见到潘新之后,潘闻像变了一个似的,在吃饭的时候,潘新只和我在说话,他的弟弟对他来说就像空气一样,视而不见。

这可能是我吃的最压抑的一顿饭了,期间我数次想把潘闻拉到对话中,潘闻仅回复一两个词,就又躲闪了出去。饭后,一起回到了公司。潘闻在门口远离他哥的地方找了一个位置坐下,变戏法似的拿出一本英语单词背了起来。

天越来越黑,风越来越大,大风吹着办公室的窗户在颤抖。

突然之间,潘新平静的问了一句:“你考试考的怎么样?”我看到潘闻的身体颤抖了一下,半个屁股悬空在椅子上,嘴半张着,似乎那句回答像航班因为天气晚点了一样,迟迟不肯出来。见到此景,我有些心疼潘闻,又想转移话题,就说了一句:“好像要下雨了。”

老天爷仿佛听见了这句话,以一声炸雷作为回应,天被这个炸雷炸出了一个大洞,水冲了出来。雨越下越大,像决堤的大坝,水已经分不出你我,挤着一起浇了下来。乌云中的墨汁似乎把雨水染黑,雨越下,天越黑。透过玻璃,只能看到红色的尾灯连成一条路的模样。

兄弟俩仿佛没有被这瓢泼大雨所打扰,迟到的回答还是潘闻的嘴里讲了出来:“没太考好。《Java程序设计》考的马马虎虎,《计算机组成原理》没通过。”

相比于潘新的愤怒,窗外下的就像毛毛雨。潘新在咒骂着他的弟弟,数落着他弟弟的不是,我在那里感到手足无措,我被那一幕吓呆了,只听到潘新怒吼着让他的弟弟滚。潘闻默默的走了出去。我意识到,他没地方可去,就追了出去。

我说:“真遗憾事情变成这个样子。”

突然间,潘闻笑了,露出一口白牙:“你不必担心,我了解我哥,他已经不生气了,只要他骂了我,这些天就没事了。”

“真的么?”

“当然是真的,这么多年了,我早就摸清他的脾气了。他现在知道我的《计算机组成原理》没考过,过几天他肯定会让我写Java的项目,在路上你不是说公司的软件是用Java写的么?”

“是的,幸好你的Java通过了。”

“我说了我通过了么?我只是说我的Java考的马马虎虎,我可没说我通过了。”

“你的意思是……”

“是的,我的Java也没通过,两门主课都没通过,只有那些副课通过了。你一定得教教我,否则我下学期补考也不一定能通过。”

雷阵雨就是雷阵雨,不一会儿,天所若无其事的晴朗,幸好这钢筋水泥修成的城市排水系统出奇的不好,保留着下过雨的证据。

Footnotes

  1. 如果大家对FIFA的操作有兴趣,可以到xueban.app这个网站中查看具体的动作,我当年闲的蛋疼,练了不少花哨动作。实际上,一般玩家常用的动作在10种以下。而且吧,如果你跟哥们踢球,玩的太花,容易被打。↩︎

  2. 在2010年的时候,EA通过不停的收购,拥有了很多世界知名的游戏品牌,但是也带来了一个巨大的麻烦,当时EA有十三款不同的游戏引擎,有虚幻引擎开发的《质量效应2》,有变色龙引擎开发的《极品飞车》,这导致暴涨的开发费用。↩︎