分类:
2006-09-19 13:48:10
面向对象软件开发和过程(四)重用 |
级别: 初级 林星, 项目经理 2003 年 12 月 01 日 重用是面向对象开发中的一个非常重要的特性,由于重用的特点,它能够降低开发投入,并提高软件的质量。那么,在面向对象开发中,究竟该如何掌握重用呢?又该如何将重用应用到开发过程中呢? 上一章中所讨论的分析框架是一种清晰的分析方法,但是接下来的内容中我们却不能够完全使用这个框架。一来内容较多,二来分析框架需要结合实际情况才有意义。所以在下面的讨论中,我们仍然会按照框架的思路来处理问题,但并不严格的描述所有的框架要素。 我们把面向对象中的问题分为以下一些主题,每个主题都体现出了面向对象技术的价值,我们会针对每一个主题进行框架分析,在后续的篇幅中,我们准备讨论四个主题:
对于我们来说,最重要的是要了解面向对象技术对一个软件开发过程有何帮助,所以,我们不是像一般的面向对象的教科书那样从继承、多态开始学习。我们的精力应该放在过程和设计上。 这四个主题涉及了大量的面向对象相关知识,它们都能够大幅度的提高软件的质量,同时降低人员之间的沟通成本: 重用和优化代码组织有很多的关联,例如,代码组织的优化能够帮助重用的实现,而重用的思路能够引导代码组织的方向。 针对契约设计为严谨的软件设计提供了一种可行的操作思路。例如,针对契约设计就有利于业务模型的质量。 业务建模是OOAD方法的核心,业务模型定义了软件的设计原型,它的好坏直接影响到软件的质量。 后续文章我将逐步分析这四个方面。 在面向对象中,重用是一种基本的思路。XP方法的最佳实践-重构的一个重要目标,就是使同样功能的代码只出现一次。这就是典型的实现重用,这种做法有两个好处,一是带来可重用的代码,二是减少维护的成本。 继承和泛型(或是模板)是两种很基本的重用技术。为了能够清楚的描述问题,首先我们要弄懂类型的概念: 1.1 类型(Type)
在 计算机看来,任何数据(甚至质量)都是一些bit流,但是要人类看懂这些bit流那就太为难了。因此类型的目的就是为了能够对bit流进行分类,告诉我们 这部分的字节流表示数字,这部分的字节流表示字符串。因此,就像上面那段话中所说的,在面向数值的编程中,类型通常用作数据的表示。在Java这样的强类 型语言中,在编译期,每一个变量和表达式都有一个类型与之相对应。可能有些人觉得不方便(例如VB程序员),但是这种机制能够有效的避免错误。所以我本人 比较喜欢强类型的语言,虽然有时候它并不是很方便,但是它能够使我避免许多错误。如果你开发一个企业应用,强类型的语言虽然速度上可能稍逊于弱类型语言, 但是强类型语言带来的严谨性是更重要的。 在面向对象中,类型除了用于表述值的含义之外,还包括了在这个值上的各种操作。所以呢,那句话中又说,在面向对象的世界中,类型更重要的是引出行为。在现实世界中,类型总是伴随着一定的特性的,所以仅靠数值类型是无法描述多姿多彩的现实世界的。 在OO 中,类可以是一个类型,在一些纯OO语言中,例如Eiffel、SmallTalk,类代表了全部,但是在一些为了向前兼容的OO语言中,类类型和非类的 类型是区分开来的,典型的代表是Java和C#这两种语言。在Java中,包括引用类型(类类型)和原生类型两大类类型,原生类型包括布尔值、数字值。在 C#中,类型也分为值类型和引用类型,为了统一的表示两者,还引入了Boxing和UnBoxing的操作。由于篇幅所限,我们不可能在这个话题上花费太 多的时间,如果要深入了解的话,可以参考Java的虚拟机规范。 在引入泛型机制之后,类可以接受各种各样的类型,那类是否还能够表示一个类型呢。这个问题等到我们讨论泛型机制的时候再谈。 1.2 继承 面向对象系统中最主要的元素就是类型,因此面向对象中的抽象的主要形式是类型抽象,也就是使用类型来表示现实生活中的各种事物的形式。 一 般我们认为继承可以分为两种基本的形式:实现继承和接口继承。实现继承的主要目标是代码重用,我们发现类B和类C存在同样的代码,因此我们设计了一个类 A,用于存放通用的代码,基于这种思路的继承称为实现继承。但接口继承不同,它是基于现实生活中的语义的,表现了IsA的关系。例如,我们认为存款帐户和 结算帐户都是帐户的子类,这种继承我们称之为接口继承。注意,有些文章中一个类实现一个接口的行为定义为接口继承,这和我们讨论的接口继承是不同的概念, 为了区分两种概念,我们可以使用接口继承的另一种称呼-类型继承。继承的关键就在于如何灵活的运用两种继承方式。 实 现继承是实现代码复用的关键技法,虽然接口继承也能够提供一定的代码复用,但是效果不如前者。但是,这决不意味着接口继承不如实现继承。相反,我认为接口 继承能够提供更优秀的重用性(这一点我们在下一篇中就能够看到),因为接口继承更为抽象,能够适用更大的范围。接口就是典型的例子,由于它什么代码都没 有,什么都不做,所以接口的抽象性是最好的。这时候我突然想到"言多必失"这句话,软件设计是不是很有一些哲学的味道?但事实上,我们是两种继承方法同时 使用,以达到最佳的效果。 继承能够获得比委托(委托将会在下文提到)更大的灵活性,但是这种灵活 性并不是没有代价的。在一个软件团队中,继承的灵活性可能会带来代码的混乱或失控。Effective Java的条款14给我们的建议是组合(也就是我们指的委托)要优先于继承,一个重要的理由是继承破坏了封装性,因为子类需要了解父类的相关信息。 我 们做软件设计的思路有两种,如果我们希望客户端的调用简单一些,那么服务类就比较复杂,反之,如果希望服务类简单一些,那么客户端的调用就会复杂一些。继 承的语言也面临类似的问题。Java、C++都倾向于将继承的语法简单化,由编译器来负责继承语法的分析。但是这种做法会出现一些潜在的问题。例如,脆弱 基类(fragile base class)的问题。这个问题说的是当在基类中增加新的功能时,可能会对现存的类产生影响,当基类中增加一个虚方法时,而子类也存在一个同名的虚方法,那 么子类中的同名方法就会替换新加入的方法。这种做法是合法的,因此编译器是不会报错的,但这可能造成一些潜在的问题。出现这样的问题是我们在书写继承语法 时的自动化程度比较高,我们不需要对各个方法进行显示的指定,而是按照既定的规则进行。而Eiffel语言的做法则不同,Eiffel语言为继承定义了各 种各样的语法。在C#语言中,也有类似作用的关键字。 继承是面向对象中非常重要的技巧,而继承树的设计也特别的重要,在一些单根继承的语言中更是如此,因为父类只有一个,不能够轻易的使用继承。一般来说,继承树在一个设计决策中占有非常重要的位置,是需要重点考虑的。这里是设计的重点。 继 承本身并不是什么特别难的技巧,但是要能够把继承运用的好却是很难的。在一个软件开发团队中,不同的成员有着不同的背景,不同的知识,对继承、面向对象也 有不同的看法,如何协调这么多的要素,以保证设计的一致性呢?面向对象的老手和新手的最大区别,往往就表现在继承的处理上。在软件过程中,我比较提倡由架 构师或老鸟级的程序员来负责主要的继承层次的设计。这样,软件的结构不容易乱,千万不能够象以前的非面向对象代码那样做一刀切,把不同模块交给不同人了 事,这样最后代码一定失控。 继承的设计往往是比较难以进行测试的。有其是那些为了扩展的目的设计 的类,因为父类中的代码往往只是最终功能的一部分。这里有一个测试的思路,如果在你的代码中还必须实现子类,那么针对子类进行测试。如果你的代码中不实现 子类,那么请设计一个测试子类,并对测试子类进行测试。 1.3 泛型 如 果说继承实现了子类型的多态的话,那么泛型则是实现了参数的多态,两者都是抽象机制的重要组成部分,两者能够在一定程度上相互替代,因此一种观点认为泛型 是能够在很大程度上替代继承,在C++的标准模板库中,泛型就被大量的使用。但泛型的思路和继承的思路仍有差别,继承往往代表了不同的算法,但泛型往往代 表了相同的算法,不同的类型。这也是为什么STL中大量使用泛型的原因,因为算法是类似的。泛型对我们最大的好处是引入了静态(编译期)类型检查。回想我 们Java语言中的很多容器都是用于容纳Object类型的,其目的是为了让容器更加的通用,但是导致了一个问题,编译器不会检查你放入容器的到底是一个 什么东西,比如我们把人放到存放货物的容器中,虽然违反了现实世界中的规则,但编译器仍然放行。
多么恶劣的行 为,容器中装的是货物,可出来的却是人,有贩卖人口的嫌疑吧,但对编译器来说完全合法,而在运行期间会出现问题,因为类型转换是非法的。这种方法能够得到 大量运用的关键技术几乎所有的对象都是继承自一个根对象(例如Object)。这在一定程度上实现了泛型,但这种行为不值得提倡。 这 时候,类型的不安全性对软件质量造成了影响。使用泛型就不会出现这样的尴尬局面,编译器会帮助你检查类型,保证类型安全。泛型的另一个好处是减少了类型转 换。从某个角度上来说,类型转换是一种非安全的编程方法,在现实生活中很少看到这种现象(基因突变),但是在编程语言中,我们大量使用这种技巧,更多时候 是被迫的。 Java中的泛型机制 在名为"猛虎"的J2SE1.5中,Java引入了新的泛型机制(Java在1.4版本中就引入了泛型机制)。Java是否该引入泛型机制一直都是争论的焦点,毕竟,泛型机制是一种非常优秀的抽象方法。引入了泛型机制的Java语言看起来像是这样的:
从Java核心 团队的两名成员-Joshua Bloch和Neal Gafter的谈话(http: //developer.java.sun.com/developer/community/chat/JavaLive/2003/jl0729.html) 来看,Tiger只是对泛型机制做了一些编译器上的处理,例如编译器帮助你检查类型安全,从集合返回值时无需对类型进行转化。不管如何,对程序员来讲,这 就足够了,不是吗? 泛型解决了我们的一大问题,我们可以定义更加严格、自然的类型处理机制。而不 是把类型转换来转换去。在软件开发过程中,尽可能引入泛型机制来解决现实中的问题。在业务建模中,利用泛型机制来描述需要,能够更加准确的解决问题。例 如,当我们对书店中货架建模的时候,我们规定书架上只能够存放书和CD之类的产品。因此,我们使用下面的方法:
以上的代码 是使用Eiffel语言编写,SHELF类只能够存放SHELF_ITEM类型的物品。因此其子类BOOK、CD都是合法的,但是其它的物品,例如生肉, 那就是非法的。试想如果我们仍然使用类型转换的方式的话,那么我们的SHELF又变成了百宝箱了,除了能够存放书和CD,还能够存放钢琴、演员、核武器。 哈!真是太牛了。 现代的语言都将引入或将要引入泛型机制,看来泛型机制成为通用技术的日子已经不远了。 继承和泛型是两种方向上的重用技术,继承提供的是类型的纵向扩展,而泛型提供的是类型的横向抽象。两种技术都能够提供优秀的重用性。而对于我们来说,关键的问题仍然在于,如何在开发过程中引入这些技术。
为重用定义规范是一件困难的事情。很难定义一个规范,要求开发人员必须按照某种方式来设计继承树或泛型类。但是,继承和泛型保持统一的风格是较好的处理方式。所以,合适的做法是采用指南和范例的形式。
不论是继承还是泛型,都要求有丰富的面向对象的编码和设计经验。重用的目标对设计师和程序员的要求很高,如果组织中的人员没有能够达到这种要求,那么就需要考虑在人员技能上进行强化。 对于泛型来说,学习泛型机制意味着原先处理集合的思路发生了变化,需要在一个新的抽象高度上理解集合操作。
重 用涉及到设计的问题。所以对于组织来说,问题在于,谁负责设计。正如我们在技能这一节中看到的,重用对技术的要求很高,我们无法要求组织的任何一个人都达 到这种要求,所以,较好的方式是,把重用技术的职责交给合适的设计人员,由他们负责对软件的整体结构进行重用上的设计。 重用的最终目标是能够在软件组织范围内建立起一个框架,软件组织能够利用这个框架降低开发成本,提高开发质量。
我 们之前讨论继承时,曾经提到,由架构师或老鸟级的程序员来负责主要的继承层次的设计。重用最能够发挥效用的地方是在设计的时候考虑重用。所以这就需要过程 上的保证了,在设计活动中,如果需要使用到继承或是泛型技术,那么就需要有相应的设计、测试方案、复审、文档化的过程,并确保设计思路能够顺利的形成代 码。形成代码的思路有两种,一种是由程序员根据设计编写代码,另一种是设计人员自己编写实现代码。我比较倾向于第二种思路。第一种思路会产生很多额外的沟 通成本,造成成本的浪费,影响代码的质量。
Java语言能够优雅的支持继承和泛型,大部分的面向对象语言都能够支持继承,在未来,泛型也将成为标准的技术。 C+ +中将泛型实现为模板的方式,在软件开发中,模板的运用是一个非常巧妙的方法。在企业应用程序中,数据库的CRUD方法是非常常用的,但是不同表、不同目 的的CRUD方法都有少许的不同。如果使用继承或委托等方式来进行代码级别的重用,会造成代码的意图不清晰,令人难以理解。所以,这些方式难以得到好的效 果,但是我们同时又应该看到,不同的代码块之间的重复程度也是很高的。所以,我们想到能不能象泛型机制那样,对代码本身进行抽象呢?例如,一个标准的 Create方法中,方法的流程基本相同,但是使用的值对象、表名、连接等都有所不同,我们把这些看作参数,将嵌入参数的代码制作为模板,并提供这些参数 的使用说明和范例。在使用的时候,只要用具体的值替换相应的参数就可以了。很多的Case工具中都支持模板。这种方法要比你写大量的指导性文档要好的多, 原因在于它的操作性很强,程序员很容易接受。 |