天天看点

《思考OO》在蔡先生的微信(JerryTsai1218)里面看到的,觉得非常好,不敢独享,稍整格式,与大家分享。《思考OO》

在蔡先生的微信(JerryTsai1218)里面看到的,觉得非常好,不敢独享,稍整格式,与大家分享。

《思考OO》

十多年前在读大学时,我对于OO(Object-Orientation,物件导向)兴致正浓,看了不少OO的书,有外文书(例如Grady Booch、Bertrand Meyer),也有中文书。其中,中文书为了帮助读者理解,都会用现实生活中的物件做比拟,比方说:哺乳动物、交通工具,我记得我读过的一个范例中提到:「斑马」继承自「马」。

 【学习OO的重点】当时我在工研院当实习生,老板要我报告OO,我于是拿了书上的例子当解说,当老板听到我宣称「斑马继承自马」时,他开玩笑地说:「那么马子(女朋友)应该也是继承自马」。我当时深受羞辱,感觉被IT中文书荼毒了。

OO的三大基础是封装、继承、多型。用现实生活的物件做OO解说上的比拟,通常不会太恰当,因为只能解释封装和继承,却无法解释多型。而多型却是OO真正的重点,也是学习OO的门槛。没有解释多型,就等于小学而大遗。对于OO,比较恰当的例子是「形状」,一来容易理解,二来适合同时解说封装、继承、多型。我认为OO的书不用看太多,只要看Bertrand Meyer的名著OOSC第二版就够了,但这本书可不薄。前面提到,物件导向的三大基础是封装、继承、多型。你会在特定OO语言上看到一些其他机制,例如Template,RTTI(Run-Time Type Information),但这些都不是重点。学习OO的时候,焦点应该放在封装、继承、多型这三方面。这三者是有次序性的,没有封装就不可能有继承、没有继承就不可能有多型。只支援封装的语言称为Object-Based语言(例如传统的Visual Basic),同时支援封装、继承、多型的语言才能称为OO语言(例如.NET时代的Visual Basic)。有没有可能,存在某个语言只支援封装和继承,却不支援多型?不会有语言这么无聊,基本上继承往往只是一个中间过程,真正的目的是多型。既然支援了继承,却不支援多型,这是没有意义的。 

【封装】

封装(encapsulation)的目的是要将程式码切割成许多模组(module),每个模组之间的关连性降到最低,这么一来比较不会产生「牵一发而动全身」的状况,降低相互依赖的程度,也等于是降低复杂度,可以让开发与维护更容易。事实上,没有人用「模组」一词来称呼封装的结果,而是称为「类别」,把模组一词做更高阶的包装用途。因此我们现在应该将「类别」视为封装的结果,把「模组」视为整个程式切割出来的许多片段。而在OO的世界,一般来说,一个程式有多个模组,一个模组内包含多个类别。模组的概念不是OO独具的,许多非OO语言也具有模组,但是OO的语言几乎都具备模组,例如Java的Package;D语言的模组;而.NET更是细分成组件( assembly)和模组,其实.NET的组件与模组都具备一般模组的概念,但程度有别(组件包含模组)。封装是以资料为核心,将 ​​相关的资料放在一起,将会用到这些资料的函式也放进来。封装等于是将资料和函式放在一起。尽管有的语言还有其它的东西,例如event、property,但是从内部来看,这些都是函式的变形。为了和非OO的世界做出区隔,OO也做了一些名词上的改变,将Function(函式)改称为Method(方法)、将Call(呼叫)改称为Invoke(调用)。但是新旧词汇基本上还是通用的。 

【能见度】

封装的目的既然是要「降低互相依赖的程度」,就牵涉到能见度的问题:这个「类别/方法/栏位」该不该暴露给别的模组、同一个模组的不同类别、自己的「次类别」、友伴类别(Friend Class)、内部类别(Inner Class)?这就是所谓的「能见度」(visibility)。我们当然希望尽可能降低能见度,这才能「降低互相依赖的程度」。也就是,别人不需要知道的,就不要让它知道,这就是所谓的「资讯隐藏」(information hiding)。最该被隐藏的是资料。极致的封装主义者,主张所有的资料一定都不可以直接被外部(包括次类别)存取。上面提到,封装将相关的资料和使用到这些资料的方法包成类别。最理想的状况是,让资料的能见度为最低,外面完全看不见。留下的对外介面(Interface)只剩下method。换句话说,每个物件的Interface是一些方法的集合,完全没有资料。设定能见度不是一件容易的事,往往需要深思熟虑。特别是对于设计「框架」(framework)的人来说,能见度设定得太宽,造成资讯隐藏效果不佳,可能会带来相当多负面的效果(例如复杂度提高、程式容易出错、非thread- safe…等);能见度设定得太紧,造成效率变差、扩充程度变差(有些设计因而做不出来)。 

【继承】

被继承的对象称为基底类别(base)或超类别(super)或亲类别(parent),继承者称为衍生类别(derived)或次类别(sub-)或子类别(child)。继承的目的,是要达到「程式码再用」(Code Reuse)或「介面再用」。而继承的手段,就是「扩充」或「修改」。这是继承的重点,请务必牢记。继承所导致的程式码再用,是指次类别能自动沿袭超类别的所有程式码,好让你可以不用写太多程式码,只需要稍微扩充或修改,就能符合你的需求。「扩充」指的是定义新的方法(Method),修改指的是「针对超类别中的某方法重新定义其行为」。请注意,继承所产生的次类别,和其超类别之间,两者在记忆体内是独立的。继承所做的扩充与修改,并不会影响到超类别。在Windows程式设计中,有所谓的SubClassing技巧,其实并不是继承的概念,因为它会修改到原本类别的记忆体。继承所导致的介面再用,是在为OO的下一个阶段(也就是多型)作准备。介面再用,搭配方法的修改,就形成了多型。如果你不想再用程式码,也不想再用介面,或者说你不进行扩充、也不进行修改,那么透过继承产生次类别,几乎是没有意义的。唯一的一个小小的意义是,次类别和超类别两者是不同的类别,你可以在程式中依据这一点做判断,做不同的行为。但是这是一种琐细的程式技巧,和OO无关,而且OO也不鼓励你这么做。对OO来说,透过多型的机制造成行为的差异,才是正确的作法。但即使是为了此目的,我们也会使用空介面当作特殊标签(Mark),而不会使用类别当作标签,因为介面当标签的副作用小,成本低,且不是垂直的关系。将许多类别之间的继承关系,绘制出一张关系图,如果绘制的时候依循「超类别在上,次类别在下」,或者「超类别在左,次类别在右」,就可以形成一个类别阶层(Class Hierarchy)。由于大多数的类别阶层设计都是采用单一继承(Single Inheritance),而非多重继承(Multiple Inheritance),所以阶层图往往是树状结构,符合树状结构的阶层图,也称为继承树、类别树。 

【多重继承与介面】

单一继承指的是,只有一个超类别;多重继承指的是,具有多个超类别。应用框架设计几乎都是采用单一继承(例如MFC、.NET Framework、Borland VCL、AIR),只有极少数以前的设计会采用多重继承(例如Borland OWL)。不只是如此,连语言本身的设计上,也往往禁止多重继承(例如Java、Delphi、C#、VB.NET),只剩下极少数语言允许多重继承(例如C++、Eiffel)。这个趋势似乎会延续下去,主要是,多重继承「可能」会造成「不知继承的方法是来自那个超类别或祖先类别的困扰」。C++要求编程员要主动指明继承的方法来自何处,但Eiffel的作法则更巧妙(请参考http://www.eiffel.com/)。姑且不论多重继承的缺点,多重继承显然表达能力比单一继承更佳,至少,有不少原本在单一继承时必须透过AOP(Aspect-Oriented Programming)解决的问题,在多重继承之下可以轻易解决,不需要AOP。从Java开始,多数的语言使用Interface来解决多重继承的问题,它们号称『利用介面可以享用多重继承的优点,又没有多重继承的困扰』。但事实根本不是如此!介面只能让你继承到介面,无法继承到程式码(介面不带程式码)。因此,如果你在Java中继承多个介面,你必须亲自定义所有介面的每个方法,也就是说,你必须写许多程式码。但如果是在C++/Eiffel中,你可以继承许多类别,不需要再定义这些方法。所以介面是在「舍弃多重继承缺点的同时,也舍弃了多重继承的优点」。也就是说,介面舍弃了「程式码再用」,保留了「介面再用」。从这个角度来看,「介面再用」比「程式码再用」更重要。这是因为多型的缘故,多型才是OO的终极目的 

【其他和继承相关的问题】

继承某些程度上破坏了一部份的封装,造成次类别和超类别的相依程度提高。超类别如果改变,且次类别没有跟着做出改变,可能会造成次类别出问题。类似DLL Hell的观念。法律上有所谓的「限定继承」与「抛弃继承」,目前的编程语言似乎都没有这样的概念,就算有,权力也是放在超类别上,由超类别所控制,而不是在次类别上。设计继承时,必须先考虑介面是否共享,再考虑程式码是否共享,再考虑分类。但是经验不足的编程员,反倒会先考虑分类和程式码再用,而忽略了「介面再用」是其中最重要的事。 

【多型与虚拟】

  Polymorphism中文一般称为「多型」,早期也有人称为「同名异式」。我比叫喜欢前者,不喜欢后者。「多型」让人觉得物件可以以「多种面貌」出现,同名异式则太强调「不同的函式」。其实,「型别的不同」是因,而「函式的不同」是果。当一个物件具有不同的型别,就有可能会引发多型机制。一个物件为何为有不同的型别?这是因为继承而来(的)物件可以扮演所有祖先类别的角色。例如当某物件的类别是Sub,当此物件被「转型」成超类别Super之后,此物件就具有两种不同的类别,「实际类别」是Sub,「形式类别」是Super,此时呼叫此物件的方法m,会执行到的是Super定义的方法m?还是Sub定义(修改)的方法m?答案是实际类别的方法,也就是Sub定义的方法m。所以所谓的多型就是:不管形式类别是什么,一定会执行到实际类别的方法。你可能会觉得疑惑,为何当初要将物件转型为祖先类别,导致「形式类别」(宣告类别)和「实际类别」(定义类别)不一样?这个问题留待下一节时再回答。如果你的程式中,大量使用switch/case语法(大家去看看用形状来教学OO的例子,就知道为什么了,zeek添加),很有可能是你的设计不良,而没有好好地使用多型。你最好能「重构」(Refactoring)你的程式。类别的方法,可以分成虚拟(Virtual)与非虚拟两种。只有虚拟方法才能搭配多型机制使用。如果是非虚拟方法,则会执行到形式类别(而非实际类别)的方法,因为多型没有发挥作用。关于虚拟,每个语言有不同的作法。Java强调动态,所以预定是虚拟;C++注重效率,所以预定不是虚拟。 

【应用框架】

为了方便软体的开发,许多软体厂商都会提供应用框架(Application Framework),现今流行的框架相当多,例如:.NET Framework、Borland VCL、Java Class Library。1980年代OO开始兴起,1990年代框架开始兴起。有了框架,我们终于可以享受到OO的好处,重复利用别人写好的程式码,不用一切自己重头写。框架厂商先将一大部分的程式先写好,编程员只需要「利用继承来做修改」,就能套用整个框架,为了要让你修改的部分能够确实被执行到(而不是执行到框架本身的方法),所以这些允许修改的方法都是定义成虚拟的。因为编程员「利用继承来做修改」所以产生了次类别和重新定义的方法。框架比这个次类别更早被定义,当然不认识这个次类别,所以框架内都是以此次类别的祖先类别为「形式上」的处理对象(处理介面)。当此次类别物件被传入框架中,就会被自动转型成为祖先类别,因此产生「形式类别」和「实际类别」的差异。正因为这样的类别差异,加上次类别有重新定义方法,所以多型机制出现了。单一继承架构中,良好的框架设计(例如Java Swing)会将程式码不需要被修改的部分,设计成类别。至于需要被继承修改的部分,设计成介面(介面的方法全都是虚拟的),以及实践这些介面的类别。框架内的类别尽量只使用到这些介面。学习框架往往需要付出相当多心力,以Java Swing来说,就是一个相当复杂,不好学习的框架。框架设计上,近年来比较比较不一样的是,阶层有变深的趋势(例如AIR和WPF的框架);也就是说,继承树的叶节点到根节点之间的距离变大了。这样的好处是程式码重复利用度增加(所以框架档案的体积变小),介面重复利用度增加(学习速度可以加快)。 

【OO是生产力的最终解答?】

和物件导向程式设计关系紧密的是前一个阶段「设计」和下一个阶段「测试」。「设计模式」(Design Pattern)将许多好的设计整理出来,让我们设计功力大增。如果既有的设计不太好,你可以利用「重构」(Refactoring)的技巧来重新整理你的程式。现在讲求TDD(Test-Driven Development),对OO来说,「单元测试」(Unit Test)正是以类别为最小单元的。千万别忘了UML!设计OO系统的时候,UML可以整理你的想法,方便大家沟通,甚至当MDA(Model Driven Architecture)成熟之后,号称可以用UML把架构设计图画出来,用OCL(Object Constraint Language)描述一些规范,然后就可以产生出程式码了。OO太美好了!OO是软体开发的极致灵丹!OO真棒!我爱OO。…你醒醒吧!…尽管OO主宰现今的主流语言,OO对我们的开发效率似乎有一些提升,但我可没看过什么人用了OO之后就若有神助。更不用说OO还有学习门槛、各种OO框架的学习曲线、设计模式的学习曲线、过度工程化的问题。最近,我觉得真正可以达到更高生产力的关键在于更高阶的抽象,也就是DSL(Domain Specific Language)。尽管有的技术号称有支援DSL,依然有程度上的差异。关于DSL,我将另辟专文介绍。至于OO,在我认识到DSL的威力之后,已经被我打入冷宫了。因为当DSL发挥到极致的时候,OO似乎是派不上用场的。

继续阅读