一、面向对象开发方法概述
一般来说,软件开发都会经历以下声明周期:
- 软件分析:分析问题领域,了解用户的需求;
- 软件设计:确定软件的总体架构,把整个软件系统划分成大大小小的多个子系统,设计每个子系统的具体结构;
- 软件编码:用选定的编程语言来编写程序代码,实现在设计阶段勾画出的软件蓝图;
- 软件测试:测试软件是否能实现特定的功能,以及测试软件的运行性能;
- 软件部署:为用户安装软件系统,帮助用户正确地使用软件;
- 软件维护:修复软件中存在的Bug,当用户需求发生变化时(增加新的功能,或者修改已有功能的实现方式),修改相应的软件;
为了提高软件开发效率,降低软件开发成本,一个优良的软件系统应该具备以下特点:
- 可重用性:减少软件中的重复代码,避免重复编程;
- 可扩展性:当软件必须增加新的功能时,能够在现有系统结构的基础上,方便地创建新的子系统,不需要改变软件现有的结构,也不会影响已经存在的子系统;
- 可维护性:当用户需求发生变化时,只需要修改局部的子系统的少量程序代码,不会牵一发而动全身,修改软件系统中多个子系统的程序代码。
备注:这里多次使用了系统结构的说法,系统结构是指系统由多个子系统组成,以及子系统由多个更小的子系统组成的结构。
如何才能使软件系统具备以上特点呢?加入能把软件系统分解成多个小得子系统,每个子系统相对独立,把这些子系统像搭积木一样灵活地组装起来就构成了整个软件系统,这样的软件系统便能获得以上优良特性。软件中的子系统具有以下特点:
- 结构稳定性:软件在设计阶段,在把一个系统划分成更小的子系统时,设计合理,使得系统的结构比较健壮,能够适应用户变化的需求;
- 可扩展性:当软件必须增加新的功能时,可在现有子系统的基础上创建出新的子系统,该子系统继承了原有子系统的一些特性,并且还具有一些新的特性,从而提高软件的可重用性和可扩展性;
- 内聚性:每个子系统只完成特定的功能,不同子系统之间不会由功能的重叠。为了避免子系统之间功能的重叠,每个子系统的粒度在保持功能完整性的前提下都尽可能小,按这种方式构成的系统结构被称为精粒度系统结构。子系统的内聚性会提高软件的可重用性和可维护性。
- 可组合型:若干精粒度的子系统经过组合,就变成了大系统。子系统的可组合型会提高软件的可重用性和可维护性,并且能够简化软件的开发过程。
- 松耦合:子系统之间通过设计良好的接口进行通信,但是尽量保持相互独立,修改一个子系统,不会影响到其他子系统。当用户需求发生变化时,只需要修改特定子系统的实现方式,从而提高软件得到可维护性。
在精粒度系统结构中,大系统可以分解为多个松耦合的精粒度子系统。与此相反,在粗粒度系统结构中,粗粒度的大系统无法进一步分解,或者只能被分解为有限的几个粗粒度子系统。
目前在软件开发领域有两种主流的开发方法:结构化开发和面向对象开发。结构化开发是一种比较传统的开发方法,早期的高级编程语言,如Basic、C、Fortran和Pascal等,都是支持结构化开发的编程语言。随着软件开发技术的逐步发展,为了进一步提高软件的可重用性、可扩展性和可维护性,面向对象的编程语言及面向对象设计理论应运而生,Java语言就是一种纯面向对象的编程语言。
二、结构化的软件开发方法简介
结构化开发方法主要是按照功能来划分软件结构的,它把软件系统的功能看作是根据给定的输入数据,进行相应的运算,然后输出结果,如下图所示:
进行结构化设计时,首先考虑整个软件系统的功能,然后按照模块划分的一些基本原则(比如内聚性和松耦合)等,对功能进行分解,把整个软件系统划分成多个模块,每个模块实现特定的子功能。为了提高软件的内聚性,在模块中还会把功能分解到更小的子模块中。在完成所有的模块设计后,把这些模块拼装起来,就构成了整个软件系统。软件系统可看作是多个子系统的集合,每个子系统都是具有输入/输出功能的模块,如下图所示:
结构化设计属于自顶向下的设计,在设计阶段就不得不考虑如何实现系统的功能,因为分解功能的过程其实就是实现功能的过程。结构化设计的局限性在于不能灵活地适应用户不断变化的需求。当用户需求发生变化,比如要求修改现有软件功能的实现方式或者要求追加新的功能时,就需要自顶向下地修改模块的结构,有时候甚至整个软件系统的设计被完全推翻。
在进行结构化编程时,程序的主体是方法,方法是最小的功能模块,每个方法都是具有输入/输出功能的子系统,方法的输入数据来自于方法参数、全局变量和常量,方法的输出数据包括方法返回值,以及指针类型的方法参数。一组相关的方法组合成大的功能模块。
备注:结构化编程刚出现的时候,主要通过编写实现各种功能的函数(也称作过程)来实现,故也称为“面向过程编程”。
三、面向对象的软件开发方法简介
面向对象的开发方法把软件系统堪称是各种对象的集合,对象就是最小的系统,一组相关的对象能够组合成更复杂的子系统。面向对象的开发方法具有以下几个优点:
- 把软件系统看成是各种对象的集合,这更接近人类认识世界的自然思维方式。
- 软件需求的变动往往是功能的变动,而功能的执行者——对象一般不会有大的变化。这使得按照对象设计出来的系统结构比较稳定。
- 对象包括属性(数据)和行为(方法),对象把数据及方法的具体实现方式一起封装起来,这使得方法和与之相关的数据不再分离,提高了每个子系统的相对独立性,从而提高了软件的可维护性。
- 支持封装、抽象、继承、多态等各种特征,提高了软件的可重用性、可维护性、可扩展性。
备注:广义地讲,面向对象编程是结构化编程的一种改进实现方式。传统的面向过程的结构化编程的最小系统是功能模块,而面向对象编程的最小子系统就是对象。
(1) 对象模型
在面向对象的分析和设计阶段,致力于建立模拟问题领域的对象模型。建立对象模型既包括自底向上的抽象过程,也包括自顶向下的分析过程。
1) 自底向上的抽象
建立对象模型的第一步是从问题领域的陈述入手。分析需求的过程与对象模型的形成过程一致,开发人员与用户的交谈是从用户熟悉的问题领域中的事物(具体实例)开始的,这就使用户与开发人员之间有了共同语言,使得开发人员能彻底搞清用户需求,然后再建立正确的对象模型。开发人员需要进行以下自底向上的抽象思维:
- 把问题领域中的事物抽象为具有特定属性和行为的对象。比如一个模拟动物园的程序中,存在各种小动物:阿猫、阿狗等;
- 把具有相同属性和行为的对象抽象为类。比如尽管各种阿猫、阿狗等对象各自不同,但是它们具有相同的属性和行为,所以可以将各种小猫对象抽象为小猫类,而各种小狗对象抽象为小狗类;
- 当多个类之间存在一些共性(具有相同属性和行为)时,把这些共性抽象到父类中。比如小猫类和小狗类又可以进一步归于哺乳动物类。
在自底向上的抽象过程中,为使子类能更合理地继承父类的属性和行为,可能需要自顶向下的修该,从而使整个类体系更加合理。由于这种类体系的构造是从具体到抽象,再从抽象到具体的,符合人类的思维规律,因此能更快、更方便地完成任务。这与自顶向下的结构化开发方法构成鲜明的对照。在结构化开发中,构造系统模型是最困难的一步,因为自顶向下的“顶”(即系统功能)是一个空中楼阁,缺乏坚实稳定的基础,而且功能分解有相当大的任意性,因此需要开发人员有丰富的软件开发经验。而在面向对象建模中,这一工作可由一般开发人员较快地完成。
2) 自顶向下的分解
在建立对象模型的过程中,也包括自顶向下的分解。例如对于计算机系统,首先识别出主机对象、显示器对象、键盘对象和打印机对象等。接着对这些对象再进一步分解,例如主机对象由处理器对象、内存对象、硬盘对象和主板对象等组成。系统的进一步分解因有具体的对象为依据,所以分解过程比较明确,而且也相对容易。所以面向对象建模也具有自顶向下开发方法的优点,既能有效地控制系统的复杂性,又同时避免了结构化开发方法中功能分解的困难和不确定性。
(2) UML:可视化建模语言
面向对象的分析与设计方法,在诸多流派的思想和术语上有很多不同的提法,对术语和概念的运用也各不相同,统一是继续发展的必然趋势。需要用一种统一的符号来描述在软件分析和设计阶段勾画出来的对象模型,统一建模语言(Unified Modeling Language,UML)应运而生。UML是一种定义良好、易于表达、功能强大且普遍适用的可视化建模语言。它吸取了诸多流派的优点,而且有进一步的发展,最终成为大众所共同接受的标准建模语言。
(3) Rational Rose:可视化建模工具
Rational Rose是Rational公司开发的一种可视化建模工具,之后归属于IBM公司。它采用UML语言来构建对象模型,是分析和设计面向对象软件系统的强有力的工具。这里不做Rational Rose工具本身的用法介绍,如有需要可参考其他相关书籍资料。
备注:目前流行的UML绘制软件还有Visio、RSA和StarUML等,一些开发的IDE(集成开发环境)工具(如Eclipse等),也具有绘制UML图的功能模块插件。由于UML是一种标准的面向对象分析和设计的图形符号化标准,各个软件的绘制效果大同小异。
四、 面向对象开发中的核心思想和概念
在面向对象的软件开发过程中,开发者的主要任务是先建立模拟问题领域的对象模型,然后通过程序代码来实现对象模型。到底如何建立对象模型,如何用程序代码实现对象模型,并且能保证软件系统的可重用、可扩展、可维护性呢?这不是三两句话就能回答的问题。这里主要学习一下面向对象开发中的核心思想和概念,这些核心思想为从事面向对象的软件开发实践提供了理论武器。
1. 问题领域、对象、属性、状态、行为、方法、实现
问题领域,是指软件系统模型的真实世界中的系统。随着计算机技术的发展和普及、软件系统渗透到社会的各个方面,几乎可用来模拟任意一种问题领域,如学校、医院、商场、银行、电影摄制组、太阳系等。
对象是对问题领域中事物的抽象。对象具有以下特性:
(1) 万事万物皆为对象。问题领域中的实体和概念都可以抽象为对象。例如在学校领域,对象包括学生、成绩单、教师、课程、教室等;在银行领域,对象包括银行账户、出纳员、支票、汇率、现金、验钞机等;在商场领域,对象包括客户、商品、订单、发票、仓库、营业员等;在用Java语言成绩的图形用户界面中,窗口、滚动面板、按钮、列表、菜单、文本框等都是对象。
(2) 每个对象都是唯一的。对象的唯一性来自于真实世界中事物的唯一性。世界上不存在两片一模一样的叶子,因此在软件系统中用来模拟每片叶子的对象也具有唯一性。例如学校领域的学生小张、小王、小张的成绩单、小王的成绩单,这些都是唯一的对象。在Java虚拟机提供的运行时环境中,保证每个对象的唯一性的手段是为它在内存中分配唯一的地址。
备注:Java虚拟机是Java程序的解析器和执行器,它为Java程序提供运行时环境,并且执行程序代码。
(3) 对象具有属性和行为。例如小张,性别女,年龄15,身高1.6米,体重40kg,能够学习、唱歌、打羽毛球。小张的属性包括:姓名、年龄、性别、身高、体重。小张的行为包括:学习、唱歌、打羽毛球。
对象的行为包括具有的功能及具体的实现。在建立对象模型阶段,仅仅关注对象有什么样的功能,但是不考虑如何实现这些功能。对象的属性用成员变量来表示,对象的行为用成员方法来表示,如下图所示是学生的UML类图:
到了编写程序代码阶段,必须为所有的非抽象方法提供具体的实现,“实现”在程序代码中体现为方法后面带有的方法主体。
(4) 对象具有状态。状态是指某个瞬间对象的各个属性的取值。对象的某些行为往往会改变对象自身的状态,即属性的取值。例如小王本来体重80kg,经过减肥后,结果体重减到了45kg,如下图所示:
(5) 对象都属于某个类,每个对象都是某个类的实例。类是具有相同属性和行为的对象的集合。
同一个类的所有实例具有相同属性,表明它们的属性和含义相同,但是它们的状态不一定相同,也就是属性取值不一定相同。同一个类的所有实例包括类本身的所有实例,以及其子类的所有实例。类的所有实例具有相同行为,意味着它们具有一些相同的功能。类本身的所有实例按同样的方式实现相同的功能,而子类与父类之间,以及子类之间的实例则可能采用不同的方式来实现相同的功能。
2. 类、类型
类是一组具有相同属性和行为的对象的抽象。类及类的关系构成了对象模型的主要内容。对象模型用来模拟问题领域,Java程序实现对象模型,Java程序运行在Java虚拟机提供的运行时环境中,Java虚拟机运行在计算机机器上。
计算机受其存储单元的限制,只能表示和操作一些基本的数据类型,比如整数、字符、浮点数。对象模型中的类可以看作是开发人员自定义的数据类型,Java虚拟机的运行时环境封装了把自定义的数据类型映射到计算机的内置数据类型的过程,使得开发人员不必受到计算机的数据类型的限制,对任意一种问题领域,都可以方便地根据识别对象、再进行分类(创建任意的数据类型)的思路来建立对象模型。如下图所示,为从对象模型中的类型到计算机的内置数据类型的映射:
面向对象编程的主要任务就是定义对象模型中的各个类。
如何创建对象呢?Java语言采用new语句创建对象,new语句会调用对象的构造方法。如下所示分别创建两个学生对象:
Student stuOne = new Student("小张", "女", 18);
Student stuTwo = new Student("小王", "男", 16);
在运行时环境中,Java虚拟机首先把Student类的代码加载到内存中,然后依据这个模板来创建两个Student对象:stuOne和stuTwo。所以说,对象是类的实例,类是对象的模板。
3. 消息、服务
软件系统的复杂功能是由各种对象协同工作来完成的。例如电视和遥控器之间就存在这种协同关系。当用户按下遥控器的“开机”按钮,遥控器对象向电视机对象发送一个“开机”消息。电视机对象接受到这个“开机”消息,就执行相应的开机操作。此外,遥控器还能向电视机发送其他消息,例如选择频道、调节音量、播放VCD、关机等等。
每个对象都具有特定的功能,相对于其他对象而言,它的功能就是为其他对象提供服务。例如电视机具有的功能包括:开机、选择频道、调节音量、播放VCD、关机等;遥控器为了获得电视机的服务,需要向电视机提出获得特定服务的请求,提出请求的过程称为发送消息。
对象提供的服务是由对象的方法来实现的,因此发送消息实际上也就是调用一个对象的方法。
从使用者的角度出发,整个软件系统就是一个服务提供者。操作软件系统的用户是系统的边界,在UML语言中,系统边界称为角色(Actor)。在系统内部,每个子系统(对象或对象的组合)也都是服务提供者,它们为其他子系统提供服务,子系统之间通过发送消息来互相获得服务。一个孤立的不对外提供任何服务的系统是没有任何意义的。
对于电视机系统,看电视的观众就是它的系统边界。电视机系统是观众的服务提供者,电视机系统内的电视机对象是遥控器对象的服务提供者。
4. 接口
既然每个对象是服务提供者,那么如何对外提供服务呢?对象通过接口对外提供服务。例如电视机的红外线接收器就是为遥控器提供的接口。在现实世界中,接口也是实体,比如电源插口、洗衣机上的按钮、灯具开关等都是为用户提供的接口。
而在面向对象的范畴中,接口是一个抽象的概念,是指系统对外提供的所有服务。系统的接口描述系统能够提供哪些服务,但是不包含服务的实现细节。这里的系统既可以指整个软件系统,也可以指一个子系统。对象是最小的子系统,每个对象都是服务提供者,因此每个对象都有接口。
站在使用者的角度,对象中所有向使用者公开的方法的声明构成了对象的接口。使用者调用对象的公开方法来获得服务,使用者在获得服务时,不必关心对象到底是如何实现服务的。
在设计对象模型阶段,系统的接口就确定下来了。例如在学生Student类的UML类图中,它的方法声明,就是学生的接口。
备注:这里多次提到了使用者,若系统A访问系统B的服务,那么系统A就是使用者,系统B就是服务提供者。有时也会把系统A称为系统B的客户程序。
接口是提高系统之间松耦合的有力手段。例如电视机向遥控器公开了红外线接收器接口,使得电视机和遥控器之间相互独立,当电视机的内部实现发生变化时,如由电子显示器改为液晶显示器,只要它的红外线接收器接口不变,就不会影响遥控器的实现。计算机系统也是个充分利用接口来提高子系统之间松耦合性的例子。计算机的各种外围设备,如移动硬盘、打印机、移动光驱等都通过接口和主机通信。主机和它的外围设备都是独立的子系统,即使某个子系统内部的实现方式发生变化,只要接口不变,就不会影响到其他子系统。例如把激光打印机更换为喷墨打印机,只要打印机的接口不变,就不会影响到主机子系统。
接口还提高了系统的可扩展性。例如,台式计算机的主板上预留了许多供扩展的插槽(接口),只要在主板上插上声卡,计算机就会增加播放声音的功能,只要插上网卡,计算机就会增加联网的功能。
在Java语言中,接口有两种意思:一是指以上介绍的概念性的接口,即指系统对外提供的所有服务,在对象中表现为public类型的方法的声明;二是指用interface关键字定义的实实在在的接口,也成为接口类型,它用于明确地描述系统对外提供的所有服务,它能够更加清晰地把系统的实现细节于接口分离。