敲响OO时代的丧钟!

来源:互联网 发布:威风堂堂mmd动作数据 编辑:程序博客网 时间:2024/05/16 19:15

一、基本概念
1、软件与硬件的区分
“拜托!”肯定有朋友会说:“我们都是X年的老程序员了,就算水平不是出类拔萃,至少软件硬件总是分得清的!”那么什么是“软件”?什么是“硬件”呢?从维基百科抄来看看吧:
软件是一系列按照特定顺序组织的计算机数据和指令的集合。
硬件包括电脑中所有物理的零件,以此来区分它所包括或执行的数据和为硬件提供指令以完成任务的软件。
就是这么简单?如果真的就这么简单,我何必讲OO的要绕那么远的路,先来区分软件和硬件?要真正理解硬件与软件的本质,首先要理解计算机的本质——“废话,你不就是想说图灵机吗?”——下面的同学快忍不住了,为什么这个庄表伟老是在说一些我们知道的东西呢?
ok,先别急着关窗口,咱们来看一张图片:
简单的说,这是一个小机器,左边的那个按钮按下去,右边的灯泡就会亮起来,而如果中间的那个开关拨下去,那么这个小机器的行为就反过来,按钮按下去,灯泡就灭掉,而放手以后,灯泡就亮起来。
接着说昨天的那个小机器,它和计算机什么关系吗?和软件、硬件有什么关系吗?
大有关系,我们现在看到的这个图,其实也可以算是一个最简单的计算机的。这个计算机有输入——那个按钮,有输出——那个能亮的灯泡,内部它是如何如何做到这一点的我们不用关心。关键在于这个计算机的能力极其有限,只能使得灯亮或者不亮。而想要改变它的行为也很简单,通过拨动中间的那个开关,我们就可以改变这个机器的行为。
在这样一个简单的模型中,什么是硬件呢?就是那个机器,硬件的能力的边界,就是整个系统的能力的边界。在这个能力的边界范围内,这个机器究竟能够表现出什么能力,表现出多少能力,就是由软件决定的。具体到我们这个简单的机器,它的软件就是那个开关所记录的状态。如果我们拨动了那个开关,就相当于重新安装了另外一个软件,因此改变了机器的行为。
“快点,快点,再不切入正题就退票了!”
ok,我保证不再插入常识性的内容,咱们直接说高深的。如果咱们开发软件,不是在真正的机器上,而是在VMWare上呢?VMWare是不是软件呢?对于在VMWare中运行的软件来说,VMWare就是硬件了。那么我们在操作系统上开发的软件,对于这种软件来说,操作系统可不可以算作是硬件呢?如果我们在一个Web容器上开发Web应用,这个Web容器是不是可以算作硬件呢?如果......
在我看来,硬件与软件,并不是非此即彼的两个存在,而是一个连续体。从非常非常硬,到非常非常软。软硬的程度,取决于容易被改变的程度。这就是我对软件与硬件区别的定义。
2、什么是软件开发的实质?
继续我们前面讨论的那个软件、硬件的区别,如果你同意软件就是在一个系统中可以被改变的部分,而硬件就是不能被改变的部分。那么我就可以说出软件开发的定义了:“软件开发,就是在一个受到限制的环境中,利用环境提供的可能性,修改或添加环境允许的各种状态,去满足某一组需求。”
我们来展开讨论这个定义:
1) 软件开发所处的环境,不仅仅是一个限制,同时也是一个可能性。软件的能力,受限与硬件的能力,比如说,如果计算机没有喇叭,那么任何软件都不能使计算机播放音乐。但是,另一个必须考虑的方面是,同样有能力发声的计算机,要想使他播放音乐,可能很容易,也可能很困难。用专业一点话来描述就是:“有些硬件的API设计很合理,有些则非常愚蠢。”由于我们对于软、硬件的定义是一个连续体,因此,这个观点不只是可以用来评价硬件API设计,也可以用来评价语言、虚拟机、框架、平台等等软件的一个方面的优缺点——是否有利于二次开发,这是一个重要的评价标准。
2) 修改、添加状态,比较拗口,其实就是编程的意思。在一个受限制的范围内编程,我们需要考虑很多东西,语法、接口、规范、内存大小诸如此类,当然,不同级别的,不同领域的编程,需要考虑的限制是有巨大差别的。软件开发的水平高低也就体现在,满足同样的需求,有些方法速度更快,有些方面却要慢很多。而软件开发的方法的选择,受到很多因素的影响:环境限制,经验多少以及对于需求的了解程度等等。
3) 满足需求,是啊!提起这个需求,每一个程序员都会有好多的苦水要倒出来。为什么满足需求就这么难呢?因为,对于程序员来说,那是另外一个世界(这是比较客气的说法),那些提需求的家伙根本不懂怎么说话(这个说法稍为激烈一些),那是一些不知道自己要什么的蠢货(你遇到过这样的用户吗?)作为程序员,我知道我有很多同行,非常苦恼于与客户谈需求这样的任务——“至少电脑不会出现前后矛盾的逻辑错误”——这就是做程序员的难处。如果我们不仅仅是抱怨的话,也必须承认,程序员是非常挑战的职业,一个好的程序,不但得是软件开发领域的专家,还得是他开发的那一类软件所在领域的专家。但事实上,其他行业的人,只需要做一种专家就能够混得很好了。
3、软件开发的概念演变
软件开发发展的真是波澜壮阔,新鲜的概念也是层出不穷。这其中有什么规律呢?
首先是因为需求难度的提高与种类的扩展,出现了新的硬件,进而导致新的软件开发中的概念的产生。比如为了保存状态信息,出现了存储器。为了持久的保存状态信息,出现了外部存储器。为了显示彩色图像出现了彩色显示器,为了发出声音而出现了喇叭,为了在多台计算机之间通讯而出现了网络。这些新硬件的出现,自然会引发产生新的软件开发中需要考虑的概念。
其次是因为软件开发中的概念划分与细分的需要,例如从函数到模块,从模块到层次是在描述越来越复杂的系统时必然出现的新的命名,再进一步说,众多类似的应用,都出现了类似的函数、模块、层面,自然需要对这一常见事物命名,这样的命名现在已经被清晰的识别出来,被称之为“模式”。
再次是因为外部需求的表述规范化,别的行业的需求描述,一定是采用自己的语言体系来表达的,而这样的表达并不一定容易理解,特别是对于程序员来说,理解一个完全不相干的行业用户的需求是非常吃力的,当他理解之后,很自然的就会转述成自己人能够理解的语言,当然,也有可能外来语直接就成了软件开发中的内在概念。比如:表达式;表格;电子签名;密码;事务等等等等。当软件开发需要满足越来越种类繁多软件需求时,软件开发本身的概念也会越来越多,越来越复杂。
最糟糕的概念来源于软件开发内部的自作聪明,生造概念,Big Words,对于同一个概念重复命名,使得很多程序员迷失其中,进一步的声讨这种现象,显然不是本文的范围了,就此打住。
上面的这些原因,可以解释软件开发中出现的大多数概念,然而,却没有一种能够用来解释OO这个概念为什么会出现!这是一个大问题,我们会专门有一个部分,讨论OO概念何以会出现。
二、怀念失落的世界
曾经有一个完整的世界摆在我们的面前,而我们却无法把握,直到世界分裂之后,才追悔莫及
——庄表伟
计算机编程曾经很容易学习。因为它的语言是自然的汇编语言。
汇编语言很自然?是啊,在考虑到识别的方便性之后,汇编语言是最自然的计算机语言了。为什么这么说呢?我们编程操纵的就是计算机,而汇编语言的每一个语句,都是毫无疑义的、对计算机发出的命令,所有的程序都围绕这一个思维模式——就是计算机本身的运行逻辑与限制。我们只要记忆很少的语句,就能够看懂:“每一个命令在做什么。”,然而,要做的事情越来越复杂,我们需要看很多句,才能够看懂:“这一系列命令想要做什么。”
为了解决这个问题,我们开始使用“函数调用”,一方面是不再为重复功能写多遍代码,另一方面也可以给这些被调用的函数进行一些有意义的命名。“过程”、“函数”、“方法”、“模块”等等概念,都是这么自然而然的诞生出来的。不同的功能被赋予不同的名称,不同的特性由不同专长的人去实现......一切都没有超出程序员的理解范围,大家都是最聪明的棒小伙。
直到现在,这样的思维模式依然埋在很多程序员的心底,如果一个程序无法完成预想的功能,很多程序员的最后一招依然是“debug”、“单步跟踪”。在那个熟悉的世界里,程序员又重新找回了自信,一切都在控制之中,一切都是可以理解的,一切问题都能够被最终解决。

值得怀念的世界,却不值得回去
——庄表伟
面向过程的世界是完整的,统一的,也是容易理解的——对于程序员来说——或者说他只需要一种理解能力。这个世界虽然值得怀念,却不值得再回去。因为,我们不再像当年的程序员那样,只开发那些简单的软件了。很多人崇拜那些早起的“大牛”,其实平心而论,我们现在面对的问题的复杂程度,在他们当年可以说几乎无法解决。需求的复杂程度也不是他们当年能够设想到的。
这是在秘鲁发现的神秘的纳斯卡巨画,这样巨大的地面艺术,可以给我们对于面向过程的编程的结论一个可视化的比喻。面向过程的编程,只有一个统一的世界,他对于软件的理解,始终不会离开计算机的操作与运算本质,这就像在平地上作画那样,我们需要的一根长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这样的泛型容器里去。
  注意看这其中的左右为难的状况,继承、多态、泛型,并不是真正正交的、互不干扰的,而是在一个相当普通的目标面前,他们就起了冲突了。

重用为什么那么难?
  程序员都是聪明人,没有谁愿意干重复劳动这样的傻事,因此,程序中出现重复代码是程序员的耻辱。就算不能消除重复代码,至少也可以对于相同的功能,用不同的代码来实现,所以发明新轮子的程序员才会那么多。
面向对象作为一种横空出世的新技术,首先承诺的就是“更好的重用性”,而“重用性”这样一个闪闪发光的词,也的确能够吸引程序员的实现,那么多新的理论、新的技术、新的方法、新的框架、新的思想,用来说服别人接受的一个最大的理由,就是“更好的重用性”。然而,OO以及一直以来不断发展的 OO相关技术,对于重用性的提高,作出了多大的贡献呢?
JavaEye的age0有一段话特别让我佩服:“我还是得反复强调,OO设计的价值并不在于所谓的“代码重用”或者“接口重用”,任何一种设计方法都能够获得“重用”的能力,就算是写汇编一样可以“重用”。”一个同志能够如此决绝的对于重用不屑一顾,真是了不起。我们还是来面向大多数希望更好的重用的程序员,分析一下在OO出现之后程序员是如何追求重用这一目标的吧。
在面向过程的时代,重用很简单,就是代码的重用,函数、函数库、模板、模板库。如此而已。在ADT出现之后,世界分裂了。代码重用的需求,现在分裂为三个部分:数据类型的重用、ADT内部代码的重用、操作ADT的代码的重用
这句话特别关键,让我再仔细分析给大家看看:ADT=抽象数据类型。就是封装了操作和数据的一种用户自定义数据类型。
1、如果仅仅是ADT,那么相近的用户自定义数据类型就无法重用,因此出现了一个数据类型的重用需求;
2、因为ADT封装了对于数据的操作,对于A类数据的操作,就无法被B类数据重用,因此出现了一个ADT内部代码的重用需求;
3、因为对于ADT的操作是借助ADT的外部界面执行的,也就是说,对于接近但是不同的类型的操作,必须写出不同的代码,因此出现了对于操作ADT的代码的重用需求。
这样的分裂的三个需求,在随后的OO的发展历史中,分别被两种方法来初步满足。第一、第二种需求,被继承这样的技术来试图满足;第三种技术被泛型类试图满足。这两个技术满足了三种重用的需求了吗?没有!不但没有满足,而且还带来的诸多麻烦的问题,更在分别满足三种需求的时候,起了冲突。
由于封装与重用性之间,存在着本质性的冲突,因此,OO的分析、设计、编程方法就始终处于一个难学、难用、难懂的状态。我们说给OO下定义非常困难,但是大家都应该承认,ADT是OO的根。数据与操作的封装是一切OO思想的基础,也是所有OO信奉者从来没有怀疑的“前提”!
在继承与泛型不能解决重用难题之后,OO大师们提出了OO设计原则,提出了OO设计模式,这是我接下来的文章里将要细细批驳的两大“贡献”。但是OO的原则、模式,依然没有解决重用难题。在此之后,又有人提出了AOP、IoC这样的概念,还有人真正开始和我一样怀疑封装的意义,而开发了 CGLib,Mixin这样的动态改变对象行为与结构的技术,这也是我将要批判的“最新进展”。到了这个时候,真正理解OO本质的人,应该已经看出来了,OO时代即将结束,因OO而带来的混乱也该结束了。现在唯一的问题是:“什么样的技术,才是可行的、替代的方案呢?”

OO设计原则批判
  OO设计原则,这是很多开发资源网站必备的一个栏目、专题、至少也要转载一篇放在自己的网站上的东西。所有的程序员,如果你不开发面向对象的程序也就罢了—— 反正你已经落伍很久了,如果你要想开发OO程序,而竟然没有把那些OO设计原则熟读背诵,搞得滚瓜烂熟。那么你就完了,一个公司面试你的时候,问你:“你对SRP的理解是怎么样的?”,而你居然不知道SRP是什么,那么这家公司你也就别想进去了。作为OO程序员的《旧约圣经》(设计模式自然是《新约圣经》)他怎么就会那么神圣呢?
  介绍OO设计原则的文章很多,我在google上搜索了一下:“约有58,200项符合OO设计原则的查询结果”。真正能够介绍得透彻的,还真是没几个。正好我手边有一本Bob大叔的《UML for JAVA Programmers》那上面的介绍,在我看来,是最好的OO设计原则介绍之一了。另外一本不在手边的《敏捷软件开发原则、模式与实践》也是Bob大叔的,还要详尽一些。如果要批判,自然要找这样的靶子来练!

  1、单一职责原则(SRP)
  一个类只能因为一个原因而改变。
  “A class should have one, and only one, reason to change.”
  这个原则何等的简单,又是何等的模糊呢?什么叫做一个原因呢?我们来看下面这个类:
java代码:
class User{
    private String name;
    private int age;
    public void setName(String name){
     this.name=name;
 }
 public void setAge(int age){ 
    this.age=age;
 }
}
请问,这个类是不是违反了SRP原则呢?设置用户的名字与设置用户的年龄,是一个原因,还是两个原因呢?Bob大叔在自己的书里举了一个例子,说明了违反SRP原则的情况,一个Employee类,包含了计算工资和扣税金额、在磁盘上读写自己、进行XML格式的相互转换、并且能够打印自己到各种报表。我说拜托啊大叔!一个类里的方法多到如此惊人的程度,自然是违反了SRP原则,但是我们要为它瘦身,该瘦到什么程度呢?按照大叔继续给出的自己的答案,它把计算工资和扣税金额的两个功能留给了Employee,其他的分离了出去。这个答案正确吗?员工的工资和税收是自己算的?还是有一个“财务部”对象来计算的呢?且不说那么扫兴的事情,就看看那个类图里分离出来的那几个类:
  EmployeeXMLConverter、EmployeeDatabase、TaxReport、EmployeeReport、 PayrollReport。这些类还需要有自己的内部数据吗?请注意,他们事实上都是通过接受Employee对象的内部数据而工作的,换句话说,这些所谓的类,根本就不是什么类,只不过是一个个用Class关键字包裹起来的函数库!当我们看到一个臃肿的Employee类,被拆成6个各不相同的类之后,内心自然升起了“房子打扫干净之后的喜悦”。但是,且慢!灰尘到哪里去了呢?当我们把一个类拆成6个类之后,那个原本的类自然已经遵守了SRP原则,然后新诞生的5个类,是不是也该遵守SRP原则呢?如果我们不能将一个原则应用于整个系统设计中的所有的对象,仅仅像小孩打扫卫生一样,把灰尘扫到隔壁房间,这剩下的事情,谁来处理呢?
好吧,我们不要这么严厉,毕竟这只是一个原则,追问太深似乎并不合适。我只想再搞清楚几个问题:按照SRP原则,C++中是不是一律不应该出现多重继承呢?按照SPR原则,Java中的一个类是不是一律不应该既继承一个类,又实现一个对象呢?一个简单的POJO,被动态增强之类的办法,添加出来的新的持久化能力,是不是也是违反SRP原则的呢?归根结蒂,我的问题是:按照SPR原则,我那些剩下的,但是又必须要找地方写的代码,究竟应该写在哪里呢?

  2、开放-封闭原则(OCP)
软件实体(类、模块、方法等)应该允许扩展,不允许修改。
  “Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
  这个原则倒是非常的清楚明白,你不能改已经写好的代码,而应该扩展已有的代码!如何做到这一点呢?Bob大叔举了一个经典的例子:个人认为这个例子说明的是一个使用接口,隔离相互耦合的类的通常做法。而且这个做法不应叫做OCP,而应该叫做DIP。查了一下c2.com里的OCP的解释:
  In other words, (in an ideal world...) you should never need to change existing code or classes: All new functionality can be added by adding new subclasses and overriding methods, or by reusing existing code through delegation.
  但是在Bob大叔的OCP解释中,这个原则的具体实现被偷换了概念,从“鼓励多使用继承”,变成了“鼓励面向接口编程”。为什么?因为继承式OCP实践已经被证明会带来相当多的副作用,而面向接口编程又如何呢?我们在讨论DIP的时候再详细讨论吧。
  有一个在JavaEye的讨论的连接可以参考:对于OCP原则的困惑

  3、里斯科夫替换原则(LSP)
  子类型必须能够替代他们的基本类型。
  “Subtype must be substitutable for their base types.”
  对于这个问题,我都不用多说什么,只引用Bob大叔在c2上的一句话,以作为我的支持。
  “I believe that LSP is falsely believed by some few to be a principle of OO design, when really it isn't.”

  4、依赖关系倒置原则(DIP)
  A.上层模块应该不依赖于下层模块,它们都依赖于抽象。
  B.抽象不应该依赖于细节,细节应该依赖于抽象。
  “A. High level modules should not depend upon low level modules. Both should depend upon abstractions. ”
  “B. Abstractions should not depend upon details. Details should depend upon abstractions.”
  Bob大叔还补充说明了一下,这个原则的意思就又变了:更确切的说,不要依赖易变的具体类。也就是说,不容易变的具体类,还是可以依赖的。那么,当我们开始一次系统开放的时候,那些类是易变的具体类呢?那些类是不易变的具体类呢?怎么才算是易变、怎么才算是不易变呢?我们来看看代码吧:
java代码:
class A{
     public void doA(){
     }
}
class B{
     A a=new A();
     a.doA();
}
按照DIP原则,Class B依赖于一个具体实现类Class A,因此是可耻的!最起码你应该写成这样:
java代码:
interface A{
     public void doA(){
     }
}
class AImpl implements A{
     public void doA(){
     }
}
class B{
     A a=new AImpl();
     a.doA();

  这样,AImpl和B都依赖于Interface A,很漂亮吧。还不够好,A a=new AImpl();还是很丑陋的,应该进一步隔离!A a=AFactory.createA();在AFactory里,再写那些见不得人的new AImpl(); 代码。然后呢?这还没完,更加Perfect的办法,是采用XML配置+动态IOC装配来得到一个A,这样,B就能够保证只知道这个世界上有一个 Interface A,而不知道这个Interface A是从哪里来的了。这么做的理由是什么呢?有一个很吓人的理由:“如果A被B直接使用,那么对于A的任何改动,就会影响到B了。这就叫依赖,而这样的依赖会很危险!”
  我们来看看这颇有说服力的话,漏洞何在?A的变化有两种情况,一种是只修改A中的方法的内部实现,无论是直接使用A还是使用Interface A的某一个实现,这时候B的代码都不用改。另一种是修改了方法的调用接口,如果是直接使用A的Class B,就需要修改相关的调用代码,而如果是使用接口的呢?就需要同时修改Class B和Interface A。这样看来,采用接口方式,反而需要修改更多的代码!这使用接口的好处何在?

  5、接口隔离原则(ISP)
  客户端不应该依赖于自己不用的方法。
  “The dependency of one class to another one should depend on the smallest possible interface.”
  这个我就不说了!因为这个原则和SPR原则是矛盾的!就像合成复用原则(CRP)与LSP原则矛盾一样。
  关于这个批判,我昨天晚上只写了一半,今天算是虎头蛇尾的写完了。最后录一段Bob大叔的话,作为结尾:
  什么时候应该运用这些原则呢?首先要给您一个痛苦的告诫,让所有系统在任何时候都完全遵循所有原则是不明智的。
  运用这些原则的最好方式是有的放矢,而不是主动出击。在您第一次发现代码中有结构性的问题。或者第一次意识到某个模块受到另一个模块的改变的影响时,才应该来看看这些原则中是否有一条或者多条可以用来解决问题。
  ......
  找到得分点的最佳办法是大量写单元测试。如果能够先写测试,再写要测试的代码,效果会更好。
  让我来翻译一下上面的告诫。原则不是你可以用来预防问题的,而是当你已经陷入麻烦的时候,你可以停下来悔恨一下。至于解决之道,依然不是很清楚,因此,你需要写大量的单元测试。而且,大量的单元测试并不是帮你检查你的设计漏洞,而是帮你更真切的感受自己的设计是否正确。至于他究竟是不是正确,这就看个人自己的感觉了。更为惊人的是,在测试驱动开发的建议中,如何驱动开发的准则,竟然是循环的来自于OO设计原则的。
  这样的OO设计原则,就像老爸老妈给我们的人生教诲:“你要做好人啊”,别的什么都没说。而且我们还遇到了话都说不清的糊涂爹娘,怎么才算好人,不清楚,怎么才算坏人呢?被警察抓了,肯定就是坏人了。至于如何才能做得更好?自己体会吧。

设计模式批判
 为什么要批判设计模式?设计模式不是OO开发的救星吗?作为“可复用的面向对象”的基础设施,设计模式大大的超越了OO设计原则给予我们的承诺,还记得我们前面的分析吗?OO设计原则并不担保你在设计之前就能避免错误,相反的,你往往需要在屡屡受伤之后,才会明白设计原则的真谛。而设计模式是如此的伟大,他甚至可以帮你提前避免问题,只要你可能遇到的问题,符合设计模式手册中,所描述的那种场景,基本上你就可以直接采用相应的设计方案了。如果找不到正好合适的,你也可以改造自己面对的问题,使得他看起来究就像设计模式手册中描述的那样,然后你就可以放心使用相应的设计方案了。如果你无法在那23个模式中找到合适的答案——你可真是太挑剔了——那么你只能自己想法组合一下那23个中的2~3模式,总之,一切尽在其中。
好吧,事实其实没有那么夸张,“GoF”从来没有宣称“设计模式”能够包治百病,更没有说过使用“设计模式”可以预防疾病,他们也的确谦虚的承认,设计模式肯定不止23个。但是,GoF也必须承认的一点就是:“Design Patterns原本是用来指导设计的。大多数时候,都是在实际开发之前完成的。”而且,按照设计模式原本的思维模式,应该把一个系统中的各个类,按照他们所说的一堆模式组织起来,其根本目的,就是不让后来的改动,再去修改以前的代码,所谓OCP原则在设计模式中的实际体现,就是对扩展开放、对修改封闭。
In other words, (in an ideal world...) you should never need to change existing code or classes: All new functionality can be added by adding new subclasses and overriding methods, or by reusing existing code through delegation.
再强调一遍:“设计模式认为,灵活性、可复用性是设计出来的”,而在此之后的发展我们可以看到,新的大师们又偷换了概念,将“设计——实施”的两阶段过程,变成了一个“TDD+重构”的持续改进过程,他们不但提倡改以有的代码,而且要大改特改,持续不断的改,唯一还带着的帽子是:“重构的目标是得到设计模式。”重构真的能以设计模式为目标吗?我们下一篇再讨论。
请允许我先借力打力,利用重构这一新生事物,攻击一下设计模式这个老东西。为什么灵活性、可复用性不能是设计出来的?
软件开发,一个很重要的工作,就是对付“需求变更”,软件工程的办法是尽可能的抵挡、或者有效控制变更的发生。技术人员的目标,则是努力开发出更加灵活的技术,以适应可能的变化。值得一提的是,现在软件开发的管理者,也开始相信,拥抱变化比限制变更,是更为可取的手段了
更加灵活的技术,更加容易理解,方便描述事实的语言,设计模式等等等等,都是用来适应可能的变化的。对于技术人员来说,如果能够预测可能的变化,并在系统设计期就将其考虑进去,那么变化就成为计划的一部分,注意这句话,因为实际的情况往往是:“计划赶不上变化”。越是自信的技术人员,越是自以为能够预测可能的变化,将变化提前设计进入自己的系统。所以,滥用设计模式的人,往往是那些自以为水平很高的半桶水。而重构这一思路的出现,就是对于设计模式这种“企图预测变化”的否定。事实上,即使是重构,也是危险的,因为从原始状态,到第一个变化发生时,我们能够得到的只有两个状态点,这两个点联成直线所指向的一个方向,并不一定就是变化的方向,因此,重构是一个好办法,而将得到设计模式作为重构的目标,却相当危险。
设计模式背后的思路非常清楚,就是将可能变化纳入设计模式的考虑之中,所以我们看到设计模式的目标“可复用”,其实是一个转了一个弯以后的目标,“在尽可能重用已有代码的前提下,适应变化”。我的观点是:“首先需要满足的不是复用,而是快速修改”,这个问题太大以后有机会再讨论吧。
这篇关于设计模式的批判,我写了好几天,始终感觉难以下手。今天和徐昊讨论,他的话我认为非常有道理:“设计模式的成功,正好证明了OO的失败”。这个思路相当有用,我决定就按这个调子来写。当然,设计模式并不是只有一个,而是有很多很多个,作为一种“专家经验交流的规范描述格式”,设计模式已经非常多了。我们今天也不批判更多的模式,仅仅对GoF的23个模式下手吧。
GoF的23个设计模式,主要分为三类:创建型模式、结构型模式、行为型模式。我们就分别批判这三种模式吧。
创建型模式之所以需要,其实正好证明了OO的失败。因为在OO的思想中,创建对象这种事情,天然就应该是对象自己处理的。一个类在定义时,可以定义一个以上的构造函数,来决定如何得到自己的实例,那为什么还需要把创建对象这个事情,交到别人手里?按照SRP原则,无论出于什么原因,创建自己总是自己的职责吧!所以按照SRP原则,所有的创建型模式都不应该出现,出现了也是错误的。但是为什么我们需要创建型模式呢?先看看GoF的解释:“随着系统演化得越来越依赖于对象复合而不是类继承,创建型模式变得更为重要。当这种情况发生时,重心从对一组固定行为的硬编码,转移为定义一个较小的基本行为集,这些行为可以被组合成任意数目的复杂的行为,这样创建有特定行为的对象要求的不仅仅是实例化一个类。”
这样的解释,有一定的道理,但是却局限于“用组合取代继承”这样一个“当年的热门话题”。在我看来,根本的原因在于:“在OO的经典思想中,对象之外的环境是不存在的,因此,所有的对象,都要考虑如何产生自己,如何销毁自己,如何保存自己,总之,自己的事情自己做。”Java的一个巨大进步就在于,销毁自己这件事情,不再强求对象自己去考虑了,因为这种事情过于琐碎,而且复杂易错。但是创建自己这件事情,java依然没有考虑到也不该交给对象自己考虑的。于是设计模式中的种种创建模式被发明出来,用以满足复杂多变的创建需求。这个根本原因同时也解释了为什么单例模式会被发明,按照GoF的解释,是无法说明为什么我们会需要单例模式地。而当我们有了对象环境的概念之后,各种创建自然有“容器环境”来完成,“单例”模式也只需要在环境中配置,有了OO容器之后,所有的创建模式都被一个概念所代替了。在没有这样的概念之前,我们需要用各种模式技巧,来实现“支离破碎”的环境。而在真正的容器环境出现之后,我们就不再需要这些设计模式了。
让我再说一遍:“如果你能够理解为什么现在会出现那么多容器,就能理解设计模式中的创建模式,只不过是用来解决OO概念欠缺的一种不完善的手段了。”
再来看结构型模式,个人认为将“Adapter、Bridge、Composite、Decorator、Facade、 Flyweight、Proxy”统统归入结构型模式,是不恰当的。除了Composite模式算是结构模式Flyweight算是一种“节约内存的技术手段”之外,其他的模式,都属于打破OO静态封装的技巧。我们知道,按照OO的设定,一个类,就是一种类型,它在代码写下的时候,就已经决定了自己的内部数据与外部行为。怎么还能在运行的时候再改变呢?事实证明,这样需求不但存在,而且重要,设计模式之所以被大家欣赏,一个重要的原因,就是他能够帮助程序员部分摆脱“静态封装属性与行为”的限制。

OO能从关系型数据库借鉴些什么?
今天这篇是关于OO VS. RDB的,OO作为一种编程范型,主要是集中于处理“操作”,而RDBMS作为一种数据管理工具,主要是集中于“数据”。但是,在一个需要数据库的系统中,必然的情况是:操作的对象自然是各种各样的数据,而数据的管理,自然要通过操作。因此,OO与RDB,从最初浅的角度来理解,虽然分别位于“业务逻辑层”与“数据层”,但是相互之间却又有着非常紧密的联系。在OO与RDB之间存在着的紧张关系,其根源在于:“OO世界中的数据是被封装的孤立数据;RDB世界中的操作是针对行集的。”
因此,一个对象实例内部的数据,应该保存到数据库中的哪些表、哪些行、哪些列,是一个非常复杂的问题。反过来说,当我们要在内存中恢复一个对象实例时,要从多少表、多少行、多少列中采集数据,并正确转化为对象实例的内部数据,也是相当的复杂。O/R Mapping,需要考虑的问题还不止于此,在RDB中自然存在的“关系”这一概念,在OO当中,却没有明确的对应概念。在OO的世界里,对象之间存在各种各样的关系,却非常难以进行规范化的描述。再加上:“添加、修改、删除、查询”等等情况的O/R映射,以及与“关系”相关的“级联操作”——“级联添加、级联修改、级联删除、级联查询”,一个好的O/R Mapping工具,要做出来真是千难万难
很多人都意识到了这一点!是的,问题的确存在。但是,为什么呢?该怎么办呢?几乎没有人会反思OO的不是,而是想当然的认为:“关系数据库技术太老了,跟不上OO技术的进步,因此,我们需要OODB这样的能够直接支持OO的数据库。”
“以其昏昏,使人昭昭”的事情,从来没有发生过。依着我前面的分析,在OO这样的基础薄弱的理论上,怎么可能搞出有实用价值的数据库呢?
在看到了徐昊的《关于面向对象设计与数据模型的精彩论述》之后,我相信自己找到了知音。他说:“OO在数据模型设计上不具有思维简洁性。”并且提出了一个重要的词汇:“边语义”!这使我相信,和我有类似想法的同志,是存在的。后来又现场听到了曹晓钢同志的《ORM时代的数据访问技术》的演讲,并且在他的笔记本里看到了他做的一些代码,居然与我的很多想法不谋而合!再加上后来与徐昊的几次MSN交流,终于使我敢于开始写这样一篇“OO丧钟”的文章,因为,我相信自己并不是孤独的反对者。

OO可以从关系型数据库那里借鉴些什么呢?
1、关系:也就是徐昊所说的边语义。在 OO中,对象与对象之间是否存在关系,在对象之外是不知道的。当一个对象被封装起来以后,他内部是否使用、关联、组合了其他的对象,是不可知的。因此,我们看到的通常的OO图,只能说是Object被剖开了以后的对象图。事实上,关系是被隐藏起来的。而在RDB中,关系非常明确的被定义与标识出来,一目了然。这将带来巨大的描述效果。相比起UML Class图,E-R要容易理解得多。
2、Primary Key:这是RDB 特有的概念,在OO中没有对应概念。因此,我们要判断两个对象是否相等,就相当困难。如果每个对象都有一个“一次设置,终身不变的Primary Key”,那么对象之间的比较语义,就能够被清楚的区分为:IS和LIKE。IS就是Primary Key相同的两个对象,他们应该完全一致,甚至在内存中,也只应该保存一份。LIKE,就是成员数据相同的两个对象,他们不是一个东西,仅仅是像而已。
3、SQL:这也是RDB特有的语言,而在OO的世界里,查找一个对象的工作,从来没有被规范过