目的
  • 确保类可为用例实现提供必需的行为。
  • 确保提供充足的信息来明确无误地实施类。
  • 处理和类有关的非功能性需求。
  • 包含用于类的设计机制。
步骤
输入工件: 生成工件:
角色:设计员
工具向导:

工作流程明细:

类是设计工作的核心,系统的实际工作其实也是由类执行的。子系统、包、封装体以及协作关系等其他设计元素只是说明了类的组合方式或协同操作方式。

在封装体表示并行执行线程的设计之中,设计类是“被动”类,它们存在于由封装体提供的执行环境内。在封装体不起作用的设计中,有些设计类将是“主动”类,这些类协调和驱动被动类的行为。

创建初始设计类返回页首

指南:

首先为指定为活动输入的分析类创建一个或多个(初始)设计类,然后指定追踪依赖关系。在后续步骤中指定各种描述分析类的设计方式的“设计”特征(如操作、方法和状态机)时,将对在此步骤中创建的设计类加以改进、调整、拆分和/或合并。

根据将要设计的分析类类型(边界类、实体类或控制类),可以采用不同的特定策略来创建初始设计类。

设计边界类

用户界面中的每个窗口或每个窗体都使用一个边界类是分析中的一条通则。作为相应的结果,边界类担负了相当高的层次的责任,并需要在此步骤中加以改进和详细说明。

边界类的设计还要依赖项目可用的用户界面(或 GUI)开发工具。使用当前的技术,直接在开发工具中采用可视化方式构建用户界面是比较普遍的作法,并可由此自动创建与控制类和/或实体类的设计有关的必要用户界面类。如果 GUI 开发环境自动创建了实施用户界面必需的支持类,那么就无需再考虑它们,只要设计开发环境没有创建的支持类即可。

可执行用户界面原型的草图或屏幕转储也是此项工作的输入,它们可能已经建好,并用于进一步说明对于边界类的需求。

由于边界类的内部行为通常比较复杂,所以一般用子系统作为表示现有系统接口的边界类的模型。如果接口行为简单(仅作为现有 API 到外部系统的传递),则可以选择一种或多种设计类来表示这一接口。这样选择需要为每一协议、接口或 API 都采用一个单一的设计类,并需要在该类的特殊需求中说明有关使用标准以及有关信息的特殊需求。

设计实体类

在分析中,实体类代表受控的信息单元;实体对象通常是被动的和永久性的。这些实体类可能已在分析中确定,并且与永久性分析机制相关联。基于数据库的永久性机制的细节是在活动:数据库设计中设计的。出于性能的考虑,可能要强制重组某些永久类,并将因此变更角色: 数据库设计员角色:设计员之间共同讨论得出的设计模型。

有关永久类的设计问题的进一步讨论,请参见以下的确定永久类部分。

设计控制类

控制对象负责管理用例流并协调用例流的大部分操作;控制对象还封装与用户界面问题(边界对象)或数据工程问题(实体对象)没有特殊关系的逻辑。这一逻辑有时也称为应用逻辑业务逻辑

因此,设计控制类时至少需要考虑以下问题:

  • 复杂性。边界类和/或实体类可以处理简单的控制或协调行为。然而,随着应用程序的复杂程度不断增加,也将暴露这种方法的明显缺点,如下所示:
  • 用例协调行为嵌入到了 UI 中,很难对系统作出变更。
  • 不同的用例实现无法轻松地使用相同的 UI。
  • UI 受到其他功能的影响,性能下降。
  • 实体对象可能受到特定用例行为的影响,通用性降低。

要避免上述问题,可采用控制类来提供与协调事件流有关的行为。

  • 变更的可能性。如果变更事件流的可能性较小,或成本可以忽略,就无法验证附加控制类的额外费用和复杂性。
  • 分布和性能。在不同节点或不同处理空间运行部分应用程序的需要引发了对于设计模型元素专用性的需要。这种专用性一般是通过添加控制对象,并将边界类行为与实体类行为分布到控制类上来实现的。这样边界类就提供纯粹的 UI 服务,实体类提供纯粹的数据服务,而控制类提供其余服务。
  • 事务管理。管理事务是典型的协调活动。如果缺乏处理事务管理的框架,则需要一个或多个事务管理器类用于交互作用,以确保事务的完整性。

注意,在后两种情况下,如果控制类代表独立的控制线程,则采用工件: 封装体进行控制线程建模更为恰当。

确定永久类 返回页首

必需在永久媒介上保存状态的类被称为“永久类”。保存其状态可以满足下列需要:永久记录有关类的信息、为应对系统发生故障的情况而进行备份或交换信息。永久类可能同时具有永久性和暂时性实例;标记为“永久性”的类只是表明类的某些实例需要是永久性的。

确定永久类通知了角色: 数据库设计员特别注意类的物理存储特征。此外还通知了角色: 构架设计师,类必需是永久性的;并通知负责永久性机制的角色:设计员,类的实例必需是永久性的。

由于需要协调永久性策略,角色: 数据库设计员负责使用永久性框架将永久类映射到数据库中。如果项目正在开发永久性框架,则框架开发人员还要负责理解设计类的永久性需求。要为上述人员提供必需的信息,只需在此说明类(或更准确一点,是类的实例)是永久性的即可。此外还要包含分析中与永久性机制有关的所有设计机制。

示例

永久性的分析机制可以通过以下任意一种设计机制实现:

  • 内存存储
  • 快擦写存储卡
  • 二进制文件
  • 数据库管理系统 (DBMS)
  • 取决于类的需要。

请注意永久对象不仅可从实体类中派生;而且永久对象还需要处理一般的非功能性需求。以上示例是保持与流程控制有关的信息或事务之间的状态信息所需的永久对象。

定义类的可见性 返回页首

确定每个类在其所在的包中的可见性。“公有”类可由它所在的包之外的类引用。“私有”类(或类的可见性是“实施”)只能由同一包内的类引用。

确定操作 返回页首

确定操作

要确定有关设计类的操作,需要:

  • 研究每一有关的分析类的职责,为每项职责创建一个操作。将职责的说明作为操作的初始说明。
  • 研究类所参与的用例实现,查看用例实现如何使用操作。扩展操作(此时是一个用例实现),改进操作及其说明、返回类型和参数。用例实现事件流使用文本描述了每个用例实现对于类的需求。
  • 研究用例的特殊需求,确保没有遗漏对于此处所述操作的隐含需求。

由于脚本和消息(临时消息规约,尚未指定给操作)说明了类要执行的既定行为,因此操作应该支持序列图中的消息内容。序列图的示例如下:

消息是确定操作的基础。

用例实现无法提供足以确定所有操作的有关信息。要查找其余操作,可以考虑下列问题:

  • 是否存在一种方法,可以初始化类的新实例,包括连接这一新实例与其关联关系的其他类的实例?
  • 需要进行测试来查看类的两个实例是否“等同”吗?
  • 是否需要创建类实例的副本?
  • 类执行的所有操作都是由操作机制所要求的吗(例如,为了释放不再使用的资源,“垃圾收集”机制将要求对象能够删除它对所有其他对象的引用。)?

不要定义只获取和设置公有属性值的操作(请参见确定属性和定义关联关系);这些操作一般是代码生成器产生的,不必作出明确定义。

命名和说明操作

在命名操作、返回类型和参数及其类型时,应该使用实施语言的命名约定;工件:设计指南对这些约定作出了说明。

对于每项操作,您应该定义下列内容:

  • 操作名。操作名应该简短,并可说明进行操作所得到的结果。
    • 操作名应该遵循实施语言的语法。示例:C++ 或 Visual Basic 可以接受操作名 find_location,而 Smalltalk 则不能(Smalltalk 中不使用下划线);因此,对于所有语言,最佳操作名是 findLocation
    • 避免使用暗示操作执行方法的名称(示例:Employee.wages()Employee.calculateWages() 好,因为后者暗示了要执行的计算操作。但此操作在实际上可能只返回数据库中的数值。)
    • 操作名应该明确表示操作的目的。避免使用含义不确定的操作名,例如 getData,此类名称无法说明操作的返回结果。使用可以准确表达预期结果的名称,例如 getAddress。将返回或设定的特征名称用作操作名是一种不错的方法;如果有操作参数,则操作名可以表示设定的特征;如果没有操作参数,则操作名可以表示获取的特征。示例:操作 address 返回客户地址,而 address(aString) 设定或更改客户的地址。即操作名暗示了操作的“获取”和“设定”特征。
    • 概念相同的操作,即便由不同的类定义、实施方法也完全不同或具有不同的参数个数,也应该具有相同的操作名。例如,创建对象的操作的名称在所有类中都应该相同。
    • 如果若干类中的多个操作具有相同的名称,则操作必须返回适用于接收方对象的同一类结果。这也正是多态性概念的一个示例,多态性是指不同对象应该以类似方式响应相同的消息。示例:无论对象名的保存或获取方式是什么,操作 name 都应该返回对象名。遵循这一原则可以使模型更易于理解。
  • 返回类型。返回类型应该是操作返回的对象的类。
  • 简短说明。尽管我们试图使操作名具有实际意义,但尝试理解操作的含义时,操作名的作用往往不是很明显。可以为操作提供几句话说明,一般应该从操作用户的角度来编写这种说明。
  • 参数。使用简明易懂的参数名称,确定参数的类,为参数提供简介。指定参数时切记参数越少,复用性就越高。参数个数少可以使操作更加易于理解,进而找到类似操作的可能性也就比较大。您可以将具有很多参数的操作拆分为若干个操作。操作对于其用户来说必须易于理解。操作的简短说明应该包括如下内容:
    • 参数含义(如果从名称来看不是很明确的话)
    • 参数是通过值传递,还是通过引用传递
    • 必须指定参数值的参数
    • 参数的可选性及其默认值(如未提供参数值)
    • 参数的有效范围(如果有的话)
    • 操作完成的内容
    • 操作改变了哪些通过引用传递的参数

定义操作之后,即可使用为每个消息调用的操作的有关信息来完成序列图。

详情请参考指南:设计类中的“类操作”部分。关于 Visual Basic® 的详细指南,亦可参考指南:Visual Basic 中的设计类,“定义操作”部分。

定义操作可见性

对每项操作,需要确定操作的导出可见性。可见性的可选类型如下:

  • 公有:除了类本身以外,操作对其他模型元素也是可见的。
  • 实施:操作只在类本身的内部是可见的。
  • 保护:操作只对类本身、它的子类或友元(取决于语言)是可见的。
  • 私有:操作只对类本身和类的友元是可见的。

选择限定性最强但仍可完成操作目标的可见性。为此需要查看序列图,对于每条消息,需要确定消息是来自接收方所在包外的类(要求公有的可见性),还是来自所在包内的类(要求实施可见性),或者是来自子类(要求保护的可见性),或者来自类本身或类的友元(要求私有的可见性)。

定义类操作

绝大部分操作是“实例”操作,即它们是对类的实例执行的操作。但在有些情况下,如果一个操作适合于类的所有实例,则它就是类范围之内的操作。“类”操作的接收方实际上是元类的实例,是对于类本身的说明,而不是类的任何特定实例。类操作的示例包括创建(实例化)新实例和返回类的 allInstances 等消息。

为了表示类范围之内的操作,操作字符串加有下划线。

定义方法返回页首

方法制定了操作的实施。在已使用操作名、说明和参数充分定义了操作所需的行为的大多数情况下,方法是直接由编程语言实施的。如果实施操作需要采用特定算法,或需要操作说明之外的更多信息,则要采用单独的方法说明。方法不仅说明了操作的内容,而且还说明了其工作方式。

说明方法应该涉及到以下内容:

  • 操作的实施方式。
  • 属性的实施方式及其用于实施操作的方式。
  • 关系的实施方式及其用于实施操作的方式。

需求当然会因情况而异。但类的方法规约务必应该陈述如下内容:

  • 要根据需求完成什么操作?
  • 要使用哪些其他的对象及其操作?

更为明确的需求需要侧重于:

  • 参数的实施方式。
  • 要使用的任何特殊算法。

序列图是上述信息的重要来源。从序列图中可以清楚地了解执行操作时,在其他对象中使用的操作。有关要在其他对象中使用哪些操作的规约对于完全实施操作是十分必要的。因此,生成完整的方法规约要求您确定用于所涉及对象的操作并检查相应的序列图。

有关 Visual Basic® 的详细指南,请参考指南:Visual Basic 中的设计类,“定义(UML)方法”部分。

定义状态 返回页首

对于某些操作,操作行为依赖于接收方对象的状态。状态机是一种适用于此的工具,它描述对象可以具有的状态和导致对象状态变化的事件,请参见指南:状态图。 状态机对说明“主动类”非常实用,并且它在定义工件: 封装体时也起到了很大的作用。

简单状态机的示例如下:

燃油分配器的简单状态图

每一状态转移事件都与一个操作关联关系。取决于对象的状态,操作可以具有不同的行为;转移事件对此作出了说明。

对于关联关系操作的方法说明应该和相应状态的信息一起更新,对每个有关状态要说明应该执行的操作。状态通常采用属性表示;状态图可以作为确定属性的步骤的输入。

有关详细信息,请参见指南: 状态图

关于 Visual Basic® 的详细指南,请参考指南:Visual Basic 中的设计类,“定义状态”部分。

确定属性 返回页首

方法的定义和状态的确定中,确定了类执行其操作所需的属性。 属性为类实例提供了信息存储空间,并通常用于表示类实例的状态。类本身保留的任何信息都是通过属性实现的。对于每种属性,需要定义以下内容:

  • 属性名,它应该遵循实施语言和项目的命名约定;
  • 属性类型,它应该是实施语言支持的基本数据类型;
  • 属性默认值或初始值,创建类的新实例时实例的初始化值
  • 属性的可见性,其可用值如下:
    • 公有:属性对于包含类的包内和包外都是可见的。
    • 保护:属性只对类本身、子类或类的友元(取决于语言)是可见的。
    • 私有:属性仅对类本身或类的友元可见。
    • 实施:属性仅对类本身可见。
  • 对于永久类,属性是永久的(默认)还是暂时的。尽管类本身可以是永久性的,但类的所有属性并没有必要是永久性的。

检查以确保所有属性都是必需的。应该验证属性 - 在流程早期添加属性比较容易,并且如果属性不是由于缺乏预见性而变得毫无用处,它存活的时间也可以更长。由于成千上万的实例而成倍增加的过多属性将会严重影响系统性能和存储需求。

有关属性的详细信息,请参考指南: 设计类中的“属性”部分。

定义依赖关系 返回页首

对于要求在对象之间进行通信的情况,需要作出以下提问:

  • 对接收方的引用是作为参数传递给操作的吗? 如果肯定,则应该从包含发送方类和接收方类的类图中,在两种类之间建立依赖关系。此外,如果为交互采用了协作图格式,则应该限定链接的可见性,将其设置为“参数”。
  • 接收方是“全局”的吗?如果肯定,则应该从包含发送方类和接收方类的类图中,在两种类之间建立依赖关系。此外,如果为交互采用了协作图格式,则应该限定链接的可见性,将其设置为“全局”。
  • 接收方是由操作本身创建和破坏的临时对象吗?如果肯定,则从包含发送方类和接收方类的类图中,在两种类之间建立依赖关系。此外,如果为交互采用了协作图格式,则限定链接的可见性,将其设置为“局部”。

定义关联关系 返回页首

关联关系是对象之间相互通信的机制。它们为对象提供了一个消息流动的“通道”。关联关系还记录了类之间的依赖关系,强调了对于某类所作变更将会影响到很多其他的类。

检查每种操作的方法说明,以便理解类实例和其他对象进行通信与协作的方式。为了给另一对象发送消息,对象必须具有对于消息接收方的引用。协作图(序列图的另一表示形式)利用链接描述对象之间的通信,如下图所示:

定义关联关系和聚合关系

其余消息可以采用关联关系聚合关系来确定进行通信的两个类的实例之间的关系。 有关选择适当表示方法的信息,请参见指南:关联关系指南:聚合关系。对于这两种关联关系,在协作图中将链接的可见性设置为“”。其他任务包括:

  • 建立关联关系和聚合关系的导向性。进行这一操作时,要考虑在交互图中对于它们的链接进行实例化所需的导向性。因为导向性的默认值是 true,所以如果关联关系的某个类的所有对象对应的所有链接角色都不需要导向性,您只需查找关联关系(和聚合关系)即可。在这种情况下,将类的角色的导向性设置为 false
  • 如果关联关系本身具有属性(表示为“关联类”),则应创建设计类,用正确的属性来表示“关联类”。在其他两种类之间插入这一设计类,即可在关联类和其他两种类之间建立具有适当多态性的关联关系。
  • 指定“关联关系端”是否应该排序;如果与关联关系另一端相关联的对象具有必须保留的次序,则需要确定这一点。
  • 如果关联类(或聚合关系类)仅由当前类引用,则可以考虑是否应对该类进行嵌套。嵌套类具有许多优点,其中包括更快地传递消息和更简单的设计模型;其缺点包括不管是否存在嵌套类实例都必须要为嵌套类分配空间,即缺少独立于嵌入类的对象身份,不能从嵌入类之外引用嵌套类实例。

最好在描述关联类的类图中定义关联关系和聚合关系。类图应该由包含关联类的包拥有。描述关联关系和聚合关系的类图示例如下:

此类图示例显示类之间的关联关系、聚合关系和泛化关系。

处理分析类之间的订阅关联关系

分析类之间的订阅关联关系用于确定类之间的事件依赖关系。在设计模型中,我们必须明确处理这些事件依赖关系,或者采用现有的可用事件处理器框架,或者通过设计和构造自己的事件处理器框架来进行处理。在某些编程语言(如 Visual Basic)中,只需简单地声明、提出和处理相应事件即可。在其他语言中,您可能需要使用某些可复用函数的附加库来处理订阅关系和事件;如果无法获取相应功能,则必须加以设计和创建。另请参见指南: 订阅关联关系

定义泛化关系 返回页首

类可以组织成泛化关系分层结构,来反映公共的行为和结构。在此可以定义公共超类,以便子类可以继承该超类的行为和结构。泛化关系是一种用于标记的便利手段,只要使用它在一个地方标定公共的行为和结构,那么如果在其他地方发现重复性的行为和结构,就可以再次使用它们。有关详细信息,请参考指南: 泛化关系

在发现可以泛化关系的行为和结构时,应创建一个公共超类,来包含公共属性、关联关系、聚合关系与操作。并从将要成为公共超类的子类的类中删除公共的行为。定义从子类到超类的泛化关系

解决用例冲突 返回页首

目的
  • 避免由于两个或多个用例使用不同方式、同时访问许多设计类实例所导致的并行冲突。
概念:概念:并行

通过设计流程顺序处理用例的困难之一是,两个或多个用例可能使用冲突的方式、同时尝试调用对设计对象进行操作。此时必须确定并行冲突并加以解决。

如果采用同步消息传递的方式,则只有一个操作完成,才可以进行后续的调用对象操作。同步的消息传递表明按照消息的先后顺序处理消息。在所有消息具有相同优先级,或每个消息在同一执行线程中运行的情况下,这一方式可以解决并行冲突。在不同的执行线程(以工件:封装体表示)访问对象的情况下,必须采取显式机制来避免或解决并行冲突。

不同的执行线程可以同时调用对同一对象执行的不同操作,而不会产生并行冲突;同时修改客户名和地址也不会产生冲突。只有在两个不同的执行线程试图修改对象的同一特征时,才会产生冲突。

对每个可能同时被不同执行线程访问的对象而言,需要确定不可同时访问的受保护代码区段。但在精化阶段初期,由于必须保护的操作很多,确定具体的代码段几乎是不可能的。接下来,选择或设计合适的访问控制机制来避免发生同时进行访问的冲突。串行化访问的消息队列、使用每次仅允许访问一个线程的信号(或“令牌”)或其他形式的锁定机制都是此类机制的示例。机制的选择往往在很大程度上要依赖于实施,并且会因编程语言和操作环境而异。有关选择并行机制的指南,请参见项目专用的工件: 设计指南

处理一般的非功能性需求返回页首

设计类应该加以改进,以便处理一般的非功能性需求,项目专用的设计指南对此作出了说明。对分析类的非功能性需求是此步骤的一个重要输入,分析类的特殊需求和职责可能已对这些需求作出了说明。这种需求通常都根据实现类需要的构架(分析)机制而定;然后在此步骤中加以改进,合并与这些分析机制有关的设计机制。

可用的设计机制由构架设计师在工件: 设计指南中确定,并说明其特征。对于每种所需的设计机制,尽可能多地限定它们的特征,并给定适用范围。有关设计机制的详细信息,请参考活动: 确定设计机制概念: 分析机制概念: 设计机制与实施机制

设计类时,需要考虑的通用设计指南和机制如下:

  • 如何利用现有产品和构件,
  • 如何适应编程语言,
  • 如何分布对象,
  • 如何获取可以接受的性能,
  • 如何获取特定的安全性级别,
  • 如何处理错误,
  • 等有关信息。

有关 Visual Basic® 的详细指南,请参考指南:Visual Basic 中的设计类,“处理一般的非功能性需求”部分。

评估结果返回页首

在此阶段,您应该检查设计模型来核实工作是否偏离正轨。您没有必要详细复审模型,但应该考虑以下检查点:

© 1987 - 2001 Rational Software Corporation。版权所有。

分栏显示 Rational Unified Process

Rational Unified Process