UML2面向对象分析与设计(第2版)
上QQ阅读APP看书,第一时间看更新

1.4 面向对象技术的相关原则

在介绍完对象和类的基本概念后,本节将介绍面向对象技术的几个重要的相关原则。对象和类作为面向对象技术的核心,存在着很多与之相关的原则,这些原则决定了面向对象技术的本质特征,只有遵循了这些原则,才是一个符合面向对象技术的方案。

1.4.1 抽象

世界是复杂的,为了处理这种复杂性,需要将其中的内容抽象化。抽象(Abstraction)的过程就是揭示事物区别于其他事物的本质特征的过程,是一个分析和理解问题的过程,这个过程取决于使用者的目的,它应该包括使用者所感兴趣的那些职责问题,而忽略掉其他不相关的部分。从对象到类的过程就是抽象的过程,即将所见到的具体实体抽象成概念,从而可以在计算机世界中进行描述和对其采取各种操作。

抽象过程并没有唯一的答案,同一个实体在不同的业务场景中可能有不同的抽象。同样是一批人,根据使用选课系统的目的不同,可以将其中的一部分人抽象为老师,而将另一部分人抽象为学生,这个过程与具体应用场景密切相关。这也是面向对象系统中最难应用的一个关键技术。

关于抽象的概念,这里可以举一个简单的例子。例如,针对“我想买一斤水果”这件事,根据不同人的喜好,会产生不同的实例(有人可能买了一斤香蕉,有人可能买了一斤苹果,或买了一斤橘子,这都是满足要求的)。在这里,“水果”就是一个抽象,通过这个抽象概念,可以代表很多种不同的情况,从而适应不同人的胃口,而实际上并不存在水果这个实体(即对象)。面向对象的系统也是这样的,通过抽象技术,可以使软件能够快速适应不断变更的需求。

1.4.2 封装

封装(Encapsulation)是指对象对其访问者隐藏具体的实现,它是软件模块化思想的体现。

通过封装实现信息隐藏和数据抽象。信息隐藏的出发点是对象的私有数据不能被外界存取,从而保证外界以合法的手段(对象所提供的操作)访问。同时,将数据抽象为一组行为,而不是内部的具体数据结构,把用户隔离在实现细节之外,从而使得软件各个部分依赖于抽象层,各模块获得自由。

通过封装还可以保证数据的一致性。使用传统的结构化方法,是很难保证这一点的。例如,邮政地址由地址和邮政编码两部分组成,而这两部分信息应该是一致的,北京市市区的邮政编码应该为100×××,上海市市区的邮政编码应该为200×××,如果一个北京的地址对应的邮政编码为200001,这肯定是不正确的,会造成系统异常。因此,为避免这种情况出现,所有操作这个数据结构的程序员必须严格遵守一系列业务逻辑规则;否则,很容易破坏数据的一致性。而这在处理大型项目、多人协同开发项目时,是很难保证的。面向对象的封装就能够保证这一点,外部用户并不直接操作这些属性,而是通过特定的操作来完成指定的运算,外界只知道操作的接口,而不关注具体的业务逻辑规则,从根本上杜绝了数据的不一致问题(见下面的代码)。

    class ShippingAddress{
        private long cityCode;
        private string address;
        public longModifyAddress(String address)
    }

1.4.3 分解

分解(Decomposition)是指将单个大规模复杂系统划分为多个不同的小构件。分解后的构件通过抽象和封装等技术形成相对独立的单元,这些单元可以独立地设计和开发,从而实现化繁为简、分而治之,以应对系统的复杂性,降低软件开发成本。

在传统的结构化方法中,开发人员可以通过函数、模块等进行功能分解,实现模块化设计,可以通过耦合和内聚来判断分解的合理性,将系统分解为多个高内聚、低耦合的模块。而面向对象的分解则更为复杂,在基于类和对象分解的基础上,还需要进一步考虑类之间依赖程度、复用问题和稳定性问题等,进行合理的打包和分层,从而形成更加复杂的分解结构。

抽象、封装和分解是系统设计中3个最基本的原则,它们相辅相成。一个对象围绕着单一的抽象概念建立了一个封装体,而系统则可以被分解为多个对象,并对这些对象进行进一步打包,从而形成更高层的抽象概念。

1.4.4 泛化

泛化(Generalization)是类与类之间一种非常重要的关系,通过这种关系,一个类可以共享另外一个或多个类的结构和行为。为了实现泛化关系,我们引入了继承(Inheritance)机制。一个子类(Subclass)继承一个或多个父类(Superclass),从而实现了不同的抽象层次。这些层次之间所建立的is a或is kind of关系,即为泛化关系。通过这种关系可以很容易地复用已经存在的数据和代码,并实现多态处理。根据父类的个数不同,存在着单一继承和多重继承两种情况。

单一继承(Single Inheritance)是指一个类继承另外一个类,图1-6展示了两个单一继承的实例,类Saving和类Account、类Checking和类Account通过单一继承构成两个泛化关系,表明一个存储账户(Saving)是一种账户(Account),一个支出账户(Checking)也是一种账户;它们都包含账户的信息(账号no、用户名name、余额balance),也都可以进行取款(Withdraw)操作。

图1-6 单一继承

多重继承(Multiple Inheritance)是指一个类继承另外多个类的属性和行为。如图1-7所示,类Bird同时继承类FlyingThing和类Animal,这是一个多重继承,表明鸟(Bird)即是一种飞行物(FlyingThing),又是一种动物(Animal)。

图1-7 多重继承

在实际系统应用中,对多重继承的使用一定要谨慎。因为有些编程语言(如Java)不支持多重继承,这会造成设计方案无法被实现。此外,即使像C++这样支持多重继承的语言,在实际应用过程中也会存在诸如名称冲突、二义性等问题。

泛化关系提供了有效的复用手段,那么在实际应用中,一个子类到底继承了父类的什么元素呢?继承后的子类又可以进行什么样的操作呢?

可以这样认为,一个子类会继承父类所有的元素(可能有些元素对于子类不可见),这包括属性、操作和关系。此外,子类还可以根据自己的需要添加额外的属性、操作或关系,还可对父类已有的操作进行重新定义。

图1-8展示了一种继承层次关系,其中子类(GraduateStudent)从父类(Student)继承,它继承了父类全部属性和操作,所以即使GraduateStudent中没有定义getName(),其也会从Student中得到getName()方法的全部实现。此外,子类也会继承父类中的关系,因此GraduateStudent与Account也有聚合关系。

图1-8 继承层次关系

1.4.5 多态

多态(Polymorphism)是在同一外表(接口)下表现出多种行为的能力,它是对象技术的根本特征,是将对象技术称为面向对象的原因所在。对象技术正是利用多态提供的动态行为特征,来封装变化、适应变更,以达到系统的稳定目标。

图1-9展示了一个多态的应用案例。面向对象的多态必须要有泛化关系的支持(有的文献会把模板这种机制也称为多态,这种参数化多态不需要泛化关系支持),如Rectangle(矩形)和Circle(圆形)均继承自Shape(Shape以斜体字表示,表明该类是一个抽象类)。通过Shape提供的接口draw()实现画图功能的多态性,即根据目标的不同画出不同的形状。

图1-9 通过泛化支持多态

现在假设有一个数组sharr,其中放着一排形状Shape,但不知道哪些是矩形,哪些是圆形。利用多态性,完全可以不关注这些细节,而直接画出目标形状(见下面代码)。

    for(int i=0; i  sharr.length; ++i){
        Shape shape=(Shape)sharr[i];
        shape.draw();
    }

在遍历整个数组的过程中,各个Shape知道应当如何在画布上绘制自己的形状。shape.draw()这行代码在Shape指向不同的对象时将表现出不同的行为,这就是所谓多态。

1.4.6 分层

通过分解和抽象可以很容易对系统进行划分。然而即使是简单的应用,也可能很难一次性地完成系统的分解——无法想象一次性地将系统分解为几十个,甚至上百个类。人们往往首先将系统分解成几个独立的部分(如先划分为若干层或若干模块),然后在此基础上对每个部分再进一步分解小的部分,这些小的部分有的还可以进一步分解,直至形成最小的独立单元(如类或函数)。这种逐级分解的思想就是分层。

分层(Hierarchy)是指面向不同的目标建立不同的抽象级别层次,从而在不同的抽象层次对系统进行分解,进一步简化对系统的理解。在面向对象系统中,主要有两种层次结构:类层次结构和对象层次结构。

类层次结构是指在不同的抽象级别进行对象的抽象,高层的类抽象层次更高,其描述能力也越强,而越往下抽象层次越低,底层的类则最具体,代表具体的事物。这些类之间通过泛化关系形成一种层次结构,也称为继承层次结构。此外,在这种层次结构中,一般同一层次的抽象级别是一样的。图1-10展示了一个继承层次结构的实例,最高层的父类(Food)抽象层次最高,代表所有类型的食物(Food);第二层的类(Fruit、Vegetable和Meat)则相对要具体一些,代表某一类食物,如水果(Fruit);而最低层的类抽象层次最低,为具体类,可以实例化对象,本图中代表具体的食物类型,如苹果(Apple)、橙子(Orange)等。前两层的类都是抽象类,不能构造具体的对象(UML类图中用斜体字表示)。

图1-10 类的继承层次结构

类的继承层次结构是面向对象系统中最普遍的结构,通过这种层次结构,可以分门别类地描述各类事物。很多设计良好的面向对象系统都是基于这种层次结构而构造的。

对象层次结构是指对象间的组成结构,即大的对象由小的对象组成(即分解成小的对象)。这种结构是通过类之间的聚合关系来实现的有关类之间聚合关系的概念。参见本书第5.5.3小节。,也称为聚合层次结构。这一种整体和部分的关系是逐层分解思想的具体体现。图1-11给出了一个对象的聚合层次结构的实例,大学(University)由学院(School)和管理部门(Administration)组成,而学院又包含多个系(Department),每个系又由班级(Class)组成;管理部门则包括多个办公室(Office)。

图1-11 对象的聚合层次结构

1.4.7 复用

复用(Reuse)是借助于已有软件的各种有关知识建立新的软件的过程,以缩减新软件开发和维护的成本。将软件看成是由不同功能部分的构件所组成的有机体,每个构件在设计编写时可以被设计成完成同类工作的通用工具,如果完成各种工作的构件被建立起来以后,编写特定软件的工作就变成了将各种不同构件进行组合的简单问题,从而对软件产品的最终质量和维护工作都有本质性的改变。第1.2.2小节把“复用”视为面向对象所带来的优势之一,而事实上要获得这种优势,在设计时就需要遵循复用的原则,设计可复用的构件。

在系统开发的各个阶段都可能涉及复用,如从最低层的代码复用到设计复用、架构复用,再到需求复用,甚至于延伸到特定业务领域的复用。复用原则要求设计者不仅针对当前的业务需求开展设计,还需要考虑业务的通用性和可扩展性等问题,从而设计抽象层次高、复用粒度大的组件。本书将在第6~7章中介绍一些具体用于可复用设计的原则和模式。