值得怀念的世界,却不值得回去!
——庄表伟
面向过程的世界是完整的,统一的,也是容易理解的——对于程序员来说——或者说他只需要一种理解能力。这个世界虽然值得怀念,却不值得再回去。因为,我们不再像当年的程序员那样,只开发那些简单的软件了。很多人崇拜那些早起的“大牛”,其实平心而论,我们现在面对的问题的复杂程度,在他们当年可以说几乎无法解决。需求的复杂程度也不是他们当年能够设想到的。
这是在秘鲁发现的神秘的纳斯卡巨画,这样巨大的地面艺术,可以给我们对于面向过程的编程的结论一个可视化的比喻。面向过程的编程,只有一个统一的世界,他对于软件的理解,始终不会离开计算机的操作与运算本质,这就像在平地上作画那样,我们需要的一根长1米的直线,非常容易,两点一线,一拉就出来了。但是当我们需要在地面上画一根5000米甚至更长的直线时,如何保证画出一条直线,就成为一个巨大的挑战。当视角无法升到足够的高度时,如此复杂的图案几乎是无法把握的。仅仅依靠结构化的划分,并不能完全的隔离复杂度的交互影响。单步跟踪一个1000行代码的程序并不困难,但是如果是100万行代码,甚至更多呢?
再看一张照片:
这是世界上最大的“埃及胡夫金字塔”。我们假设,如果当年法老在工程进行到80%的时候,提出需求变更,希望金字塔尖能够向右移动10米。情况会如何?——会死好多劳动人民的!如果希望向右移动100米呢?如果希望有四个塔尖各在一个方向呢?如果。。。还好这一切都没有发生,否则我们就不可能看到一个真正完工的金字塔。然而在软件开发领域,当“结构化编程”面对“移动金字塔”的需求变更时,它只能破产!
可以得出一个比较关键性的结论是:
仅仅从计算机的角度出发,对于更为复杂的需求,描述力不足。对于巨大的需求变更,应变力不足。而这正是对于的软件需求的必然发展趋势。
所以,那个世界不值得回去,但是,OO真的帮到我们了吗?
多年以后的今天,我们依然在思考这样一个问题:“OO怎么就流行起来了呢?”学术一点分析,这个问题可以分为以下几个部分:
1、OO之前的软件开发困境何在?
2、当时的开发人员如何解决那些困境?
3、那些解决困境的努力,为何会汇入OO的名下?
4、OO这个概念,从何而来?
5、OO的核心内容是什么?
6、OO的实际目的是什么?
7、OO的理想目标是什么?
困境
“需要一个超越于机器执行层面的的认识。”或者说,不能仅仅以“解空间”的语言描述解决方案,最好能够以“问题空间”的语言描述解决方案。这是OO得以流行的真正动力,因为OO宣称自己能够更好的描述“真实世界”。
注意我要区分的几个概念:“解决困境的努力”、“困境的根本原因”、“OO所宣称的目标”、“OO实际达到的效果”。因为在以往的OO的宣传中,这些概念是一个有机的整体,而却认为,其中有诸多“断裂破碎”之处。
面向过程的编程,面对的困境其实相当多,最根本的原因前面也已经指出了。但是在当时,在具体的项目中,在特定的人看来,他们碰的,是各自不同的问题。在人工智能领域,在图形化界面领域,面对的是模拟的问题。在企业应用领域,面对的是数据访问与保护的问题。从共同的困境来看,适应变更,方便重用,系统健壮之类的要求,也是需要考虑的。
概念的发展历程
首先声明,这是一个假想的历程,并非真实的历史。真实的历史,可以参考以下URL中的介绍:
http://heim.ifi.uio.no/~kristen/FORSKNINGSDOK_MAPPE/F_OO_start.html
模拟:模拟的概念由来已久,但是如何模拟却是一个大问题。
抽象数据类型(ADT):在对结构(structure)进行简单的扩展之后,ADT就顺理成章的出现了。
封装:对于ADT的理论总结可以表述为“封装带来了数据的安全性”。
继承:一堆不能重用的代码是“罪恶”的。继承首先出来试图解决这个问题。
多态:是一个意外的收获,如果各个对象都提供同一个名字的方法,似乎就可以不去管他们的区别。
在这些努力与尝试之后,面向对象横空出世,从哲学的高度总结了这些努力:“你们都是正确努力的一部分,你们都在试图更好的描述真实世界,真实的世界里一切都是对象,所有的对象在一个分类系统里各安其位,对象之间通过消息相互‘招呼’。应用OO思想,描述真实世界的运行,就是编程的主要工作。”
但是事实上呢?编程并不是描述真实世界!而是描述需求世界。不同的需求世界,需要不同的“世界观”。这一点,面向对象并没有考虑到。当时流行的思想是通用编程语言,使用一种语言解决世界上所有的开发难题。而要整体解决各不相同的开发难题,只能将目光投向“真实世界”,那是各个差异巨大的“问题空间”的唯一一致的背景。
面向对象的哲学破绽
在此特别感谢徐昊,这一部分该如何写,我始终没有想好,在与他讨论之后,我基本理出了一个思路。
面向对象有两个哲学基础,原子论与形而上学。这两大基础,在哲学的发展历程中,曾经如日中天,无可置疑(在古希腊那时候),如果说这样的哲学不伟大,那就是我太狂妄了,但是如果有人说:“西方哲学在之后的几千年里没有进步,古希腊哲学就是西方哲学的顶点,因此面向对象理所当然的应该建立于这两大哲学之上!”你会相信吗?
1、原子论
西方哲学的发展,经历了两次变革,一次是认识论转向;一次是语言转向;第一次转向使哲学的基础从本体论和形而上学变为认识论,从研究超验的存在物转到研究认识的主体和主客体关系;第二次转向把对主客体的关系的研究变成了对主体间的交流和传达问题的研究。把对主体的研究从观念和思想的领域转到了语言的领域(语句及其意义);这两次转向的代表人物分别是笛卡尔和维特跟斯坦。
————《OO, OO以后, 及其极限》waterbird
看来我可能是比较浅陋了,在我看了waterbird的《OO, OO以后, 及其极限》之后,曾经深深的自责过。看来OO没有我想的那么土,不是直接来自古希腊哲学,而是从维特根斯坦继承而来的。waterbird的引用而后总结的一段维特根斯坦的话,使我对维特根斯坦大为佩服。
小结2: 2 主要说明 --- 事实(facts)由原子事实(atomic facts)所组成;原子事实(atomic
facts)由更基本的对象(objects)所组成;我们的关于外部世界的主观描述图画,与它所描述的外部世界具有相同的逻辑结构;注:
(这即是相当于软件开发中的"建模")
还好,在我昨天列出了阅读书目之后,gigix提醒我看了另外一篇文章:《维特根斯坦早期思想及其转变》,这是一个正儿八经的哲学家的文章,总算让我见识到了软件开发这个行当里,颇有些不懂哲学的家伙,拿着哲学来唬人的。
原子事实是最简单的事实,无法再从中分析出其他事实,分析的结果只能是对象。因此,原子事实是对象的结合或配置。“对象是简单的”〔2.02〕,不可再加以分析,所以,对象就是简单对象,不过,为清楚起见,维特根斯坦还是经常采用“简单对象”这个说法。简单对象这个概念引起很多困惑和争论,其实维特根斯坦自己也很犹豫,他在笔记中写道:“我们的困难是,我们总说到简单对象,却举不出一个实例来。”
他曾考虑过关系、性质、视域上的小片、物理学里的物质点。他还说个体如苏格拉底、这本书等,“恰恰起着简单对象的作用”。一条可能的思路是把简单对象理解为一种逻辑要求,一个逻辑终点:“简单对象的存在是一种先天的逻辑的必然性。”
在《逻辑哲学论》中,维特根斯坦大致采用了这条路线,这本书里从未举例说明什么是简单对象。
维特根斯坦说的对象,是OO中的对象吗?一个正儿八经的现代哲学家的困惑,OO大师们考虑到了吗?只有朴素、甚至可以说是幼稚的原子论观点,才会轻松的混淆:事实、原子事实、对象和具体的物质,物体。对于OO来说,对象非常容易被发现,几乎随手一指,都能点到一个对象。
从语言哲学来说,最为困难的是:“有没有一种语言,可以清晰地、完整地描述这个世界?”逻辑原子论原本认为有可能。但是,维特根斯坦的后期哲学转向,恰恰指出了一个困境,而这个困境即时是人类历史上最为天才的头脑,也无法“走出”的。德国人最具有理性的思维能力,而其中最为天才的头脑却碰上了理性思维的天花板。维特根斯坦很难理解,越是德国天才,他的语言越是晦涩。倒是从中国哲学的角度,往往能够看透其中的挣扎。老子在几千年前就说:“道可道,非常道,名可名,非常名”。因为试图准确、无误、无失漏的命名、描述世界的努力,是不可能成功的。
因此,我现在可以断言,面向对象背后的原子论,不过是直接师承古希腊哲学的简单、朴素、幼稚的原子论,这样的原子论哲学,早已破产。作为哲学史的研究对象,自有其价值,而作为指导软件开发那么现代活动的哲学理论,实在是太不适用了。
2、形而上学
当我写下这个标题的时候,内心无比惶恐。这么大个题目,是我这个半路出家,Google成才的家伙能够谈论的吗?多少哲学家一辈子“皓首穷经”,也不过就是研究个形而上学啊。
当初,维特根斯坦去找罗素,问到:“你看我是不是一个十足的白痴?”罗素不知他为什么这样问,维特根斯坦说:“如果我是,我就去当一个飞艇驾驶员,但如果我不是,我将成为一个哲学家”。可见哲学这东西,只有真正的天才才有能力去研究它。
还好,我并不是要研究形而上学,我只是要研究面向对象背后的形而上学哲学基础。
我也不是要证实这个哲学基础的正确性与适用性。我只需要证明“面向对象背后的那个形而上学基础是不正确的、是不适用于软件开发的。”
面向对象的两大核心概念是:“对象”与“类”。“一切皆是对象”是由朴素原子论而来的。“万物皆有类属”就是由亚里斯多德的形而上学来的。
对于亚里斯多德的形而上学理论不熟悉的朋友,可以即时补课,中国人民大学哲学系的《西方哲学史》有好几节专门讲这个方面:《亚里斯多德的实体论I》、《亚里斯多德的实体论III》。还有就是到Google上去专门搜一下亚里斯多德的逻辑学说,看完以后,咱们回来接着说。
咱们用自己的话说一下:“种”、“属”、“属差”以及“定义”这几个概念。
种:是一个大的概念,假设已经预先定义好了的。
属:所有属于某一种的概念,都是那一种下面的属。
属差:同属一种的、同一级别的属之间的差别,或者说个性。
定义:通过种加属差,可以定义一个属的概念。
举例说明:人是二足直立动物。人是一个需要被定义的属,动物是人之所属的种,二足直立是人作为动物的共性之外,拥有的个性,也就是属差。
懂得初步的面向对象编程的同志们,你们都看出来了吧,大多数OO语言也是这么定义类的。你定义一个Animal,再用Person去继承Animal。在Animal里有一些属性与方法,在Person里再加一些人所特有的。很多很多的面向对象的教科书里,甚至就是直接用这个定义来举的例子。
问题出在哪里?或者有人会问:“这样有什么不对吗?”
我们可以通过“种+属差”来定义一个新的属吗?定义成立的前提是什么?先要有种的定义。然后才可能有属的定义。种的定义又是哪里来的呢?在一个种的概念之上,必然存在一个更普遍的种,一个更大的范畴。在亚里斯多德来说,在所有的种之上的种是“存在”,而存在是无法被定义的。而在面向对象的哲学里,即使是这一个最基本的哲学困境也被忽略了,无法被定义的概念,被代换为无需由程序员定义的概念(Object)。属差的区别在哲学家看来,是本质的,是基于深刻认识才能提出的。而在面向对象的哲学里,种的共性就是基类所定义的“属性与方法”,而属的个性,就是对于基类的扩展。“种+属差”变成了“公用代码+扩展代码”。
当概念定义这样一个“问题域的描述手段”,演变成“减少重复代码原则”之后。Class继承的概念就越发的模糊不清了。我们来总结一下:
1、面向对象原本声称的描述真实世界的目标,采用的工具却是朴素的“种加属差”的方式。
2、面向对象分析中,发现具体的对象还算是容易的,发现“种”的概念却是困难的。
3、在实际应用中,种概念的发现与定义,被偷换为公共代码的抽取。
4、由于基类的定义的随意性,导致子类不但可以扩展基类的行为与特性,还可以覆盖(改变)基类的行为与特性。
5、由于哲学概念的与开发概念的混淆,使得在OO领域IS-A、Has-A、Like-A成为最为绕人的概念。
在写完了哲学分析部分之后,我总算是喘了一口气,仿佛穿越了最幽暗的深谷,终于走出了自己最不擅长的领域了。
后来在MSN上和曹晓钢聊了挺长时间,对于OO的批判,他认为有点过头了。经过我的解释,他提出了一个更好的建议,清楚的说明自己批判的OO,究竟是哪一个阶段的OO,然后才不至于误伤到已经改善过后的OO。所以我打算整理一下对于OO发展阶段的看法,写在下面:
1、面向对象的语言:先有语言
2、面向对象的分析与设计理论:再有理论
3、面向对象的设计原则的全面总结:再有原则
4、设计模式的初步提出:然后才有了真实的经验总结
5、重构方法的提出:然后才考虑到代码设计的细节上的改善
6、AOP概念的提出:打破OO封装的“封印”
7、新语言的出现:Python、Ruby之类面向对象的动态语言:更加方便的语言?
8、ASM、CGLIB、Mixin之类技术的出现:OO丧钟的先声
具体的对于各个阶段的分析,将在随后展开,目前对于OO的哲学分析,基本上是针对原始的OO概念的。随后的OO技术的发展,也在试图解决由于OO的哲学基础假设带来的问题,当然,越是解决问题,也就离OO的本意越远,现在有人还以为OO在不断发展,而事实上,OO早就盛极而衰,目前已经处在破产的前夜了,我的这篇文章,就是打算使这一天,早日到来!
类型系统
“事实上,我们猜想是,如果没有知识表示和自动推力工作的帮助,这些问题(指类,继承)是无法仅仅通过计算机语言设计的方式来处理的。”——SICP
2.5,中文版136页,角注118。
Elminster那篇论述,正好和我的文章形成一个互补关系,他以极为清晰的表达语言,说明了OO打算以类型化方式描述真实世界,所面临的难题。这也使我不必再次动脑子思考如何回答JavaCup的哲学方面的疑问了。而下面这一段话我想特别再次引用一下:
就我个人来说,比较倾向于认为这条最终是走不通的死路,人是从事物的特征与属性归纳出它的“类型”,而不是相反。某种意义上说,“类型”是为了节省描述时间而产生的 ……
唔,这个太远了,所以就此打住。
大家记住这段话中的,特征、属性、类型这几个关键字。我先绕个小弯再回到这个话题上来。
我之前分析的面向对象的哲学漏洞时,也有不少朋友认为,说:“面向对象不能很好的描述真实世界,并非一个有意义的指控。OOA、OOD本来就是用来对需求建模的。也就是打算描述需求世界。”
其实我的指控分为两个阶段,一方面,OO所依据的哲学导致了软件开发的苦难,而且至今余毒未清。另一方面,即使是指打算对需求建模,OO的技术手段也是有缺陷的。
就这么说吧:OO的类型系统,原本是从ADT来的。一个抽象数据类型,将数据与操作封装在一起,是出于对于数据被“莫名其妙的修改”的担心。但是,结果呢,一个ADT如果不支持继承,那么它的代码就无法被重用。所以OO就在ADT的基础上增加的一个继承,通过继承的方式来复用以有的代码。这样的思路原本没有太大的问题,如果它仅仅只想表达各种自定义数据类型的话。
但是在OO的哲学提出之后,一切皆是对象,所以一切出于类型体系之内,对于数据类型的定义,扩展到了对于现实世界中各种实体的类型定义,整个一个类型系统,其内在的语义大大扩展复杂化了。更糟糕的是——引用Elminster的话是从事物的特征与属性归纳出它的“类型”——而因为OO封装也就是隐藏了内部数据,事物的特征与属性,从其本质属性,被转义为对外的提供的操作接口。但是,要分析一个实体的本质,而不是实体的外部表现,更不仅仅是“我能对他做什么”。这才是实体分析有可能成功的关键,而在OO的语言设定中,这却是难以做到的。
我们来看两张图片:
这是在SICP里讨论类型系统的第一张图片,我称之为“OO成功案例”。
这是在SICP里讨论类型系统的第二张图片,我称之为“OO失败案例”。
为什么一个能够成功,而另一个却会失败?以往的解释其实比较“直觉”。看着这个图,就想当然的以为:“这是因为多重继承导致的!”事实上呢?
第一张图中所显示的成功,很多人会认为这是由于这一个对象塔中的每一个对象都能够支持加减乘除运算。而在几何图形中,这样一致的操作接口不存在了。而事实上,正是因为复数、实数、有理数、整数,在本质属性上有一致之处,他们才能表现出一致的“可加减乘除性”。而不是相反。当我们画出第二张对象关系图的时候,也不是根据几何图形可以接受的操作类型来进行分类与显示继承关系的,而是根据不同的几何图形的本质属性的相近程度来划分类型体系的。多边形的本质是:
“多条有限长直线组成了一个封闭图形”,而三角形与四边形的本质则是,边的数量分别为三和四。等腰三角形的本质是,不但边的数量为三,而且其中有两条边的长度相等,直角三角形的本质是不但边的数量为三,而且其中有一个直角。如此等等......
各位,请再次思考这样的分类体系的内涵。
我的结论是:“一个类型,是由其本质决定了所能表现出的可操作性,而不是有其所能接受的操作决定了其本质。然而,OO正好把这个问题搞反了!”
继承、重用、多态
OO的核心思想就是继承,那么,OO为什么要继承呢?对于这个问题,OO的理论大师们有好多个版本的解释:
1、“这是OO的类型系统自然的要求。设想一下生物学的分类系统:动物——>哺乳动物——>灵长类动物——>人类。或者设想一下我们的概念系统:机器——
>交通工具——>汽车——>小轿车。这样的现象在你的生活中难道不是随处可见吗?”
2、“如果你有一个类,叫做车辆,这个车辆类能够移动,现在你要建立一个子类,叫做家庭型轿车,你就可以直接继承车辆这个类,而不需从头再写移动部分的代码了呀!”
3、“如果你有三个类,三角形、四边形、正方形,他们都需要显示在屏幕上,那么你就可以建立一个基类叫多边形,提供一个draw()方法,然后让那个三个类都继承这个多边形类并且覆盖那个draw()方法,这样,你就可以在绘图的时候,统一对一种多边形类进行操作,不用去管那个对象究竟是哪一种多边形。”
这三种解释,是最为典型的OO继承的好处的解释了。但是你如果仔细的看,就能发现这三种好处,分别描述的是:“概念的特化”、“代码的重用”以及“接口的重用”。或者可以分别命名为:“继承”、“重用”、“多态”。
“这样有什么问题吗?”,也许有人会问。问题就出在这三个好处是用一种方法提供,而这三个好处相互之间有时是相通的,有时又是矛盾的!当我们运用OO语言,来写这样的继承的语句时,一切都是“搅和在一起的”!
假设Class A有两个属性和两个方法:String a1;int i;void f1();void f2();当我们另外写一个Class
B去继承Class
A的时候,我们可以继续使用某些属性,而覆盖另一些属性,还可以继续使用某些方法,而重写另一些方法。还可以添加一些新的属性,还可以添加一些新的方法。如果在加上各种访问控制符的限定与修正。谁能够告诉我:“这个Class
B究竟想干什么?!”
也许有人会继续为这样的现象辩解:“这是对于继承的误用,正确的OO程序不会这样的!”
但是,这个辩解并不成立,几乎所有的OO的编程语言,都没有在继承问题上做出太多“非此即彼”的限制,原因只有一个,那就是,在某些特定的场合,这样的“拼盘”是相对最合理的编码方式。
我们前面还没有提到多重继承,一个允许多重继承的语言,会让这个问题更为复杂,也可以说会使得场面越发的混乱。让我们举一个例子,这是Eiffel语言的继承语法,让我们看一看面对继承这样一件事情,一个程序员,究竟需要考虑多少问题。来源是《对象揭密》,我就一边抄,一边直接翻成中文了。
继承 :inherit 父类列表
父类列表 :{父类 ";" ... }
父类 :类名[特性适配说明]
特性适配说明:[Rename] :重命名以消除名字冲突
[New_exports] :重新设定特性导出的分组
[Undefine] :撤销定义
[Redefine] :重定义以取代虚函数
[Select] :更加高级的功能
end
最值得看的就是这个特性适配说明,更加深入的说明还是各位自己去找书看吧。这就是号称优雅的解决了OO继承问题的Eiffel语言。他所谓的优雅,可以不客气的说,就是把所有的麻烦都明确的告诉你,而不是像C++和Java那样,假装什么事情都没有发生过。但是,麻烦依然在那里,并没有减少,根本的解决方法,是应该不让这样的麻烦出现呀!可是OO确实无法做到这一点。
因为他把数据和操作封装在了一起,然后又偷换了实体本质的概念,在这样的情况下的OO,他的继承是肯定搞不好的!
接口、泛型与重用
先说点提外话,我从小学开始学习BASIC和LOGO,到后来学习了FoxBase、FoxPro、C/C++、Visual
Basic、VBScript、JavaScript、PHP,之后才开Java编程,之后也没有再换过语言。诚实的说,只有Java语言,是我认认真真的学习和研究的。对于面向对象的理解,也是在学习和使用Java之后,才真的开始深入思考这方面的问题,在此之前,我甚至认为所有的语言都没有什么本质的差别,面向某某和面向某某之间也没有什么大不了的差别。
所以当我想要写这篇文章的时候,其实内心是相当惶恐的,我对于面向对象的了解,其实只来自于一种语言,那就是Java,而Java是不是就等于是面向对象呢,只怕是不能这么说的吧。
JavaEye有人留言:“不要到时候说不圆影响了一世英名。”;“讨论这个问题,我还是建议去看 SICP,你会发现所有OO具有的思想SICP都讲到了”;“实际上我很怀疑庄某最后还是跑不出SICP的框架,如果是这样,那么其理论的价值就要打折扣了。”我那个慌啊,赶紧到书店去买了SICP回来仔仔细细的啃,然后再在MSN上向T1好好的请教过几次,最后总算放心了,我的思路,不在SICP的框架内,或者说,不在SICP作者的思考局限之内。
还有人留言,提到了C++:“OO门槛较高是不争的事实。的确很多人并没有进入。有句话可以套
用,没有三年的功底,最好不要说懂C++。幸运的是这门东西的回报,会告诉你所付出的是完全值得的。”我又慌了,C++背后的面向对象,何等高深,我却从来没有用C++做过哪怕1000行以上的程序,这等门槛都没有入的人,有资格评价面向对象那么大的事情,赶紧的,我又到书店去买了一本《对象揭秘》,我对于当年gigix的一篇介绍《编程语言的宗教狂热和十字军东征》始终记忆犹新,里面提到了面向对象,提到了C++的无数缺点,还提到了Eiffel,一个据说是比C++要好无数倍的面向对象的语言。如果我要想加快速度,又想保证质量的话,从《对象揭秘》里面应该可以找出很多现成的弹药吧。
抱着急于求成的功利主义目的,我开始仔细看这本《对象揭秘》,一看之下,真是大有收获:
*C++果然毛病多多,而且作为第一个大面积流行的OO语言,OO的实际含义更多的来自于C++。
*Java的毛病少了很多,因为它引入的一些概念,不再使用的一些概念,大大的减少了C++式OO编程的陷阱,只是这样一来,在复杂问题上使用Java语言,往往会写出很丑陋的程序。
*Eiffel同样也是反思C++缺点的语言,但是它的改进基本上是表面的,Java是使问题简化,哪怕牺牲语言的表达能力,而Eiffel是使问题表面化、集中化,陷阱虽然没有了,但是问题一个都没有减少,反而因为“让人眼花缭乱的复杂语法”,让人望而却步。
*《对象揭秘》是一本很一般的书,作者花了十多年的时间攒出一本书来,实质上还是BBS里一段一段讨论的水平。
————————————————————————————————————————
好了,题外话结束,接下来讨论正题,今天主要研究OO的概念中两个较为边缘的概念“接口”与“泛型”,以及探讨一个实际上最为重要的误用——“重用”。
1、关于“接口”
接口是什么东西?接口是一个很奇怪的东西!接口之所以奇怪,因为他的来龙去脉实在是让人看不懂。基础的OO概念中,并没有接口的位置,因为按照“经典的”面向对象思维,一个没有代码、没有内部数据,只有方法说明的东西,是无法理解的。
追根溯源的说,首先是在C++的面向对象实践中,发现了对于“抽象类”的需要,为什么需要抽象类呢?因为代码重用的需要,比如一个基类,其实就是一堆公用代码,有一个名字把它框起来叫一个类,但是完全没有道理把它实例化。像这种“发育不完全的类”,该拿它怎么办呢?OK,就让它不能“出生到这人世间来”。抽象类的本质就是这个样子的。
到了Java,因为对于多继承的恐惧,Java完全摈弃了多重继承,这是Java被攻击得最多的地方,因为这样的单根继承,实在是因噎废食——因“怕被继承体系搞乱”废“更加方便道代码重用”。于是Java就说了:我们有一个“安全的”多重继承,叫做“接口”,这个接口,完全没有代码,只有说明,所以绝对安全,但是由能够实现多重继承的好处云云。
而事实上呢?多重继承的根本目的,并不是像Java所宣称的那样为了“同时继承多种类型”,而是为了“同时重用多组代码”。接口这一发明,完全不能达到多重继承的代码重用效果,却被宣称为多重继承的替代品。其实质是:“从一个发育不完全的实体,变成了一张彻底没有发育的皮”。
最为令人感到奇怪的,还不是“接口的出现”,而是“面向接口编程”的出现,Java被冠以“面向接口的语言”的美名,“面向接口设计”成了OO的设计原则之一,“针对抽象,不要针对具体”,成了OO名言之一。实在是......
关于OO的设计原则,我下面还会专门讨论,这里先指出一个大漏洞:“抽象类的那个抽象,和与具体相对的那个抽象,根本就不是一回事!”
继承、多态与泛型冲突的一个例子
写技术文章,例子其实很难举,特别是找到有杀伤力的,决定性的例子,相当困难。昨天我接着看《对象揭密》,总算被我找到一个,当然,它那上面的解说,实在是比较模糊,因此我决定用自己的话重新叙述一遍,代码示例用Java的泛型语法,但是要表达的意思,确实所有的具有泛型的OO语言都需要面对的。
java代码:
public class X {
protected int i;
public void add(int a){
i=i+a;
}
}
public class Y1 extends X {
public void add(int a){
i=i+a*2;
}
}
public class Y2 extends X {
public void add(int a){
i=i+a*3;
}
}
这是三个最简单的类,Y1和Y2都继承了X,并且重写了add函数。当然,这只是举例,实际上这三个add中,有两个是不合理的。
java代码:
ArrayList listx=new ArrayList();
ArrayList listy1=new ArrayList();
ArrayList listy2=new ArrayList();
listx.add(new X());
listx.add(new Y1());
listx.add(new Y2());
listy1.add(new Y1());
listy2.add(new Y2());
这几行代码都非常简单,只是为了说明一个道理,在ArrayList和ArrayList中,能够放的就只有Y1和Y2了,而在以X为泛型的ArrayList中,就可以放X、Y1、Y2了。而当然了,这样的用法,只怕是不合泛型的目标的,本来就是希望能有一个类型的自动检查与转换,都放在ArrayList中,几乎就等于都放在ArrayList中了。
现在我们有这样一个需求,对于得到的ArryaList,能够一一调用里面的对象的add(int a)方法,当然了,只要这个ArrayList里的对象都是X或者X的子类就行了。我们可以写出这样的代码:
java代码:
public void addListX(ArrayList listx){
for(int i=0;isize();i++){
X x=listx.get(i);
x.add(1);
}
}
是不是很简单?且慢,这个addListX函数,我们能够把listx传递给它,但是能不能把listy1和listy2
也传递给它呢?如果我们能够把listy1和listy2传递给它,就相当于执行了如下的类型转换代码:
java代码:
ArrayList listy1=new ArrayList();
ArrayList listx=listy1;
这样做行不行呢?在Java和C++中,是不行的。也就是说,如果我们要想只写一遍addListX这样的函数,而不用再多写两遍addListY1();addListY2();这样的函数,就需要把所有的X,Y1,Y2这样的类型都放到ArrayList这样的容器里,否则,addListX函数,是不接受ArrayList和ArrayList类型的。即使Y1和Y2是X的子类型,ArrayList与ArrayList也毫不相干。不能相互转换。
有人也许会说,为什么这么限制严格呢?Eiffel就没有这么这么严格的限制,他允许ArrayList自动转型为ArrayList,这样是好事情吗?如果listy能够被转型为ArrayList,那么就可以往里面添加Y2类型的对象了,这又是原来的泛型ArrayList不允许的。也就是说:除非addListX能够保证只对listy1做只读操作,否则,类型安全性这个泛型原本要追求的目标就不能实现了。而如果要追求绝对的类型安全性,像C++和Java那样,那么代码要么就得写三遍,要么X、Y1、Y2类型的对象就得都放到ArrayList这样的泛型容器里去。
注意看这其中的左右为难的状况,继承、多态、泛型,并不是真正正交的、互不干扰的,而是在一个相当普通的目标面前,他们就起了冲突了。