天行健 君子当自强而不息

【ZT】敲响OO时代的丧钟(2)


重用为什么那么难?

程序员都是聪明人,没有谁愿意干重复劳动这样的傻事,因此,程序中出现重复代码是程序员的耻辱。就算不能消除重复代码,至少也可以对于相同的功能,用不同的代码来实现所以发明新轮子的程序员才会那么多。

面向对象作为一种横空出世的新技术,首先承诺的就是“更好的重用性”,而“重用性”这样一个闪闪发光的词,也的确能够吸引程序员的实现,那么多新的理论、新的技术、新的方法、新的框架、新的思想,用来说服别人接受的一个最大的理由,就是“更好的重用性”。然而,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的世界里,查找一个对象的工作,从来没有被规范过。
 



posted on 2007-10-01 14:00 lovedday 阅读(840) 评论(0)  编辑 收藏 引用 所属分类: ▲ Software Program


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


公告

导航

统计

常用链接

随笔分类(178)

3D游戏编程相关链接

搜索

最新评论