最近在读「松本行弘的程序世界」一书,才读到第二章(面向对象)就忍不住要写篇文章了,写得极好,启发很多

  1. 之前看多了欧美系的技术书籍,逻辑感虽强,文笔略显学术化。这次读松本著作,感观很不一样,阅读过程就像是一位学识渊博的长者在讲故事,枯燥的技术与历史娓娓道来,妙笔生花,代入感很强
  2. 松本功力深厚,思考又多,技术上的各个知识点都能够串起来,旁征博引,而且层层推进,才看到第二章,就已经做了不少笔记,须要写篇文章释放一下了

本文与其说是「读后感」不如说是「思考集」,会以「原文 + 笔记」的形式展开,但是我还是建议大家读!原!著!Kindle电子书有售


前言 & 第一章:我为什么开发Ruby?

每种技术、思想都有其特定的目的、渊源和发展进步的过程。本书试图换一个角度重新考察各种技术。如果你看过后能够感觉到“啊,原来是这样的呀!”或者“噢,原来这个技术的立足点在这里呀!” 那么我就深感欣慰了

(尘泥:我一直认为,想要深入学习一个技能,了解它的发展历史是必须的,它从哪里来,立足点是什么,为什么会发展成当前的形态?了解历史,才可以掌握当下,预见未来,这其实是一个有趣又有益的过程。回想在学校做毕业论文之前,都要先做一个「开题报告」,把研究对象的历史/流派/现状等等扒拉清楚才行)

在语言学领域里,有一个Sapir-Whirf假说,认为语言可以影响说话者的思想。也就是说,语言的不同, 造成了思想的不同。人类的自然语言是不是像这个假说一样,我不是很清楚,但是我觉得计算机语言很符合这个 假说。也就是说,程序员由于使用的编程语言不同,他的思考方法和编写出来的代码都会受到编程语言的很大 影响。

(尘泥:确实是这样的,我的体会就比较深,我比较重度地使用过3种语言:Ruby/Java/JS,这三者及其使用者个性鲜明:Ruby代表着自由独立,就像千里独行客;Java是老成持重,好比江湖老前辈;JS和Ruby贴近,但更为激进奔放,就像初出茅庐,渴望在江湖上扬名立万的少年剑客)

Ruby编程语言的设计目标,是为了让作为语言设计者的我能够轻松编程,进而提高开发效率,根据这个目标,我制定了3个设计原则:简洁性,扩展性,稳定性

(尘泥:所谓「没有规矩不成方圆」,语言设计之初就精心敲定了三大原则,后续的各种技术路线的选型与取舍都会考虑这三大原则。松本为了表示郑重其事,特地花了3个小章节逐个解释3个设计原则)

编程语言不是从安全性角度考虑以减少程序员犯错误,而是在程序员自己负责的前提下为他提供最大限度发挥 能力的灵活性。Ruby看重的不是明哲保身,而是如何最大限度地发挥程序员自身的能力。

(尘泥:这句话实在是令人脑洞大开,一直听到强调或者认为好的语言应该是可以从语言设计上降低犯错概率,无论是使用语法糖还是类型检查,等等各种手段,但是松本的想法更进一步,要求程序员自己负责!所谓的自己负责,我想很大程度上是须要靠自测保障的,这也是Ruby以及后续JS社区在单元测试等各种测试框架技术上欣欣向荣的一个源头吧。另一方面,Ruby的特色元编程能力,也好比双刃剑,即赋予了程序员极大的灵活性,又要求其肩负起相应的责任,别把代码玩坏了)

虽然Ruby非常重视扩展性,但是有一个特性,尽管明知道它能带来巨大的扩展性,我却一直将其拒之门外。那就是宏,特别是Lisp风格的宏。
宏可以替换掉原有的程序,给原有的程序加入新的功能。如果有了宏,不管是控制结构,还是赋值,都可以随心所欲地进行扩展。事实上,Lisp编程语言提供的控制结构很大一部分都是用宏来定义的。所谓Lisp流,其语言核心部分仅仅提供极为有限的特性和构造,其余的控制结构都是在编译时通过用宏来组装其核心特性来实现的。这也就意味着,由于有了这种无与伦比的扩展性,只要掌握了Lisp基本语法S式(从本质上讲就是括号表达式),就可以开发出千奇百怪的语言。Common Lisp的读取宏提供了在读取S式的同时进行语法变换的功能,这就在实际上摆脱了S式的束缚,任何语法的语言都可以用Lisp来实现。
那么,我为什么拒绝在Ruby中引入Lisp那样的宏呢?这是因为,如果在编程语言中引入宏的话,活用宏的程序就会像是用完全不同的专用编程语言写出来的一样。比如说Lisp就经常有这样的现象,活用宏编写的程序A 和程序B,只有很少一部分是共通的,从语法到词汇都各不相同,完全像是用不同的编程语言写的。对程序员来说,程序的开发效率固然很重要,但是写出的程序是否具有很高的可读性也非常重要。从整体来看,程序员读程序的时间可能比写程序的时间还长。读程序包括为理解程序的功能去读,或者是为维护程序去读,或者是为调试程序去读。
我相信,作为在世界上广泛使用的编程语言,应该有稳定的语法,不能像随风飘荡的灯芯那样闪烁不定。

(尘泥:这一段很长,但很有思考性。松本提到了「宏」和「Lisp」以及他设计Ruby时的取舍,「宏」是一种非常常见的增强语言扩展性的技术,威力巨大,「Lisp」则是很古老很重要的一门语言,在后续的章节中有不少戏份,前网易著名程序员伞哥就是Common Lisp的顶级玩家)


第二章:面向对象

(尘泥:光看标题是不是觉得弱爆了?面向对象?哥(姐)八百年前就会拉!且慢,这次我们听听松本老师讲「面向对象」)

多态性、数据抽象和继承被称为面向对象编程的三原则。

(尘泥:据说这是面向对象的三要素,但是并不是所有的面向对象语言都一定具备这三要素,例如,JS就没有「继承」功能,而是采用了「原型」技术作为替代)

我认为面向对象编程语言中最重要的技术是“多态性”。我们就先从多态性说起吧。
多态性,英文是polymorphism,其中词头poly-表示复数,morph表示形态,加上词尾-ism,就是复数形态的意思,我们称它为多态性。换个说法,多态就是可以把不同种类的东西当做相同的东西来处理。

(尘泥:「可以把不同种类的东西当做相同的东西来处理」这才是多态性的核心奥义!白猫黑猫,能抓老鼠就是好猫!如果没有多态性,代码就要写成if(白猫){白猫.catch()}elseif(黑猫){黑猫.catch()}了,有了多态性,直接简单一句{猫.catch()}就行了,哪管你是白猫黑猫!多态性大大简化了代码逻辑,此时的代码关注的是做什么(catch())而不需要了解谁在做怎么做,具体的细节会被隐藏在实现类里面)

算法和特定的数据结构关系很大。所以有一位计算机先驱曾经说过:“程序就是算法加数据结构“

(尘泥:下文会讲到,算法的抽象化,就是结构化编程,而数据结构的抽象化,带来的就是面向对象编程)

Simula的“发明”
如前所述,面向对象编程思想起源于瑞典20世纪60年代后期发展起来的模拟编程语言Simula。以前,表示模拟对象的数据和实际的模拟方法是互相独立的,需要分别管理,编程时需要把两者正确地结合起来,程序员的负担是很重的。因此,Simula引入了数据和处理数据的方法自动结合的抽象数据类型。随后,又增加了类和继承的功能。

(尘泥:这里插叙一下面向对象编程语言的历史,其实在20世纪60年代后期,现代面向对象编程语言的基本特征Simula都已经具备了)

Smalltalk的发展
Simula的面向对象编程思想被广泛传播。从20世纪70年代到80年代初,美国施乐公司的帕洛阿尔托研究中心开发了Smalltalk编程语言。当时的开发宗旨是“让儿童也可以使用”。在Lisp和LOGO设计思想的基础上,Smalltalk又吸取了Simula的面向对象思想,且独具一格。不仅如此,它还有一个很好的图形用户界面。这个创新的语言使得世人开始了解面向对象编程的概念。

(尘泥:Smalltalk对以后面向对象语言影响很大,往往被认为是面向对象语言之母)

Lisp的发展另外
位于美国东海岸的麻省理工学院及其周边地区,用Lisp语言发展了面向对象的思想。Lisp和FORTRAN、COBOL语言一样,都是最古老的语言。与同时期登场的其他语言不同,Lisp语言具有非常浓厚的数学背景,所以它本身具有很强的扩展功能。面向对象的特性也是Lisp所拥有的。因此,编程语言规格的变更、功能的扩展和实验都很容易进行,由此产生了很多创新的想法。多重继承、混合式和多重方法等,许多重要的面向对象的概念都是从Lisp的面向对象功能中诞生的。

(尘泥:Lisp又上镜了,一些重要的面向对象概念就来自它)

和C语言的相遇
20世纪80年代,世界上很多地方都在研究面向对象编程思想。AT&T公司的贝尔实验室在C语言中追加了面向对象的功能,开发出了“C with Class”编程语言。开发者是Bjarne Stroustrup,他来自距离Simula的起源地瑞典不远的丹麦。在英国剑桥大学的时候,Stroustrup就使用Simula语言。加入贝尔实验室以后,为了能够把C语言的高效率和Simula的面向对象功能结合起来,他开发了“C with Class”编程语言。因为当时Simula的处理速度是非常缓慢的,所以在他的研究领域中不能使用。“C with Class”语言就演变成了后来的C++语言。从这些情况来看,C++是直接受到了Simula语言的影响,而没有受到Smalltalk多大影响。

(尘泥:C + Simula = C++)

Java的诞生
为了克服低级语言的缺点,在20世纪90年代Java编程语言应运而生。Java语言放弃了和C语言的兼容性,并增加了Lisp语言中一些好的功能。此外,通过Java虚拟机(JVM),Java程序可以不用重新编译而在所有操作系统中运行。现在,Java作为在20世纪90年代诞生的最成功的语言,被全世界广泛应用。

(尘泥:Java大家都知道的,就不啰嗦了…)

软件开发的最大敌人是复杂性。人的大脑无法做太复杂的处理,记忆力和理解力也是有限的。
最初对这种复杂的软件开发提出挑战的是「结构化编程」

(尘泥:所谓「结构化编程」,就是把代码的控制流程限制为「顺序」「分支」「循环」三个结构,不提倡使用GOTO,通过限制控制结构来降低复杂性。结构化编程,现在看起来似乎就是个编程常识而已,但在历史上可以说是巨大的一步。)

(尘泥:相应的,从数据结构层面对抗复杂性的发明,就是「面向对象编程」)

让我们再回到对象的话题上。同样的对象大量存在的时候,为了避免重复,可以采用两种方法来管理对象。
一种是原型。用原始对象的副本来作为新的相同的对象。Self、Io等编程语言采用了原型。有名的编程语言用原型的比较少,很意外的是,JavaScript也是用的原型。
另外一种是模板。比方说我们要浇注东西的时候,往模板里注入液体材料就能浇注出相同的东西。这种模板在面向对象编程语言中称为类(class)。同样类型的对象分别属于同样的类,操作方法和属性可以共享。跟原型不同,面向对象编程语言的类和对象有明显区别,就像做点心的模具和点心有区别一样
为了清晰地表明类和对象的不同,对象又常常被称作实例(instance)。叫法虽有不同,但实例和对象是一样的。

(尘泥:一直搞不明白为什么有的地方「对象」叫「对象」,有的地方「对象」叫「实例(instance)」,噢,原来背景是这么回事儿!)

通过抽象把共通的部分提取出来生成父类,与利用已有的类来生成新类,是同一方法的两种不同表现形式。前者称为自底向上法,后者称为自顶向下法。
但是,从用自底向上的方法提取共通部分的角度来看,一个子类只能有一个父类的限制是太严格了。其实,在C++、Lisp等编程语言中,一个子类可以有多个父类,这称为“多重继承“
但实际上,最初引入继承的Simula编程语言,只提供单一继承。同样,在随后的很多面向对象编程语言中也都是这样的。因此我认为,继承的原本目的实际上是逐步细化。

(尘泥:讲到了对象,也讲到了类,那么自然而然就要讲继承了。继承有2种套路,一种是多重继承,一个子类可以有多个父类,从而继承多份代码,但是继承关系复杂化,难以把控;一种是单一继承,一个子类只能有一个父类,继承关系清晰,但是如何继承多份代码呢?围绕着多重继承 VS 单一继承,松本展开了一系列探讨)

既想利用多重继承的优点,又要回避它可能会带来的问题,那我们就需要寻找解决问题的方法。结构化编程解决goto问题的原则是,用3种有限制功能的控制语句来代替自由度太高的goto语句。这3种控制语句虽然有限制,但是用它们的组合可以实现任意算法。像这样引入有限制的多重继承应该是一个好的方法。没错,受限制的多重继承,这个解决或者改善多重继承问题的方法出现了,它在Java编程语言中被称为接口(interface),在Lisp或者Ruby中是Mix-in。

(尘泥:松本的思路很有意思,如何解决goto的问题?使用有限制的流程控制,即结构化编程。那么相应的,如何解决多重继承的问题?使用有限制的继承能力,在Java,这就是接口(interface),一个类可以继承多个接口;在Ruby,这就是混入(Mix-in),一个类可以include多个Mix-in。都是通过降低灵活性来换取代码的可读性和清晰性)

到现在为止我们一直都在讨论继承,其实继承包含两种含义。一种是“类都有哪些方法”,也就是说这个类都支持些什么操作,即规格的继承。另外一种是,“类中都用了什么数据结构和什么算法”,也就是实现的继承。静态语言中,这两者的区别很重要。Java就对两者有很明确的区分,实现的继承用extends来继承父类,规格的继承用implements来指定接口。

(尘泥:又开了一次脑洞,原来「继承」还可以细分为「规格的继承」和「实现的继承」,对于静态语言,例如Java,就是extends与implements的区别了)

Mix-in是降低多重继承复杂性的一个技术,最初是在Lisp中开始使用的。实现Mix-in并不需要编程语言提供特别的功能。Mix-in技术按照以下规则来限制多重继承。通常的继承用单一继承,第二个以及两个以上的父类必须是Mix-in的抽象类。Mix-in类是具有以下特征的抽象类:1. 不能单独生成实例 2. 不能继承普通类

(尘泥:神奇的Lisp又又又上镜了。Mix-in也是一种受限制的继承形式,它解决了多重继承的问题,但是有2个限制:1. 不能单独生成实例(打个比方,就是不允许有自己的后代,好惨…) 2. 不能继承普通类(打个比方,就是无父无母,惨上加惨…)Mix-in无父无母无后,存在的唯一目的就是帮别人承载有复用价值的代码块,牺牲小我完成大我,致敬!)

在编程世界中,经常提到静态(static)与动态(dynamic)这样的词汇。静态是指程序执行之前,从代码中就可以知道一切。程序静态的部分包括变量、方法的名称和类型以及控制程序的结构等等。相对于静态,动态是指在程序执行之前有些地方是不知道的。程序动态的部分包括变量的值、执行时间和使用的内存等等。如果知道程序使用的算法和输入值,虽然有时候不执行也可以知道输出的结果,但是现实中这种单纯的情况很少。通常情况下,程序本来就是不被执行就不知道结果的,所以从一定程度上说程序都具有动态特性。因此,严格地说,静态和动态之间的界限是很微妙的。

(尘泥:松本说了「静态和动态之间的界限是很微妙的」,有多微妙?看看Java就知道了。Java是出了名的静态类型语言,那么问题来了,真正的纯粹的静态类型本质上是无法实现多态性的:如果你把白猫和黑猫区分得泾渭分明还怎么实现多态性?没有多态性,还能叫面向对象?Java在这里又一次使用了「受限制」的思路,即:采用受限制的静态类型/受限制的多态?怎么讲?Map map = new HashMap() 左边的类型是Map,右边的类型是HashMap,HashMap是Map的子类,这样一来,一方面享受了类型检查的好处,另外一方面Map类型的变量允许赋予其Map子类(实现类)的实例从而享受了多态性的好处)


不知不觉写了3个多小时了,匆匆收尾,意犹未尽。久不写作,笔力生疏,此书内容广博,也使我落笔很是吃力,但求抛砖引玉,大家还是看原著为佳:)

 

One thought on “「松本行弘的程序世界」读书笔记:面向对象编程的再思考”

  1. 写的极好(原文+思考)。这本书我也看过,但没像侃爷这样把思考过程记录下来,看来我也有必要再看一遍。

发表评论

电子邮件地址不会被公开。 必填项已用*标注