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