zoukankan      html  css  js  c++  java
  • [转] 面向对象软件开发和过程(四)重用

    重用是面向对象开发中的一个非常重要的特性,由于重用的特点,它能够降低开发投入,并提高软件的质量。那么,在面向对象开发中,究竟该如何掌握重用呢?又该如何将重用应用到开发过程中呢?

    上一章中所讨论的分析框架是一种清晰的分析方法,但是接下来的内容中我们却不能够完全使用这个框架。一来内容较多,二来分析框架需要结合实际情况才有意义。所以在下面的讨论中,我们仍然会按照框架的思路来处理问题,但并不严格的描述所有的框架要素。

    我们把面向对象中的问题分为以下一些主题,每个主题都体现出了面向对象技术的价值,我们会针对每一个主题进行框架分析,在后续的篇幅中,我们准备讨论四个主题:

    • 重用
    • 优化代码组织
    • 针对契约设计
    • 业务建模

    对于我们来说,最重要的是要了解面向对象技术对一个软件开发过程有何帮助,所以,我们不是像一般的面向对象的教科书那样从继承、多态开始学习。我们的精力应该放在过程和设计上。

    这四个主题涉及了大量的面向对象相关知识,它们都能够大幅度的提高软件的质量,同时降低人员之间的沟通成本:

    重用和优化代码组织有很多的关联,例如,代码组织的优化能够帮助重用的实现,而重用的思路能够引导代码组织的方向。

    针对契约设计为严谨的软件设计提供了一种可行的操作思路。例如,针对契约设计就有利于业务模型的质量。

    业务建模是OOAD方法的核心,业务模型定义了软件的设计原型,它的好坏直接影响到软件的质量。

    后续文章我将逐步分析这四个方面。

    1. 重用的概念。

    在面向对象中,重用是一种基本的思路。XP方法的最佳实践-重构的一个重要目标,就是使同样功能的代码只出现一次。这就是典型的实现重用,这种做法有两个好处,一是带来可重用的代码,二是减少维护的成本。

    继承和泛型(或是模板)是两种很基本的重用技术。为了能够清楚的描述问题,首先我们要弄懂类型的概念:

    1.1 类型(Type)

    The term type is often used in the world of value-oriented programming to mean data representation. In the
                object-oriented world it usually refers to behavior rather than to representation.

     
    在计算机看来,任何数据(甚至质量)都是一些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类型的,其目的是为了让容器更加的通用,但是导致了一个问题,编译器不会检查你放入容器的到底是一个什么东西,比如我们把人放到存放货物的容器中,虽然违反了现实世界中的规则,但编译器仍然放行。

    Stack s = new Stack();
    s.Push(
    new Product());
    Employee e 
    = (Employee) s.Pop();



    多么恶劣的行为,容器中装的是货物,可出来的却是人,有贩卖人口的嫌疑吧,但对编译器来说完全合法,而在运行期间会出现问题,因为类型转换是非法的。这种方法能够得到大量运用的关键技术几乎所有的对象都是继承自一个根对象(例如Object)。这在一定程度上实现了泛型,但这种行为不值得提倡。

    这时候,类型的不安全性对软件质量造成了影响。使用泛型就不会出现这样的尴尬局面,编译器会帮助你检查类型,保证类型安全。泛型的另一个好处是减少了类型转换。从某个角度上来说,类型转换是一种非安全的编程方法,在现实生活中很少看到这种现象(基因突变),但是在编程语言中,我们大量使用这种技巧,更多时候是被迫的。

    Java中的泛型机制

    在名为"猛虎"的J2SE1.5中,Java引入了新的泛型机制(Java在1.4版本中就引入了泛型机制)。Java是否该引入泛型机制一直都是争论的焦点,毕竟,泛型机制是一种非常优秀的抽象方法。引入了泛型机制的Java语言看起来像是这样的:

    static void someAction(Collection<String> c){
        
    for (Iterator<String> I=c.iterator();i.hasNext();)
            doSomeAction();
    }



    从Java核心团队的两名成员-Joshua Bloch和Neal Gafter的谈话(http://developer.java.sun.com/developer/community/chat/JavaLive/2003/jl0729.html)来看,Tiger只是对泛型机制做了一些编译器上的处理,例如编译器帮助你检查类型安全,从集合返回值时无需对类型进行转化。不管如何,对程序员来讲,这就足够了,不是吗?

    泛型解决了我们的一大问题,我们可以定义更加严格、自然的类型处理机制。而不是把类型转换来转换去。在软件开发过程中,尽可能引入泛型机制来解决现实中的问题。在业务建模中,利用泛型机制来描述需要,能够更加准确的解决问题。例如,当我们对书店中货架建模的时候,我们规定书架上只能够存放书和CD之类的产品。因此,我们使用下面的方法:

    class SHELF[I->SHELF_ITEM]
    feature
     put(item:I) is
     …
     end
     book_shelf:SHELF[BOOK]
     CD_shelf:SHELF[CD]



    以上的代码是使用Eiffel语言编写,SHELF类只能够存放SHELF_ITEM类型的物品。因此其子类BOOK、CD都是合法的,但是其它的物品,例如生肉,那就是非法的。试想如果我们仍然使用类型转换的方式的话,那么我们的SHELF又变成了百宝箱了,除了能够存放书和CD,还能够存放钢琴、演员、核武器。哈!真是太牛了。

    现代的语言都将引入或将要引入泛型机制,看来泛型机制成为通用技术的日子已经不远了。

    继承和泛型是两种方向上的重用技术,继承提供的是类型的纵向扩展,而泛型提供的是类型的横向抽象。两种技术都能够提供优秀的重用性。而对于我们来说,关键的问题仍然在于,如何在开发过程中引入这些技术。




    2. 规范

    为重用定义规范是一件困难的事情。很难定义一个规范,要求开发人员必须按照某种方式来设计继承树或泛型类。但是,继承和泛型保持统一的风格是较好的处理方式。所以,合适的做法是采用指南和范例的形式。




    3. 技能

    不论是继承还是泛型,都要求有丰富的面向对象的编码和设计经验。重用的目标对设计师和程序员的要求很高,如果组织中的人员没有能够达到这种要求,那么就需要考虑在人员技能上进行强化。

    对于泛型来说,学习泛型机制意味着原先处理集合的思路发生了变化,需要在一个新的抽象高度上理解集合操作。




    4. 组织

    重用涉及到设计的问题。所以对于组织来说,问题在于,谁负责设计。正如我们在技能这一节中看到的,重用对技术的要求很高,我们无法要求组织的任何一个人都达到这种要求,所以,较好的方式是,把重用技术的职责交给合适的设计人员,由他们负责对软件的整体结构进行重用上的设计。

    重用的最终目标是能够在软件组织范围内建立起一个框架,软件组织能够利用这个框架降低开发成本,提高开发质量。




    5. 过程

    我们之前讨论继承时,曾经提到,由架构师或老鸟级的程序员来负责主要的继承层次的设计。重用最能够发挥效用的地方是在设计的时候考虑重用。所以这就需要过程上的保证了,在设计活动中,如果需要使用到继承或是泛型技术,那么就需要有相应的设计、测试方案、复审、文档化的过程,并确保设计思路能够顺利的形成代码。形成代码的思路有两种,一种是由程序员根据设计编写代码,另一种是设计人员自己编写实现代码。我比较倾向于第二种思路。第一种思路会产生很多额外的沟通成本,造成成本的浪费,影响代码的质量。




    6. 工具

    Java语言能够优雅的支持继承和泛型,大部分的面向对象语言都能够支持继承,在未来,泛型也将成为标准的技术。

    C++中将泛型实现为模板的方式,在软件开发中,模板的运用是一个非常巧妙的方法。在企业应用程序中,数据库的CRUD方法是非常常用的,但是不同表、不同目的的CRUD方法都有少许的不同。如果使用继承或委托等方式来进行代码级别的重用,会造成代码的意图不清晰,令人难以理解。所以,这些方式难以得到好的效果,但是我们同时又应该看到,不同的代码块之间的重复程度也是很高的。所以,我们想到能不能象泛型机制那样,对代码本身进行抽象呢?例如,一个标准的Create方法中,方法的流程基本相同,但是使用的值对象、表名、连接等都有所不同,我们把这些看作参数,将嵌入参数的代码制作为模板,并提供这些参数的使用说明和范例。在使用的时候,只要用具体的值替换相应的参数就可以了。很多的Case工具中都支持模板。这种方法要比你写大量的指导性文档要好的多,原因在于它的操作性很强,程序员很容易接受。



  • 相关阅读:
    开源围棋A.I. FoolGo
    再写围棋的MC模拟
    棋串的数据结构
    一种Lua到C的封装
    用vim写ios程序
    一种C函数到Lua的封装
    Unix命令
    RSA java rsa加密,解密,生成密钥对工具类
    UOS 开启远程登录
    UOS 设置 java 程序开机启动
  • 原文地址:https://www.cnblogs.com/luqingfei/p/857836.html
Copyright © 2011-2022 走看看