1
 软件工程
1.9.3.1 7.3.1 面向对象编程

7.3.1 面向对象编程

1.面向对象语言的特征

面向对象语言(Object-Oriented Language)是一类以对象作为基本程序结构单位的程序设计语言,用于描述的设计是以对象为核心,而对象是程序运行时的基本成分。面向对象语言中提供了类、继承等成分。

面向对象语言借鉴了20世纪50年代的人工智能语言Lisp引入的动态绑定和交互式开发环境的思想,始于20世纪60年代的离散事件模拟语言Simula67引入的类的概念和继承,以及成形于20世纪70年代的Smalltalk。面向对象语言的发展有两个方向:一种是纯面向对象语言,如Smalltalk、EIFFEL、C#和Java等;另一种是混合型面向对象语言,即在过程式语言及其他语言中加入类、继承等成分,如C++、Delphi和Objective-C等。这些语言尽管在程序和方法上存在一定的差异,但是它们都支持诸如对象、类、继承等面向对象的核心概念,并在不同程度上支持以下一些重要的特性。

1)多重继承

C++、Java等许多面向对象语言支持多重继承,但是也有一些其他面向对象语言并不支持多重继承。支持多重继承的语言能把设计直接转换成实现。但是需要注意的是,多重继承机制引入可能会在属性和操作名之间导致名字的冲突。

2)封装

不同的语言对封装的支持程度存在一定的差异。当代码允许一个类直接访问另一个类的属性时,继承使用不当就可能会导致违反封装原则的事情发生。

3)重用性

面向对象程序中的软件构件是用类来实现的,它具有很高的独立性,为软件的重用提供了良好的基础和保证。

4)类库

大多数面向对象语言包含一个可使用的通用类库,它可以被程序员直接使用,或通过某些子类创建所定制的特殊需要的类库。类库的可用性意味着许多类库中的组件不需要程序员重新实现。

类库中往往包含实现通用数据结构(如动态数组、表、队列、栈、树等)的类,通常把这些类称为包含类。在类库中还可以找到实现各种关联的类。

更完整的类库通常还提供独立于具体设备的接口类(如输入/输出流),此外,用于实现窗口系统的用户界面类也非常有用,它们构成一个相对独立的图形库。

5)强类型语言和弱类型语言

强类型语言要求每个变量和属性都必须精确地属于某一个特定的类。弱类型语言则仅将变量和属性看做广义的对象。例如,C++语言是强类型语言,Smalltalk语言是弱类型语言。

强类型语言主要有两个优点:一是有利于在编译时发现程序错误,二是增加了优化的可能性。通常使用强类型编译型语言开发软件产品,使用弱类型解释型语言快速开发原型。总的来说,强类型语言有助于提高软件的可靠性和运行效率。现代的程序语言理论支持强类型检查,大多数新语言都是强类型的。

6)内存管理

所有面向对象语言都允许用户动态创建对象,并且可以用指针引用动态创建的对象。允许动态创建对象,就意味着系统必须处理内存管理问题,如果不及时释放不再需要的对象所占用的内存,动态存储分配就有可能耗尽内存。

有两种管理内存的方法,一种是由语言的运行机制自动管理内存,即提供自动回收“垃圾”的机制;另一种是由程序员编写释放内存的代码。自动管理内存不仅方便而且安全,但是必须采用先进的垃圾收集算法才能减少开销。某些面向对象的语言(如C++)允许程序员自己定义析构函数(Destructor)。每当一个对象超出范围或被显式删除时,就自动调用析构函数。这种机制使得程序员能够方便地构造和唤醒释放内存的操作,却又不是垃圾收集机制。

7)打包

大多数面向对象语言都缺少类与类之间控制可见性的分区机制,为此,可在分析阶段使用包含类模块的结构机制。在Ada语言中,包构造提供了把一个系统的结构分割成具有它们自己名字空间独立组建的手段。一个系统嵌套打包组件的能力可以对可见性形成较好的控制。

8)元数据

元数据是描述有关数据的数据。在运行程序时允许合理的应用出现的元数据,甚至允许改变对象支持的操作、属性的拥有或属性类型的结构和能力等。Smalltalk语言包含有关类的元数据。

9)可视化开发环境

目前,许多面向对象语言提供了功能强大、使用方便的可视化集成开发环境,它不仅大大提高了软件开发的效率,而且有效地减少了错误,提高了软件的质量。开发环境至少应该包含下列一些最基本的软件工具:编译程序、编译程序或解释程序、浏览工具、调试器(Debugger)等。

编译程序或解释程序是最基本、最重要的软件工具。编译与解释的差别主要是速度和效率的不同。利用解释程序解释执行用户的源程序,虽然速度慢、效率低,但却可以更方便、更灵活地进行调试。编译型语言适合于开发正式的软件产品,优化工作做得好的编译程序能生成效率很高的目标代码。有些面向对象语言(如Objective-C)除提供编译程序外,还提供一个解释工具,从而给用户带来很大方便。

当开发大型系统时,需要有系统构造工具和变动控制工具,因此应该考虑语言本身是否提供了这种工具,或者该语言能否与现有的这类工具很好地集成起来。经验表明,传统的系统构造工具(如UNIX的Make)目前对许多应用系统来说都已经太原始了。

程序设计语言是人与计算机进行交流的重要工具,其特性必然会影响人们的思维和解决问题的方式,也会影响人与计算机通信过程的质量和效率。因此,选择一种合适的程序设计语言是面向对象开发过程中一项非常重要的工作。

2.面向对象语言的选择

在使用面向对象的软件开发过程中,面向对象语言明显优于非面向对象语言。因此,除了在很特殊的应用领域,开发人员一般应该选择面向对象的程序设计语言,但是,目前面向对象的程序设计语言种类繁多,究竟应该选择何种语言才能更有利于系统的开发和维护呢?在充分考虑到程序设计语言特点(如应用领域、算法与计算的复杂性、数据结构的复杂性和效率等)的同时,还应该着重考虑以下一些实际因素。

1)未来能否占主导地位

在若干年以后,哪种面向对象的程序设计语言将占主导地位呢?为了使自己的产品在若干年后仍然具有很强的生命力,人们可能希望采用将来占主导地位的语言来编程。

根据目前占有的市场份额,以及专业书刊和学术会议上所做的分析和评价,人们往往能够对未来哪种面向对象语言将占据主导地位做出预测。但是,最终决定选择哪种面向对象语言的实际因素,往往是诸如成本之类的经济因素,而不是技术因素。

2)可重用性

采用面向对象方法开发软件的基本目的,是通过重用来提高软件质量和软件生产率,增强系统的可维护性。面向对象语言的主要优点是能够最完整、最准确地表达问题域语义,因此在开发系统时,应该优先选择面向对象语言。

3)类库和开发环境

语言、开发环境和类库是决定可重用性的三个因素。可重用性除了依赖于面向对象程序语言本身以外,同时还依赖于开发环境优劣和类库内容的丰富程度。只有语言、开发环境和类库这三个因素综合起来,才能共同决定可重用性。

考查程序语言不但应该考查是否提供了类库,更重要的是考查类库中提供了哪些有价值的类。类库的日益成熟和丰富,会给开发应用系统带来很大的方便,需要开发人员自己编写的代码将越来越少,以致会有事半功倍或更高的效率。

为便于积累可重用的类和重用已有的类,在开发环境中,除了提供前述的基本软件工具外,还应该提供使用方便的类库编辑工具和类库浏览工具,其中类库浏览工具应该具有强大的联想功能。

4)其他因素

在选择编程语言时,应该考虑的其他因素还有:对用户学习面向对象分析、设计和编码技术所能提供的培训服务;在使用这个面向对象语言期间所能提供的技术支持;能提供给开发人员使用的开发工具、开发平台、发行平台;对计算机性能和内存的需求;集成已有软件的容易程度等。

5)几种常见的面向对象程序设计语言

目前,有不少的面向对象程序设计语言,这里主要介绍以下几种。

(1)C++。

算法研究、数据计算、各种底层系统等传统领域中,C++被广泛采用,尤其在Unix类计算机上。由于编程资源非常集中,以致很难不选择C++。C++有统一的标准,各种硬件平台都有其编译器。理论上,C++能做任何事情。C++有强大的类型定义能力,如无所不包的对象模型、算符重载、模版、宏,可以对自己做扩充和定义;另一方面,也导致C++异常复杂、难以维护,且编译速度很慢。在新兴领域中,C++的处境就比较艰难,没有统一的高层工具库,而且工作量很大。

(2)Delphi。

确切地说,Delphi就是Object Pascal。简单、直观而又强大是其最大的特点。不需要去花费过多心思考虑语言实现,想什么就写什么,而又不失C++的高效,甚至某些功能的执行速度比C++还快,如部分字符串操作和文件读/写缓冲等,编译速度快(由语言特性决定)。Delphi包含大量好的新语言特性,拥有既简洁又强大的运行库和对象库,直接集成COM、Corba、网络组件、数据库,支持Windows和Linux平台。虽然Delphi提供了大部分的系统API接口,但也有很多缺点。

(3)Smalltalk。

Smalltalk是第一个流行的、最具有代表性的面向对象程序设计语言,由Xerox DARC研究小组开发,并且它成功地产生了许多其他的面向对象语言。它将所有数据结构和控制结构表示为对象和消息,实现了集成化、交互式的程序设计环境,在扩充性和可重用性方面独具特色。Smalltalk语言系统所有方面均通过联机解释器和类浏览器来使用。Smalltalk语言的语法、句法和语义都比较简单,对象、类、消息和方法等少数几个概念就构成了Smalltalk的编程基础,比较容易学习和使用。Smalltalk所提供的高度交互的开发环境允许程序的快速开发,避免了传统的编译型程序语言的编辑-编译-连接周期的延迟。Smalltalk的另一个长处是提供了一个类库,这个类库可以扩充设计并随着应用的需要添加子类,因为Smalltalk是无类型语言,所以库的组件可以组合成快速原型来应用。

(4)Java。

Java由C++简化而来。Sun在Java的设计上有很大创新。Java由虚拟机执行Java程序,不依赖于平台。Java的口号是“一次编写,到处运行”,这意味着用Java编写的程序不用修改就可以在不同类型的计算机系统上运行。在Web开发方面,Java占据了主导地位。但由于Sun的一些失误,也使Java有了些不好的名声并导致Java没有达到预期的前景。一是Sun的虚拟机速度太慢,且不好的垃圾收集算法导致宝贵的内存资源被极度浪费,除非空闲物理内存大于程序所需全部内存,否则系统就会严重受到垃圾收集的影响,这个弊病遭到了强烈的抨击。二是糟糕的类库,这个问题到发布Java 1.2时才有所改善。三是Sun拒绝将Java交给标准局,做虚拟机需要Sun的授权。因此,Java现在集中在电子商务领域,由于其跨平台的能力,其地位基本上是不可替代的。

(5)C#。

C#即新品种的C++,被称为“C Sharp”(Sharp的中文译音为“夏普”),可以说迎合了大部分C++程序员的愿望,既保持了C++的强大功能又做了适度的简化,同时加入了流行的语言特性——基于.NET平台。由于C#出现相对较晚,可利用资源相对较少。C#是微软的主要开发工具,它是Java的劲敌。

在前面的教务信息管理系统的开发中,选择C#作为主要的程序设计语言,采用Asp.net技术,以Visual Studio 2008作为开发工具,有如下原因。

①完全面向对象的程序设计语言。

C#吸收了C++、Visual Basic、Delphi、Java等语言的优点,体现了当今最新的程序实现技术的功能和精华。C#继承了C语言的语法风格,同时又继承了C++的面向对象特性。

②强大的类库支持。

.NET平台为系统开发人员提供了一个统一的、面向对象的、分层的、可扩展的、庞大类库,能够帮助系统开发人员完成常见的编程任务。开发人员只要重用这些类库中的类,就能很快地实现功能,能够大大地提高开发系统的工作效率。

③跨平台性。

.NET在原理上具有跨平台的特性。所有的编程语言首先都编译成IL(软中间语言),然后IL通过CLR(公共语言运行库)的JIT(即时编译器)编译成本地字节码。IL本身与平台无关,能在装有CLR的任何一台计算机上运行。

④强大性和适应性。

因为Asp.net是基于通用语言的编译运行的程序,所以它的强大性和适应性可以使它运行在Web应用软件开发人员的几乎全部的平台上。通用语言的基本库、消息机制、数据接口的处理都能无缝地整合到Asp.net的Web应用中。

⑤简单易学。

Asp.net使运行一些很平常的任务变得非常简单,如表单的提交、客户端的身份验证、分布系统和网站配置。例如,Asp.net页面构架允许建立自己的用户分界面,使其不同于常见的VB-Like界面。

⑥世界级工具支持。

Asp.net可以用微软公司最新的产品Visual Studio.net开发环境进行开发,所见即所得的编辑方式让开发过程更简单、快捷。

3.面向对象程序设计的风格

程序是软件设计的自然结果,程序的质量主要取决于设计的质量。根据设计的要求选择了程序设计语言之后,编程风格在很大程度上影响着程序的可读性、可测试性和可维护性。保证程序质量的重要方法是要有良好的程序设计风格。对于面向对象实现来说,良好的程序设计风格也是非常重要的,它不仅能够减少系统维护或扩充所带来的系统开销,而且更有助于在新项目或工程中重用已有的程序代码。因此,良好的面向对象程序设计风格,既要遵循传统的结构化程序设计风格和设计准则,同时也要遵循为适应面向对象方法所特有的概念(如继承性)而必需的一些新的风格和准则。

1)提高可重用性

软件重用是提高软件开发生产率和目标系统质量的重要方法。因此,设计面向对象程序时,要尽量提高软件的可重用性。软件重用有多个层次,在编码阶段主要涉及代码重用问题。一般来说,代码重用有两种:一种是内部重用(本项目内的代码重用),另一种是外部重用(新项目重用旧项目的代码)。内部重用主要是找出设计中相同或相似的部分,然后利用继承机制共享它们;外部重用则必须反复精心设计。但是实现这两类重用的程序设计准则却是相同的,准则如下。

(1)提高方法的内聚,降低耦合。

一种方法应该只完成单个功能,如果某种方法涉及两种或多种不相关的功能,则应该把它分解成几种更小的方法。尽量不使用全局信息,尽量降低方法与外界的耦合程度。

(2)减小方法的规模。

如果某种方法的规模过大,则应该把它分解成几种更小的方法。一般每种方法的规模代码行数不应超过25行。

(3)保持方法的一致性。

这有助于实现代码重用。功能相似的方法应该有一致的名字、参数特征、返回值类型、使用条件及出错条件等。

(4)尽量做到全面覆盖。

如果输入条件的各种组合都可能出现,则应该针对所有组合写方法,而不能仅仅针对当前用到的组合情况写方法。另外,还应该考虑到一种方法不能只是处理正常值,也要处理空值、极限值及界外值等异常情况。

(5)分开采用策略方法和实现方法。

根据完成功能的不同,方法分为两种:一类是策略方法,这类方法负责做出决策,提供变元,管理全局资源;另一类是实现方法,这类方法只负责完成具体的操作,不做出任何决策,也不管理资源。为了提高可重用性,建议在编程时不要把策略和实现放在同一方法中,应该把算法的核心部分放在一个单独的具体实现方法中,从策略方法中提取具体参数,作为调用实现方法的变元。

2)使用继承机制

在面向对象程序中,使用继承机制是实现共享和提高重用程度的主要途径。具体准则有以下几种。

(1)调用子过程。

调用子过程就是把公共的代码分离出来,构成一个被其他方法调用的公用方法。可以在基类中定义这个公用方法,供导出类中的方法调用。

(2)分解因子。

有时提高相似类代码可重用性的一个有效途径是从不同类的相似方法中分解出不同的“因子”(不同的代码),把余下来的代码作为公用方法中的公共代码,把分解出的因子作为名字相同算法的不同方法,放在不同类中进行定义,并被这个公用方法调用。使用这种途径通常需要额外定义一个抽象基类,并在这个抽象基类中定义公用方法。把这种途径与面向对象语言提供多态性机制结合起来,让导出类继承抽象基类中定义的公用方法,可以明显降低为添加新子类而需付出的工作量,这是因为只需在新子类中编写其特有的代码即可。

(3)使用委托。

继承关系的存在意味着父类的所有方法和属性都应该适用于子类。如果继承机制使用不当,则会造成程序难以理解、修改和扩充。当对象之间逻辑上不存在一般-特殊关系时,为了重用已有的代码,可以利用委托机制。委托机制是把一类对象作为另一类对象的属性,从而在两类对象间建立组合关系。使用委托机制时,只有有意义的操作才委托另一类对象实现,因此,不会出现继承了无意义操作的问题。

(4)把代码封装在类中。

程序员往往希望重用其他方法编写的、解决同一类应用问题的程序代码。重用这类代码的一个比较安全的途径就是把被重用的代码封装在类中。例如,在开发一个数学分析应用系统的过程中,已知有现成的实现矩阵变换的商品软件包,程序员不想用C++语言重写这个算法,于是他可以定义一个矩阵类把这个商品软件包的功能封装在该类中。

3)提高可扩充性

用户的需求是容易发生变化的,时代也总在变化,设计实现手段也可能要变化升级,因此对于对象的理解、设计和实现有可能要进一步的完善。系统提供和实现的相关部件必须具有良好的可扩充性,让这种修改和变化比较容易实现,从而更好地满足系统的要求。以下的面向对象程序设计准则,将有助于提高系统的可扩充性。

(1)封装实现策略。

应该把类的实现策略(包括描述属性的数据结构、修改属性的算法等)封装起来,对外只提供公有的接口,否则将降低今后修改数据结构或算法的自由度。

(2)慎用公有方法。

方法根据所在位置的不同分为公有方法和私有方法。公有方法是向公众公布的接口,对这类方法的修改往往会涉及许多其他类,所以修改起来的代价比较高;私有方法是仅在类内使用的方法,通常利用私有方法来实现公有方法,修改私有方法所涉及的类少,所以代价比较低。为了提高可修改性,降低维护成本,应该精心选择和定义公有方法。

(3)控制方法的规模。

一种方法应该只包含对象模型中的有限内容。方法规模太大既不易理解,也不易修改扩充。一种方法表达一个功能上单一、独立的单元,代码规模一般在25行以内。

(4)合理利用多态性机制。

一般情况下,建议不要根据对象类型选择应有的行为,这样在增添新类时将不得不修改原有的代码,影响效率,不易扩充;可以利用多分支条件语句判断和测试对象的内部状态,合理利用多态性机制,根据对象的当前类型自动决定应有的行为。

4)提高稳健性

程序员在编写实现方法的代码时,既应该考虑效率,又应该考虑健壮性。对于任何一个软件来说,健壮性都是不可忽略的质量指标,为提高健壮性应当遵循以下几条准则。

(1)预防用户的错误操作。

软件系统必须具有处理用户操作错误的能力。当用户在输入数据时发生错误,不应该造成程序运行中断,更不应该造成“死机”。任何一种接收用户输入数据的方法,对其接收的数据必须进行检查,即使发现了非常严重的错误,也应该给出适当的提示信息,并准备再次接收用户的输入。

(2)检查参数的合法性。

对于公有方法,尤其应该着重检查其参数的合法性,因为用户在使用公用方法时可能违背参数的约束条件。

(3)不要预先确定限制条件。

在设计阶段,往往很难准确地预测到应用系统中使用的数据结构的最大容量需求,因此,不应该预先设定限制条件。必要时,应使用动态内存分配机制,创建未预先设定限制条件的数据结构。

(4)先测试后优化。

为在效率和健壮性之间作出合理的折中,应该在为提高效率而进行优化之前,先调试程序。应仔细研究应用程序的特点,以确定哪些部分需要着重测试。例如,最坏情况出现的次数及处理时间可能需要着重测试。经过测试得知,合理地提高性能是着重优化的关键部分。如果实现某个操作的算法有很多种,则应综合考虑内存需求、速度及实现的难易程度等因素,经过合理折中后再选定适当的算法。