4.3 面向对象的设计
4.3 面向对象的设计
4.3.1面向对象的设计原则
A 面向对象设计的特点
-
▪面向对象设计强调定义软件对象,并且使这些软件对象相互协作来满足用户需求
-
▪面向对象分析和设计的界限是模糊的,从面向对象分析到面向对象设计是一个逐渐扩充模型的过程。分析的结果通过细化直接生成设计结果,在设计过程中逐步加深对需求的理解,从而进一步完善需求分析的结果
-
▪分析和设计活动是一个反复迭代的过程
-
▪面向对象方法学在概念和表示方法上的一致性,保证了各个开发阶段之间的平滑性
B 面向对象设计的四个层次
-
确定系统的总体结构和风格,构造系统的物理模型,将系统划分成不同的子系统
-
中层设计:对每个用例进行设计,规划实现用例功能的关键类,确定类之间的关系
-
进行底层设计:对每个类进行详细设计,设计类的属性和操作,优化类之间的关系
-
补充实现非功能性需求所需要的类
C 注意点
-
面向对象设计与结构化设计的过程和方法完全不同,要设计出高质量的软件系统,记住:
-
对接口进行设计
-
发现变化并且封装它
-
先考虑聚合然后考虑继承【第三点原因:子类对父类的继承是强耦合】
D 强内聚
-
类内聚——设计类的原则是一个类的属性和操作全部都是完成某个任务所必须的,其中不包括无用的属性和操作。
- 例如设计一个平衡二叉树类,该类的目的就是要解决平衡二叉树的访问,其中所有的属性和操作都与解决这个问题相关,其他无关的属性和操作在这里都是垃圾,应该清除。
E 弱耦合
- •在面向对象设计中,耦合主要指不同对象之间相互关联的程度。
- 如果一个对象过多地依赖于其它对象来完成自己的工作,则不仅使该对象的可理解性下降,而且还会增加测试、修改的难度,同时降低了类的可重用性和可移植性。
- 对象不可能是完全孤立的,当两个对象必须相互联系时,应该通过类的公共接口实现耦合,不应该依赖于类的具体实现细节。
耦合方式
-
交互耦合
- 即对象之间的耦合是通过消息连接来实现的。在设计时应该尽量减少对象之间发送的消息数和消息中的参数个数,降低消息连接的复杂程度。
-
继承耦合
- •继承耦合是一般化类与特殊化类之间的一种关联形式,设计时应该适当使用这种耦合。在设计时要特别认真分析一般化类与特殊化类之间继承关系,如果抽象层次不合理,可能会造成对特殊化类的修改影响到一般化类,使得系统的稳定性降低。另外,在设计时特殊化类应该尽可能多地继承和使用一般化类的属性和服务,充分利用继承的优势。
F 可重用性
- 软件重用是从设计阶段开始的,所有的设计工作都是为了使系统完成预期的任务,为了提高工作效率、减少错误、降低成本,就要充分考虑软件元素的重用性。
- 重用性有两个方面的含义:
- 设计一个可重用的软件比设计一个普通软件的代价要高,但是随着这些软件被重用次数的增加,分摊到它的设计和实现成本就会降低。
- 尽量使用已有的类,包括开发环境提供的类库和已有的相似的类;
- 如果确实需要创建新类,则在设计这些新类时考虑将来的可重用性。
H 框架
-
框架是一组可用于不同应用的类的集合。框架中的类通常是一些抽象类并且相互有联系,可以通过继承的方式使用这些类。
- 例如,Java应用程序接口(API)就是一个成功的框架包,为众多的应用提供服务,但一个应用程序通常只需要其中的部分服务,可以采用继承或聚合的方式将应用包与框架包关联在一起来获得需要的服务。
-
一般不会直接去修改框架的类,而是通过继承或聚合为应用创建合适的GUI类。
面向对象基本设计原则
-
开闭原则
-
Liskov替换原则
-
依赖倒置原则
-
接口分离原则
-
发布复用等价性原则
-
共同封装原则
-
共同复用原则
开闭原则(The Open-Closed Principle,OCP)
•组件应该对外延具有开放性,对修改具有封闭性
•例如:类类型的扩充(增加动作传感器)
里氏(Liskov)替换原则
-
从基类导出的类传递给组件时,使用基类的组件应该仍然能够正确完成其功能
- Java运行时(动态)多态体现
依赖倒置原则
- 依赖于抽象,而非具体实现。
- 例:OCP原则的例子
接口分离原则
- 多个客户专用接口比一个通用接口好
- 目标:去除不必要的接口方法依赖
例:接口污染:有太多的不必要的接口方法
例:接口分离的设计
发布复用等价性原则
- 复用的粒度就是发布的粒度
- 以包为单位,将可复用的类分组打包成能管理和控制的包并作为一个更新的版本,而不是对每个类分别进行升级
共同封装原则
- 应该将易变的类放在同一个包里,将变化隔离出来
共同复用原则
- 根据内聚性进行分组,只有那些一起被复用的类才应该包含在一个包中
4.3.2 面向对象的架构设计
面向对象设计的活动
面向对象的架构设计
-
架构设计的目的是要勾画出系统的总体结构,这项工作由经验丰富的架构设计师主持完成。
-
输入:用例模型、分析模型。
-
输出:物理结构、子系统及其接口、概要的设计类。
架构设计的步骤
第1步:构造系统的物理模型
-
首先用UML的配置图(部署图)描述系统的物理架构
-
将需求分析阶段捕获的系统功能分配到这些物理节点上。
-
配置图上显示的内容:计算节点的拓扑结构、硬件设备配置、通信路径、各个节点上运行的系统软件配置、应用软件配置。
- 一个图书馆信息管理系统的物理模型如后图所示。
第2步:设计子系统
-
对于一个复杂的软件系统来说,将其分解成若干个子系统,子系统内还可以继续划分子系统或包。
-
每个子系统与其它子系统之间应该定义接口,在接口上说明交互信息,注意这时还不要描述子系统的内部实现。
-
可用UML组件图表示。
•1、划分各个子系统的方式
- 按照功能划分,将相似的功能组织在一个子系统中;
- 按照系统的物理布局划分,将在同一个物理区域内的软件组织为一个子系统;
- 按照软件层次划分子系统,软件层次通常可划分为用户界面层、专用软件层、通用软件层、中间层和数据层,如下,图书馆图书信息管理系统层次划分:
图书馆图书信息管理系统层次划分:
2、定义子系统之间的关系:
子系统之间的关系:
- “请求-服务”关系,“请求”子系统调用“服务”子系统,“服务”子系统完成一些服务,并且将结果返回给“请求”子系统。
- 平等关系,每个子系统都可以调用其它子系统。
- 如果子系统的内容相互有关联,就应该定义它们之间的依赖关系。在设计时,相关的子系统之间应该定义接口,依赖关系应该指向接口而不要指向子系统的内容。
注意:
- 如果两个子系统之间的关系过于密切,则说明一个子系统的变化会导致另一个子系统变化,这种子系统理解和维护都会比较困难。
- 解决子系统之间关系过于密切的办法基本上有两个:
- 重新划分子系统,这种方法比较简单,将子系统的粒度减少,
- 重新规划子系统的内容,将相互依赖的元素划归到同一个子系统之中;
- 解决子系统之间关系过于密切的办法基本上有两个:
- 定义子系统的接口,将依赖关系定义到接口上;
- 分析阶段定义了整个系统的非功能需求,在设计阶段要研究这些需求,设计出可行的方案。
- 非功能需求包括:系统的安全性、错误监测和故障恢复、可移植性和通用性等等。具有共性的非功能需求一般设计在中间层和通用应用层,目的是充分利用已有构件,减少重新开发的工作量。
3、定义子系统的接口
- 每个子系统的接口上定义了若干操作,体现了子系统的功能,而功能的具体实现方法应该是隐藏的,其他子系统只能通过接口间接地享受这个子系统提供的服务,不能直接操作。
4.3.3面向对象设计 类
根据分析阶段产生的高层类图和交互图,由用例设计师研究已有的类,将它们分配到相应的用例中。检查每个用例功能,依靠当前的类能否实现,同时检查每个用例的特殊需求是否有合适的类来实现。细化每个用例的类图,描述实现用例的类及其类之间的相互关系,其中的通用类和关键类可用粗线框区分,这些类将作为项目经理检查项目时的重点。
类
•类是包含信息和影响信息行为的逻辑元素。类的符号是由三个格子的长方形组成,有时下面两个格子可以省略。最顶部的格子包含类的名字,类的命名应尽量用应用领域中的术语,有明确的含义,以利于开发人员与用户的理解和交流。中间的格子说明类的属性。最下面的格子是类的操作行为。
类间关系
分析类图
如何找实体类?
•实体类用于对必须存储的信息和相关行为进行建模
实体类源于业务模型中的业务实体,但出于对系统结构的优化,可以在后续的过程中被分拆、合并
如何找边界类?
边界类是一种用于对系统外部环境与其内部运作之间的交互进行建模的类。用于描述外部参与者与系统之间的交互,对系统中依赖于环境的那些部分进行建模。
- •参与者与用例之间应当建立边界类
- 用例与用例之间如果有交互,应当为其建立边界类
- 如果用例与系统边界之外的非人对象有交互,应当为其建立边界类
- 在相关联的业务对象有明显的独立性要求,即它们可能在各自的领域内发展和变化,但又希望互不影响时,也应当为它们建立边界类
如何找控制类?
- •控制类来源于对用例场景中动词的分析和定义
- •控制类主要起到协调对象的作用,例如从边界类通过控制类访问实体类,或者实体类通过控制类访问另一个实体类。
- •如果用例场景中的行为在执行步骤、执行要求或者执行结果上具有类似的特征,应当合并或抽取超类
细化用例
面向对象设计活动之三:详细设计一个类
由构件工程师详细设计每个类的属性、方法和关系。
第一步:定义类的属性
•用所选择的编程语言定义每个类的属性。类的属性反映类的特性,通常属性是被封装在类的内部,不允许外部对象访问。
•注意点:
•分析阶段和概要设计阶段定义的一个类属性在详细设计时可能要被分解为多个,减小属性的表示粒度有利于实现和重用。但是一个类的属性如果太多,则应该检查一下,看能否分离出一个新的类。
•如果一个类因为其属性的原因变得复杂而难于理解,那么就将一些属性分离出来形成一个新的类。
•通常不同的编程语言提供的数据类型有很大差别,确定类的属性时要用编程语言来约束可用的属性类型。定义属性类型时尽可能使用已有的类型,太多的自定义类型会降低系统的可维护性和可理解性等性能指标。
•类的属性结构要坚持简单的原则,尽可能不使用复杂的数据结构。
第二步:定义类的操作
•由构件工程师为每个类的方法设计必须实现的操作,并用自然语言或伪代码描述操作的实现算法。一个类可能被应用在多个用例中,由于它在不同用例中担当的角色不同,所以设计时要求详细周到。
•注意事项:
•分析类的每个职责的具体含义,从中找出类应该具备的操作。
•阅读类的非功能需求说明,添加一些必须的操作。
•确定类的接口应该提供的操作。这关系到设计的质量,特别是系统的稳定性,所以确定类接口操作要特别小心。
•逐个检查类在每个用例实现中是否合适,补充一些必须的操作。
•设计时不仅要考虑到系统正常运行的情况,还要考虑一些特殊情况,如中断/错误处理等。
第三步:定义类之间的关系
•设置基数:一个类的实例与另一个类的实例之间的联系。在图书馆信息管理系统中,“图书”类和“读者”类关联,如果需求说明中有“一位读者可借图书的数量为0至10本”,那么它们之间的基数为1:0..10。
•使用关联类:可以放置与关联相关的属性。例如 “图书”类和“读者”类,如果要反映读者的借书情况,该如何处理呢?可以创建一个关联类,这个类中的属性是“借书日期”。
对象的创建和撤销
4.3.4 UML顺序图
什么是顺序图?
•顺序图是强调消息时间顺序的交互图。
•顺序图描述了对象之间传送消息的时间顺序,用来表示用例中的行为顺序。
•顺序图将交互关系表示为一个二维图。即在图形上,顺序图是一张表,其中显示的对象沿横轴排列,从左到右分布在图的顶部;而消息则沿纵轴按时间顺序排序。创建顺序图时,以能够使图尽量简洁为依据布局。
建立顺序图的步骤
购买小车的顺序图示例
什么时候会用到顺序图?
顺序图的组成
•顺序图包含4个元素:
•对象(Object)
•生命线(Lifeline)
•消息(Message)
•激活(Activation)
对象
•顺序图中对象的符号和对象图中对象所用的符号一样。
•将对象置于顺序图的顶部意味着在交互开始的时候对象就已经存在了,如果对象的位置不在顶部,那么表示对象是在交互的过程中被创建的。
•参与者和对象按照从左到右的顺序排列一般最多两个参与者,他们分列两端。启动这个用例的参与者往往排在最左边;接收消息的参与者则排在最右端;对象从左到右按照重要性排列或按照消息先后顺序排列。
对象的命名
生命线
- 每个对象都有自己的生命线,用来表示在该用例中一个对象在一段时间内的存在
- 生命线使用垂直的虚线表示
- 如果对象生命期结束,则用注销符号 X 表示
- 对象默认的位置在图顶部,表示对象在交互之前已经存在
- 如果是在交互过程中由另外的对象所创建,则位于图的中间某处。
激活
•激活表示该对象被占用以完成某个任务,去激活指的则是对象处于空闲状态、在等待消息。
•在UML中,为了表示对象是激活的,可以将该对象的生命线拓宽成为矩形。其中的矩形称为激活条(期)或控制期,对象就是在激活条的顶部被激活的,对象在完成自己的工作后被去激活。
激活期
-
当一条消息被传递给对象的时候,它会触发该对象的某个行为,这时就说该对象被激活了。
-
在UML中,激活用一个在生命线上的细长矩形框表示。
-
矩形本身被称为对象的激活期或控制期,对象就是在激活期顶端被激活的。
-
激活期说明对象正在执行某个动作。
-
当动作完成后,伴随着一个消息箭头离开对象的生命线,此时对象的一个激活期也宣告结束。
消息
- 面向对象方法中,消息是对象间交互信息的主要方式。
·在任何一个软件系统中,对象都不是孤立存在的,它们之间通过消息进行通信。
·消息是用来说明顺序图中不同活动对象之间的通信。因此,消息可以激发某个操作、创建或撤销某个结构化程序设计中,模块间传递信息的方式主要是过程(或函数)调用。 - 结构化程序设计中,模块间传递信息的方式主要是过程(或函数)调用。
·对象A向对象B发送消息,可以简单地理解为对象A调用对象B的一个操作(operation)。 - 在顺序图中,消息是由从一个对象的生命线指向另一个对象的生命线的直线箭头来表示的,箭头上面丕可以表明要发送的消息名及序号。
·顺序图中消息编号可显示,也可不显示。协作图中必须显示。 - 顺序图中,尽力保持消息的顺序是从左到右列的。在各对象之间,消息的次序由它们在垂直轴上的相对位置决定。
·—个顺序图的消息流开始于左上方,消息2的位置比消息1低,这意味着消息2的顺序比消息1要迟。因为西方的阅读习惯是从左到右。 - 在UML中,消息使用箭头来表示,箭头的类型表示了消息的类型。
消息类型
- 简单消息
- 同步消息
- 异步消息
- 反身消息
- 返回消息
同步消息
- 同步消息最常见的情况是调用,即消息发送者对象在它的一个操作执行时调用接收者对象的一个操作,此时消息名称通常就是被调用的操作名称。
- 当消息被处理完后,可以回送一个简单消息,或者是隐含的返回。
异步消息
- 表示发送消息的对象不用等待回应的返回消息,即可开始另一个活动。
- 异步消息在某种程度上规定了发送方和接收方的责任。
即发送方只负责将消息发送到接收方,至于接收方如何响应,发送方则不需要知道。
对接收方来说,在接收到消息后它既可以对消息进行处理,也可以什么都不做。
反身消息
-
顺序图建模过程中,一个对象也可以将一个消息发送给它自己,这就是反身消息。
-
如果一条消息只能作为反身消息,那么说明该操作只能由对象自身的行为触发。
-
这表明该操作可以被设置为private属性,只有属于同一个类的对象才能够调用它。
-
在这种情况下,应该对顺序图进行彻底的检查,以确定该操作不需要被其他对象直接调用。
返回消息
•返回消息是顺序图的一个可选择部分,它表示控制流从过程调用的返回。
•返回消息一般可以缺省,隐含表示每一个调用都有一个配对的调用返回。
•是否使用返回消息依赖于建模的具体/抽象程度。如果需要较好的具体化,返回消息是有用的;否则,主动消息就足够了。
顺序图示例:存款用例的顺序图
顺序图示例:借阅图书用例的顺序图
借阅图书的过程为:
- 图书管理员选择菜单项“借阅图书”,弹出BorrowDialog对话框;
- 图书管理员在该对话框中输入借阅者信息·然后由系统查询数据库,以验证该借阅者的合法性
- 若借阅者合法,则再由图书管理员输入所要借阅的图书信息,系统记录并保存该借阅信息。