面向对象技术
面向对象技术
对象是面向对象的程序设计的核心,它由描述状态的属性(变量)和用来实现对象行为的方法(函数)组成,完成了从数据模型到处理模型的结合与统一。面向对象方法论的出发点和基本原则是尽可能模拟人类习惯的思维方式,使开发软件的方法与过程尽可能接近人类认识世界解决问题的方法与过程;也就是使描述问题的问题空间(也称为问题域)与实现解法的解空间(也称为求解域)在结构上尽可能一致。这样就解决了系统在分析过程中获得的分析模型与设计过程中所获得的设计模型进行转换时,由于理解上的差异而造成的系统不稳定性。面向对象方法论中产生的设计模型是分析模型的进一步完善和细化,使得模型之间的转换成为一种平滑的过渡。
1.面向对象与面向过程的区别
在软件工程里,面向对象是继结构化革命之后的又一次软件开发方式革命。面向对象的主要思想是基于抽象数据类型的(Abstract Data Type, ADT):在结构化编程过程中,人们发现把某种数据结构和用于操纵它的各种操作以某种模块化方式绑定到一起会非常方便,使用这种方式进行编程时数据结构的接口是固定的。如果对抽象数据类型进一步抽象,就会发现把这种数据类型的实例当作一个具体的东西、事物、对象,就可以引发人们对编程过程中怎样看待所处理的问题的一次大的改变。
所以对于面向对象而言,系统是交互对象的集合,对象与人或其它对象交互,对象发送与响应消息。
面向过程(Process Oriented)这个词是在面向对象(Object Oriented)出现之后为与之相对而提出的。它在以前基本被叫做“结构化编程”。早期的程序设计,大量使用共享变量(全局变量)和使用三种基本流程(顺序、选择、重复)来实现,每种语言都提供这些基本控制结构的实现方式,并提供把数据访问局部化的能力,以及某种形式的模块化/分别编译机制。在这些基础上,人们所进行的编程活动基本是通过写用于不同目的的功能函数/过程来实现,这也是“面向过程”的由来。
所以对于面向过程而言,系统是过程的集合,过程与数据实体交互,过程接受输入并产生输出。
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
例如五子棋游戏,面向过程的设计思路就是首先分析问题的步骤:
1)、开始游戏,
2)、黑子先走,
3)、绘制画面,
4)、判断输赢,
5)、轮到白子,
6)、绘制画面,
7)、判断输赢,
8)、返回步骤2,
9)、输出最后结果。
把上面每个步骤用分别的函数来实现,问题就解决了。而面向对象的设计则是从另外的思路来解决问题。整个五子棋可以分为:
1)、黑白双方,这两方的行为是一模一样的,
2)、棋盘系统,负责绘制画面,
3)、规则系统,负责判定诸如犯规、输赢等。
第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
可以明显地看出,面向对象是以功能来划分问题,而不是步骤。同样是绘制棋局,这样的行为在面向过程的设计中分散在了总多步骤中,很可能出现不同的绘制版本,因为通常设计人员会考虑到实际情况进行各种各样的简化。而面向对象的设计中,绘图只可能在棋盘对象中出现,从而保证了绘图的统一。
功能上的统一保证了面向对象设计的可扩展性。比如我要加入悔棋的功能,如果要改动面向过程的设计,那么从输入到判断到显示这一连串的步骤都要改动,甚至步骤之间的循序都要进行大规模调整。如果是面向对象的话,只用改动棋盘对象就行了,棋盘系统保存了黑白双方的棋谱,简单回溯就可以了,而显示和规则判断则不用顾及,同时整个对对象功能的调用顺序都没有变化,改动只是局部的。
需要说明的是,面向过程和面向对象是解决问题的思考方式,即使是面向对象的方法也是含有面向过程的思想。
2.面向对象的重要概念
面向对象的软件工程将以面向对象视角,采用面向对象的分析设计方法来解决搭建软件系统的问题,因此对于面向对象概念的理解和把握将直接影响面向对象系统的构成质量。下面将简单介绍一下面向对象中几个非常重要的概念。
2.1对象(Object)
现实世界中,随处可见的一种事物就是对象,对象是事物存在的实体,如人类、书桌、计算机、高楼大厦等。人类解决问题的方式总是将复杂的事物简单化,于是就会思考这些对象都是由哪些部分组成的。通常都会将对象划分为两个部分,即动态部分与静态部分。静态部分,顾名思义就是不能动的部分,这个部分被称为“属性”,任何对象都会具备其自身属性,如一个人,它包括高矮、胖瘦、性别、年龄等属性。然而具有这些属性的人会执行哪些动作也是一个值得探讨的部分,这个人可以哭泣、微笑、说话、行走,这些是这个人具备的行为(动态部分),人类通过探讨对象的属性和观察对象的行为了解对象。
面向对象方法学中的对象是由描述该对象属性的数据(数据结构)以及可以对这些数据施加的所有操作封装在一起构成的统一体。这个封装体有可以唯一地标识它的名字,而且向外界提供一组服务。从程序设计者来看,对象是一个程序模块;从用户来看,对象为他们提供所希望的行为或服务;从对象自身来看,这种服务或行为通常称为方法。
要想深刻理解对象的特点,就必须明确对象的下述特点:
• 以数据为中心。
所有施加在对象上的操作都基于对象的属性,这样保证对象内部的数据只能通过对象的私有方法来访问或处理,这就保证了对这些数据的访问或处理,在任何时候都是使用统一的方法进行的。
• 对象是主动的。
对象向外提供的方法是自身向外提供的服务。对于数据的提供不是被动的,而是根据自身的特点及接收发来的消息进行处理后向外反馈信息。
• 实现了数据封装。
使用对象时只需知道它向外界提供的接口形式而无须知道它的内部实现算法。不仅使得对象的使用变得非常简单、方便,而且具有很高的安全性和可靠性,实现了信息隐藏原则。
• 对象对自己负责
对象可以通过父类得知自己的类型,对象中的数据能够告诉自己它的状态如何,而对象中的代码能够使它正确工作。
• 模块独立性好。
对象中的方法都是为同一职责服务的,模块的内聚程度高。
2.2类(Class)
我们一般习惯于把有相似特征的事物归为一类。在面向对象的技术中,把具有相同属性和相同操作的一组相似对象也归为一“类”。类是对象的模板。即类是对一组有相同属性和相同操作的对象的定义,一个类所包含的方法和属性描述一组对象的共同属性和行为。类是在对象之上的抽象,对象则是类的具体化,是类的实例。类可有其子类,也可有其它类,形成类的层次结构。
例如:三个圆心位置、半径大小和颜色均不相同的圆,是三个不同的对象。但是,它们都有相同的属性(圆心坐标、半径、颜色)和相同的操作(计算面积、绘制图形等等)。因此,它们是同一类事物,可以用“Circle类”来定义。
实例(Instance)就是由某个特定的类所描述的一个具体的对象。
2.3消息(message)
消息就是向对象发出的服务请求。它包含了提供服务的对象标识、服务(方法)标识、输人信息和回答信息等。
面向对象方法的一个原则就是通过消息进行对象之间的通信。初学面向对象方法的人往往把消息等同于函数调用,事实上两者之间存在区别。消息可以包括同步消息和异步消息,如果消息是异步的,则一个对象发送消息后,就继续自己的活动,不等待消息接收者返回控制,而函数调用往往是同步的,消息的发送者要等待接收者返回。
2.4封装(encapsulation)
封装性是面向对象的主要特征之一,它是一种信息隐蔽技术,它体现于类的说明。封装使数据和加工该数据的方法(函数)封装为一个整体,使得用户只能见到对象的外部特性(对象能接受哪些消息,具有哪些处理能力),而对象的内部特性(保存内部状态的私有数据和实现加工能力的算法)对用户是隐蔽的。封装的目的在于把对象的设计者和对象的使用者分开,使用者不必知晓行为实现的细节,只须用设计者提供的接口来访问该对象。对象具有封装性的条件如下:
(1)有一个清晰的边界。所有私有数据和实现操作的代码都被封装在这个边界内,从外面看不见,更不能直接访问。
(2)有确定的接口(即协议)。这些接口就是对象可以接受的消息,只能通过向对象发送消息来使用它。
(3)受保护的内部实现。实现对象功能的细节(私有数据和代码)不能在定义该对象的类的范围外访问。
2.5继承(Inheritance)
继承性是子类自动共享父类之间数据和方法的机制。它由类的派生功能体现。一个类直接继承其它类的全部描述,同时可修改和扩充。三种继承方式:
第一,继承能够直接获得已有的性质和特征,而不必重复定义它们。
第二,在子类中可以增加或重新定义所继承的属性或方法,如果是重新定义,则称为覆盖(override)。
第三,与覆盖很类似的一个概念是重载(overload ),重载指的是一个类中有多个同名的方法,重载关心的只是参数,有参无参,参数类型不同,参数数量不同,不同类型的参数顺序不同,都可以实现方法的重载。举个例子:
public class Shape { public static void main(String[] args){ Triangle tri = new Triangle(); System.out.println("Triangle is a type of shape? " + tri.isShape());// 继承 Rectangle Rec = new Rectangle(); Shape shape2 = Rec; System.out.println("My shape has " + shape2.getSides(Rec) + " sides."); //重载 } public boolean isShape(){ return true; } public int getSides(){ return 0 ; } public int getSides(Triangle tri){ //重载 return 3 ; } public int getSides(Rectangle rec){ //重载 return 4 ; } } class Triangle extends Shape { public int getSides() { //重写 return 3; } } class Rectangle extends Shape { public int getSides(int i) { //重载 return i; } }
继承具有传递性。继承分为单继承(一个子类只有一父类,使得类等级成为树形结构)和多重继承(一个类有多个父类,多重继承的类可以组合多个父类的性质构成所需要的性质)。类的对象是各自封闭的,如果没继承性机制,则类对象中数据、方法就会出现大量重复。继承不仅支持系统的可重用性,而且还促进系统的可扩充性。
一个类实际上继承了它所在的类等级中在它上层的全部基类的所有描述。也就是说,属于某类的对象除了具有该类所描述的性质外,还具有类等级中该类上层全部基类描述的一切性质。
多继承虽然比较灵活,但多继承可能会带来“命名冲突”的问题。同时,对象的本质问题模糊不清。例如,我们要实现会飞的汽车,我们可以继承汽车类和飞机类,但我们的飞行汽车已经成了杂交品种,分不出到底是车还是飞行器了。这里就可以知道为什么C#和JAVA都不支持多重继承基类了。避免杂交,减少耦合。当然,如JAVA给出了解决多继承的方法,用接口和抽象类。
2.6多态性(Polymorphism)
多态性是允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
利用多态性用户可发送一个通用的信息,而将所有的实现细节都留给接受消息的对象自行决定,这样,同一消息即可调用不同的方法。例如:print消息被发送给一个图或表时调用的打印方法与将同样的Print消息发送给一个正文文件而调用的打印方法会完全不同。多态性的实现受到继承性的支持,利用类继承的层次关系,把具有通用功能的协议存放在类层次中尽可能高的地方,而将实现这一功能的不同方法置于较低层次,这样,在这些低层次上生成的对象就能给通用消息以不同的响应。在面向对象编程中可通过在派生类中重定义基类函数来实现多态性。从一定角度来看,封装和继承几乎都是为多态而准备的。
在提到多态的同时,不得不提到抽象类和接口,因为多态的实现并不依赖具体类,而是依赖于抽象类(abstract class)和接口(interface)。
抽象类(abstract class):对具体对象的最高抽象,这个对象拥有自己的最基本特征。在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。
在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。
接口(interface):是某类行为或功能的抽象。是一种开关或者是契约。
如果一个抽象类中的所有方法都是抽象的,就可以将这个类用借口定义。借口是一个特殊的抽象类,没有变量和方法实现。
接口隔离原则,指的是不要把多个功能全部都集中在一个接口里面。接口实现的功能要相对单一;衍生开来可以得到另外一个结论:对一组或者称一系列功能的实现,尽量定义相对功能单一的小模块来实现这一组功能。这其实也是解耦和的体现。
所以,接口抽象行为,抽象类抽象对象;用抽象类来实现接口,子类再继承基类;抽象类和接口本质上都是是系统的最高抽象。
如上飞行汽车的例子,设定我们对于问题领域的理解是:飞行汽车在概念本质上是汽车,同时它有具有飞行的功能。我们该如何来设计、实现来明确的反映出我们的意思呢? abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。所以对于汽车这个概念,我们应该使用abstarct class方式来定义。另外,飞行汽车又具有飞行功能,说明它又能够完成飞行概念中定义的行为,所以飞行概念可以通过interface方式定义,interface表示的是"like a"关系。代码如下:
interface IFlyable { void Fly(); } public abstract class Car { public abstract string Wheel { get; set; } public virtual void Move() { Console.WriteLine("车移动了"); } } public sealed class FlyCar extends Car implements IFlyable { private string wheel = string.Empty; public override string Wheel { get { return wheel; } set { wheel = value; } } public void Fly() { base.Move(); Console.WriteLine("汽车起飞成功!"); } } //在这里应用任何模式都很简单了 static void Main(string[] args) { FlyCar c = new FlyCar(); ((IFlyable)c).Fly(); ((Car)c).Move(); }
面向对象无处不在,弄懂它很重要。