资源描述
7.1概述-面向对象方法论 7.2面向对象技术的基本概念 7.3面向对象技术的基本特点 7.4面向对象分析方法 7.5面向对象技术与程序结构 7.6面向对象软件工程 7.7设计模式(Design Pattern)与框架(framework) 7.8基于构件的软件体系结构(com/dcom, corba,internet) 7.9 面向对象分析解决(描述)问题的模式,第7章 面向对象技术总论,7.1 概述面向对象方法论面向对象技术的内容包括面向对象系统分析技术、系统设计技术、程序设计技术、测试技术以及各种基于面向对象技术的体系结构、框架、组件、中间件等。面向对象技术的基础是面向对象程序设计,后者是程序结构化发展的必然产物。众所周知,高级程序设计语言经历了非结构化、结构化、面向对象三个发展阶段,这三个阶段的进化都是针对程序的结构和系统分析方法而做出的。程序的结构是指程序代码之间的关系。每到一个新的阶段,程序的结构就更加完善、更加复杂,代码也更容易重用,抽象程度也更高。,软件系统分析方法研究将问题域(现实世界)向求解域(程序域)转换和映射的方法,其目的是把问题域(现实世界)中的概念或者处理过程转换或映射成程序的元素和算法。问题域(现实世界)相对来说是不变的或者变化较缓慢的,但系统分析方法却根据程序设计方法的不同而改变,因此系统分析方法依赖于程序设计技术,依赖于程序设计元素,参见图7.1。结构化分析方法和面向对象分析方法则又分别依赖于结构化程序设计语言和面向对象程序设计语言。,图7.1 系统分析方法对程序设计技术的依赖性,非结构化分析方法就是要把问题域或现实世界中的概念和处理,如员工工资、计算工资等转换成变量定义和对变量进行处理的语句,其基本元素是变量和语句,即所谓的数据结构+算法。由于当时程序规模普遍比较小,运行和应用环境也比较单一,因此不太考虑程序结构或者软件结构问题。整体来说,非结构化技术是重视算法轻视结构的一种方法。,结构化技术的基本元素是定义良好的程序结构元素,如子程序(函数)结构和单入口/单出口的控制结构。前者是程序的静态结构,后者则是程序的动态结构。子程序结构构成了整个程序的静态结构,是动态控制结构的基础(严格地说,每个语句都可以看成是对函数的调用)。结构化分析方法就是要把问题域(现实世界)中的问题(概念、处理)转换成程序中的数据结构和子程序(函数)。在这类方法中,可以认为:程序=数据结构+函数结构+函数调用。相应的数据流分析方法则将现实世界或者问题域中的业务处理转换为程序的函数结构,这个过程称为分析过程,将系统的结构变成更适合于程序域的形式,比如说具有重用、高效、稳定等特征的结构,则是设计过程。,到了面向对象技术阶段,程序的基本元素是数据结构和函数结构的统一体“类”。类是程序中的静态元素,而动态元素是对象和消息。面向对象方法认为:程序=类结构+对象+消息。面向对象分析的任务则是把现实世界中的概念或者处理都转换为程序域中的类和方法,将现实世界中的过程转换为对象之间的交互过程。面向对象设计使这种类和对象交互更加适合于计算机系统实现,更加合理和高效,更加容易重用。例如将员工、工资都转换成求解域中的类,计算某位员工工资的过程称为向该员工对象发消息。,如上所述,新一代的程序设计语言技术并不是简单地否定上一代语言,而是在上一代语言的基础上增加新的程序结构元素(函数、类),从而实现更复杂的程序结构。这种新的程序元素更直观、更真实、更自然、更完整地抽象了现实世界中的数据和处理(或者事物与概念),更好地抽象了程序中的变量和代码,也进一步增强了程序的易读性、安全性、稳定性和重用性,同时改变了系统的分析和设计方法。归根结底,程序设计语言的发展就是程序结构以及建立在其基础上的分析、设计方法的发展。,上面的例子表明,实现同样的功能可以采用不同的程序元素、程序结构或者程序设计技术。高级的程序设计方法更擅长解决复杂的问题,因为其程序元素和程序结构更为复杂。这实际上是自然界和社会系统的一个普遍规律,即内部结构决定外部功能。如果把系统解决的问题比做该系统实现的外部功能,而把实现这些功能的程序元素及其关系看做是内部结构,越复杂的内部结构就预示着系统的功能越复杂、越强大,比如说,人的大脑结构要比动物的大脑结构复杂得多,因此其功能也要强大得多。,面向对象的程序结构要比面向过程的程序结构更复杂,因此可以实现的功能也就更加强大。程序设计语言技术的发展历史充分证明了客观和主观系统进化中,功能和结构之间关系的一般规律。图7.2表现了系统外部功能和内部结构之间的关系,其内部结构也是分层次的,这也符合了人认识世界的一般规律:由外向内、由表及里。,图7.2 软件系统外部功能和内部结构之间的关系,图7.3 软件系统外部功能和内部结构关系的例子,7.2 面向对象技术的基本概念7.2.1 类对象是指现实世界或者概念世界中的任何事物,类是具有相同结构特征的对象的结构抽象。此结构特征包括对象的属性特征和操作接口特征。属性特征定义了所有对象都具有的属性名称及类型;操作接口特征则定义了所有对象都具有的操作和方法。,面向对象程序语言中的类是一个代码的预定义模块或者程序结构元素,类似于函数、结构体或者记录类型定义,其中包含了相关的变量和函数定义。类中的变量称为属性或者成员变量;类中的函数称为成员函数(操作和方法)。操作和方法的区别在于:操作强调其操作接口,方法则强调实现方式和算法。可以把现实世界中的对象映射为程序语言中的类,有时候这种映射比较困难且不明显,则可在其间增加一个概念世界或者概念模型。这种映射的过程即为面向对象的系统分析,如图7.4所示。,图7.4 现实世界向计算机世界的转换,现实世界中的对象一定能够抽象成类,但程序语言中的类则不一定有现实世界原型。这种不对称性反映了信息系统的特殊性,即程序模型并不一定是现实世界的原始或者简单的等价。因为现实世界中的任何事物都可以被抽象为类,因此可以把面向对象看成是一种世界观和方法论,在这种世界观和方法论基础上,现实世界被转换成程序世界就显得比较自然。例如:企业中的员工、仓库、库存帐目、商品及类别、物体、力等。,在程序设计语言中,类是一个完整的、独立的、可重用的,具有低耦合、高内聚特性的程序模块。类相当于一种自定义数据类型,它类似于C语言中的结构体类型(C+本身就可以使用strut关键字来定义类),不仅包含数据结构也包含操作结构。数据类型作为程序语言中进行变量内存分配、类型匹配、操作检查的基础,为程序的一致性和安全性提供了重要的保证。因此,类概念的引入从类型角度进一步提高了程序的安全性。,7.2.2 对象及对象实例现实世界中的具体事物就是对象或者对象实例,类则是对象实例的结构抽象。每个对象实例一般具有三方面的特性(亦称对象“三要素”):(1) 确定的标识,能够被唯一地确认。(2) 具有一定的属性,表示其性质或状态。(3) 具有一定的行为能力或者操作能力,可给外界提供服务或者通过操作改变其状态。,对象标识也是对象属性之一,是一类特殊的属性,类似于实体关系模型中的主码。应当注意,对象标识和对象在程序中的标识是不一样的,前者是对象本身的固有属性,后者是在程序空间中给该对象起的名字,仅供程序中使用。现实世界中的任何事物都可以抽象成对象,例如具有具体物质形态的对象,如员工、计算机、汽车等;具有抽象物质形态的对象,如银行账户、力等。,面向对象本身就是一种世界观以及由此派生的方法论,是一种观察世界和认识世界的方式。在面向对象的世界观中,世界是由对象及其关系组成的,对象是由属性和行为组成的,对象之间具有各种各样的联系。类是对象结构的抽象,关系是对象之间联系的抽象。对象世界的发展变化过程就是对象之间的交互过程(面向过程也是一种世界观,认为世界是由过程组成的,每个过程都有自己的目标和处理的对象)。面向对象的世界观中的属性和操作刚好对应了程序世界中保存数据或者状态的变量和实现一定功能的函数。属性是事物的性质和状态描述;行为是对属性的操作,可以对外提供服务,属性则是行为的基础和操作的内容。,根据属性和行为的侧重点不同,对象又可以分成实体对象和过程对象,前者强调对象的状态描述,例如“学生”对象;后者强调对象的过程特性,例如“流程”对象。现实世界中的事物都是有联系的,对象之间也有联系。例如对象间的组成关系:如汽车对象是由零件对象组成的;关联关系(信息联系):如老板有员工的联系方式。对象之间有些联系是暂时的:如某人向你问了一下路(发送消息),你向其提供了服务;有些联系则是永恒的、持久的:如父母、子女之间的关系;有些则是介于两者之间的:如配偶关系等。,从静态角度来看,对象实际上就是由类(包括数据结构和函数结构定义)模板派生的变量;从动态角度来看,对象之间的交互(包括自交互)完成或者实现了功能。在面向对象程序中,类是一种代码模板定义,它类似于函数结构和数据结构,必须要实例化才能使用。实例化就是使用类创建具体的对象变量(也叫对象实例),由类产生对象变量相当于在工厂中使用模具生产产品。在程序中只有创建了具体的对象变量,系统才会给其分配内存,程序才能访问其属性和操作。,由于对象变量占用的内存空间不固定,其内存分配方式多采用动态分配和回收机制。在面向对象程序设计语言中,除了对象变量本身,还有一种引用变量(reference variable),其本质上是指针或者地址,可以指向对象变量,其类型必须是一致的。通过引用变量访问对象变量简化了对动态分配的对象内存空间的访问。如Student是Java语言中定义的类,语句Student s = new Student();声明了一个引用变量s,创建了一个对象变量new Student(),并通过赋值语句将该对象变量所在的内存区域的首地址存放到s中,通过s可以对此内存区域进行操作。一般来说,引用变量和对象变量是在不同的内存空间中,前者存放在堆栈空间中,后者是放在堆空间中,这两种内存空间所允许的操作是有所不同的(如图7.5所示)。,图7.5 引用变量和对象变量之间的关系,7.2.3 消息机制纯面向对象程序设计语言如Java中,所有的函数或者操作都依附于一个对象主体,这种依附于某个对象的函数叫做成员函数或者对象操作(方法)。在纯面向对象程序中,对任何一个函数的调用都是对某个对象的方法调用,同时会把该对象的信息(一般是内存地址)传递给被调用函数,这就是函数的实例化过程。在面向对象程序中,调用一个对象的方法或者操作叫做向该对象发消息,调用者叫做消息发出者,被调用者叫做消息接受者。消息、事件和函数调用或者事件响应是相辅相承的。程序中,消息就是现实世界中的请求或者通知事件,这一般都是通过对系统执行一定的操作完成的。对该请求或者通知可以响应,也可以不响应,响应可以是同步的,也可以是异步的。,例如,客户如果想从ATM机中取钱,通常会按下取钱按键,这实际上就是向ATM机发送了取钱消息,也是向ATM机发送了取钱请求,ATM机会显示一个取钱界面,让用户输入取款数额,这是通过ATM机的一个方法或者操作实现的。用户输入取款金额后按下确定键,相当于又向ATM机发送新的消息,导致ATM机的另一个方法的调用,通常在该方法中又会向其他对象发送消息,例如该客户的账户Account对象,通过调用该账户对象的draw()操作实现账户上资金的更新。用户通过和ATM机一系列的请求/响应的交互活动完成了执行系统的某个功能,如取钱。客户对象、ATM对象、Account对象之间的消息交互见图7.6。,图7.6 消息机制,7.3 面向对象技术的基本特点7.3.1 封装性程序的基本元素是变量定义以及对变量的处理。对于规模比较小的程序,每个变量的意义和访问都由程序员自己控制,但对于需要多人长时间开发的大规模程序,存在访问其他人定义的变量的情况,这时候变量的安全性则成为问题。程序中有很多错误都是由于错误地访问了不该访问的变量而引起的。面向过程程序设计语言中的函数结构中的局部变量在一定程度上解决了这个问题,但是全局数据结构或者变量中还是存在不安全的访问问题。面向对象程序将存储数据的变量和对数据(变量)的处理(方法)封装起来(私有化),从程序外部不能直接访问数据,而必须通过对外公开的方法进行访问,这就避免了对正确数据的错误操作,如图7.7所示。,图7.7 对象的封装性,将Account对象中表示余额的属性balance(实型变量)隐藏起来,对外部程序不可见,外部程序只能通过公开的方法save ( )或者draw ( )来改变balance的值,这样就可以避免外部程序对其进行错误的操作。比如说:在程序中直接对balance变量做乘除法操作(例如实现存钱操作时将“+”敲成“*”号),而编译程序是没有办法发现此错误的,因为对于实型变量来说,不能限制对其做乘除操作,从语法上来看这些都是合法的操作。对于账户余额来说,乘除法则是无意义的操作。因此可以通过隐藏数据,公开合法的操作接口来限定账户这种对象的内涵和外延,增加程序的安全性。,面向对象的封装性实现的是信息隐藏。仅将需要向外公开的方法和属性向外公开;所有不需要向外公开的方法和属性都被隐藏起来。正像电视机一样,内部电路元件对用户都隐藏起来,对外只公开用户可用的接口,如开关、音量调节、调台等。这种封装性通过定义合适的操作接口来进一步确定事物的本质特征。对于传统的面向过程的程序设计语言来说,表示银行存款和图形尺寸时都是使用实型变量,没有区别,但实际上对这两种量的操作是有区别的。很明显,银行存款只允许增加、减少或者查询;对尺寸来说却能按比例放大或者缩小,即执行乘除法。类似的还有数据结构中的例子,保存线性表、堆栈、队列元素的数据存储结构可以是一样的,但对其允许的操作是不一样的。封装性增强了程序中变量的安全性,同时也增强了程序中数据类型、数据结构和变量的语义内涵,提高了程序分析方法的直观性。,7.3.2 继承性软件重用技术始终是软件开发技术研究中的一个重要课题,从程序行的简单复制到宏替换,从数据结构定义再到函数定义,从类的定义到类的继承,从构件到框架等,都是代码重用技术的体现。继承机制是一种高级的代码重用技术,子类自动继承父类的所有代码并且可以进行任意的覆盖和扩充。如图7.8所示,子类研究生继承了父类学生中的属性和操作,又扩充了新的属性和操作。子类和父类保持一种动态关系:当父类代码改变的时候,子类继承的那部分代码会自动修改,即子类对父类的继承不是简单的复制,而是动态的链接,子类和父类始终保持一种联系,这是一种与生俱来的、静态的联系,一旦定义,则无法改变。,子类对父类的扩充包括对父类同名属性和方法的覆盖以及增加新的属性和方法。过多的继承层次会使程序结构变得异常复杂、难以理解并且难以维护。子类不仅仅继承了父类的代码,也继承了父类的类型,或者说和父类型是相容的。程序设计语言中的继承关系区别于生物界中的遗传关系,如父子关系,更像事物分类中的分类关系。父类往往是各子类的共性的抽象,是对所有子类对象的抽象。例如:汽车、自行车、三轮车的父类是车,车是一个抽象的概念,凡是具有轮子可以滚动行走的物体都可以称为车。又比如三角形、矩形、圆等具体图形的父类可以定义为图形类等。图7.8中,研究生属于学生中的一种,是特殊的学生,因此是从一般到特殊的分类关系。,图7.8 类的继承关系,7.3.3 多态性多态性(polymorphism)是指同一种事物可以有多种不同的形态或者含义,也可以认为是从不同的角度观察同一事物,可以得到不同的视图。在面向对象程序设计语言中,多态性有三种含义。多态的第一种含义是方法的重载(Overload),在同一个类中可以存在同名不同参数的操作,这些操作的具体实现是不同的,即同名方法在不同的参数情况下有不同的实现。这反映了客观世界中对事物操作的复杂性和联系性,如对银行来说,存钱是一种操作,但根据用户出示的是存折还是银行卡,其具体的实现有所不同。编译程序通过参数类型或者个数来确定具体调用哪个方法,因为这种多态性是在编译阶段完成的,所以可以称为静态的多态性或者编译期多态性。操作符重载也是基于这种多态性的。例如圆对象可以根据不同的初始条件进行构造,见下面代码。,class Circle public Circle(double x,double y,double r) /已知圆心坐标和半径 public Circle(Point center,double r) /已知圆心和半径 public Circle(double x1,double y1,double x2,double y2,double x3,double y3)/已知圆上三点坐标 public Circle(Point p1,Point p2,Point p3) /已知圆上三点 public Circle(Point p1,Point p2,double r) /已知圆上两点和半径 ,下面是引用上述定义创建圆对象的代码:Circle c1 = new Circle(); /创建一个默认的圆对象Circle c2 = new Circle(new Point(2,2),5); /创建已知圆心和半径的圆对象Circle c3 = new Cirlce(0,0,2,3,4,5); /创建已知圆上三点坐标的圆上述代码中同名方法具有不同的参数,给出了在不同初始条件下构建圆对象的方法。,多态的第二种含义是子类对父类的方法进行覆盖(override),程序会根据当前对象状态而自动调用父类或者子类对象的方法。这一点和下面讲的多态性的第三种含义本质上是相同的。这种多态性由于在运行期间才能确定,因此称为动态的多态性或者运行期多态性。,例如:Shape是图形类,Circle是其子类,父类和子类的方法getArea()具有不同的实现。public class Shape double getArea()return 0.0;public class Circle extends Shape double r; double getArea()return Math.PI * r * r;,上面类的对象实例创建代码Shape s = new Circle()将子类对象实例赋值给父类的引用变量(这一点正是类型的多态性,本节后面详细解释),那么s.getArea()方法将调用父类还是子类的方法呢?按照类型分析,s是Shape类型,则s.getArea()方法应该是Shape的方法,但是s实际上指向的是Circle对象,所以s.getArea()方法应该是Circle的方法,但这是在运行期间才能确定的,因为s具体指向的对象只有在运行期才能确定。不同的语言在此处有不同的处理方法。在C+中,上述代码调用的是Shape的getArea()方法,但是如果在Shape的getArea方法定义前面加上virtual关键字,即:virtual public double getArea();,则同样的代码s.getArea()调用的方法是子类Circle的方法。方法定义前面加上virtual的意思是指该方法的具体执行代码直到运行期才能确定。在Java中,对象的方法默认都是运行期确定的,因此s.getArea()一定是调用s实际指向的对象的方法,即Circle对象的方法。如果要想调用父类的area()方法,只需要让s指向父类对象即可:Shape s = new Shape();这种运行期确定方法的执行代码正如运行期分配或者删除对象空间一样,是非常灵活高效的,所以Java取其作为语言的默认特性,这也是Java语言简洁高效且能迅速普及的原因之一。,多态的第三种含义是类型的多态或者类型造型(type cast),前者是指一个对象可以看成是多种类型,后者是指子类对象可以造型成父类型,即将子类对象看成是父类类型,类似于类型强制,注意只是“看成”是父类类型,而不是真正的父类对象,子类对象永远不能变成父类对象,而只能扮演成父类对象,以通过程序语法的类型匹配检查,但其本质上还是子类对象。这在生活实际中也是经常遇到的,例如一个研究生去应聘,如果企业只招聘本科生的话,他也可以将自己看成是本科生而降级使用,但他自己本质上还是研究生。除了子类对象可以扮演(造型)成父类类型外,某个类的对象也可以造型成该类实现的接口类型。,造型的方法就是将该类对象赋值给某种类型的引用变量,如:A obj = new B(),B要么实现A接口,要么B是A的子类。定义引用变量并没有创建对象,相当于定义了一种类型约束。在面向对象语言中,这种造型称为向上造型(upcast),它是安全的,因为子类型肯定满足父类型的要求,子类型要么等于父类型,要么大于父类型(扩充)。从类的现实世界语义来看,子类和父类的关系是ISA(读作“是一个”)关系,即子类对象首先是一个父类。图7.9中的关系读作研究生是一个学生,这无疑是客观正确和可理解的。从哲学概念上来看,子类和父类正反映了特殊和一般、个性和共性的关系,一般性寓于特殊性之中,共性寓于个性之中。这是面向对象技术中继承或者接口概念的本质,这也是只有具有继承或者实现接口关系的对象才能造型的原因。,图7.9 ISA关系,数据类型在程序设计语言中的重要性毋庸置疑,它是内存分配、类型检查、操作检查的基础。程序语言中数据类型的多少是衡量该语言功能是否强大的依据之一,现在的程序语言都提供了大量标准数据类型以及自定义数据类型的机制。在面向对象程序设计语言中,类可以看成是一种自定义类型。类型检查也是程序安全性检查机制之一,通过检查相关操作的类型匹配可以发现很多不兼容的错误。比如:赋值操作、各种运算操作,如果没有类型检查的话,将会出现很多程序员难以察觉的错误,这也是过去类型检查机制较弱的弱类型语言(如C语言)容易出错的原因。,面向对象机制中类的定义实际上增强了类型检查的安全性,一个类就相当于一种自定义的数据类型,不同类定义的变量是不允许相互赋值的,这虽然增强了程序的安全性,但却带来了另外一个问题,那就是代码效率很低,针对每一个事物都需要定义一个类,例如定义堆栈类就得针对不同的元素类型定义多个类,每个类只能接收一种类型元素。C+采用一种叫做模板(template)的机制来处理这个问题,而Java则采用多态性来解决这个问题。多态(polymorphism)的原意就是同一个事物可以具有多种状态,或者说同一个事物在不同的场合、从不同的角度可以看成是不同的东西,这是客观事物复杂性的程序表现。,7.3.4 抽象性抽象是人类思维的本质特性之一。抽象性是对复杂事物本质和特性的提炼和概括的能力,可以说没有抽象就没有理论思维,就没有指导认识世界改造世界的一般性结论,也就没有系统分析和设计。在程序中始终存在着抽象性,数据类型是数据结构和操作接口的抽象,常量是程序中常数的抽象,变量是程序中变化的数据的抽象,函数是实现某个功能或者处理代码的抽象,类是多个实例对象结构(属性和操作接口)共性的抽象,抽象类或者接口又是多个类公共接口的抽象。,面向对象技术将抽象性引入到代码的实现中,可以实现很多更抽象、更一般、更统一的方法。比如说:Shape是所有图形对象的父类,是抽象的,则方法getArea(Shape s)可以返回任何图形的面积,totalArea(Shape s)则可以返回任意多个任意图形的面积和,draw(Shapes)则可以画出任意多个任意图形。注意:这些方法本身都具有抽象性、一般性的含义。当然,这些抽象的方法并没有真正去实现计算每个具体图形的面积或者画出具体的图形,而只是完成通用的操作,如对所有的图形进行求和或者画出所有的图形(两者都是对集合进行遍历),而把具体的操作都交给具体的子类(如圆、矩形)来实现。在面向对象程序设计语言中,抽象类、接口或者模板类都很好地实现了抽象的功能。,由于程序中引入了抽象的操作,因此使得程序结构出现依赖倒置的现象。过去结构化分析方法得到的系统结构是自上而下、逐步求精,上层抽象的模块依赖于下层具体的模块,上层模块通过调用下层模块完成任务,或者说先有下层模块,才有上层模块。面向对象程序的继承结构中则是下层依赖于上层,因为上层是父类,下层是子类,先有上层再有下层,因此程序变得更抽象。程序结构依赖性对比如图7.10所示,左边部分的每个矩形框是函数,右边部分的每个矩形框是类。,图7.10 顶层模块和底层模块之间的依赖关系,7.4 面向对象分析方法系统分析方法就是把现实世界或者问题域中的概念和过程转换成求解域或程序域中元素的方法。因此系统分析方法是和程序域元素密切相关的。结构化或者面向过程的分析方法是把现实世界或者问题域中的过程处理抽象成概念模型中的数据输入、数据处理和结果输出,然后将处理过程转换成程序结构中的函数。程序中的函数有些是直接来自于现实世界中的处理过程,如计算工资,但仅靠现实世界中的处理过程是不够的,在程序中还有很多处理过程是和计算机或者程序本身密切相关的,如输入数据的过程等。,面向对象的分析方法就是把现实世界或者问题域中的事物、概念、过程等抽象成概念模型中的对象或类,然后转换成程序语言中的对象或类。程序语言中的类概念本身也来自于现实世界中的对象,因此概念模型中的类和程序语言中的类是一致的。但是仅靠现实世界中的类是不够的,作为信息系统或者程序模型,本身也有自己独特的机制和规律,具有区别于现实世界或者问题域的特点,例如每个程序都具有一定的运行环境和操作界面等,这些因素也被抽象为系统中的类,视为系统类。现实世界中抽取的类一般称为业务类或者领域类,即在现实世界中存在有原型的类。业务类又可以分为分析类和设计类,前者是从现实世界中直接抽取的类,后者是对分析类的抽象、包装、组合、扩充和修饰,如设计模式中的一些父类、实用类等。,按照目前广泛流行的MVC模式,现实世界中的类相当于模型类(Model),确定了现实世界事物本身的逻辑、联系和规律等,如学生、职工、工资、力等。其他的类,如表现类(View),则是对模型数据的计算机表现,是人机接口界面类;控制类(Control)则控制系统的运行流程。,从系统的顶层结构到系统的微观结构中,MVC结构都是存在的,例如:Struts框架结构是系统的整体架构,是符合MVC模式的,而Java的图形界面swing组件类本身也是按照MVC模式设计的。MVC模式实际上给出了构建信息系统的一种过程和方法:即从业务对象到系统对象,从业务模型到系统模型的演化过程。首先抽象现实世界中的相关对象,称为业务对象,在此基础上添加对其的表现类即视图类,系统流程(宏观)和业务流程(微观)的控制则转换成控制类。一般来说,业务类可以做到与实现环境或者实现语言无关,但界面类或者系统控制类则可能和实现环境及实现语言有关。从现实世界对象模型到系统对象模型的演化过程如图7.11所示,系统对象中最上层的是业务对象,第二层是控制对象,最底层是界面对象。,图7.11 现实世界的对象模型向程序世界的对象模型转换举例一,有些系统模型的类会更多一些,另一个现实世界模型向程序世界模型转换的例子超市购物向网上购物网站的转换,其中,业务模型包括产品(Product)、购物车(Cart)、订单(Order)和客户(User)四种;系统模型类包括产品(Product)、购物车(Cart)、订单(Order)、订单数据库(DB Order)和客户(User)五种。从业务世界模型向程序世界模型转换的示意图如图7.12所示。,图7.12 现实世界的对象模型向程序世界的对象模型转换举例二,7.5 面向对象技术与程序结构7.5.1 概述程序对外提供的功能是其外部特性,内部也有着自己独特的结构,即程序结构。程序中具有完整语义的最小语法单位是表达式,语句是程序执行的最小单位,语句的集合组成了程序。语句相当于原子,语句与语句之间的关系形成程序结构,如顺序、选择、循环。数据结构、函数定义也是一种程序结构,称为程序模块,它由完成某个特定功能的多条语句组成。模块的类型包括数据模块、算法模块或者二者的综合。程序模块是作为一个整体存在的,可以独立地开发存储,具有独立性、安全性、语义性和重用性。其中数据模块类似于数据结构定义,比如C语言中的结构体;算法(程序)模块类似于函数。数据和算法(程序)的综合就是面向对象程序中的类。,在程序从非结构化到结构化再到面向对象的进化过程中,程序结构越来越复杂、越来越丰富、重用粒度越来越大、结构层次也越来越深,对外呈现的功能也越来越强大。其内部结构和外在表现之间的关系符合自然界和社会进化的一般规律,即内部结构越复杂,其对外呈现的功能也就越强大。但是,这种内部结构必须是建立在一种优化合理的基础上,而不是简单的堆积。目前的包类函数的层次结构就是一种公认的优化合理结构。,程序结构中包含静态的结构和动态的结构。静态的结构就是程序代码之间的关系,如类的继承、关联关系等,是代码定义阶段的关系;动态结构是函数的调用或者向对象发消息的过程,是代码执行时的关系。动态结构要以静态结构为基础。图7.13和图7.14分别表示了面向过程和面向对象程序之间的静态结构关系和动态结构关系,从图中可以看出,面向对象程序的结构要比面向过程程序的结构复杂得多。,软件工程的实践证明,程序的结构是通过不断的改进而得到优化的,不会一步到位。好的程序结构可以通过有经验的开发人员在设计阶段通过精心设计而得到,也可以通过对已有的结构中不太好的代码进行不断改进而得到。通过代码改进程序结构的方法也称为“重构”。由于重构是从质量欠佳的代码基础进行改进的,而不是一下拿出高质量的结构设计,因此这种方法更受初学者的欢迎,也越来越受到软件界的重视,目前几乎所有的主流开发环境都提供了对重构工具的支持。,图7.13 结构的层次静态结构,图7.14 程序执行的线索动态结构,7.5.2 重构重构(Refactor)是指在不改变代码的外在功能的前提下重新设计已有代码,以获取代码新的特性。这里新的特性主要就是由于结构的改进而带来的高效性、安全性、稳定性、可维护性和可扩充性等。根据上一小节中所描述的程序结构的改进过程可以看出,这种重构是完全可能的,也是很有必要的。根据外在功能和内部结构的关系来看,重构的目的主要在于改进程序结构。需要注意的是,重构不是整体重新建造(Reconstructor),可能只是局部的修改(Refactor),如把一些代码抽象成方法,以提高程序结构的粒度,增加重用度。甚至可能只是非常简单的重命名,以改善程序的可读性等。重构对于那些没有做充分的设计而直接编码的软件开发过程而言是非常有效的。,常见的重构方法有:提取方法(Extract Method)、引入父类(Introduce Super Class)或者接口(Interface)、属性和方法上移或者下移(Attribute or Method Move up or Move down)等。重构所影响的程序范围可能很小或者很大,但是即使最小的变化也可能引入bug。一处修改可能导致整个代码变化,所以重构后必须要测试所有可能受影响的地方,这样才能保证对外的功能不会改变。,重构的种类有很多,重构的工作量有时候也会很大,尤其是当现存代码比较多的时候。目前主流的开发环境如Eclipse、Microsoft Visual Studio 2005等,都提供自动进行重构的工具。当然,不同的语言和开发环境提供的重构工具种类会有所不同,下面列举了一些常见的重构方法:(1) try/catch重构:将普通代码块置于try/catch块中,将代码的正常执行过程和错误处理过程分离开来,可以增加代码的安全性和鲁棒性。,(2) 重命名(Rename)和移动(Move):由于各种可能的原因,对现存代码中的包、类、接口、方法、属性或域变量、局部变量等进行重新命名,同时对所有相关的引用(reference)都作相应的修改。该重构还包括移动类和包进行重构的方法。即将包或者类从现有的位置移到另外的包中,或把静态成员从一个类中移到另外的类中。这些重构方法都属于静态的结构重构,也可以在设计阶段针对模型进行。如果在设计阶段进行此类重构,则需要重新进行正向工程(Forward Engineering),而且原来的代码会自动注释掉,需要重新编码。如果在代码环境中进行重构,则会自动实现重构之后的所有代码。,(3) 引入常量、变量和方法(Introduce Constant and Variable,Extract Method):将程序中的常数定义成常量、表达式定义成变量、代码片断定义成方法都是代码结构的改进。也可以把局部变量转换成属性变量,即全局变量。(4) 改变方法参数(Change Method Parameter):方法的参数是方法签名的一部分,是方法调用中动态变化的地方。修改参数实际上就是修改方法签名。在提取方法重构中,可以将方法中表达式代码中的常数、变量或者表达式转换成方法参数,这样该方法就具有更一般的意义。,(5) 泛化类型(Generalize Type):使用父类型代替子类型。这样的程序更具有一般性,更能适应变化。例如:定义一个集合类型变量,使用ArrayList v= new ArrayList(); 重构成List v = new ArrayList(),这样将来代码无论改成List v = new LinkedList()还是List v = new Vector(),其余代码都不受影响。(6) 匿名类转换为内部类(Anonymous class to Inner Class):匿名类转换成内部类,内部类转换成外部类,从而提高可重用性。,(7) 还原方法:是抽取方法(Extract Method)的逆过程。使用“方法调用”可以改善程序结构,但是这样势必要增加调用开销。若某些方法只有一个调用者,就可以把该方法的代码放入调用者,以减少调用开销,改进性能。(8) 封装属性(Encapsulation attribute):是旨在提高程序封装性的一种重构方法。将属性定义成私有的,并提供set/get方法访问。 (9) 属性、方法上移或下移(Attribute Move up):将子类中的属性或者方法上移到父类中,该属性和方法可为所有子类共享;或者将父类中的属性和方法下移到子类中,这样该属性和方法只能为该子类独有。,(10) 提取父类或者接口(Extract Interface):根据类中的公有方法,创建父类或者接口,并让该类继承所提取的父类,或者实现提取的接口。此类重构方法主要适用于当多个客户使用同一个接口的子集(不同的客户使用不同的接口,以保持安全性),或两个类拥有公共父类或公共接口的时候(两个类都继承同一父类或实现同一接口)。例如,下面代码中的MyClass对外提供了三个公共操作接口。class MyClass public void f1() public void f2() public void f3(),假设客户A只能访问f1()方法,客户B只能访问f2()方法,客户C只能访问f3()方法,则应该设计成:MyClass实现三个接口InterfaceA、InterfaceB、InterfaceC:class MyClass implements InterfaceA,InterfaceB,InterfaceC public void f1() public void f2() public void f3()interface Interface A public void f1();Interface Interface B Public void f2();,在客户A的访问代码中,这样声明引用变量,可以保证客户A只能访问f1()方法。InterfaceA aobj = new My Class();aobj.f1();在客户B的访问代码中,这样声明引用变量,可以保证客户B只能访问f2()方法。InterfaceB bobj = new MyClass();bobj.f2();,(11) 创建代理(Proxy):创建多个方法的代理类。具体地说,这种重构要创建一个类(代理类),该类引用被代理的类实例,提供选定的方法接口。通过调用被代理类的方法即可实现该方法。这种重构适合于将一个类的某些方法公开给某些客户,或者用以增加新的方法。创建代理可以实现多种设计模式,如包装(wrapper)、修饰(decorator)、适配器(adapter)等。,(12) 对象工厂方法(Object Factory Method):这种重构将会自动定义创建对象实例的工厂方法。这种代码的优势是可以把创建对象实例的过程标准化、工业化,从而为对象容器和框架提供基础。例如,Spring框架就可以根据在配置文件中定义的类自动实例化对象,支持实现在程序代码之外替换类的功能。除了上述的重构方法外,诸如把顺序结构的代码修改成选择结构或者重复结构、将离散的数据结构转换为整体的集合变量等也属于代码重构的范畴。经过合理重构的代码结构会更加稳定,重用程度会更高,也更容易维护和改进。从开发者的角度来看,无经验的开发人员需要重构的次数明显要比有经验的开发人员多,所以掌握重构技术也是由初学者进化到专家的必由之路。,7.5.3 一个程序结构改进(重构)的例子本节通过计算多个图形面积之和的例子来说明程序结构的改进过程和面向对象技术应用的关系,也具体地说明重构技术的应用方法。问题:计算几种图形的面积之和。例如:圆、矩形、三角形等。初始的程序代码如下:,double s1 = 50 * 50 * 3.1416; /半径为50的圆double s2 = 20 * 30; /长、宽为20、30的矩形double s3 = 0.5 * 10 * 20; /底边为10,高为20的三角形double sum = s1 + s2 + s3;/求和 如果要把该段代码重用于其他计算面积之和的程序中,则几乎没有任何可重用的地方。,第一次改进,数据结构+控制结构:使用数组表示多个图形面积,然后利用循环计算其面积之和。double s = new double3;s0 = 50 * 50 * 3.1416;s1 = 20 * 30;s2 = 0.5 * 10 * 20;double sum = 0.0;for (int i=0;is.length;i+) sum += si;,在这次改进中,有些代码可以用于其他计算面积之和甚至更一般的程序中,如计算多个实数之和的程序中。但这种重用只是通过复制代码来实现的。第二次改进:提取方法,引入变量、参数等。例如:计算圆面积、三角形面积、矩形面积,计算面积之和的方法等。根据简单的几何公式,对于计算圆、矩形、三角形面积及计算多个实数和的方法分别定义如下:,double circleArea( double r) / 计算半径为r的圆面积的方法double rectArea(double a, double b) / 计算长、宽分别为a、b的矩形面积的方法double triangleArea(double s, double h) / 计算底边为s、高为h的三角形面积的方法double tota(Area(double s),如果给定参数的具体值,调用这些方法就可以计算出具体的圆、矩形、三角形的面积并求其面积和。例如:s0 = circleArea(50); / 计算半径是50的圆面积s1 = triangleArea(10,5); / 计算底是10,高是5的三角形面积s2 = rectAtrea(10,20); / 计算宽是10,高是20的矩形面积sum = totalArea(s);现在程序中定义的方法可以用于计算多个圆、多个矩形、多个三角形的面积,以及任意多个面积的和。,试想一下,对于计算三角形面积,还可以已知三角形三边、两边夹角、三角形三个顶点等条件,这样就会增加如下方法:triangleArea(double s1, double s2, double s3)triangleArea(double s1, double s2 , double alfa);triangleArea(double x1,double y1, double x2,double y2, double x3,double y3),对于其他图形也有类似情形,这样计算面积的方法数目也会很快地增长。为了更好地分类这种方法,现在可以引入类,以完成第三次改进,将计算三角形面积的三个方法封装在三角形类中,但此时方法是静态的,也就是说仅仅是命名空间的包装,此时并不包含数据,如图形尺寸的封装。图形尺寸是通过三角形方法的参数传递进去的,此时的方法也可以看成是一些公共的实用方法。,class Triangle public static area(double w, double h) public static area(double s1, double s2, double s3) ,这次改进的结果和上次改进的结果没有本质上的改变,都是结构化的改进,也就是程序的结构是由方法的定义和调用组成的。第四次改进:封装对象。对于上面封装的三角形类来说,只包含一些实用方法,并没有共享三角形的数据。实际上作为一个三角形对象,其状态或者数据是唯一的,所有的方法只是返回其状态或者对外提供服务的。只有定义了数据及对该数据的操作的对象整体,才能更好地反映客观现实,如下面代码:,class Triangle double width, height; public double area() ,此时的类也可以看成是一种自定义数据类型,定义了一组数据及其操作。该类可以派生无数的对象实例,就像使用数据类型可以定义很多变量一样,可以用作定义变量或者分配空间的模板。,类的引入正像数据类型的引入,可以增强程序的安全性,只有同种类型的变量才可以在一起运算。以totalArea(double s)为例,该方法本意是计算多个图形面积和,但实际上只要传递进来的是实数,都可以计算,甚至负数。这是因为方法参数是实型,如果该参数定义为totalArea(Triangle shapes),则该方法只能接收Triangle类型的值,这样就避免了因为错传了其他类型或者其他意义的参数而产生错误。但是这样一来,针对每种图形类型都需要定义一个计算面积和的方法。这是由于类型个性化所带来的问题,增加了语法的严格性和程序安全性,但是降低了效率。可以通过定义每种具体图形类的父类来提取其共性,提高代码效率。,第五次改进:引入父类和继承机制。public Triangle extends Shape public double area()return 0.0;这样计算多个图形面积和的方法就可以定义成如下形式:public double totalArea(Shape s) double sum = 0.0;for(int i=0;is.length;i+) sum += si.area();,该方法的最大特点是可以接收Shape类型或者其子类的所有对象,包括:各种图形对象,该方法实现了计算任意多个任意图形面积和的功能,具有高度的重用性,而且保证了方法的安全性。即方法的参数只能接收图形对象,这要比最初方法的实型参数更安全、更有语义性。类似的结构还可以用于图形系统中的更新画布的算法(绘制任意多个任意图形)中。第六次改进:将父类定义成抽象类或者接口。abstract class Shape abstract double area();,在第五次改进中引入的父类提高了类型的效率,但是也带来了一个问题,那就是父类的方法实现问题。父类Shape是一般的图形类,是一个抽象概念,其计算面积的方法肯定是不存在的或者没办法实现的,在上面代码中令其返回0,实际上是假设或者权宜之计,因为在程序中实际上是没法调用该方法的,因此其实现方法也没有什么意义。因此将其定义成抽象类更合适,这样计算面积的方法就可以定义为抽象的,只给出接口而不给出实现。这时候父类就纯粹变成一种类型了,失去了对象的含义。这种方法可以防止误用父类,因为抽象类不可以创建实例;也可以统一子类接口,子类必须要实现父类的抽象操作,还可以延迟实现,即把操作的实现延迟到子类中去,从而建立更抽象、更通用的程序。,接口的含义和抽象类类似,其所有的操作都是抽象的。接口是用来抽象几乎没有相似性的对象的公共操作机制的。在上述例子中,抽象类是可以利用接口来代替的,表示了计算面积这样的操作接口。interface CanCalculateArea public double area();,改成接口实现后,可以计算面积的对象就不止上面的圆、矩形、三角形等图形,还可以包括长方体、圆柱、锥,甚至实物,如汽车、厂房、零件等。上述程序的主要演化过程用图表示为如图7.15所示。从图7.15中的进化过程可以看出,程序从简单的变量计算结构最终进化到具有继承关系的复杂类结构,程序功能也从只能计算确定的简单图形面积之和,进化到可以计算任意多个任意图形的面积之和。这实际上是一段抽象的程序代码,其模式可以应用于很多情况,例如绘制多个图形、通知所有视图对象、让多个对象都执行同名操作等。这段程序代码的功能就在于遍历对象集合,执行每个对象的同名操作,代码模板如下:for(Object o: objSet) o.operate (),由于对象的继承性和多态性,集合中的对象可以是任意的,只要继承自同一个父类即可。每种对象的同名操作operate的实现是不一样的。代码即体现了对不同对象操作的共性(如计算面积),又通过子类和多态体现了每种对象的个性(每种图形的面积公式是不一样的)。这段代码简洁高效,即保证了安全性,又保证了可扩充性和可维护性,也代表了一种模式(pattern)。从程序结构的角度来看,模式就代表了一种可以解决很多类似问题的抽象程序解决方案,这种抽象的解决方案往往给出的只是程序结构的设计方案,关于模式在7.7节中有较详细的说明。,图7.15 程序结构的演化过程,7.6 面向对象软件工程7.6.1 传统的面向对象软件工程传统的面向对象软件工程是将结构化的软件工程生命周期、管理方法和面向对象技术结合的一种软件工程方法。它将面向对象软件开发过程分成面向对象系统分析、面向对象系统设计、面向对象系统实现、面向对象系统测试等阶段,其核心技术是面向对象建模和面向对象程序设计。,在面向对象建模中一般要建立三种模型:对象模型、动态模型和功能模型。对象模型描述系统中的类及其关系,属于系统的静态结构;动态模型描述在系统功能的实现过程中系统对象之间的交互过程;功能模型类似于系统的高层数据流图,抽象了系统的主要功能。在传统的面向对象软件工程的分析和设计阶段,都需要建立这三种模型,只不过涉及的对象范围、抽象的层次、描述的粒度和细度等有所不同。,在系统分析期间比较关注现实世界的建模。此时,建立起来的对象模型是业务模型或称概念模型。它是对现实世界事物及其关系的直接反映,较少涉及系统实现方法和系统对象的描述(分析阶段涉及到的系统对象多是抽象的,如界面对象等)。在此阶段的对象一般称为业务对象或者分析对象。此时建立的动态模型也是针对在业务功能的实现过程中,业务对象之间交互过程的描述。功能模型则从系统外部或比较高的层次上去抽取系统的功能。,系统设计阶段则要考虑系统的实现方案,比如说确定系统运行模式、考虑系统软件分层架构等,要考虑系统的界面、控制等因素。分析阶段建立的各种模型作为设计阶段的输入,在设计阶段中对前期工作进行系统化的包装,使其更适合于信息系统中的实现。例如给业务对象包装上界面类,将其放在某个系统架构(如基于组件的架构)下进行物理设计等。即使是原来的概念对象模型(业务模型),也需要进一步的综合、优化、分离、抽象(如使用分析模式和设计模式等),以适合于系统的目标和信息系统的特点,满足系统的稳定性、维护性、重用性、移植性、分布性等要求。设计阶段的动态模型则是针对在系统功能执行过程中所有系统对象的交互过程进行建模。功能模型则应该进一步细化成整个系统的功能而不仅仅是业务功能。,可以这样说,分析的任务是将现实世界中或者问题域中的概念抽象出来,提
展开阅读全文