面向对象的软件工程
-
- 1 面向对象的演化
- 2 对象模型
- 3 重新认识类和对象
- 4 面向对象分析与设计
- 5 统一建模语言
- 6 项目开发的过程
1 面向对象的演化
1.1 生活中复杂系统的特点
1. 复杂系统是有层次化结构的,而且这种层次也代表了不同的抽象级别,一层构建于另一层之上,每一层都可以分开来理解。同一层抽象中的所有部分之间,以某种定义良好的方式来进行交互。
2. 给定抽象层的内外之间总有清晰的边界,在不同抽象层的不同部分之间,存在着清晰的关注点分离。
3. 复杂系统中,往往上层抽象的行为要大于下层抽象行为之和,在复杂性科学中把这种称为突现行为。
4. 一种非凡的共性以普适机制的方式,统一了这种宏大的层次结构。
1.2 软件系统的复杂性
1.2.1 复杂性的四个方面
1.2.1.1 问题域的复杂性
问题域的复杂性主要来源于系统用户和系统开发者之间的沟通困难,用户常常很难用开发者能够理解的形式对他们的需求给出准确的描述。
1.2.1.2 管理开发的困难性
对于开发团队来说,主要的管理挑战主要来自于维持设计的一致性和完整性。
1.2.1.3 软件中的灵活性
1.2.1.4 描述离散系统行为
1.2.2 复杂系统的五个属性
1.2.2.1 层次结构
复杂性常常以层次结构的形式存在,复杂的系统由一些相关的子系统构成,这些子系统又有自己的子系统,如此下去,直到达到某种最低层次的基本组件。一个系统所提供的价值肯定来自各个组成部分之间的相互关系,而不是来自单个的组成部分。
1.2.2.1.1 对象结构
对象结构也称为“组成部分”的层次结构,对象结构很重要,因为它展示了不同的对象之间如何通过一些交互模式进行协作(对象所拥有的方法进行消息传递)。
1.2.2.1.2 类结构
类结构也称为“是一种”的层次结构。类结构强调了系统中的公共结构行为。在类结构中我们根据抽象明确地区分了不同对象的共同属性和特有属性。在高级语言中,类结构为抽象继承。
1.2.2.1.3 对象结构和类结构的关系
类结构和对象结构不是完全独立的,对象结构中的每个对象都代表了某个类的一个具体实例。我们将系统的类结构和对象结构统称为系统的架构。在可能的多种系统组件结构、复杂系统的五种属性以及系统用户的需求之间寻找平衡点是系统架构具有挑战性的原因,经验表明,最成功的的复杂软件系统就是在设计中包含了深思熟虑的类结构和对象结构,并具备复杂系统的五个属性。
1.2.2.2 相对本原
选择那些作为系统的基础组件相对来说比较随意,这在很大程度上取决于系统观察者的判断。对于一个观察者来说很基础的东西,对另一个观察者可能具有很高的抽象层次。
1.2.2.3 关注点分离
组件内的联系通常比组件间的联系更强,这一事实实际上将组件中高频率的动作(涉及组件内部的交互)和低频率的动作(涉及组件间的交互)分离开来。让我们能够以相对隔离的方式来研究每个部分。
1.2.2.4 共同模式
层次结构通常只是由少数不同类型的子系统按照不同的组合和安排方式构成。
1.2.2.5 稳定的中间形式
复杂系统毫无例外都是从能工作的简单系统演变而来的,从头设计的复杂系统根本不能工作,永远也不能第一次就正确打造出这些基础对象,必须在上下文环境中使用它们,然后随着时间的推移不断改进它们,因为我们对系统的真实行为了解的越来越多。
1.3 软件系统的设计
1.3.1 需求分解
1.3.1.1 算法分解
自顶向下的结构化设计,也是我们在生活中做事的方式,对于一件事第一步干啥,第二步干啥,算法的分解强调的是顺序。在这个事件处理过程中我们的视角应该是第一人称视角。
1.3.1.2 面向对象分解
面向对象的分解是根据问题领域中的关键抽象概念对系统进行分解,面向对象分解的底层概念是设计者应该将系统建模为一组协作的对象,将单个对象作为类的实例,而类之间具有层次关系。面向对象的观点强调了一些代理,它们要么发出动作,要么是这些操作执行的对象。在这个事件处理过程中我们的视角应该是上帝视角。面向对象分解通过复用共同的机制,得到一些较小的系统,从而提供了重要的表达经济性。面向对象系统在应对变化时也更有弹性,从而能够随时间演变,因为他们的设计是基于稳定的中间状态的。
1.3.2 设计的目的
构建一个如下的系统
1. 满足给定的(可能是非正式的)功能规格说明。
2. 符合目标介质的限制。
3. 满足隐含和明确地性能及资源使用需求。
4. 满足隐含和明确地关于产品形式方面的设计限制条件。
5. 满足对设计过程本身的限制条件,如时间、费用、或进行设计可用的工具。
设计的目的是创建一个干净的、相对简单的内部结构,有时候也称为架构,一份设计是设计过程的最终产物。
1.3.3 软件设计方法的共同要素
1. 表示法:表达每个模型的语言。在面向对象的设计中就是UML统一建模语言。
2. 过程:导致有序构建系统模型的过程。
3. 工具:消除建模中的枯燥工作并强制实现模型本身的规则的工具,可以揭示错误和不一致性。在面向对象的设计中就是可以画出UML各种图的工具。
2 对象模型
对象模型包括抽象、封装、模块化、层次结构、类型、并发和持久等原则。
2.1 对象模型的演进
面向对象开发不是自发产生的,他是建立在以前技术的最佳思想之上的。第一代程序设计语言的基本物理构成单元只包含全局数据和子程序,全局数据对所有子程序共享。设计者虽然可以在逻辑上划分全局数据到不同的子程序上,但是没有任何机制来强制确保这些设计决策,程序某个部分的错误可能给系统带来毁灭性的影响。第二代程序设计语言在前一代增加了参数传递机制并且把子程序作为一种抽象,发展出了结构化程序设计语言,解决了第一代程序中全局数据对所有子程序共享的问题。但是在日益增长的大规模程序设计方面仍然显得无能为力。第三代程序设计语言在前一代的基础上增加了模块化来应对大规模程序设计的问题,模块可以独立编译,模块很少被看作一种重要的抽象机制,在实践中,它们只是用于对最有可能同时改变的子程序分组。这一代语言中的大部分虽然支持模块化结构,但是很少有规则要求模块间接口的语义一致性。因为这些语言中的大部分对数据抽象和强类型的支持都不好,这样的错误只有在执行程序时才能被检测出来。数据抽象对于把握复杂性是很重要的,通过过程可以实现的抽象在本质上很适合描述抽象操作,但并不太适合描述抽象的对象。数据驱动的方法和类型概念的理论出现对于基于对象和面向对象的程序设计语言提供了巨大的帮助。
这类语言的构建块是模块,它表现为逻辑上的一组类或对象,在大型系统中,一组抽象可以构建在另一组抽象层之上,在任何一个抽象层上,都可以找到某些有意义的对象,他们可以协作实现更高层的行为。如果我们仔细看一组对象的实现,会看到另一组协作的抽象。
2.2 对象模型的基础
面向对象分析和设计代表了一种演进式的开发,而不是一种革命性的开发。面向对象设计方法利用类和对象作为基本构建块,指导开发者探索基于对象和面向对象编程语言的能力。
对象统一了算法抽象和数据抽象,在这一思想下,可以将对象定义为“包含了属性、过程和数据的实体,它们执行计算并保存局部的状态”。在对象模型中,重点在于灵活的刻画物理系统或者抽象系统的组件,存在一些不变的特征,这些特征刻画了一个对象和它的行为,对象只能按照适合它的方式来改变状态、改变行为、实现操作或与其他对象发生联系。
2.2.1 面向对象和基于对象的区别
基于对象和面向对象区别主要是看这种语言是否支持继承,如果一种语言不支持继承,那么它就是基于对象的,而不是面向对象的。
2.2.2 面向对象分析(OOA)
面向对象分析是一种分析方法,这种方法利用从问题域的词汇表中找到的类和对象来分析需求。
2.2.3 面向对象设计(OOD)
面向对象设计是一种设计方法,包括面向对象分解的过程和一种表示法(UML统一建模语言),这种表示法用于展现被设计系统的逻辑模型、物理模型、静态模型以及动态模型等等。
2.2.4 面向对象编程(OOP)
面向对象编程是一种实现的方法,在这种方法中,程序被组织成许多组相互协作的对象,每个对象代表某个类的一个实例,而类则属于一个通过继承关系形成的层次结构。
2.2.5 OOA、OOD、OOP之间的关系
面向对象分析的结果可以作为开始面向对象设计的模型,面向对象设计的结果可以作为蓝图,利用面向对象编程方法最终实现一个系统。
2.2.6 概念模型、逻辑模型和物理模型
- 概念模型记录了系统中存在(或将存在)的领域实体以及它们与系统中其它领域实体的关系。概念模型的建模是利用业务领域的术语来完成的,是技术无关的。
- 逻辑模型利用了概念模型中创造的概念,建立起关键抽象和机制的意义,并确定系统的架构和整体设计。
- 物理模型描述了系统实现的具体软件和硬件构成。
2.3 对象模型的要素
2.3.1 抽象
抽象描述了一个对象的基本特征,可以将这个对象与所有其他类型的对象区分开来,因此提供了清晰定义的概念边界,它与观察者的视角有关。
2.3.1.1 特点
- 抽象来自于对真实世界中特定对象、场景或处理的相似性的认知,并决定关注这些相似性而忽略不同之处。
- 抽象是对一个系统的一种简单的描述,强调系统的某些细节或属性同时抑制另一些细节或属性。
- 只有当一个概念可以独立于最终使用和实现它的机制来描述、理解和分析时,我们才说这个概念是抽象的。
- 抽象关注一个对象外部视图,所以可以用来分离对象的基本行为和它的实现。这种行为/实现的分界称为抽象壁垒,它是通过应用“最少承诺”原则来达成的。根据这个原则,对象的接口只提供它的基本行为,此外别无其他。还有另一个原则,我们称之为“最少惊奇”原则,这个原则是指抽象捕捉了某个对象的全部行为,不多也不少,并且不提供抽象之外的惊奇效果。
- 把一个对象可以调用的整个操作集以及这些操作合法的调用顺序称为它的协议,协议表明了对象的动作和反应的方式,从而构成了抽象的完整静态和动态外部视图。对于对象的每个操作,可以定义前置条件和后置条件,如果调用出现异常表明某个不变量没有满足或不能满足,某些语言允许对象抛出异常,这样就可以中断处理,向其他对象报告问题,然后这些对象就可以捕获异常并处理问题。
2.3.1.2 抽象分类
2.3.1.2.1 实体抽象
一个对象代表了问题域或解决方案域实体的一个有用的模型。
2.3.1.2.2 动作抽象
一个对象提供了一组通用的操作,所有这些操作都执行同类的功能。
2.3.1.2.3 虚拟机抽象
一个对象集中了某种高层控制要用到的所有操作,或者这些操作将利用某种更底层的操作集。
2.3.1.2.4 偶然抽象
一个对象封装了一组相互间没有关系的操作。
2.3.2 封装
封装是一个过程,它分隔构成抽象的结构和行为的元素。封装的作用是分离抽象的概念接口及其实现。
2.3.2.1 特点
- 抽象和封装是互补的概念,抽象关注的是对象可以观察到的行为,而封装关注这种行为的实现。
- 抽象帮助人们思考他们做什么,而封装让程序可以借助最少的工作进行可靠的修改。
- 要让抽象能工作,必须将实现封装起来。在实践中,这意味着每个类必须有两个部分,一个接口和一个实现,类的接口描述了它的外部视图,包含了这个类所有实例的共同行为的抽象。类的实现包括抽象的表示以及实现期望行为的机制。通过类的接口,我们能知道客户可以对这个类的所有实例做出那些假定,实现封装了细节,客户不能对这些细节做出任何假定。
- 明智的封装让可以改变的设计决策局部化。抽象让我们既能改变表示方法,同时又不影响其客户,这就是封装的根本好处。
2.3.3 模块化
模块是一个系统的属性,这个系统被分解为一组高内聚、低耦合的模块。
2.3.3.1 特点
- 分解为模块的的总体目标是通过允许模块独立地设计和修改,从而减少软件的成本,每个模块的结构都应该足够简单,这样它就能被完全理解。模块之间的联系是模块相互之间所做出的假定。应该能够在不知道其他模块的实现方法,并不会影响其他模块的行为的情况下,修改某个模块的实现。修改设计的容易程序应该能够满足需要变更的可能性。
- 随意的模块化有时候比不实现模块化还要糟。
- 类和对象构成了系统的逻辑结构,将逻辑上相关的抽象放在一个模块中,形成系统的物理架构。
- 抽象、封装和模块化的原则是相辅相成的,一个对象围绕单一的抽象提供了一个明确地边界,封装和模块化都围绕这种抽象提供了保障。
- 努力创造出高内聚(将逻辑上相关的抽象放在一起)、低耦合(减少模块间的依赖关系)的模块。
2.3.3.2 影响模块化决定的因素
- 由于模块通常是软件的基本可分割单元,可以跨应用复用,所以开发者可能以方便复用的方式对类和对象进行打包。
- 许多编译器以分段的方式产生目标代码,每个模块生成一段。因此,对单个模块的规模可能有实际的限制。
- 模块中声明的位置可能在很大程度上影响引用变量的局部性,从而影响到虚存系统的分页行为。
- 通常开发团队是根据模块来分配工作的,所以建立模块边界时要尽量减少开发组织中不同部分之间的接口。
- 有时候文档方面的要求会影响模块设计的决定。
- 安全性也可能成为问题,大多数代码可能是非保密的,但最好将那些可能需要保密的代码放在一个独立的模块中。
2.3.4 层次结构
层次结构是抽象的一种分级或排序。分为是一种(继承)层次结构和组成部分(聚合)层次结构。
2.3.4.1 是一种(继承)层次结构
- 继承代表了一种抽象的层次结构,在类之间定义了“是一种”关系,在这个层次结构中,一个子类从一个或多个超类中继承。一般来说,子类会扩展或重新定义超类中的结构和行为。
- 当发展继承层次结构时,不同类中共同的结构和行为会被迁移到共同的超类中。这就是常把继承称为“一般/特殊”层次结构的原因。超类代表了一般化的抽象,子类代表了特殊的抽象,会添加、修改甚至隐藏来自超类的属性和方法。
- 对于某一个类,通常有两种客户,调用该类实例的方法的对象以及从这个类继承的子类。
- 类的接口可以有三个部分,私有部分,声明只能够由该类本身访问的成员;保护部分,声明可以由该类及其子类访问的成员;公有部分,可以让所有客户访问。
- 子类通常扩展或限制了超类中原有的结构和行为。扩展超类的子类被称为扩展继承,与此相对的是,限制超类的子类被称为限制继承。
- 在继承中,对于超类一般没有实例对象,超类在编写时就预期它的子类会添加结构和行为,通常是完成它未完成的方法实现。继承意味着子类继承了超类的结构。
- 多继承为编程语言引入了一些复杂性,主要是来自不同超类的名字的冲突以及重复继承的问题,当两个或多个超类提供了同样名字的属性或操作时,就会发生名字冲突的情况;当两个或多个同类超类具有共同的超类时,就会发生重复继承的情况。
2.3.4.3 组成部分(聚合)层次结构
当且仅当两个两个对象之间存在整体/部分关系时,它们对应的类之间必然存在一种聚合关系。对于“组成部分”的层次结构,一个类相对于组成他的实现的类来说,处于更高的层次。聚合根据所有权的问题分为组合和聚合两种。当聚合的类的生命周期和更高层次的类的生命周期是紧密联系在一起时,我们称为聚合,反之则为组合。
2.3.5 类型
类型是关于一个对象的类的强制规定,这样一来,不同类型的对象不能够互换使用,或者至少它们的互换使用受到非常严格的限制。
2.3.5.1 特点
强类型让我们可以利用编程语言来强制保证某些设计决策,这样当我们的系统的复杂度增长时,仍能保持特定的关联。但是,强类型也有不利的一面。从实践上来看,强类型引入了语义上的依赖关系,这样,即使是基类接口上的一个小改动,也需要重新编译所有的子类。
2.3.5.2 好处
- 如果没有类型检查,大部分语言的程序可能在运行时以神秘的方式“崩溃”。
- 在大多数系统中,编辑-编译-调试循环相当繁琐,所以早期的错误检查是必不可少的。
- 类型声明有助于为程序编写文档。
- 如果声明类型,大部分编译器可以生成更有效率的目标代码。
2.3.5.3 静态类型和动态类型
类型的强与弱和类型的静态与动态的概念是完全不同的。类型的强与弱指的是类型一致性,而类型的静态与动态指的是名字与类型绑定的时间。静态类型(也称为静态绑定或早期绑定)意味着所有变量和表达式的类型在编译时就固定了,动态类型(也称为延迟绑定)意味着所有变量和表达式的类型直到运行时刻才知道。
2.3.5.4 多态
多态是动态类型和继承互相作用时出现的一种情况。多态代表了类型理论中的一个概念,即一个变量声明可以代表许多不同类的对象,这些类具有某个共同的超类。这个名字所代表的对象因此可以响应一组共同的操作。利用多态,一个操作可以被“是一种”层次结构中的类以不同的方式实现。通过这种方式,子类可以扩展超类的能力,或者覆写父类的操作。多态和延迟绑定是分不开的,在出现多态时,方法和名字的绑定要在执行时确定。与多态相对的是单态,这在强类型和静态类型的语言中都可以发现。
多态可能是面向对象语言中除了对抽象的支持以外最强大的功能,也正是它,区分了面向对象编程和较传统的抽象数据类型编程。
2.3.6 并发
并发是一种属性,它区分了主动对象和非主动对象。主动的对象有自己的控制线程,而被动的对象没有。
2.3.6.1 特点
- 并发分为重量级并发和轻量级并发。重量级并发通常是由目标操作系统独立管理的,有自己的地址空间,重量级并发之间的通信开销很大,涉及某种进程间通信技术。轻量级进程通常与其他轻量级进程一起处于单个操作系统进程之内,共享同样的地址空间,轻量级进程开销很小,涉及共享数据。
- 在最高层次的抽象中,通过将并发隐藏在可复用的抽象中,OOP可以减轻大多数程序员在并发问题上的负担。
- 每个对象(来自于真实世界的一个抽象)都可以代表一个独立的控制线程(一种过程抽象)。这样的对象被称为主动对象。
- 当在一个系统中引入并发时,必须考虑主动对象之间、主动对象与串行执行的对象之间如何同步它们的活动。
- 在并发的情况下,仅定义对象的方法是不够的,还必须确保这些方法的语义在多个控制线程的情况下仍然有效。
- 主动的对象通常是自动的,这意味着它们不需要由其他对象操作,就能表现出一些行为。而对于被动对象来说,只有在显示地操作它时,才会发生状态变化。在系统中,主动对象是中心,如果系统包含多个控制线路,通常会有多个主动对象。
2.3.7 持久
持久是对象的一种属性,利用这种属性,对象跨越时间(例如,当对象的创建者不存在的时候,对象仍然存在)和空间(例如,对象的位置从它被创建的地址空间移开)而存在。
2.4 应用对象模型的好处
- 使用对象模型帮助我们探索基于对象和面向对象编程语言的表达能力。在设计过程中利用类层次结构的好处,能够得到更进一步,更显著的改进。
- 利用对象模型不仅鼓励软件的复用,而且鼓励整个设计的复用,这导致了可复用应用框架的产生。
- 使用对象模型将得到构建在稳定的中间状态之上的系统,这样的系统更适合变化。
- 因为集成散布在生命周期的各个时刻,而不是作为一个大事件发生。对象模型设计明智的关注点分离的原则也减少了开发的风险,增加了我们对设计正确性的信心。
3 重新认识类和对象
3.1 对象的本质
3.1.1 定义
对象是一个具有状态、行为和标识符的实体。结构和行为定义在他们共同的类中。
3.1.1.1 状态
对象的状态包括这个对象的所有属性(通常是静态的)以及每个属性当前的值(通常是动态的)。属性是一种内在或独特的特征、特点、品质或特性,使一个对象区别与别的对象。不同的对象,即使它们有相同的属性,这些对象也不会共享内存空间。
3.1.1.2 行为
对象的行为代表了它外部可见的活动,行为是对象在状态改变和消息传递方面的动作和反应的方式。对象的行为会受到其状态的影响,一个对象的行为是它的状态以及施加在它上面的操作的函数。一个对象的状态的行为共同决定了这个对象可以扮演的角色。一个对象的所有方法共同构成了它的协议。因此,一个对象的协议定义了对象允许的行为的封装,构成了这个对象完整的静态视图和动态视图。
3.1.1.2.1 操作的分类
一个操作代表了一个类提供给它的对象的一种服务。
- 修改操作:更改一个对象的状态的操作。
- 选择操作:访问一个对象的状态但并不更改这个状态的操作。
- 遍历操作:以一种定义良好的方式访问一个对象的所有部分的操作。
- 构造操作:创建一个对象并初始化它的状态的操作。
- 析构操作:释放一个对象的状态并销毁对象本身的操作。对于实现了垃圾回收机制的语言,没有这个操作。
3.1.1.3 标识符
标识符是一个对象的属性,它区分了这个对象和其它所有对象。一个对象的标识符是在对象的整个生命周期都被保持的,即使它的状态改变时也是如此,在JAVA中对象的标识符就是对象在内存中的地址。
3.2 对象之间的关系
两个对象之间的关系包括了一个对另一个所做的假定,即包括可以执行那些操作以及将导致怎样的行为。
3.2.1 链接
3.2.1.1 定义
链接表明了一种端到端的关系或客户/服务提供者的关系。两个对象之间物理上或概念上的联系。一个对象通过它与其他对象的链接,与其他对象进行协作。换言之,链接代表了具体的关联,通过这种关联,一个对象(客户)请求另一个对象(服务提供者)的服务,或者通过这种关联从一个对象导航到另一个对象。
3.2.1.2 特点
- 虽然消息传递是由客户发起的,指向服务提供者,但是数据可以通过链接双向流动。
- 在实际设计时,必须考虑跨越链接的可见性,此时的考虑决定了链接两端对象的访问范围。
3.2.1.3 角色
作为链接的参与者,一个对象可能扮演以下三种角色之一。
- 控制器:这个对象可以操作其他对象,但不会被其他对象操作。在某些地方,控制器和主动对象两个术语可以互换使用。
- 服务器:这个对象不操作其他对象,它只被其他对象操作。
- 代理:这个对象既可以操作其他对象,也可以被其他对象操作。创建代理通常是为了表示问题域中的一个真实对象。
3.2.1.4 并发
当一个对象通过链接向另一个对象发送一条消息时,这两个对象就称为同步了。在完全串行式的应用中,这种同步通常是通过简单的方法调用来完成的。但是,在存在多控制线程的情况下,对象需要更复杂的消息传递机制来处理并发系统中可能出现的互斥问题。前面曾提到,主动对象拥有自己的控制线程,所以我们期望它们的语义在其他对象存在时仍然能得到保障。但是,当一个主动对象与一个被动对象之间有链接时,我们必须选择以下三种同步方式之一。
- 顺序:只有在某一时刻只存在一个主动对象时,被动对象的语义才得到保证。
- 守卫:在多个控制线程的程序下,被动对象的语义也能保证,但主动的对象之间必须协作,以实现互斥访问。
- 并发:在多个控制线程的程序下,被动对象的语义也能保证,服务提供者保证互斥。
3.2.2 聚合
3.2.2.1 定义
聚合表明了一种整体/部分层次结构,提供了从整体(也称为聚合体)导航到它的部分的能力。
3.2.2.2 特点
如果一个对象是另一个对象的一部分,就意味着它到它的聚合体有一个链接。通过这个链接,聚合体可以向它的部分发送消息。
3.3 类的本质
类是一组对象,他们拥有共同的结构、共同的行为和共同的语义。类对于分解是必要的,但不是充分的。
3.3.1 特点
- 一个单独的对象是一个具体实体,在整体系统中扮演某个角色,而类则记录了所有相关对象的共同结构和行为。因此,类起到的作用是在一种抽象和所有它的客户之间建立起协议。
- 对象的状态必须在它对应的类中有某种表示形式,所以通常表示为常量或变量声明,作为类接口的保护和私有部分。通过这种方式,一个类的所有实例的共同表示形式被封装起来,对这种表示形式的修改不会在功能上影响任何外部客户。
- 抽象的类主要由所有的操作声明构成,这些操作适用于这个类的所有对象,但它也可能包括其他类、常量、变量和异常的声明,因为这种抽象可能需要这些东西。与抽象不同,类的实现是它的内部视图,它包含类行为的秘密。一个类的实现主要由类抽象中定义的所有操作的实现构成。
3.3.2 类和对象的可见性
- 公有:所有客户都可以访问的声明。
- 保护:只能由该类本身及其子类访问的声明。
- 私有:只能由该类本身访问的声明。
- 包:只能由同一个包中的类访问的声明。
3.4 类之间的关系
类和对象一样,也不是孤立存在的。对于一个特定的问题域,一些关键的抽象通常与各种有趣的方式联系在一起,形成了我们设计的类结构。
3.4.1 关联
关联只代表一种语义上的依赖关系,它不表示这种依赖关系的方向,如果没有特别说明的话,关联意味着双向导航。关联存在多重性,有以下三种常见的多重性分别为一对一、一对多、多对多。
3.4.2 继承
继承代表了一般/特殊关系。根据我们的经验,给定问题域的关键抽象之间存在着丰富的关系,继承对于表示所有这些关系是不够的。继承的一种替代方式是一种所谓“委托”(聚合)的语言机制,通过这种机制,对象将它们的行为委托给一些相关的对象。
3.4.3 聚合
我们也需要聚合关系,它提供了类实例中的整体/部分关系。类之间的聚合关系与这些类的对象之间的聚合关系是并存的。多继承常常与聚合产生混淆。当考虑继承还是聚合时,要记得应用他们的判别测试。如果不能够肯定两个类之间存在“是一种”的关系,那么应该使用聚合或其他的关系来代替继承。
3.4.4 依赖
除了继承、聚合和关联之外,还有一类关系,被称为“依赖关系”。依赖关系表明,处于这种关系一端的元素以某种方式依赖于处于另一端的元素。这警告了设计者,如果其他一个元素发生了改变,可能会影响到另一个元素。
3.5 创建类与对象
3.5.1 评判抽象的好坏方式
3.5.1.1 耦合
耦合可以定义为一个模块与另一个模块之间建立起的关联强度的测量。强耦合使系统变得复杂,因为如果模块与其他模块高度相关,它就难以独立地被理解、变化或修正。通过设计系统,使模块间的耦合降至最弱,可以降低复杂性。
模块之间的耦合也适用于面向对象的分析和设计,但类和对象之间的耦合同样重要。然而,在耦合和继承的概念之间存在着矛盾关系,因为继承引入了严重的耦合。一方面,我们希望类之间的弱耦合,另一方面,继承(超类和子类之间的强耦合)又能帮助我们处理抽象之间的共性。在设计的时候继承不能滥用,要考虑清除类之间的关系,是否确定是“是一种”关系?如果不确定的话,可以使用聚合。
3.5.1.2 内聚
内聚测量了单个类或单个对象内各个元素的联系程序。最优的内聚就是功能性内聚,即一个类的各元素一同工作,提供某种清晰界定的行为。
3.5.1.3 充分性
充分性指的是类或模块应该记录某个抽象足够多的特征,从而允许有意义的、有效的交互。例如在设计集合类的时候,应该包含从集合中删除元素的操作,但如果忘记了加入元素的操作,我们的努力就徒劳了。
3.5.1.4 完整性
所谓完整,指的是类或模块的接口记录了某个抽象全部有意义的特征。一个完整的接口就意味着该接口包含了某个抽象的所有方向。因此完整的类或模块是足够通用,可以被任何客户使用。
3.5.1.5 基础性
基础性操作就是只有访问该抽象的底层表现形式(通常是底层算法)才能够有效地实现的那些操作。因为许多高级操作可以由低级操作组合得到,这也是建议类和模块应该具有基础性的原因。
3.5.2 选择操作(定义方法)
3.5.2.1 功能语义
对于一个给定的类,我们的风格是让所有的操作保持基础性,这样每个操作都展示出了小的、定义良好的行为。我们把这样的方法称为“细粒度的”。我们也倾向于分离方法,让他们相互之间不通信。通过这种方式,我们更容易构造一些子类,它们可以有意义地重新定义超类的行为。决定将一个行为提取为一个方法还是多个方法,有两个互相竞争的原因:
1. 将一个行为塞进一个方法中将导致更简单的接口,但方法会更大、更复杂。
2. 将一个行为分散到多个方法中将导致更复杂的接口,但方法会更简单。
好的设计者知道如何在太多契约和太少契约之间平衡折中,太多契约导致片段化,太少契约导致无法管理的大模块。
在面向对象开发中,通常会整体设计一个类的所有方法,因为所有这些方法共同构成这个抽象的完整协议。因此,对于某个期望的行为,我们必须决定将它放到那个类中。有如下的判断条件,需要在做这样的决定时思考。
1. 可复用性:这个行为可以在多种上下文中使用吗?
2. 复杂性:实现这个行为的难度有多大?
3. 适用性:这个行为与打算放入的类型之间相关程度如何?
4. 实现知识:这个行为的实现依赖于一个类型的内部细节吗?
3.5.2.2 时间和空间语义
在确定存在某个操作,并定义了它的功能语言之后,必须决定它的时间语义和空间语义。这意味着我们必须决定它完成操作需要的时间以及存储空间。这样的决定通常是用最佳、平均和最差等术语来表达的,最差的情况规定了能够接受的上限。
前面也提到,当一个对象通过链接向另一个对象传递消息时,这两个对象必须以某种方式同步。在多控制线程的情况下,这意味着消息的传递比子程序调用要更复杂。在使用的大多数语音中,对象之间的同步不成问题,因为我们的程序只包含一个控制现成,这意味着所有的对象都是依次访问的。这种情况下的消息传递很简单,因为它的语义基本上与简单的子程序调用相同。但是,在支持并发的语言中,我们必须关注更为复杂的消息传递形式,以避免两个控制线程以无限制的方式访问同一个对象,从而引发问题。前面曾提到,在多个控制线程下仍能保持语义的对象要么是守卫的,要么是同步的。
3.5.3 选择关系
在类之间和对象之间选择关系与选择操作是有联系的。如果决定对象X向对象Y发送消息M,那么X必须能够直接或间接地访问Y;否则,就不能够在X的实现中命名操作M。所谓能够访问,指的是一种抽象能够看到另一种抽象,并引用它的外部视图中的资源。只有当它们的范围重叠,并且访问得到授权时(例如,类的私有部分只能被该类本身访问),一种抽象才可以访问另一种抽象。因此,耦合是度量可访问程度的指标。决定对象之间的关系主要是设计这些对象进行交互的机制。开发者要问的问题很简单:这些操作应该放在哪里。
3.5.3.1 Demeter法则
1. 在选择对象间的关系时,有一条有用的指导原则,称为Demeter法则。它指出,“类的方法不应该以任何方式依赖于任何类的结构,除了它自己类的当前(顶层)结构之外。而且,每个方法只能够对一个非常有限的类集的对象发出消息”。应用这一法则的基本效果就是创建了一些松耦合的类,它们的实现秘密被封装了起来。这样的类是没有负担的,即为了理解一个类的意思,不需要理解许多其他类的细节。
2. 在查看整个系统的类结构时,可能会发现它的继承关系宽而浅,或者窄而深,或者比较平衡。类结构宽而浅通常代表由独立的类构成的森林,它们之间可以混合或匹配。类结构窄而深则表明各个类构成的树都与一个共同的祖先有关。每种方式都有优点和不足。类的森林耦合更松,但它们可能没有体现出存在的共性。类的树体现了这种共性,所以单个类比森林中的类要小。但是要理解某个类时,通常需要理解它继承或用到的所有类的含义。类结构的正确形态与问题是高度相关的。
3. 我们必须在继承、聚合、依赖关系之间进行类似的折中。聚合关系比继承关系更合适。只有当B的每个实例都可以被看作A的实例时,才适合继承。当B的每个实例只是具有A的一个或多个属性时,适合使用聚合关系。从另外一个角度看,如果对象的行为超过了它的部分之和,在相关的类之间创建聚合关系而不是继承关系可能更好。
3.5.4 选择实现
3.5.4.1 选择表示形式
类或对象的表示形式几乎总是该抽象封装起来的秘密。这使得我们可以改变表示形式(例如,改变时间语义和空间语义),同时又不会违反客户对功能所做的任何假定。
在选择类的实现时,有一个更难的折中:计算对象状态的值,还是将它保存为一个字段。不论哪种情况,我们都应该能够在不改变类的外部视图的情况下选择实现。实际上,我们甚至应该能够在类的客户不关心的情况下,改变这些表示形式。
3.5.4.2 模块化
类似的问题也适用于在模块中声明类和对象。可见性和信息隐藏这一对矛盾的需求通常引导我们决定在哪里声明类和对象。一般来说,我们追求构建功能内聚的、松耦合的模块。许多非技术因素会影响这些决定,如复用、安全及文档。像类和对象的设计一样,模块的设计也很重要,不应忽视。关于信息隐藏,Parnas、Clements和Weiss指出,“应用这一原则并不总是很容易。它试图使软件在使用期的预期成本最小化,并要求设计者估计出变化的可能性。这样的估计基于过去的经验,通常要求应用领域的知识,并要求理解硬件和软件技术”。
4 面向对象分析与设计
分析和设计之间的边界是模糊的,但是每种活动关注的重点是不同的。
1. 在分析时,关注的重点是分析面临的问题域,从问题域的词汇表中发现类和对象,实现对真实世界的建模。
2. 在设计时,我们在模型中发明一些抽象和机制,为要构建的解决方案提供设计。
4.1 正确分类的重要性
确定类和对象是面向对象分析和设计中具有挑战性的部分。经验表明,这种确定过程既涉及发现,也涉及发明。通过发现,我们逐渐从问题域的词汇表中识别出关键抽象和机制。通过发明,我们设计出继承和一些新的机制,规定对象协作的方式。
分类帮助我们确定类之间的继承、聚合等层次结构。通过识别对象交互中的共同模式,我们逐渐发明一些机制,成为实现的核心。
在设计复杂软件系统时,分类的增量式、迭代式本质直接影响了类和对象的层次结构。在实践中,人们常常在设计的早期确定某个类的结构,然后随着时间的推移,不断修改这个结构。在设计的后期,当创建了使用这个结构的客户代码时,我们会对分类的品质有更深的认识。基于这些经验,我们可能决定从已有的类创建一个新的子类(派生),可能将一个大类分为几个小类(分解),或者创建一个较大的类来组织几个较小的类(组合)。有时候,我们甚至可能发现以前没有意识到的共性,并设计出一个新类(抽象)。
4.2 分类的方法
4.2.1 经典分类
在经典的分类方法中,“所有具有某一个或某一组共同属性的实体构成了一个分类。这样的属性对于定义这个分类是必要的,也是充分的”。
总之,经典分类法利用相关的属性作为对象间相似性的判据。具体来说,人们可以根据某一属性是否存在,将对象划分到没有交集的集合中。
4.2.2 概念聚集
概念聚集是经典方式较为现代的变种,在这种方法中,类(一些实体的聚集)的产生首先是形成类的概念描述,然后再根据这些描述对实体进行分类。
概念聚集与模糊(多值)集理论是有密切关系的,在这种理论中,对象可以按不同的适合程度属于一个或多个分组。概念聚集通过关注“最适合”来进行绝对的分类判断。
4.2.3 原型理论
对象的类是由一个原型对象来代表的,如果一个对象与这个原型表现出重要的相似性,那么这个对象就被认为是这个类中的一员。这种互动属性的观点是原型理论的中心思想。在原型理论中,我们根据事物与具体原型的关系对它们进行分组。
4.2.4 分类方式应用
在我们的经验中,首先是根据特定领域相关的属性来确定类和对象的。这时,我们关注的重点是确定构成问题空间词汇表的结构和行为。通常有许多这样的抽象可供选择。如果这种方法不能得到一个令人满意的类结构,接下来就考虑按概念来聚集对象(或者按概念来优化最初的基于领域的分类)。这时,我们关注的重点是协作对象的行为。如果这两种方法都不能表达我们对问题域的理解,我们就考虑按关联度进行分类。在这种方法中,我们根据对象与某个原型对象相似的程度来聚集对象。
4.3 面向对象分析
4.3.1 经典方法
利用经典分类构建类和对象的方法。
4.3.2 行为分析
面向对象分析的另一种思路是关注动态的行为,将这些行为作为类和对象的主要来源。这类方法更像是概念聚集,根据一组展示出类似行为的对象来形成类。可以根据系统的功能来确定类和对象。
4.3.3 领域分析
领域分析试图确定在某个领域中所有应用都通用的类和对象,如病历跟踪、证券交易、编译器或导弹电子系统。如果在设计中对于已存在的关键抽象有点疑惑,领域分析师可以提供帮助,指出这些关键抽象在其他相关系统中已证明有用。领域分析效果挺好,因为除了极特殊的情况之外,软件系统很少有真正独特的需求。
领域分析可以应用于许多类似的应用(垂直领域分析),也可以应用于同一应用的相关部分(水平领域分析)。
领域分析的步骤:
1. 咨询领域专家,构建一个通用的模型草稿。
2. 检查领域中原有的系统,以一种通用的格式展示出这方面的理解。
3. 咨询领域专家,确定系统间的相似和差异。
4. 细化通用模型,以包含原有的系统。
4.3.4 用例分析
一个执行者通过与系统进行对话的方式执行的一个行为上相关的事务序列,目的是为执行者提供某种可度量的价值”。
4.3.5 CRC卡
CRC卡是一种教授面向对象编程的工具。CRC卡已被证明是一种有用的开发工具,促进了开发者之间的头脑风暴,增进了沟通。CRC卡就是一张3×5的索引卡片,分析师在上面用铅笔写下类的名称(在卡的顶部)、它的职责(在卡的一半)以及它的协作者(在卡的另一半)。针对与场景有关的每个类,我们创建一张卡片。当团队成员分析该场景时,他们可能会为已有的类分配新的职责,将某些职责集中起来形成一个新的类,或者(更常见的情况)将一个类的职责分解到粒度更小的类中,也许会将这些职责放到其他的类上。
4.3.6 非正式英语描述
对于经典的面向对象分析,有一种基本的替代方法,它是由Abbott首先提出来的。Abbott建议写下问题的英语描述,然后划出名词和动词。名词代表了候选对象,动词代表了这些对象上的侯选操作。
4.4 面向对象设计
“关键抽象”是一个类或对象,它是问题域词汇表的一部分。确定这样的抽象的主要价值在于,它们给出了问题的边界,突出了系统中的事物——这些事物与我们的设计有关;同时,它们排除了系统之外的事物,这些事物是不必要的。
4.4.1 确定关键抽象
正确选择对象取决于应用的目的,以及要操作的信息的粒度。
前面曾提到,确定关键抽象包含两个过程:发现和发明。通过发现过程,我们意识到领域专家所使用的抽象。如果领域专家提及它,那么这个抽象通常很重要。通过发明过程,我们创造了新的类和对象,它们不一定是问题域的组成部分,但在设计或实现中也是很重要的。
4.4.2 细化关键抽象
当我们将某个关键抽象确定为候选者时,通常这意味着程序员必须关注以下问题:
1. 这个类的对象是如何创建的?
2. 这个类的对象可以复制或销毁吗?
3. 在这样的对象上可以执行什么操作?
如果对这些问题没有好的答案,这个概念可能在开始时并不‘清晰’,也许最好是再仔细考虑一下这个问题和提出的解决方案,而不是立即开始针对问题进行‘编码’。
当在一个类型层次结构中设计类时,并非总是从超类开始,然后创建子类。最常见的类层次结构的组织方式是从两个类中提取公共部分放到一个新类中,或者将一个类分成两个新类。
将类和对象放到正确的抽象层次中是很难的。有时候我们可能发现一个通用的子类,并将它在类结构中上移,从而增加共享度,这称为“类提升”。类似地,我们可能发现某个类太一般化,因此从它派生出一个子类很困难,因为语义上的差距太大,这称为“粒度规模冲突”。不论是哪种情况,我们追求的都是确定高内聚、低耦合的抽象,这样就能缓解这两种情况。
4.4.3 确定机制(对象的交互)
开发者从一种可选机制中选择哪一种通常是考虑其他因素的结果,如成本、可靠性、可生产性和安全性。
一个客户对象违反另一个对象的接口是不对的,同样,对象的行为超出某种机制打算提供的行为也是不能忍受的。
关键抽象反映了问题域的抽象,而机制是设计的灵魂。在设计过程中,开发者不仅必须考虑单个类的设计,还要考虑这些类的实例如何一起工作。
当开发者决定采用某种协作模式之后,工作会被分解到许多对象上,即在相应的类上定义适当的方法。归根到底,单个类的协议包含了实现所有行为以及实现与其实例相关的所有机制所需要的全部操作。
因此,与类层次结构的设计一样,机制代表了战略设计决策。相比之下,单个类的接口设计更多的是一种战术决策。这些战略决策必须明确表示出来,否则,我们将得到一些互相无法合作的类,所有类都努力完成自己的工作,很少考虑其他对象。最优雅、最精益、最快的程序会包含一些精心设计的机制。
机制实际上是我们在构造良好的软件系统中发现的各种模式。机制代表了一种层次的复用,它高于单个类的复用。惯用法在编写低层模式时是很重要的,许多常见的编程任务都有惯用的实现方式。
5 统一建模语言
本节内容所涉及的UML版本为2.0的版本,各个图形和其它地方可能有少许区别。
5.1 图分类
5.1.1 结构图
结构图用于展示系统中元素的静态结构。它们描述系统的架构组织、系统的物理元素、系统的运行时刻配置和业务中领域相关的元素。结构图包括:包图、类图、组件图、部署图、对象图以及组合结构图。
5.1.2 行为图
行为图用于展示系统中的动态结构。它们描述系统的事件交互次序、对象的创建与销毁、对象的交互方式等。行为图包括:用例图、活动图、状态机图、交互图、序列图、通信图、交互概述图以及时间图。
5.2 包图
在进行面向对象分析与设计时,需要组织开发过程中产出的模型(各种UML图),从而清晰地展现出问题域的分析和相关的设计,实现这种组织的主要方式是利用UML包图,它提供了表现UML元素分组的能力。包图的主要元素是包、可见性以及依赖关系。
5.2.1 包表示法
包的表示法是一个左上角带有标签的矩形。如下图
包的名称是一个字符串,有两种表示形式:简单名和路径名(限定名称)。简单名仅包含一个字符串,而路径名是以包处于的外围包的名字作为前缀并加上“::”以及本包的名称字符串。在包内可以包含另一个包或者其他UML图的模型元素。用包分组的元素通常应该在某种意义上是相关的。好的分包是松耦合、高内聚的。也就是说,应该看到包内的元素有更多的交互,包间的元素有更少的交互。应该尽量不要让继承层次结构或聚合关系跨越包的边界。
5.2.2 元素可见性
访问包中的任何元素提供的服务取决于要访问元素的可见性,包括内嵌的包。元素的可见性由包容的元素所定义。这既适用于被包容的元素也适用于导入的元素。包为它包含的元素提供了命名空间。因为包提供了命名空间,所以每个被包容的元素都有唯一的名称,至少在包内有唯一的名称。一个命名空间中包含的每个元素都可以通过包的路径名来引用,只要元素属于不同的命名空间,就可以有相同的名称。下面是可见性的的定义:
1. 公有(+):对它所在的包(包括内嵌的包内元素)以及外部的元素可见。
2. 私有(-):只对它所在的包及内嵌的包内元素可见。
3. 受保护的(#):对它所在的包(包括内嵌的包内元素)以及其它和当前元素有泛化关系的元素可见。
下面是可见性的表示法:
5.2.3 依赖关系
如果一个元素具有适当的可见性,允许对他进行访问,那么可以显示指向它的依赖关系,表示这种访问。依赖关系显示了一个元素依赖于另一个元素来实现它的系统的功能。
UML元素(包括包)之间的依赖关系是用一个虚线的开放箭头来表示的。箭头的尾部位于需要依赖其它元素的元素(客户),箭头位于支持这种依赖的元素(提供者)。依赖关系可以标上标签。包特有的依赖关系包括导入(«import»)、访问(«access»)、合并(«merge»)。由于包容的元素之间的关系而导致的包间依赖关系包括跟踪、派生、细化、允许和使用(«use»)。通过(«依赖关系类别»)表示。如果两个包中的多个元素之间存在依赖关系,这些依赖关系会聚合为包层面的依赖关系。包层面的依赖关系可以用一个关键词标签标出。但是,如果两个包之间的依赖关系是不同类型的,包层面的依赖关系就不提供标签。
5.2.4 导入、访问以及合并
导入是一种公有的导入,而访问是一种私有的包导入。对于导入来说,其他可以看到导入包的元素也可以看到被导入的元素;但对于访问来说,其他元素不能看到这些添加到导入包命名空间的元素。这些被导入的项是私有的,它们在进行访问的包之外是不可见的。执行合并的包将拥有被合并的元素的功能。执行导入的包将被导入元素的名称添加到它的命名空间中。但是,如果相同类型的被导入元素刚好与也有的元素同名,那么它们就不会被添加到执行导入的包的命名空间中。类似地,如果从不同命名空间中导入的相同类型的元素具有相同的名称,它们也不会被添加到执行导入的包的命名空间中。包元素的导入可以是广泛或集中的,即导入所有元素或只导入选定的元素。
5.3 组件图
组件代表了一块可复用的软件,它提供了某种有意义的功能集。组件是一组类,它们本身是内聚的,与其他类的耦合相对比较松散。系统中的每个类要么处于一个组件中,要么处于系统的顶层。组件也可以包含其他组件。
组件间的协作和内部结构可以利用组件图来表示。组件与组件之间通过定义良好的接口进行协作,从而提供系统的功能。组件本身也可以由一些协作的组件组成,提供它自己的功能。组件图中的基本元素是组件、它们的接口和它们的实现。
5.3.1 组件图
1. 在UML2.x的版本中,如图的«component»标签和矩形右上角的组件图标都可以表示这是一个封装好的组件。
2. 在矩形框的边界上,有7个小正方形,这是7个端口。除非另有说明,否则,端口将具有公有可见性。组件也可以拥有隐藏的端口,表示法同样是小正方形,但它们完全位于边界的内部。隐藏端口用于一些不公开提供的功能,比如测试入口等。组件通过端口与它的环境进行交互,端口为这个结构化的组件提供了封装。这7个端口是没有被命名的,但在需要阐明时应该命名,其格式为“端口名称:端口类型”,在命名端口时,端口类型是可选的。
3. 与端口连接的线以及小球和球窝定义了组件交互的细节。小球表示这个组件向外提供的接口,球窝表示这个组件需要环境提供的接口。
4. 这种图形像一种黑盒一样,把组件内的类或组件进行封装,组件的功能其实是内部的组件或者类提供的。
5. 端口和接口之间不一定是一对一的关系,端口可以用来对接口分组,某些类型的交互可以通过一个端口进行显示,在使用这种表示方法时请注意,不同接口的名称用逗号隔开。
6. 在开发中,我们利用组件图来表达架构的逻辑分层和划分方式。组件图中展现了组件间的相互依赖关系,也就是它们通过定义良好的接口进行协作,从而提供系统的功能。
使用小球和球窝表示法来指定每个组件提供的接口和要求的接口。两个组件之间的接口被称为“组装连接器”,也被称为“接口连接器”。也可以直接使用直线来表示每个连接。但是直接给的信息要少一些。向环境提供的接口以及要求环境提供的接口称为“委托连接器”。
组件是可以复用的,在上图中,只要另一个组件实现了LightingController要求的接口,它就可以取代EnvironmentalController中的LightingController。组件的这种特性意味着可以更容易地根据需要升级系统。
如果需要更详细地显示组件的接口,可以提供接口规格说明,如下图:
EnvironmentalController实现了CoolControl接口,这意味着它提供了该接口规定的功能。这个功能是让所有使用这个接口的组件能够启动、停止、设置温度和设置风扇速度,它所包含的操作说明了这一点。EnvironmentalController组件使用了AmbientTemp接口的getAmbientTemp操作获取它所需要的环境温度。
5.3.2 组件的内部结构
组件的内部结构可以通过内部结构图来展示。在上图中,标签从«component»改为«subsystem»,在这里也可以使用«component»,没什么影响,在这个图中增加了«delegate»标签,它位于内部组件的接口和EnvironmentalControlSystem边界上的端口之间的连线上,这些连接提供了一种方法,表示哪个内部组件实现了提供的接口的职责,以及哪个内部组件需要组件要求的接口提供的服务。
子系统划分了系统的逻辑模型。子系统包含其它的子系统和其它组件。系统中的每个组件要么处于某个子系统中,要么处于系统的顶层。在实践中,大的系统会有一个顶层组件图,包含处于最高抽象层的子系统。开发者通过这个图来理解系统的总体逻辑架构。
5.4 用例图
5.4.1 用例图
1. 图中的小人表示执行人员,一般与角色挂钩,不同角色的执行人员所拥有的系统权限是不一样的。
2. 图中的椭圆是具体的用例,用例是通过某部分功能来使用系统的一种具体的方式,因此,用例是相关事务的一个具体序列,执行者和系统以对话的方式执行这些事务,从用户的观点来看,每个用例都是系统中一个完整序列的事件。在确定一个具体的用例的时候,可以使用用例规格说明。用例规格说明有许多不同的形式。大多数都包括以下信息:用例的名称、关于它的目的的一段简短描述、乐观流程(即如果一切正常,用例中发生的事件流)以及一个或多个更实际的流程(即事情没有按照愿望发生时的流程)。其它有用的信息也可以加入规格说明中,如进入条件(在执行这个用例之前什么条件必须为真)、退出条件(在执行这个用例之后什么条件必须为真)、限制条件以及假定等。总的来说,用例规格说明不应该太长,如果规格说明非常长,就应该考虑一下这个用例做的事件是否太多了,有可能它实际上是多个用例。另外出于实际的原因,也不能包含所有会触发可选流程的事情,而只要包含最重要、最关键的可选流程。
5.4.2 «include»
如上图所示,当执行人员执行用例1的时候,必须先执行用例2。如果没有用例2,用例1就不能被看作是完整的。
5.4.3 «extend»
如上图所示,当执行人员执行用例1的时候,用例2可选,用例2不是必须执行的。
5.4.4 «include»和«extend»区别
«include» | «extend» | |
---|---|---|
这个用例是可选的吗? | 否 | 是 |
没有这个用例,基本用例还完整吗? | 否 | 是 |
这个用例的执行是有条件的吗? | 否 | 是 |
这个用例改变了基本用例的行为吗? | 否 | 是 |
使用这两个关系可能出现的错误!
虽然这两个概念在指定共同功能(«include»)和简化复杂用例流程(«extend»)时非常有用,但是不能使用这两个功能对用例进行分解,使用这两个功能对用例进行分解的中心思想还是结构化分析与设计。
5.5 活动图
活动图提供了活动流程的可视化描述,可以是在系统、业务、工作流或其他过程中。这些图关注被执行的活动以及谁负责执行这些活动。
活动图的元素包括动作节点、控制节点和对象节点。有三种类型的控制节点:初始和终止(终止节点有两个变例:活动终止和流程终止)、判断和合并、分叉和结合等。
5.5.1 开始、结束、判断节点和合并节点
- 如上图,活动流程的开始处(初始节点)被表示为一个实心的黑色圆心。
- 如上图,活动流程的结束处(终止节点)被表示为一个圆圈套一个黑色圆心。
- 如上图,另一种类型的终止节点是流程终止节点,它由一个圆圈和一个“X”表示。流程终止节点用于停止一个流程,但不会停止整个活动。
- 如上图,判断和合并节点控制了活动图中的流程。每种节点都由一个菱形来表示,带有进入和离开的箭头。判断节点具有一个进入流程和多个离开流程,其目的是将进入流程导向一个(且只有一个)离开流程。离开流程通常有一些约束条件,以决定选择哪一条离开的路径。
- 如上图,合并节点接受多个进入流程,并全部导向一个离开流程。合并节点上没有等待或同步。不论哪个进入流程到达合并节点(显示为一个菱形),都会通过它被导向到后面的动作。
- 如上图,大括号中的内容是约束内容,通过约束内容判断流程的走向。
5.5.2 分区(泳道)、分叉、结合以及对象流
- 如上图,整个活动图分为三个区,分区也叫泳道,在活动图中利用分区来分组。分区的目的是说明执行具体活动的职责。在业务模型中,分区可以是一个业务单位、部门或组织机构;对于系统来说,分区可以是其它系统或子系统;在应用建模中,分区可以是应用中的对象。每个分区都可以被命名,表示负责者。
- 如上图,在Gardener分区中的黑色粗横线就是分叉节点和结合节点,分叉节点和结合节点分别与判断节点和合并节点类似,关键的不同之处在于并发。分叉节点具有一个进入流程和多个离开流程,判断节点也是如此。不同之处在于,判断节点会选择一个离开流程,而进入分叉节点的一个流程会导致多个离开流程。所有离开流程是并发的。结合节点具有多个进入流程和一个离开流程,与合并节点类似。但是对于结合节点来说,必须完成所有进入流程之后,才能够进入离开流程。这里必须确保每条流程不会进行阻塞,如果出现阻塞,一定要有另一条可选路径能让控制流进行下去。
- 如上图,在WaterTank分区中的两个直角矩形就是对象流,在某些情况下,看到活动图中操作的对象可能比较有用,通过添加对象流,可以在活动图中显示对象(但是,不推荐在所有活动图中都这样做,因为添加所有的对象会使图变得复杂而笨拙)。
5.6 类图
在系统的逻辑视图中,类图用于表示类和它们之间的关系。单张类图表示了系统类结构的一个视图。在分析时,我们利用类图来说明实体共同的角色和职责,这些实体提供了系统的行为。在设计时,我们利用类图来记录类的结构,这些类构成了系统的架构。类图中有两个基本元素,即类和它们的基本关系。
5.6.1 类表示法
类图标由三个部分组成:第一个部分放置类名,第二个部分放置属性,第三个部分放置操作。
每个类都需要一个名称,而且此名称必须在它的命名空间中唯一。属性名称在类的范围内必须无二义。根据所选择的实现语言的规则,操作名称也必须如此。属性和操作规格说明的格式如下:
属性规格说明格式:
可见性属性名称:类型[多重性] = 默认值{特性字符串}
操作规格说明格式:
可见性操作名称(参数名称:类型):返回值{特性字符串}
“参数名称:类型”的组合根据需要可以重复,包含几个参数。
可见性属性名称在类图中可以通过以下的符号指定相应元素的可见性。在类关系的关联端名称上加上这些可见性符号,说明关联的可见性,表示从源端到目标端的访问。
公有可见性(+):对能看到这个类的任何元素都可见。
保护可见性(#):对这个类及其子类的其他元素可见。
私有可见性(-):对这个类的其他元素可见。
包可见性(~):对同一个包中的其他元素可见。
对于属性和操作来说,类型是一个类或数据类型的名称。在上图中,measuredTemperature属性的多重性是[0…60],这表示了一个包含0~60个温度测量值的数组。属性的默认值是在创建时给出的值,如果没有提供其他值的话。花括号中列出的特性字符串提供了额外的特性。例如,TemperatureSensor类中measuredTemperature属性后面的{list}。关键字list表示温度测量值是有序的,而且可能有重复值。
对于特定的类图,显示一个类的某些属性和操作是有用的。我们说“某些”是因为,对于凡是具有一点重要性的类,在一张类图中显示它的所有属性既不方便,也不必要。如果我们需要显示许多成员,可以放大类图标;如果我们决定不显示这样的成员,可以去掉分隔线,只显示类名。作为表示法的一般原则,属性和操作这样的元素的语法可以根据所选的实现语言的语法来定制。这分离了不同语言的特性,简化了表示法。
抽象类是不能创建实例的类。因为这样的类对于构建良好的类层次结构非常重要,所以有一种特殊的方式来表示抽象类,具体来说,就是用斜体来显示类名,表明只能为它的子类创建实例。类似的,为了表明一个操作是抽象的,用斜体来显示操作名称,这意味着这个操作可以在它的子类中以不同的方式实现。
5.6.2 类图中的类关系
类很少是独立的,相反,它们会通过不同的方式与其他类协作。类之间的基本联系包括关联、继承、聚合和组合,每种关系都可以包含一个文本标签,记录关系的名称或者说明它的目的,关联端也可以有名称,但通常不会同时存在。
- 关联图标连接了两个类,体现了一种语义联系。关联通常用名词词组来标注,如图中的Analyzes,以说明关系的实质。类可能与它自己有关联(称为自关联)。在图中,这里同时使用了关联端名称(关联端名称是在这个类中所扮演的一种角色,如上图的staff和lead)和关联名称(关联名称是两个类之间关联的语义,如上图的Analyzes),目的是提供清晰性。也有可能在同一对类之间存在多个关联。关联可以进一步通过多重性来修饰。多重性有如下例子常用:
1. 精确1个。
2. 0…* 0个或者多个。
3. 3…7 指定范围(3~7个,包含3和7)。
多重性应用于关联的目标端,说明源类的每个实例与目标类实例的连接个数。除非显示说明,否则关系的多重性就是未指定的。通常最好是显示多重性,因为这样就不会引起误解。关联具有方向性。在分析时,我们认为关联是分析类之间的双向逻辑连接。在设计时,我们将关注的焦点转到关联的导航性上。
2. 其他三个基本类关系是对一般关联图标的细化。实际上,在开发过程中,这正是关系演变的一种趋势。我们先断定两个类之间存在语义上的联系,然后随着具体地确定它们之间联系的实质,常常会将关联细化为泛继承、聚合和组合关系。
3. 继承图标表示一种一般/特殊关系,表现为带有封闭箭头的关联。箭头指向超类,关联的另一端是子类。根据所选实现语言的规则,子类继承了超类的结构和行为。同样,根据这些规则,一个类可以有一个(单继承)或多个(多继承)超类,超类间的名字冲突也根据所选语言的规则来处理。另外,继承关系不能有多重性指定。
4. 聚合图标表明一种整体/部分层次结构,也意味着能够从聚合体导航到它的部分。它表现为带有一个空心菱形的关联,菱形所在的一端是聚合体(整体),另一端的类代表它的实例构成了聚合对象的部分。自聚合和循环聚合关系是可能的。这种整体/部分的层次关系并不意味着物理上的包容。聚合关系末端的*(0个或多个)多重性进一步突出了这不是物理包容关系。
5. 选择聚合通常是分析或架构设计时的决定,选择组合(物理包容)通常是具体的、战术上的问题。区分物理包容是很重要的,因为在构建和销毁聚合体的部分时,它的语义会起作用。组合图标表示一种包容关系,表现为带有一个实心菱形的关联,菱形所在的一端是整体。在这一端的多重性是1,因为根据定义,部分在整体之外就没有任何意义,整体拥有部分,部分的生命周期与整体是一样的。
6. 在JAVA中除了继承还有实现关系,和继承图标类似,只是子类和超类连接的线是虚线。JAVA中的多继承通过接口来完成,在JAVA中继承没有多继承。
7. 约束是必须保持的某些语义条件的表达式。换言之,约束是类或关系中的不变式,当系统处于稳定状态时必须保持。进入条件和退出条件就是约束的例子,它们在系统处于稳定状态时是适用的,即在操作调用和完成的特定时间点是适用的。我们会在各种建模元素上应用约束,通常,任何元素都可以加上约束。我们使用一个包含表达式的约束修饰符号,将其放在花括号({})中,使其位于约束适用的类或关系附近。如上图应用于泛化关联的约束表明这个关联上的分类是否完整、是否重叠,利用下面定义的4种约束:
Complete:超类的一个实例至少是子类的一个实例。
Incomplete:超类的一个实例可能不是子类的一个实例。
Disjoint:不同分类之间没有共同实例。
Overlapping:不同分类之间有共同实例。
5.7 序列图
作为一种通信图,序列图用于跟踪在同一个上下文环境中一个场景的执行。实际上,序列图在很大程度上就是通信图的另一种表现形式。使用序列图的好处在于比较容易读出消息传递的相对次序。序列图倾向于关注事件而不是操作,因为事件有助于确定被开发系统的边界。
- 如上图在序列图中,我们感兴趣的实体被水平地放在图的顶部。垂直的虚线称为“生命线”,画在每个对象下面,表示对象的存在。
- 消息(它可能表示事件或操作的调用)被画成水平的。消息图标的端点与垂直线相连,这些垂直线又与图顶部的实体相连。消息从发出者指向接收者。次序由垂直位置来表示,第一个消息出现在图的顶部,最后一个消息出现在图的底部。因此,就不需要顺序编号了。
- 消息的表示法(如线的类型和箭头的类型)说明了消息的类型,如图
- 同步消息(通常是一次操作调用)显示为一条带实心箭头的实线。
- 异步消息是一条带开放箭头的实线。
- 返回的消息是一条带开放箭头的虚线。
- 丢失的消息(没有到达其目的地的消息)显示为一个同步消息,以端点(一个黑圆点)终止。
- 发现的消息(发送者未知的消息)显示为一个同步消息,起点是一个端点符号。
- 销毁事件表明何时一个对象被销毁,它表示为生命线末端的一个X。如果对象是一个组合对象,那么相关的对象也会被销毁。如上图右边的序列图。
- 简单的序列图可能没有说明消息传递时控制的焦点。例如,对象A向其他对象传递消息X和Y,我们可能不清楚X和Y是来自A的独立消息,还是作为同一个封装消息Z中的一部分。为了澄清这种情况,可以对序列图中从每个对象垂下的竖线进行修饰,加上一个盒子,表示控制流的焦点在这个对象上。如上图的netCost操作。
- UML 2.0拥有各种结构来简单化复杂的序列图。首先要介绍的就是“交互使用”。交互使用是一种手段,用于说明在一个序列图中想复用别处定义的一个交互场景。如上图的Login片段,它表现为ref标签。在这个例子中,引入了一个登录序列,要求在PlanAnalyst使用系统之前完成这个带有ref标签的框表明,在这个序列中放置这个框的位置插入了Login序列(即复制了这个序列)。实际的登录序列会在另一张序列图中定义。
- 正如我们所看到的,序列片段可以用来简化序列图,也可以用来表示序列图中的流程控制结构。如上图,这个框是以交互操作符loop命名的。在这个框内执行的序列是由条件[For each Gardening Plan]所控制的。可以通过alt交互操作符来实现判断。在这个循环中,做了一个选择,由条件[GardeningPlan is Active]和[GardeningPlan is Inactive]所控制。这些条件选择执行序列的哪一部分。alt框被分成两个区域,每个区域都有自己的条件。当条件为真时,框中这个区域的行为就得到执行。
- 虽然不是UML 2.0的正式组成部分,但可能看到在序列图中使用描述性的文字。对于涉及条件或迭代的复杂场景来说,序列图可以通过使用脚本来增强。脚本可以写在序列图的左边,脚本的步骤与消息调用对齐。脚本可以用自由格式编写,也可以采用结构化的英语文本或者采用所选实现语言的语法。
5.8 交互概述图
交互概述图是活动图和交互图的结合,目的是概述交互图元素之间的控制流。虽然可以使用任何类型的交互图(序列图、通信图或时间图),但序列图可能是用得最多的。交互概述图中的主要元素是框、控制流元素和交互图元素。
交互概述图通常由一个框围绕,但是在上下文背景清楚时,框是可省略的。围绕的框的名称是“sd MaintainTemperature lifelines :Environmental- Controller, :Heater, :Cooler”,位于框的左上角。这个名称的意义如下。
■ sd:标签,表明这是一张交互图。
■ MaintainTemperature:名称,描述这张图的目的。
■ lifelines :EnvironmentalController, :Heater, :Cooler:可选的包含生命线的列表。
交互概述图中的控制流是由活动图元素的组合来实现的,提供了可选路径和并行路径。可选路径控制是由判断节点的组合来实现的,控制流在判断节点选择合适的路径,对应的合并节点(如果需要)将所有可选路径重新会聚在一起。
5.9 组合结构图
组合结构图提供了一种手段来描述结构化类及其内部结构的定义。这种内部结构是由部件及其相互连接构成的,它们都处于这个组合结构的命名空间之内。结构化类可以嵌套,所以其中的部分可以是另一个结构化类。因此,组合结构图在设计时是很有用的,可以将类分解为组成部分,并对它们在运行时刻的协作进行建模。组合结构图和组件图的区别非常小,唯一的区别是组合结构图适用于任何类。
组合结构中部分的名称采用的格式是“角色名称: 类名称[多重性]”,其中的角色名称定义了这个部分在这个组合结构中所扮演的角色。
组合结构及其部分通过端口与它们的外部环境实现接口,端口用组合结构边界上的一个小正方形来表示。端口的用法和组件图中的用法一致。我们将接口与这些端口连接起来,这些接口定义了这个组合结构的交互细节。这些接口通常以小球和球窝的表示法来表示。和组件图的用法也是一致的。如下图是一个组合结构图的定义:
5.9.1 协作
协作是一种类型的结构化类,它说明了结构化类实例在运行时刻的交互。它与组合结构的不同之处在于,它不是实例化的,所以我们不会拥有这些实例。但是,它定义了结构化类实例必须承担的角色,以及它们协作提供功能所需要的连接器。协作可以嵌套,抽象的概念让我们能够将关注的焦点放在某一个层面上,与我们考虑的问题有关。协作的细节可能通过交互图来表示。
协作的名称显示在封装该协作的虚线椭圆内,可以用一条虚线与角色定义分开来。如下图,在这个协作中,我们定义了两个角色,即TemperatureController和TemperatureRamp,它们由一个连接器相连。由于连接器没有被命名,它将由临时的运行时刻手段来实现,如一个属性或一个参数。如果它有名称,就会实现为一个实际关联的实例,即一个链接。
角色带有名称和类型的标签,格式为“角色名称: 角色类型[多重性]”。角色名称描述了某个可以承担这一角色的类实例,角色类型是对这个类实例的约束。我们展示了TemperatureController和TemperatureRamp这两个角色的角色名称、角色类型和多重性。
一个角色定义了一些属性,这些属性是一个结构化类参与这个协作所必须具备的。用接口来定义角色的类型意味着只要满足这个接口的实例就能够承担这个角色,而不论它的内部设计或实现如何。类实例可以拥有超过某个角色所需要的功能,这样,它就能够在一个协作中同时承担多个角色,甚至同时在不同协作中承担多个角色。
5.10 状态机图
状态机在用到实时处理的行业中是众所周知的。状态机图被用来设计和理解时间关键的系统,在这种系统中,时间不正确的后果是严重的。状态机图可以在理解系统对关键事件的响应中起到重要的作用。
状态机图将行为表示为一系列的状态转换,由事件触发,并与可能发生的动作相关联。这样的状态机也被称为行为状态机。状态机图通常用于描述单个对象的行为。但是,它们也可以用于描述系统中更大元素的行为。可以利用状态机图来展示整个系统的事件次序相关的行为。在分析过程中,可以利用状态机图来说明系统的动态行为。在设计过程中,可以利用状态机图来记录单个类或几个协作类的动态行为。状态机图和活动图有亲戚关系。但是,状态图关注的是状态以及状态之间的转换,而不是活动的流程。状态机图的两个基本元素是状态和状态转换。
5.10.1 基本概念
对象的状态代表了它的行为的累积结果。在任何给定的时间点,对象的状态包含了它的所有特性(通常是静态的),以及这些属性当前的值(通常是动态的)。所谓特性,指的是这个对象的所有属性和它与其他对象的关系。可以将单个对象状态的概念进行抽象,应用于这个对象的类,因为同一个类的所有实例都处于相同的状态空间,这个状态空间包含了不确定的、有限可能性的一组状态。
在每个状态机图中,必须只有一个默认的初始状态。我们从一个特殊符号画出一个无标签的转换指向这个初始状态,这个特殊符号显示为一个实心的圆。有时候,也需要设计一个结束状态。通常,与一个类或整个系统有关的状态机永远也不会到达结束状态,只是在包含这个状态机的对象被销毁时,它就不存在了。结束状态的表示方式是从它画出一个无标签的转换,指向一个特殊的符号,这个特殊符号显示为一个空心圆套着一个实心圆。初始状态和结束状态在技术上被称为“伪状态”。对于简单的状态,状态的名称显示在代表状态的圆角矩形中。
状态之间的移动称为“转换”。在状态机图中,转换表示为状态之间的有向箭头。每个状态转换连接两个状态。在状态之间移动被称为“触发一次转换”。状态可以有指向自己的状态转换,许多不同的状态转换指向同一个状态也是很常见的,但是这样的转换必须是唯一的,即任何时候都不会从一个状态触发多个状态转换。
有多种不同的方式可以控制触发一次转换。没有标注的转换称为“完成转换”。这就是意味着,当源状态完成时,这个转换就会被自动触发,然后进入目标状态。在其他情况下,必须发生某些事件才能触发转换,这样的事件标注在转换上。事件是可能导致系统状态改变的某些发生的事情。
5.10.2 高级概念:状态活动(入口活动、执行活动和出口活动)、控制转换
活动可能与状态关联起来。具体来说,可以指定在状态相关的某个时间点上执行某种活动,:
■ 在进入一个状态时执行一个活动;
■ 在处于一个状态时执行一个活动;
■ 在离开一个状态时执行一个活动。
如下图,在进入Timing状态时,启动了计时器(由一个箭头指向两条平行线的图标表示)。
停止了计时器(由在两条平行线之间的一个箭头的图标表示)。
处于这个状态中时,测量时间的推移(由环形的箭头表示)。
前面曾提到,可以有一些方式对状态转换实现更好的控制,不仅仅是事件导致转换。可以通过指定条件来控制转换。这些条件起到了监护的作用,当事件发生时,条件将决定允许转换发生(如果条件为真)或不允许转换发生(如果条件为假)。
控制转换的另一种方法是利用效果。效果是一种行为(即活动、动作),它在指定事件发生时发生。因此,当转换事件发生时,转换会被触发,效果也同时发生。这些改进可以相互组合使用。
如上图,可以看到当时间间隔超过维护时间时,计时器将转换到Sounding Alarm状态,警告需要维护。从Sounding Alarm到Timing的转换展示了组合使用事件、条件和效果的情况。有一个clear事件,但它有一个约束条件,即制冷器必须回到工作状态(然后计时才会继续)。这个条件被放在方括号中。如果制冷器没有回到工作状态,这次转换就不会被触发。如果制冷器回到了工作状态,效果会发生(持续时间被重置为0,在斜杠后显示),转换也会被触发,计时重新开始。
5.10.3 高级概念:复合状态与嵌套状态
嵌套状态的功能让状态图有了深度,这是状态图的一个主要功能,可以缓解复杂系统中状态和状态转换产生组合爆炸的情况。
如下图,这种嵌套用一个环绕的边界来表示,称为“区域”。这个封闭的边界被称为一个“复合状态”,所以我们现在有了一个复合状态,名为Operating,它包含内嵌的状态Timing、Sounding Alarm和Paused。还要注意,这个图在视觉上进行了简化,只有一个转换从复合状态Operating指向最终状态。这意味着任何内嵌状态的off事件发生时,都会触发向最终状态的转换。
嵌套的深度可以是任意的,所以子状态也可以是复合状态,包含更低层的子状态。对于复合状态Operating和它的三个子状态来说,嵌套的语义意味着一种XOR(异或)关系。如果系统处于Operating状态(复合状态),那么它必须处于三个子状态之一:Timing、Sounding Alarm或Paused。
在画嵌套的状态转换图时,为了简单起见,可以聚焦或不聚焦于某个状态。不聚焦时略去子状态的细节,聚焦时会显示子状态的细节。
5.10.4 高级概念:并发与控制
并发行为可以用状态机来描述,只要将复合状态用虚线分成两个或多个子区域就可以了。复合状态中的每个子区域表示并发发生的行为。
5.10.4.1 进入复合状态
5.10.4.1.1 方式一
如下图,在这种情况下,并发状态机将全部被激活,并发地开始工作。每个独立的子区域都会从这个区域中默认的初始状态开始(伪状态)。虽然不是必需,但我们建议明确地在子区域中显示初始状态,这样会更清晰。
5.10.4.1.2 方式二
另一种进入复合状态的方式就是利用分叉。分叉节点实际上是另一种伪状态(像初始状态和最终状态一样),它将进入的转换分成两个或更多离开的转换。如下图展示了这种方式下分叉的用法。这个单一的输入转换可能有一个事件或一个约束条件,多个离开的转换则可能没有。分叉将控制流分开,复合状态中的并发执行将从子状态A、B和C开始,它们是从分叉离开的多个转换的目标子状态。
5.10.4.2 退出复合状态
5.10.4.2.1 方式一
5.10.4.2.2 方式二
如下图,利用一个结合节点(另一个伪状态)来执行类似的控制流合并。来自一些并发子区域的转换指向一个竖线段,在这里它们合并成一个离开的转换。这个离开转换也可以有事件或约束条件。在这种情况下,当所有的结合子状态(A、D和C)被激活,事件发生,而且条件满足时,到状态S的转换就会发生。
5.10.4.3 其他情况
如上图,因为没有结合元素的元素,在这种情况下,如果来自子状态A、D或C的转换中的任何一个被触发时,所有其他并发的子状态都会终止。
如上图,如果某些子区域没有显式地参与结合,那么当结合转换被触发时,没有参与的子区域会被强制终止。反过来,如果某些子区域没有显式地参与分叉,那么没有参与的子区域会从它们初始的状态开始执行。
5.10.4.4 示例
5.10.5 高级概念:子状态机状态
除了简单状态和复合状态之外,还有第三种主要的状态类型——子状态机状态。子状态机状态被用于简化状态机图,或者用于包含可以在不同图中复用的常见行为。这个子状态机状态代表了“一个完全独立的状态机图”。通过这种方式,子状态机让我们能够将复杂的状态机图组织成为可以理解的结构。如下图的Operating:Recording状态。
5.11 对象图
对象图用于说明系统的逻辑设计中存在的对象以及对象之间的关系。换言之,对象图代表了时间上的一张快照,记录了一组特定配置的对象上的瞬时事件流。因此,对象图是原型化的——每张对象图都代表了结构上的关系,这些关系可能发生在一组给定的类实例上。从这个意义上说,单张对象图代表了系统对象结构的一张视图。在分析过程中,对象图常用于说明主要场景和次要场景的语义,提供对系统行为的跟踪。在设计过程中,对象图常用于说明逻辑系统设计中各种机制的语义。不论在哪个开发阶段,对象图都能提供一些具体的例子,帮助实现关联类图的可视化。
对象图的两个基本元素是对象和它们的关系。
与类图相似,水平线将图标内的文字分成两个区域,一个区域代表对象的名称,另一个区域提供了可选的对象属性及值的视图。
对象的名称可以采用下面三种格式之一:
■ objectName 只有对象名
■ :ClassName 只有类名
■ objectName :ClassName 对象名和类名
所有的对象名称都加了下画线,目的是清楚地区分对象名称和类名称。如果没有指定一个对象的类,既没有用上面的语法显式地指定,也没有在对象的说明中隐式地指定,那么这个对象的类就被认为是匿名的。如果只指定了类名称,那么这个没有对象名的图标代表的就是一个明显的匿名对象。
对于某些对象来说,显示部分或全部属性可能会有用。说“某些”,是因为这些对象代表的只是对象结构的一个视图。这些属性必须引用这个对象的类或超类中定义的属性。语法中包含了指定每个属性的值的能力,
当且仅当两个对象对应的类之间存在关联时,对象之间才可能存在链接。这种类关联可以通过任何方式实现,这意味着类的关系可以是简单的关联、继承、聚合或组合。两个类中存在这种关联则表明,两个类的实例之间存在着一条通信路径(即链接),一个对象可以通过它向另一个对象发送消息。所有的类都隐式地有到自己的关联,因此对象可以向自己发送消息。
我们指出类图中的关联可以用一个角色来修饰,说明一个类关联另一个类的目的或容量。对于某些对象图,重申两个对象间对应链接的这种角色也是有用的。通常,这种修饰有助于解释为什么一个对象操作另一个对象。
我们看到一种名为“限定符”的类修饰,它的值在关联的目标端唯一地确定了一个对象。具体来说,CropEncyclopedia类利用scientificName作为限定符,来导航到CropEncyclopedia实例管理的一组条目中的具体一项。如下图的作物实例是commercialStrawberry,它选择利用Fragaria × ananassa作为scientificName限定符。
5.12 通信图(协作图)
通信图在UML 2.0以前的版本中的名称——协作图。通信图是一种类型的交互图,它关注对象在参与具体的交互时,对象之间如何链接以及传递什么消息。
5.12.1 基本概念及示例
当且仅当两个对象对应的类存在关联时,对象之间才可能存在链接。两个类中存在这种关联则表明,两个类的实例之间存在着一条通信路径(即链接),一个对象可以通过它向另一个对象发送消息。
如果对象A有一个链接到对象B,A就可以调用B中的类开放给A调用的所有操作,反之亦然。我们将调用操作的对象称为“客户”,将提供操作的对象称为“提供者”。一般来说,消息的发出者知道接收者,但接收者不一定知道发出者。
在稳定的状态下,系统的类结构和对象结构必须一致。如果我们展示了通过链接L调用了对象B上的操作M,那么B的规格说明(或某个超类的规格说明)中必须包含操作M。
可以用一条或多条消息来修饰一个链接。通过一条指向目标对象的有向线段,可以说明消息的方向。操作调用是最常见的一种消息类型(另一种类型是信号)。操作调用可以包含实际的参数,这些参数与操作的签名相符。有向线段的箭头表示法区分了操作是同步调用还是异步调用,实心箭头的有向线段表示为同步调用,开放箭头的有向线段表示为异步调用。如下图中都是同步调用。
为了显示明确的事件次序,我们在消息前面加上一个序号(从1开始)。这个顺序表达式说明了消息的相对次序,序号较小的消息在序号较大的消息之前被发送。利用嵌套的小数点数字的形式(如4.1.5.2),可以展示某些消息是嵌套在下一次更高层的过程调用之内的。每个整数部分说明了在交互过程中嵌套的层次。同一层的整数部分说明消息的次序处于同一层次。
5.12.2 迭代子句和约束条件
还可以在顺序表达式上加上附加信息来精确规定执行发生的方式。可以加上一个迭代子句来说明会发出一系列的消息。迭代子句的形式取决于个人喜好,但是使用伪代码似乎是个不错的选择。如下图turnOn()操作,这个修饰以星号开始,后面跟上方括号括起来的迭代子句。这个例子表明,turnOn消息将依次被发出,从1到n。如果消息是被并发发出的,星号后面可以跟一个双竖线(即*||[i=1…n])。
约束条件也可以用于修饰消息。表示法和迭代子句类似,但没有星号。约束条件放在方括号内,如下图中startup消息上的约束条件。这个条件表明,消息将在约束条件为真时执行,在本例中就是当温度低于预期的最低温度时。约束条件的表示方式取决于个人喜好。
6 项目开发的过程
6.1 成功项目的特征
成功的软件项目是指:提交产物达到或超出客户预期,开发过程符合时间和费用的要求,并且结果在面对变化和调整时有弹性。根据这个标准,我们注意到,我们遇到的所有成功的面向对象系统基本上都有以下特征:
1. 存在很强的架构愿景。
2. 应用了管理良好的迭代、增量式开发生命周期。
6.1.1 架构愿景
架构可以定义为“系统的基本组织结构,包括它的组件、组件之间的相互关系、环境,以及设计和演进的指导原则”。架构关注结构和行为,只关注重要的决定,例如“可能符合某种架构风格,受到涉众和环境的影响,包含理性的决定”。
具有良好架构的系统是具有概念完整性的系统。概念完整性是系统设计中最重要的考虑。系统的架构对于构建可理解、可扩展、可重新组织、可维护和可测试的系统来说非常关键的。只有对系统的架构有清晰的感觉,才可能发现共同的抽象和机制。研究这种共性最终将使得系统的构建变得更简单,从而使系统更小、更可靠。
好的架构应该是面向对象的,由组件构成。这不是说所有面向对象的架构都是好的,也不是说只有面向对象的架构才是好的。主要是应用面向对象分解的基本原则通常可以得到一些架构,这些架构展示了我们所期望的组织复杂性的特点。
好的软件架构通常具有以下一些共性:
■ 它们由定义良好的抽象层构成,每层代表了一种内聚的抽象,提供了定义良好的、受控的接口,并且建立在同样定义良好的、受控的低层抽象设施之上。
■ 在每一层的接口和实现之间有清晰的关注点分离,让我们能够改变一个层的实现而不会破坏它的客户对它的假定。
■ 架构很简单:共同的行为是通过共同的抽象和共同的机制来实现的。
以这种方式组织的架构会降低复杂性,也更为健壮和可靠。它们还促进了更有效的复用。
敏捷过程倾向于降低早期建立架构的重要性。相反,它们描述了简单设计、浮现式设计、重构和“偶然发现的”架构等概念。在这样的过程中,架构随时间而演进。对于“追求理性的开发过程”来说,你所选择的方式取决于你的具体情况。不论架构在生命周期的何时完成、如何完成,都不会降低拥有架构愿景的重要性。没有这样的愿景,系统就较难以随时间演进,也更难维护。
6.1.2 迭代和增量式开发生命周期
迭代和增量式开发是以连续系列的发布版本(迭代)的方式来开发系统的功能,完成度不断增加(增量)。在一次迭代中选择实现哪些功能,取决于对项目风险的缓解,最重要的风险先处理。作为一次迭代的结果,得到的经验和产物将应用于下一次迭代。通过每次迭代,可以逐渐优化战略和战术决策,最后得到一个满足用户真实需求(常常没有明说)的解决方案。同时,这个方案又是简单、可靠、能适应变化的。
迭代和增量的方式是大多数现代软件开发方法的核心,包括像极限编程(XP)和SCRUM这样的方法。它非常适合面向对象的方法,在风险管理方面提供了一些优点。
下面是迭代式开发方式的一些好处:
■ 允许需求变更,每次迭代关注一组具体的需求。
■ 没有在项目结束时的“大爆炸式”的集成工作,每次迭代都包括这个版本中元素的集成,集成是渐进的、持续的。
■ 风险尽早得到关注。早期的迭代缓解了关键的风险,允许在生命周期的更早时候确定新的风险,那时候更容易处理这些风险。
■ 可以实现对产品的战术改变。为了针对竞争者的动作,可以对产品或产品的早期版本进行改变。
■ 促进了复用。架构的关键组件实际上是在早期建立的,这样更容易确定可复用的组件和利用已有组件的机会。
■ 可以更早发现缺陷并修正。每次迭代都执行测试,这样缺陷就可以更早被发现,并在下一次迭代中被修正,而不用等到项目的后期才发现,那时可能已经没有时间来修复缺陷(或者修复缺陷的影响太大)。
■ 项目人员的工作更有效。迭代式开发鼓励团队成员在迭代中承担不同角色,这与管道式的组织方式不同。在管道式的组织方式中,分析师转给设计者,设计者转给程序员,程序员转给测试者,如此等等。迭代式开发充分利用了团队成员的经验,消除了转手过程。
■ 团队成员不断地学习。每次迭代都让团队成员有机会从过去的经验中学习(“实践创造完美”)。一次迭代中的问题可以在后面的迭代中解决。
■ 开发过程可以得到优化和改进。每次迭代都会对过程和组织方式中有效和无效的部分进行评估。这些评估的结果可以用于改进下一次的迭代过程。
6.2 敏捷过程、计划驱动和敏捷方法
6.2.1 敏捷过程
对于敏捷过程来说,其主要目标是在短时间内向客户提交满足他们需求的系统。过程只是实现目标的一种手段。因此,敏捷过程大致有以下特征:
■ 轻量级、松散式、不太正式(只做绝对需要做的事情)。
■ 依赖于团队成员的隐式知识(而不是有良好文档的过程)。
■ 关注战术超过关注战略(不要为将来而构建,因为将来是不可知的)。
■ 迭代式和增量式(在几个周期中提供系统的一些部分)。
■ 严重依赖于客户的协作(客户积极参与需求确定和验证)。
■ 自组织和管理(团队想出最好的工作方法)。
■ 浮现式而不是预先确定(在实际执行过程中让过程演进,而不是事先计划或确定)。
敏捷过程让软件开发团队不必遵循一组严格的步骤,使开发者能够将他们的创造力集中在被开发的系统上。
6.2.2 计划驱动
对于计划驱动的过程来说,除了在可接受的时间框架内向客户提交期望的系统之外,另一个重要的目标是定义和验证一个可预测、可重复的软件开发过程。这个过程不只是实现目标的一种手段,它本身就是一个目标。换言之,除了客户要求的系统之外,软件开发过程本身及其文档都是关键产物。因此,计划驱动的过程大致有以下特点:
■ 较重量级、较正式(按照规定的活动得到齐全的文档)。
■ 依赖于有良好文档的过程(而不是项目团队的隐式知识)。
■ 关注战略超过关注战术(建立起强大的架构框架,可以包容将来的变化)。
■ 依赖于客户的合约(制定合约并达成一致意见,合约描述了构建内容)。
■ 受管理的、受控制的(遵守详细的计划,无论是团队内部还是团队之间都包括明确的里程碑和验证点)。
■ 事先定义然后持续改进(包括明确的过程改进流程和机制)。
6.2.3 如何选择
敏捷 | 计划驱动 |
---|---|
项目小(5~10人) | 项目大(超过10人) |
有经验的团队,具有各种能力和技术 | 团队包括不同的能力和技术 |
团队成员是自我驱动的、独立的领导者及其他自己知道方向的人 | 团队是地理上分散或外包的 |
项目是内部项目,团队在相同的地理位置 | 项目具有战略重要性(例如,是一个企业首创的),范围跨越整个组织机构 |
系统是新的,充满了未知数 | 系统已经充分理解,具有熟悉的范围和功能 |
需求必须是已被发现的 | 需求相当稳定(变化的可能性小),能够事先确定 |
需求和环境是易变的,变化的可能性很大 | 系统大而复杂,具有关键安全需求或高可靠性需求 |
最终用户的环境是灵活的 | 项目涉众与开发团队之间的关系一般 |
与客户的关系密切,有很好的合作 | 存在外部合法性考虑(如合约、义务、针对具有行业标准的正式认证) |
客户随时可以找到,全职投入且在同一地理位置 | 重点是强大的、量化的过程改进 |
在开发团队内部、开发团队之间、开发团队和顾客之间存在高度信任的环境 | 重视过程的定义和管理 |
需要快速价值和快速响应 | 重视过程的可预测性和稳定性 |
6.2.4 敏捷方法
XP生命周期包括5个阶段。
(1)探索:确定可行性,理解第一个发布版本的关键“故事”,开发探索原型。
(2)计划:对第一个发布版本的日期和故事达成一致意见。
(3)迭代发布:通过一系列的迭代来实现并测试这些故事,优化迭代计划。
(4)产品化:准备支持材料(文档、培训、市场),并部署可操作的系统。
(5)维护:修复和扩展已部署的系统。
SCRUM生命周期包括4个阶段。
(1)计划:建立愿景、设定期望值、确保资金、开发探索原型。
(2)筹划:为首次迭代准备优先级和计划,开发探索原型。
(3)开发:通过一系列的冲刺来实现需求,并优化迭代计划。
(4)发布:准备支持材料(文档、培训、市场),并部署可操作的系统。
6.3 宏观过程和微观过程
定义宏观过程和微观过程时,关键的一点就是它们的关注点非常不同。宏观过程关注的是总体软件开发生命周期,而微观过程关注的是具体的分析和设计技术,即从需求到实现过程中使用的技术。生命周期风格(如瀑布式、迭代式、敏捷、计划驱动等)的选择将影响宏观过程,分析和设计技术(如结构化、面向对象等)的选择将影响微观过程。
虽然定义了过程,但和过程有关的工作并没有结束。随着开发生命周期中发现的问题,应该优化过程(理想情况是在每次迭代之后)。效果很好的过程活动应该保留,效果不好的过程活动应该去除(然后,继续并重复这一过程)。根据过程执行的实际经验对过程进行持续改进,这应该是我们的目标。
6.3.1 宏观过程:软件开发生命周期
宏观过程是总体的软件开发生命周期,它是微观过程的控制框架。它代表了整个开发团队的活动,因此,宏观过程规定了一些可测量的产物和活动,让开发团队能够有意义地评估风险,并尽早对微观过程进行调整,以便更好地关注团队的分析设计活动。
6.3.1.1 概述
宏观过程的目的是指导系统的整体开发,最终得到产品系统。宏观过程的范围从确定想法开始,直到实现该想法的第一个软件系统版本为止。
迭代增量式的宏观过程确定了系统将以演进方式,通过不断的优化,最后得到产品系统。这个过程的主要产物是一系列可执行的发布版本(迭代),体现了系统不断的优化(增量)。次要的产品包括系统行为原型,目的是探索可选的设计,或进一步分析系统功能中不明确的部分。次要的产品还包括用于记录设计决策和理由的文档。
宏观软件开发过程可以从两个维度进行描述,即内容和时间。内容这一维描述了角色、任务和工作产品,也可以从科目的方面来描述,从逻辑上对内容进行分组。时间这一维描述了这个过程的生命周期,可以从里程碑、阶段和迭代的角度来描述。
6.3.1.2 宏观过程的内容:科目
宏观过程包括下列科目,按相对次序来执行。
(1)需求:对于系统该做什么,建立并保持与客户和其他涉众的一致意见。定义系统的边界(划定界限)。
(2)分析与设计:将需求转化为系统设计,设计将作为特定实现环境下实现的规格说明。这包括逐渐形成一个健壮的系统架构,建立起系统不同元素必须用到的共同机制。
(3)实现:实现、单元测试以及对设计进行集成,得到一个可执行的系统。
(4)测试:对实现进行测试,确保它实现了需求(即需求得到了正确的实现)。通过具体的展示来验证软件产品是否像设计那样工作。
(5)部署:确保软件产品(包括已测试的实现)能被它的最终用户使用。
6.3.1.3 宏观过程的时间:里程碑和阶段
在迭代增量式的宏观过程中,一些科目是重复执行的。但是,迭代式开发过程不仅仅是一系列的迭代。必须有一个整体的框架,在这个框架下,迭代的执行反映了项目的战略计划,驱动着每次迭代的目标。这样的框架可以由一系列定义良好的里程碑来提供,通过执行一次或多次迭代来实现每个里程碑的目标。每个里程碑处将进行一次评估,确定目标是否实现。评估满意就让项目继续进入下一个阶段,去实现下一个里程碑。实现了这些里程碑就意味着项目已到达了某个成熟度,对演进的计划、规格说明和完成的解决方案有了进一步的理解。如果最初为某个里程碑设置的日期到了,但项目没有达到预期的成熟度或理解程度,那么里程碑日期就延迟了——这个日期是可变的,不是里程碑的评判标准。
6.3.1.3.1 初始
目标 初始阶段的目标是确保项目有价值并且可行(范围和业务价值)。
活动 在初始阶段,建立起系统的核心需求并排出优先级,对应该构建什么与客户达成一致意见,并确保理解了与构建系统相关的主要风险,决定使用什么开发环境(包括过程和工具)。
工作产品 初始阶段的主要工作产品是要构建的系统的愿景、行为原型、初始风险列表、确定的关键架构机制和开发环境。系统愿景提供了待构建系统的清晰描述,包括它的范围、关键特征、对已有系统的影响与关系,以及必须考虑的所有限制条件。原型提供了概念验证,说明系统是可以构建的。风险列表确定了一些关键风险,必须在生命周期的早期缓解这些风险,才能增加成功的可能性。架构机制定义了系统的一般能力,这种能力支持着基本的系统功能(如用户接口方式、错误检测和处理、持久、内存管理、进程间通信、事务管理和安全性等)。开发环境包括遵守的开发过程以及支持该过程的开发工具。
里程碑:范围已理解 如果清楚地理解了要构建的系统(系统的总体范围和关键需求),理解了这些需求的相对优先级以及构建该系统的重要业务原因,初始阶段就成功完成了。另外,客户和开发组织对系统的范围和总体交付时间表也达成一致意见。
6.3.1.3.2 细化
目标 在已经理解待构建系统的范围并达成一致意见之后,我们的注意力就转向开发整体架构框架,这个架构框架将为后续的所有迭代奠定基础。我们的目标是,尽早确定架构的缺陷,建立起一些通用的策略,以得到一个更简单的架构。细化阶段是对架构进行探索、选择,并通过多次迭代逐渐形成架构。这种形成过程是通过缓和最大风险、实现最高优先级的需求和实现架构最重要的部分来驱动的。
活动 细化阶段包括制定架构方面的决定、建立架构框架、实现该框架、测试该框架,以及根据测试的结果优化该框架。架构的演进基本上就是努力满足一些相互竞争的约束条件,包括功能、时间和空间——我们总是受到最严格的约束条件的限制。
工作产品 在细化阶段,架构是通过创建一系列可执行的架构发布版本来验证的,这些架构发布版本部分地满足了最终用户的关键使用场景的语义(在架构上很重要的场景)。细化阶段的结果不只是提供了一份架构文档,也包含了系统的一些真正的发布版本,这些发布版本成为架构设计本身的实实在在的产物。架构发布版本应该是可执行的,这样就允许我们对架构进行精确地探测、研究和评估。这些架构发布版本将成为演进产品系统的基础。
里程碑:架构已稳定 如果已针对所有关键系统需求(包括功能需求和非功能需求)进行了验证(通过实际的测试和正式的复查),并且所有风险都得到适当缓和,从而能够预期系统开发完成的费用和时间进度,那么细化阶段就成功完成了。架构已稳定的一个关键标志就是,关键架构接口和机制的变化率已大大降低,或者完全消除。对架构接口和机制的变化率的测量就是架构稳定性的首要评判标准。
6.3.1.3.3 构造
目标 当架构稳定之后,我们关注的焦点就从理解问题和确定解决方案的关键部分转向可部署产品的开发。构建阶段就是从探索转向生产,其中,生产可以看作是“一个受控的方法学过程,将产品的品质提升到可以发布的水平”。
活动 在构造阶段,我们完成系统的开发,以细化阶段得到的基线架构为基础。
工作产品 在构造阶段的迭代中,会得到一系列可执行的发布版本,满足剩下的最终用户场景的语义。随着这些发布版本在范围上逐渐增长,它们可以被精确地探测、研究和评估,并演进为产品系统。
里程碑:系统已准备好进行最终用户测试 如果发布版本的功能和品质足以部署给最终用户进行最终用户测试,构造阶段就成功地完成了。
6.3.1.3.4 交付
目标 在交付阶段中,确保软件被它的最终用户接受。
活动 在交付阶段,产品被提供给用户进行评估和测试(如alpha测试、beta测试等)。然后开发团队汇集收到的反馈意见。交付阶段的重点是产品调优,处理配置、安装和易用性问题,处理早期使用者所提出的问题。支持文档也将进行最后的开发,还包括所有相应的培训材料。所有产品相关的问题。然后,将对得到的产品进行验收测试。重要的是要注意到,即使在整个生命周期中都在进行测试,最终用户测试和最后的验收测试仍然很重要,因为这些测试确保了开发的产品在开发环境和目标安装环境下都实现了它的验收标准。
工作产品 交付阶段的工作产品包括包装好的产品、所有的支持文档、培训材料和市场宣传材料。
里程碑:系统已准备好进行部署 如果发布版本的功能和品质足以将产品提供给最终用户使用(系统通过了验收测试),交付阶段就成功地完成了。发布版本好坏的首要评判标准与构造阶段的指标类似,即报告的缺陷率的下降。但在这个阶段,是早期试用者在报告缺陷。
6.3.1.4 宏观过程的时间:迭代
在迭代式的宏观过程中,里程碑是通过执行一次或多次迭代来实现的,这些迭代可能包含部分或全部科目的活动。但是,根据这次迭代所处的阶段,在不同科目上所花的时间会有不同。如果迭代发生在初始阶段,我们会花更多的时间在需求上;如果迭代发生在细化阶段,我们会花更多的时间在分析和设计上(具体来说是架构);如果迭代发生在构造阶段,我们会花更多的时间在实现和测试上;如此等等。当然,某些科目,如配置和变更管理、环境、项目管理,是在整个生命周期中都执行的活动。
在每次迭代结束时,会进行事后分析,目的是从构建系统的状态、开发环境和团队的角度对这次迭代进行评估。每次迭代都应该被看成是调整项目路线的一次机会,要么调整后续迭代中实现的功能,要么优化环境,以改进那些效果不太好的方面。迭代的概念在大多数软件开发方法中基本上是一样的。不同之处在于每次迭代推荐的时间。
■ XP推荐,如果可能,迭代的时间应该是一周或两周。
■ SCRUM规定所有的迭代(冲刺)应该是30天。
■ RUP推荐迭代的时间应该是2~6周。
6.3.1.5 发行计划
在发行计划过程中,开发者确定发布版本是什么,以及它们包含哪些内容。发行计划的目的是确定一组受控的发布版本,每一个发布版本的功能都有增加,最终包含整个产品系统的全部需求。发行计划的主要输入是待构建系统的范围,以及所有限制因素(如费用、时间、品质)。在发行计划的活动包括建立项目的开发节奏,排列需求的优先次序,将需求分配到各次迭代中,确定迭代的发布版本是外部发布版本还是内部发布版本,最后制定详细的迭代计划。发行计划过程的结果是一份开发计划,它确定了一系列的发布版本、团队活动和风险评估。接下来将更仔细地讨论每一种发行计划活动。
发行计划的顺序
1. 第一步就是建立项目的开发节奏——决定迭代的平均时间(即决定发布版本的时间间隔)。
2. 下一步就是确定要交付的系统需求的优先级,包括功能需求和非功能需求。我们利用这些优先级,来确定哪些需求将分配到哪些迭代中。被分配到一系列的发布版本中,最高优先级的需求被分配到较早的迭代中。每次迭代都应该有一个计划好的效果,这个效果可以展示,有清晰的评估标准,可以用于评估这次迭代是否成功。
3. 计划最后一项活动是制定详细的迭代计划。在迭代计划过程中,我们针对当前迭代制定详细的项目计划,并确定实现该发布版本所需的开发资源。
在迭代开发中,发布计划是不断进行的,并且是风险驱动的。在每次迭代之后,剩下的开发计划应该重新检查,并根据需要进行调整。通常,这涉及重新调整需求的优先级,对日期的小调整,或者将功能从一次迭代移到另一次迭代。在生命周期中,应该进行定期的风险评估,并调整开发计划,先处理有风险的工作,这样风险就能被消除或减少,有助于开发团队管理将来在战略和战术上的折中。在开发过程的早期面对风险,这使我们更容易在后来进行实际的架构折中。
6.3.2 微观过程:分析与设计过程
分析和设计过程是在整体软件开发过程的背景下进行的。宏观过程驱动了微观过程的范围,为微观过程提供了输入,并利用了微观过程的输出。具体来说,微观过程使用了宏观过程提供的需求(以及前面的微观过程迭代得到的分析和设计规格说明),产生了设计规格说明(最显著的是架构),然后在宏观过程中实现、测试并部署。微观过程主要关注的两个维度是抽象层次和内容(活动和工作产品)。
6.3.2.1 抽象层次
分析的目标是提供对问题的描述。这种描述必须完整、一致、可读,并可以让不同的感兴趣的团体进行复查,可以针对现实进行测试。用我们的话来说,分析的目的是提供一个系统行为的模型。
分析关注的是行为,而不是形式。在分析时尝试对世界进行建模,从问题域的词汇表来确定元素,并描述它们的角色、职责和协作。在分析过程中,追求表示或实现问题是不恰当的。分析必须说明系统做什么,而不是系统如何做。只有出于暴露系统的行为的目的,而不是作为可测试的设计需求,在系统分析时有意说明“如何做”才是有用的。分析就是要更好地理解待解决的问题。分析是整体软件开发过程中的一个关键部分,如果执行得好,将导致更健壮、更好理解的设计,得到清晰的关注点分离,在系统各元素间平衡地划分职责。
在设计时,你会创造一些元素,它们提供了分析元素所要求的行为。当得到了关于系统行为较为完整的模型时,就可以开始设计过程了。重要的是要避免不成熟的设计,即在分析还未结束之前就开始设计。同样重要的是避免延迟设计,即组织机构进行彻底的讨论,试图得到一个完美的分析模型,因此而无法实现(这种情况常常被称为分析麻痹症)。在分析过程中,不应该预期得到系统行为的彻底理解。实际上,在设计开始之前拿出完整的分析是不可能的,也是不值得追求的。构建系统的过程中会提出一些行为方面的问题,任何合理工作量的分析都不能有效地揭示这些问题。只要完成系统所有主要行为的分析,并稍稍涉及一些次要行为,确保没有遗漏基本的行为模式,这样就足够了。
因为架构在整体解决方案中占据着非常重要的位置,所以我们需要在开发架构时理解关注点分离,同时在分析和设计时理解单个的组件。架构主要考虑的是系统各部分(即组件)之间的关系,以及它们的职责、接口和协作。相反,系统组件的分析和设计关注的是这些组件的内部,以及它们如何满足需求,这些需求是架构的分析和设计要求组件实现的。
在整个开发生命周期中,分析与设计是在不同的抽象层次上完成的。抽象层次的数目不能够事先确定。这主要依赖于系统的规模。
从不同视角来看分析与设计的不同关注点:
1. 在架构上分析:关注的是建立初始的系统架构框架,为架构设计提供指导和上下文背景。
2. 在架构上设计:关注的是优化分析得到的架构,确定系统主要的设计元素,包括必须遵守的共同机制。
3. 在组件上分析:关注的是理解当前的一组需求。提供解决方案的初始版本,将需求分配到解决方案的元素中去。
4. 在组件上设计:关注的是改进设计元素,得到一份规格说明。这份规格说明可以利用具体的实现技术有效地实现。
6.3.2.2 活动
微观过程包括下列一些活动,它们是针对一个具体的范围和一个具体层次的抽象来执行的。
■ 确定元素:发现(或发明)要处理的元素,确定面向对象的分解。
■ 确定元素间的协作:描述已确定的元素之间如何协作,以提供系统的行为需求。
■ 确定元素间的关系:确定元素之间的关系,以支持系统的协作。
■ 确定元素的语义:建立已确定的元素的行为和属性,为下一层次的抽象准备元素。
以上的活动在实现开发中是并行执行的,是没有顺序的,可能在同一时间确定元素以及元素间的协作,也可能在确定元素间的协作时确定其行为和属性。
6.3.2.3 产出
■ “架构描述”描述了系统的架构,包括对通用机制的描述。该描述包括了分析/设计模型中对架构有重要意义的方面。
■ “分析/设计模型”包括软件解决方案中的分析和设计元素、它们的组织方式以及实现。这些实现描述了系统的行为需求如何通过这些元素来实现。
6.3.2.3.1 分析/设计模型的好处
对于分析/设计模型来说,选择怎样的细节层次来描述架构取决于待开发的系统以及所选择的开发过程的类型。用文档将架构记录下来之后,需要与开发团队进行沟通。
1. 维护一个分析/设计模型有助于建立一个共同一致的词汇表,在整个项目中使用。在开发过程中,分析/设计模型成为所有元素及其语义和关系的集中存放处。随着时间的推移,我们会添加新的元素,消除无关的元素,合并相似的元素,使分析/设计模型得到优化。通过这种方式,团队不断形成一种一致的语言表达方式。
2. 拥有一个系统中元素的集成存放处不仅能确保这些元素的一致性,而且可以将其作为一种有效的载体,让我们能够以任意的方式查看项目中所有的元素。当开发团队的新成员必须很快了解已经在开发的解决方案时,这一点特别有用。
3. 分析/设计模型也让架构师能够全面了解一个项目,从而可能会发现一些共同之处,否则可能难以发现。如你所料,使用UML来表示这个分析/设计模型将进一步增强这些好处。不但有“一图胜千言”的好处,而且让分析/设计模型可视化也有助于揭示元素之间不一致的地方。
6.3.2.3.2 是否维护分析/设计模型
选择是否维护独立的分析模型与设计模型,取决于待开发的系统以及所选择的开发过程的类型。如果待开发的系统将存在数十年,有多个变体,或者为多个目标环境而设计,每个都有自己的架构,那么独立的分析模型可能会有用。在这种情况下,分析模型是作为不同设计模型(平台相关的)的一种抽象(平台无关的表示形式)被维护的。实际上,这就是“模型驱动架构(MDA)”的一项基本原则,MDA是由“对象管理组织”支持的。维护一个独立的分析模型也可能是为了提供一个复杂系统的概念概述,但是,用良好的文档记录的架构也可以起到同样的作用。要在分析模型和设计模型之间保持高度的保真性,成本会很高。在确定是否需要一个独立的分析模型时要记住,需要额外的工作来确保分析模型和设计模型保持一致,在提供一个系统概念视图的独立模型的成本和好处之间,我们需要进行折中考虑。换一种方式,分析模型可以被看作是临时的工件,逐渐演进为设计模型(在这种情况下,分析模型被认为是“初始的”设计模型)。
6.3.2.3.3 软件架构视图
软件架构应该通过一组由视角确定的视图来表达,如下是一组简单的视图,被称为4+1视图。
■ 需求视图(也被称为用例视图):需求视图描述了具有架构重要性的需求,既包括功能需求,也包括非功能需求。具有架构重要性的功能需求一般会驱动确定具有架构重要性的用例场景,这些用例场景将在软件生命周期的早期进行分析。具有架构重要性的非功能需求包括所有系统范围的架构品质(如可用性、弹性、性能、规模、可伸缩性、安全性、隐私性和易理解性)、经济上和技术上的约束(例如,利用已完成的产品、集成遗留系统、复用策略、要求的开发工具、团队结构和时间进度)以及法规上的约束(例如,遵守特定的标准和控制)。通常,正是这些非功能需求具有最大的架构重要性,它们驱动着架构机制的定义,这些架构机制通过逻辑视图被记录在文档中。
■ 逻辑视图:逻辑视图包含了具有架构重要性的分析和设计元素、它们的关系以及它们组织成的组件、包和层,还有一些有选择的实现。它说明了这些具有架构重要性的元素如何协同工作,提供了需求视图中描述的那些具有架构重要性的用例场景。逻辑视图也描述了形成系统结构的关键机制和模式。
■ 实现视图:实现视图描述了关键实现元素(可执行程序、目录)和它们的关系。这个视图很重要,因为实现的结构对当前的开发、配置管理、集成和测试具有主要的影响。
■ 进程视图:进程视图描述了系统中的独立的控制线索和哪些逻辑元素参与了这些线索。
■ 部署视图:部署视图描述了不同的系统节点(如计算机、路由器和虚拟机/容器),以及具有架构重要性的逻辑元素、实现元素或过程元素在这些节点上的分配。
在某些情况下,根据项目和使用的过程,将所有的架构信息收集起来写成软件架构文档(SAD),可能是有意义的。SAD成为了描述系统架构的主要工件,它包含了对所有具有架构重要性的工件的引用。如果有人想了解系统的架构,SAD就是开始的地方。SAD应该说明关键的架构考虑是如何处理的,所以最好按照刚才讨论的架构视图来组织。SAD应该由整个团队进行复查,并随着架构的演进而更新。
6.3.2.4 微观过程与抽象层次
在微观过程活动中,执行的工作的细节取决于当前的考虑(即架构或组件)。下面针对前面定义的每一种考虑,列出了对微观过程活动重点的进一步描述:
■ 在进行“架构分析”时,微观过程关注的是利用已有的参考架构或架构框架,创建一个初始的架构版本,并确定其他可以作为构建组件的资源。这包括系统的总体结构、它的关键抽象和它的机制。实际上,对于每个架构视图形成一种高层次的理解也是不错的。架构分析的结果被用于驱动架构设计。
■ 在“架构设计”过程中,根据我们在架构分析中学到的东西,通过架构分析所产生的初始架构得到了改进。微观过程活动关注于改进已被识别出来的分析元素、设计元素以及它们的职责和交互。在这一个层次确定的设计元素代表了整个架构框架的构建组件,它们的关系确定了系统的整体结构。分析机制也得到改进,它利用了具体的技术成为设计机制。同时,架构上的并发和分布也得到了进一步的考虑。复用也很重要,加入已有设计元素(以及它们相关的实现)的机会和影响得到了评估。
■ 在“组件分析”过程中,微观过程活动关注于确定分析元素及其职责和交互。这些分析元素代表了系统组件的第一次近似,然后应用于组件设计,以确定设计元素。重要的是要记住,微观过程分析活动的本质是提供组件的分析观点,应该避免试图在这个阶段设计组件,因为这些必需的额外考虑将在设计时进行。
■ 在“组件设计”过程中,微观过程活动关注于改进组件的设计,从设计类的角度来定义组件,它们可以直接由选定的实现技术来实现。在详细设计中,继续改进设计类,找出它们的内容、行为和关系的细节。如果有了待实现的设计类的足够细节,这种改进就应该停止了。接下来就是实现,这是宏观过程的一部分。
假设我们正处于宏观过程迭代的“细化”阶段,属于这次迭代范围的、具有架构重要性的需求已经准备好,要经历分析和设计过程(微观过程)。以下的场景描述了微观过程中可能发生什么。
(1)针对所有具有架构重要性的场景进行了架构分析。结果是一组具有架构重要性的分析元素。
(2)开始架构设计,利用所有的架构分析元素作为输入。在这次迭代中,发现一个设计元素没有得到很好的理解,所以“针对这个元素”进行了组件分析和设计,结果发现“在架构设计层”需要一些优化。这几次迭代的结果是一组具有架构重要性的设计元素。
(3)针对每个具有架构重要性的场景进行组件分析,利用架构设计元素作为输入。这些迭代的结果是一组设计元素,它们支持那些具有架构重要性的场景。
(4)针对组件分析得到的每个具有架构重要性的元素进行组件设计。
(5)在更低的抽象层次上进行其他微观过程迭代(例如,从企业级到系统级、子系统级、组件级、子组件级等)。
6.3.2.5 确定元素
第一种微观过程活动是确定元素,在对系统进行面向对象分解时,确定元素是一项关键活动。因此,这第一项微观过程活动的目的就是确定一些关键的元素,这些元素将在特定的抽象层次上描述解决方案。这项活动实际上是元素从一个抽象层向另一个抽象层的演化。在一个抽象层次上确定元素也包括了对前一个层次进行修改,这种修改将导致产生新的元素和不同的元素。在一个抽象层次上识别的元素将作为主要输入,用于确定下一个抽象层次上的元素。
在执行这项活动时,重要的是在确定元素和细化语义之间保持一种微妙平衡。确定元素时应该只关注于确定元素,并在较高层次上对它们进行描述(简要的描述)。后续的微观过程活动将逐渐细化已确定的元素的语义。
6.3.2.5.1 产出
这个微观过程活动的主要产出是分析/设计模型,它包含了在特定抽象层次上已确定的元素和它们的基本描述。下图总结了在不同的分析和设计活动中确定的元素。
6.3.2.5.2 步骤
识别面向对象元素通常包括两项活动:发现和发明。在分析过程中,识别多数是由发现来驱动的,而在设计过程中,发明起到了更大的作用。在分析过程中,设计者与领域专家合作,共同识别元素。他们必须善于发现抽象,能够研究问题域并发现有意义的分析元素。在设计过程中,架构师和设计者一起识别元素,他们必须善于从解决方案出发,创造出新的设计元素。
在设计过程中,某些在设计时识别的元素可能转化为实际的类,另一些元素可能成为某些抽象的属性或同义词。另外,在生命周期早期中确定的某些分析元素可能是错误的,但这不一定是坏事。在分析过程中,重要的是让这些决定可以在将来的开发过程中得到优化。在生命周期的早些时候遇到的实体事物和角色将一直进入到实现,因为它们对问题概念模型来说是非常基础的。随着对问题了解得更多,你可能会改变某些元素边界,重新定位职责,合并类似的元素,并且常常会将较大的元素划分为一组互相协作的元素,从而形成解决方案中的某些机制。总之,分析元素常常是相当流动、可变的,它们可能有很大的变化,然后才在设计时被固定下来。
对于所有的抽象层来说,识别元素的方法总体上是一样的,不同的是从哪里开始(已经有了哪些抽象)、关注的焦点是什么(具有架构重要性的元素或其他元素),以及要走多远(是否要深入某个设计元素,确定它由哪些元素组成)。例如,在进行架构设计时,可使用架构分析的结果作为起点,关注的是具有架构重要性的设计元素,也可能考虑这些具有架构重要性的元素由哪些元素组成,目的是确保已对每个元素的行为有了足够的了解,从而减少风险。在进行组件分析和设计时,可使用架构分析和设计的结果作为起点,识别出实现规格说明所需的其他设计元素,包括较细粒度的设计元素,它们组成了较粗粒度的元素(即提供某个组件行为的一些类)。
然后,识别元素重复递归地进行,目的是发明更多的细粒度的抽象来构造较高层的抽象,并发现已有抽象之间的共性,可利用这些共性来简化系统架构。在识别设计元素时,粒度最大的设计元素通常最先被识别出来,因为它们确定了系统的核心逻辑结构,而这些元素又由较小粒度的元素组成。但是,在实际工作中,也可以同时识别出处于不同粒度级别的设计元素,虽然这些元素之间存在着明显的顺序依赖关系(例如,只有当某个组件被识别出来以后,才能识别具体实现它的类)。
除了查看分析元素作为设计元素的灵感之外,也可以通过应用选择的架构模式、设计模式和一般的设计原则,将分析元素细化为设计元素。一些模式的例子包括IBM的eBusiness模式、架构模式和设计模式。一些设计原则的例子包括企业组件设计原则和开发业务组件的最佳实践。
在识别元素时,研究处于类似抽象层次的类似系统总是聪明的做法。通过这种方式,可以从其他项目的经验中获益,这些项目也必须做出一些类似的开发决定。一般来说,在识别元素这一步,重要的是识别吸收(复用)已有元素的机会和影响,确保潜在可复用资产的适用环境与你的环境是一致的。
在架构分析过程中,识别的逻辑部分通常是基于对特定架构模式的选择。随着设计元素的识别和分组,这些部分会在设计时被细化。某些部分划分指南包括将支持相同功能的元素放在一起。基于某个功能的其他功能被放在不同的部分中。在同一个抽象层中协作实现行为的功能应该被放在不同的部分中,它们代表一些对等的服务。这些决定具有战略上的重要意义。在某些情况下,这种划分是自顶向下完成的,通过系统的全局视图,将它划分为代表主要系统服务的抽象,这些服务在逻辑上是内聚的,并且可能独立地发生变化。这种架构也可能自底向上,将语义上相近的一些类识别出来,放在一起。随着已有的设计部分逐渐增多,或者随着新的划分变得很明显,可以引入新的设计部分,或者重新组织已有的设计部分。这样的重构是敏捷过程的一项关键实践。
在架构分析过程中确定的机制被看成是共用策略和基础设施的占位符,所有的系统元素都需要它们的支持。
如果元素是在不同的抽象层进行维护的(即分离的分析元素和设计元素),而不是某一个抽象层的元素平滑地过渡成为下一层的元素,那么从需求管理和变更管理的角度来说,保持不同层次抽象之间的可追踪性是聪明的做法。建立并维护可追踪性对于有效、准确地影响评估是非常关键的。
6.3.2.5.3 里程碑和评判标准
1. 在特定的抽象层,针对特定的范围,如果得到了一组足够的抽象,一致地加以命名和描述,你就已经成功地完成了识别元素的微观过程活动。
2. 在特定的抽象层,针对特定的范围,是否得到了足够稳定的分析/设计模型。换言之,在每次完成微观过程迭代之后,分析/设计模型没有大量的改动。
6.3.2.6 确定元素之间的协作
第二种微观过程活动是确定元素间的协作,其目的是描述已识别的元素如何协同工作,以提供系统的行为需求。在这个活动中,我们通过明智的、可测量的职责分配,对已识别的元素进行细化。
6.3.2.6.1 产出
这个微观过程活动的主要产品是一些实现,它们说明了已识别的元素之间如何协作,以提供某个范围内的行为需求。这些实现描述了一组行为需求如何通过处于某个抽象层上的元素的彼此协作来实现。实现反映了协作元素之间明确的职责分配,并在行为需求和软件解决方案之间架起了桥梁。实现最初是以分析元素描述的,后来以设计元素描述。
6.3.2.6.2 步骤
对于将工作分配给已被识别的元素来说,分析行为需求是一项杰出的技术。下面的几个步骤描述了一种方法,确定了处于某个抽象层的一组元素的语义。
(1)分析行为,将职责分配给参与提供该行为的元素(即在前一个微观过程步骤中识别出来的那些元素)。请考虑异常行为和预期行为。如果某些元素的生命周期很重要或很关键,那就为它开发一个状态机。这一步的结果是得到行为的实现,说明参与的元素和它们的协作。
在分析一个场景时,典型的事件序列可以总结如下:
1. 从行为需求中选择一个场景或一组场景作为考虑的对象。
2. 确定哪些元素与该场景有关。(元素本身可能已经在前面的微观过程活动中识别出来了。)
3. 推演这个场景,将职责分配到每个元素,以完成期望的行为。根据需要,分配一些属性来表示结构化的元素,这些属性是实现特定职责所需要的。注意,在这一步中,重要的是关注行为,而不是结构。属性代表了有结构的元素,所以这是有危险的,特别是在分析的早期阶段。危险就是由于需要特定的属性,过早地绑定实现决定。只有当属性对于构建场景的概念模型很重要时,才应该在这里确定属性。
4. 随着场景分析的进行,对职责进行重新分配,以得到比较平衡的行为分配。只要有可能,就复用或适配已有的职责。将较大的职责分解成一些较小的职责是常见的动作,将较小的职责组合成较大的行为也是可能的,但出现的频率会低一点。对单个场景的分析可能导致不相似的职责被分配到相同的元素中。需要将这样的元素分解成多个元素,每个都包含一组一致、内聚的职责。
5. 在设计过程中,必须考虑这些实现的并发和分布。如果存在并发的可能,必须指明参与者、代理和服务者以及它们之间的同步方式。在这个过程中,可能发现需要在对象之间引入新的途径,以消除或合并未用到的、冗余的对象。
(2)在实现中提取模式,用更抽象、更一般的实现方式来表达这些模式。
在提取模式时,这一步意识到了共性的重要性。当确定元素的语义时,必须对行为的模式保持敏感,这代表了复用的机会。典型的事件序列可能如下:
1. 针对在这个抽象层次的全部实现,寻找参与元素之间的交互模式。这样的协作可能代表了隐式的惯用法或机制,应该进行检查,以确保没有无必要的差异。重要的协作模式应该作为策略性的决定,明确地通过文档记录下来,以便可以复用它们,而不需要重新发明。这项活动保持了架构愿景的完整性。
2. 针对这个抽象层生成的所有职责,寻找行为的模式。共同的角色和职责应该统一为共同元素的共同职责。
3. 如果工作在较低的抽象层次,随着具体的操作的确定,寻找操作签名中的模式。消除没有必要的差异,如果发现这样的操作签名重复出现,就引入共用的类。
在分析一个场景时,可能会发现一个或多个元素的状态对于场景的发展具有重要的影响。在这种情况下,就需要花些时间仔细研究一下该元素可能经历的外部可见的状态改变,以确保场景的发展可以包容这些状态变化。使用状态机图是记录元素的关键状态及状态转换的一种准确方式。如果发现了协作的模式,就用更抽象、更一般的实现来表示它们。
6.3.2.6.3 里程碑和评判标准
如果你得到了一组一致的元素和职责,它们在某个抽象层次上,在一定的范围内提供了系统要求的功能行为,并且做到了在这些元素之间进行有意义的、平衡的职责分配,那么就成功地完成了确定元素协作这项微观过程活动。
作为这项活动的结果,你应该开发并验证了一些实现,这些实现代表了被考虑的范围的基本行为。所谓“基本行为”,指的是与应用的目标核心相关的行为。实现好坏的判别标准是完整性和简单性。每个实现都必须准确地反映参与实现的单元元素的语义。一组好的实现应该包含所有主要的场景和一定比例的、感兴趣的次要场景。我们不指望、也不追求对所有场景的实现,只考虑主要场景和一些次要场景就足够了。另外,一组好的实现也会发现行为模式,最后得到的解决方案结构体现了不同场景之间的共性。
对于单个的元素职责来说,要记住这项活动关注的是协作和确定“谁做什么”。在这个时候,记录下元素的职责就足够了。在较高的抽象层次上,可对职责进行有意义的说明。
下面列出了一些简单而有用的检查点,可以用于评估这项活动的结果。
■ 元素应该具有平衡的职责。一个元素不应该“什么都做”。
■ 元素应该具有一致的职责。如果元素的一些职责是无关联的,那么它应该分成两个或多个元素。
■ 应该没有两个元素具有相同或非常类似的职责。
■ 为每个元素定义的职责应该支持该元素所参与的场景。
■ 职责不简单或不清楚意味着给定的抽象没有得到很好的定义。
6.3.2.7 确定元素间的关系
第三项微观过程活动是确定元素之间的关系,这些关系支持了前一项微观过程活动中定义的元素协作。确定元素的关系就形成了解决方案。具体来说,在架构层次的抽象上,关键元素和关键部分之间的关系确定了系统的总体结构,并为所有其他系统元素之间的关系奠定了基础。确定关系的目的是确定每个元素的边界,清楚地表达哪些元素之间互相协作。这项活动正式确定了元素之间的关注点分离,这种关注点分离最初是在确定元素协作时建立起来的。
6.3.2.7.1 产出
这个微观过程活动的主要产品是在当前抽象层的元素之间的关系。确定好的关系被添加到正在演进的分析/设计模型之中。
尽管这些关系最终会以具体的形式表示(即通过编程语言表示),我们仍建议先利用UML图或其他的图示方式进行可视化表示。可视化的图示提供了更全面的架构视图,这样在表达这些关系时,就能够不受编程语言的限制。这些图示帮助你想象和推理这些关系,这些关系所涉及的实体可能在概念上或物理上距离遥远。绘制这些图示有一个结果,即可能会发现以前未发现的交互模式(这可能是你希望使用的),也可以发现继承关系中设计得不合理的地方。
我们不追求,实际也不可能,得到一套面面俱到的图示,能表示出元素之间所有可以观察到的关系视图。相反,我们建议你关注感兴趣的那些视图。所谓的“感兴趣”,是指一组相关的元素,它们的关系表达了某种基本的架构决定或完成实现蓝图所需的某个细节。有一组图示可能应该考虑,它们与在前一个微观过程活动(确定元素协作)中得到的实现有关。这些图包含了参与实现的元素以及它们的关系,表达了实现的结构特点。
6.3.2.7.2 步骤
一般来说,确定元素关系有两个步骤:
(1)识别关联,初步识别元素之间的语义联系。
识别关联主要是分析和早期设计时的活动。在架构分析时,确定了高层架构部分之间的关系和关键抽象之间的关系。在架构设计时,通过这项活动来确定关键组件之间的关系,并将高层设计元素划分成设计部分。在组件分析时,通过这项活动来确定分析元素之间的关系(包括关联和某些重要的继承或聚合关系)。
在确定元素关联时,典型的事件序列可能如下:
1. 收集一组元素,它们处于某个抽象层,或者与特定的场景/实现有关系。
2. 考虑任意两个元素之间是否存在语义上的关系,如果存在,就建立一个关联。如果需要从一个元素导航到另一个元素,或者需要从一个元素调用某种行为,都需要引入关联。如果两个元素必须彼此协作,它们之间就应该有关系。
3. 针对每个关联,如果角色没有和元素名称重复,就指定每个执行者的角色,并指定相关的多重性或其他类型的约束。只有当这些细节很明显时才包含它们,因为细化这些关系是下一步的工作。
4. 通过走查场景来验证你的决定,确保得到的关联对于提供参与场景的元素之间的导航和行为是必要和足够的。
(2)细化关联,成为语义上更丰富的关系(即聚合、依赖等)。
关联的细化既是分析活动,也是设计活动。在分析时,可能需要将某些关联演进为另一些关联,这些关联在语义上更精确、更具体,反映了对问题域不断加深的理解。在设计时,同样要转换一些关联,同时添加新的、具体的关系,以提供实现的蓝图。聚合、组合、依赖等关系是我们主要感兴趣的关系,包括它们的附加属性,如名称、角色、多重性等。
在细化元素关联时,典型的事件序列可能如下:
1. 寻找一组元素,它们已经通过一组关联联系在一起(例如,参与某个实现的一组元素),并考虑每一个关系的语义,根据需要细化关系的类型。这种关系代表了另一个对象的一次简单使用吗?如果是这样,关联就应该被细化为一个依赖关系。这种关系代表了对应元素之间的一种结构关系吗?如果是这样,关联就应该被细化为一个聚合或组合关系。应该逐一检查每个关系,从而确定并记录下这些关系的实质。
2. 寻找元素之间的结构模式。如果发现了这种模式,就考虑创建新的元素来记录这种共用的结构,并通过继承(将这些类放到已有的继承结构中,或者创建新的继承结构)或聚合来引入这些新元素。
3. 寻找元素之间的行为模式。如果发现了这种模式,就考虑引入共用的参数化元素,执行共同的行为。
4. 考虑已有关联的导航性,如果可能,对导航性加以约束。如果不希望使用双向导航,就使用单向导航。
5. 随着开发的进行,引入一些细节,如关于角色、多重性等的说明。不用说明所有的细节,只需包含重要的分析或设计信息,或实现所必需的信息。
6.3.2.7.3 里程碑和评判标准
如果你在某个抽象层次指定上了元素之间的关系,那么就成功地完成了确定元素关系这项微观过程活动。
在这个阶段要注意一件事,即参与实现的元素关系的一致性。具体来说,对于每个实现,参与元素之间的关系以及元素之间要求的协作必须是一致的(如果存在协作,就必须存在关系)。
好坏的评判标准包括内聚、耦合和完整性。在复查在这个活动中确定的关系时,你追求的是拥有逻辑上高内聚和低耦合的元素。另外,你希望确定给定抽象层次上所有重要的关系,这样在下一层抽象中就不需要引入新的重要关系,也不需要通过一些不自然的操作来使用已确定的那些关系。如果发现元素和关系在确定时不方便,那就表明还没有在元素之间设计出一组有意义的关系。
6.3.2.8 确定元素的语义
第四项微观过程活动是确定元素的语义,以确定在该元素参与的所有场景中语义都是一致的,同时确保已为每个元素提供了足够的信息,让该元素能进入下一个抽象层。在这个活动中,元素的语义“在当前的抽象层次上”得到细化,加入了足够的细节,确保可以在下一个抽象层次上进行元素识别。例如,在分析时,细化元素语义的目标是细化分析元素的语义,包含足够的信息,以便能够识别设计元素。在设计时,细化元素语义的目标是细化设计元素的语义,包含足够的细节,以便能够实现。
将这个活动作为微观过程的最后一个活动是有意而为之的:微观过程首先关注的是元素之间的行为和协作,而将确定单个元素的详细语义尽可能地推到最后。这种策略避免了不成熟的决定,而不成熟的决定有可能让我们丧失机会,不能得到更小、更简单的架构。同时,这种策略也让我们能够根据效率的需要,自由地改变内部的表现形式,而且不会破坏原有的架构。微观过程的前3项活动关注的是元素的外部视图以及元素之间的协作,最后一项活动则关注单个的元素,清楚地确定每个元素的外部视图,并提供额外的细节,驱动内部视图的开发。
6.3.2.8.1 产出
这个微观过程活动的主要产品是细化的分析/设计模型,其中包含了元素的详细语义。细节水平和记录元素语义的表示方式,都取决于你所面对的抽象层次。
在分析时,这项活动的结果是相对比较抽象的。你不太关心做出表示决定,相反,更关心发现新的抽象,向它们分配职责。在分析层详细确定语义可能包括:利用活动图的方式更详细地描述这些职责和总体流程。某些元素的职责受到事件的驱动,或者与状态顺序有关。对于这些元素,可利用状态机来描述每个元素协议的动态语义。
在设计时,特别是在详细设计的后期,必须对表示做出具体的决定。随着开始细化单个元素的协议,你可能为具体的操作命名,但忽略它的完整操作签名。只要可行,就会提供每个操作的完整签名。在设计时,你可能还会指明应该使用某些算法。如果工作在较低的抽象层,随着开始与给定的实现语言进行绑定(如在详细设计时),详细的语义甚至可以包含伪代码或可执行的代码。得到了正式的类接口之后,就可以开始利用编程工具来测试和确保实现这些设计决策。在这一步将得到更正式的产品,其主要好处就是可以迫使开发者思考每个抽象协议的实际可行性。不能确定清楚的语义则标志着抽象本身有缺陷。
6.3.2.8.2 步骤
详细的元素语义包括对结构和算法的选择,它们描述了元素的结构和行为。在细化元素语义时,典型的事件序列可能如下:
(1)列出该元素的角色和职责。从前面确定元素协作得到的单个实现中,收集并确定这些结果。利用这些实现帮助确定参与元素的职责。通过在所有实现中查看所有调用该元素的协作,来确定该元素的职责。(元素的职责就是其他元素可以要求它做的所有事情。)
(2)更详细地描述每项职责。绘制描述整体流程的活动图或序列图,绘制状态机图来描述状态行为,等等。如果可能,为每项职责/操作推荐一种合适的算法。在设计时,考虑引入一些辅助的操作,将复杂的算法分解为不太复杂、可复用的部分。要折中考虑存储元素的状态还是计算元素的状态。
(3)在设计时,要考虑继承。选择合适的抽象类(或者创造新的抽象类,如果问题足够一般化的话),并根据需要调整继承关系。考虑打算赋予职责的那些元素。在理想的情况下,这可能只需要稍稍调整较低层元素的职责和协议。如果元素的语义不能够通过继承、实例化或委托来提供,请考虑在下一个抽象层次中提供一种合适的表示形式(也就是说,如果正处于设计层,这可能包括实现语言提供的原语)。要记住从该元素的客户的角度来看操作,并为预期的使用方式选择最佳的表示形式,这一点很重要。但要注意,不可能针对所有使用方式都是最佳的。随着从后续的版本中得到更多的经验信息,就可以确定哪些元素的时间/空间效率不高,并对它们的实现进行局部的调整,而不必担心破坏客户程序对抽象所做的假定。
(4)在为每个元素定义职责时,考虑该元素为了实现其职责而必须具备的属性。
(5)在设计时,设计一组足够的操作,来实现这些职责。如果可能,尝试复用概念上类似的角色和职责。作为单个类来说,职责就是由该类的操作记录的;作为组件来说,职责是由该组件提供的服务来表现的,通过该组件接口的操作来记录。
■ 逐一考虑每个操作,确保它是简单的。如果不是,就将它分解成更简单的操作,并提供出来。复合操作可以被保留在该元素中(如果操作足够通用,或者理由很充分),或者被移到一个公共的类中(特别是当这个操作有可能经常改变时)。分解操作可能让你找到更多的共性。
■ 考虑构造、复制和析构的需求。最好是对这些行为有通用的策略,而不是让每个类使用自己的习惯方式,除非这样做有很好的理由。
■ 考虑完整性的需要。添加其他的一些简单操作,虽然它们不是目前的客户程序所需要的,但它们的存在使组件变得完整,所以可能会被将来的客户程序使用。由于我们认识到不可能有完美的完整性,所以更倾向于简单性,而不是完整性。
在设计过程中定义单个元素的语义时,元素之间的共性可能很明显,我们可能很想开始为这些元素定义非常精细的继承关系,以便反映共同的行为和共同的结构。但是,不要过早关注继承关系是很重要的:引入不成熟的继承关系常常导致类型完整性的丧失。继承的使用通常被认为是一项设计活动,因为这时对设计元素的语义有了更详细的了解,因此才能够更好地将它们放在继承层次之中。在设计时,遇到的类之间的共性可以通过泛化/特化层次结构来表达。当定义这种继承层次时,要注意平衡(继承层次不要太多也不要太少,不要太宽也不要太窄)。当结构模式或行为模式出现在这些类中时,重新组织继承层次,使共性最大化(但不以牺牲简单性为代价)。
在开发的早期阶段,在使用继承之前,对单个元素的语义文档是分开来的。但是,一旦有了继承层次,对元素语义的文档记录就必须显示操作在层次结构中的位置。在考虑与某个元素关联的操作时,重要的是决定把这个操作放在继承层次的哪一层上。某些操作可能被一组对等的类使用,那么就应该重构出一个共同的超类,可能是引入一个新的中间类。
在细化元素语义时,要确保“停留在当前的抽象层次上”。识别下一个抽象层次中的元素是在微观过程的下一次迭代的第一项活动中(或在宏观过程的实现阶段)完成的。
6.3.2.8.3 里程碑和评判标准
如果对处于特定抽象层次的元素的语义有了更完整的理解(即提供了足够的细节,以便能够进入下一层次的抽象),并且为这些元素指定的语义在这个抽象层次上是一致的,那么就成功地完成了细化元素语义这项微观过程活动。
作为微观过程中的最后一项活动,其最终的目标是得到一组清晰的抽象,它们具有高内聚、低耦合的特点。
评估这项活动是否成功,要看单个元素的语义。作为这项活动的结果,应该对特定抽象层上的每个元素,得到一组相当充分、简单、完整的语义。应该为每个元素提供足够的细节,以便能够为下一个抽象层次识别元素。例如,在分析时,如果你得到了分析元素的职责和属性的描述,了解的东西足以让你进入设计,那么就成功地完成了这项活动。在设计时,如果你得到了更准确的语义(即操作和属性),足够进入实现和测试(即它们的结构和使用可以由选定的实现语言来实现),那么就成功地完成了这项活动。这不是说这些元素必须以生动的细节来表述,而只是说需要足够的信息,让有能力的实现者能够完成他们的工作。
这项活动结果好坏的主要评判标准是简单性。如果元素的语义是复杂、别扭或效率不高的,就表明该元素还缺少某些东西,或者选择了一种糟糕的表现方式。
转载:https://blog.csdn.net/wangqinyi574110/article/details/128414340