目的
  • 确保类能提供用例实现所要求的行为
  • 确保能提供足够的信息来明确地实施类
  • 处理与类相关的非功能需求
  • 合并类使用的设计机制
角色:设计员 
频率:每个迭代进行一次。 
步骤
输入工件:   生成的工件:  
工具向导: 
更多信息: 

工作流程明细:  

类是设计工作的重要部分 - 它们实际上执行系统的真正工作。其它设计元素(如子系统、程序包和协作)描述类如何分组以及如何互相操作。

活动类是协调并推动被动类行为的设计类 - 活动类是这样的类:其实例是拥有自己的控制线程的活动对象。

使用设计模式和机制 回到页首

使用适合设计的类或功能、符合项目设计指南的设计模式和机制。

合并模式和/或机制是有效地执行该活动中的许多后续步骤(添加新类、操作、属性和关系),但是要符合模式或机制定义的规则。

请注意,模式和机制一般随着设计的发展而合并,不只是作为此活动中的第一步。它们还经常在一组类中得到应用,而不仅仅应用于单个类。

创建初始设计类 回到页首

为作为此活动的输入给出的分析类创建一个或多个初始设计类并分配跟踪依赖关系。本步骤中创建的设计类将在后续步骤中得到优化、调整、拆分或合并;在后续的步骤中将指定各种描述分析类如何设计的设计属性 - 如操作、方法和状态机。

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

设计边界类

边界类代表到用户或其它系统的接口。

一般情况下,代表到其它系统的接口的边界类被建模为子系统,因为它们往往有复杂的内部行为。如果接口行为很简单(也许只是充当到外部系统的现有 API 的传递),那么您可能会选择用一个或多个设计类代表接口。如果您选择这种方式,请对每个协议、接口或 API 使用单个的设计类,并记录关于您在类的特别需求中使用的标准的特别需求。

代表到用户的接口的边界类一般遵循用户界面中的每个窗口对应一个边界类或每种形式对应一个边界类的规则。结果,边界类的职责就可能处于一个相当高的级别,并需要在本步骤中进行优化和详细说明。用户界面的其它模型或原型可能是本步骤中要考虑的另一个输入来源。

边界类的设计取决于项目可用的用户界面(UI)开发工具。使用当前的技术,在开发工具中直接直观地构造 UI 是很常见的。这将自动创建需要与控制和实体类的设计相关的 UI 类。如果 UI 开发环境自动创建实施 UI 所需的支持类,那么就没有必要在设计中考虑这些支持类了。您只设计开发环境没有为您创建的内容。

设计实体类

在分析期间,实体类代表被操纵的信息单元。它们往往是被动的、持久的,并且可能被确定并与持久性分析机制相关联。 关于设计基于数据库的持久性机制的详细信息包含在活动:数据库设计中。性能考虑事项可能会迫使持久类的一定重构,从而造成在角色:数据库设计人员角色:设计人员之间联合讨论的“设计模型”的更改。

有关持久类的设计问题的更广泛的讨论展示在后面题为确定持久类的部分中。

设计控制类

控制对象负责管理用例的流动并因此协调其大多数操作;控制对象包含的逻辑不是特别与用户界面问题(边界对象)或数据工程问题(实体对象)相关。这种逻辑有时称为应用逻辑业务逻辑

在设计控制类时请考虑下列问题:

  • 复杂性 - 您可以用边界类或实体类处理不复杂的控制或协调行为。然而,随着应用变得越来越复杂,该方法的重大缺点也逐渐显露出来,例如:
  • 用例协调行为变成嵌入到 UI 中,使更改系统变得更加困难
  • 相同的 UI 不能简单地在不同的用例实现中使用
  • UI 因为增加了功能而增加负担,结果降低了性能
  • 实体对象可能因为用例特定的行为而增加负担,结果降低了其泛化程度

为了避免这些问题的发生,就引进了控制类来提供与协调事件流相关的行为。

  • 更改可能性 - 如果更改事件流的可能性很低或者成本可以忽略不计,那么其它控制类的额外开支和复杂性就可能没有正当的支持理由。
  • 分发和性能 - 在不同节点上或在不同流程空间中运行部分应用程序的需要产生了将设计模型元素专门化的需要。这种专门化往往通过添加控制对象并将来自边界和实体类的行为分发到控制类来完成。在执行这一操作期间,边界类朝着提供纯 UI 服务移动,实体类朝着提供纯数据服务移动,而控制类则提供剩余部分。
  • 事务管理 - 管理事务是典型的协调活动。因为没有处理事务管理的框架,一个或多个事务管理器类就必须进行交互以确保您能保持事务的完整性。

在后两种情况下,如果控制类代表独立的控制线程,那么使用活动类来对控制线程建模可能会更加适合。

确定持久类 回到页首

需要在永久介质上存储其状态的类被称为持久类。存储状态的需要可能是为了永久记录类信息、作为出现系统故障时的备份或用于信息的交换。持久类可能既有持久实例,也有临时实例;将类标注为持久只意味着类的某些实例可能需要持久。

根据在分析期间找到的持久性机制来合并设计机制。例如,根据类的要求,持久性的分析机制可能通过以下设计机制中的一个来实现:

  • 内存存储
  • 闪存卡
  • 二进制文件
  • 数据库管理系统(DBMS)

持久对象可能不仅仅从实体类获得;持久对象还可能需要用来处理一般的非功能需求。示例有:维护与流程控制相关的信息或者维护事务之间的状态信息需要持久对象。

确定持久类用来通知角色:数据库设计人员:类要求对其物理存储特征有特别关注。它还通知角色:软件设计人员类需要是持久类,并通知负责持久性机制的角色:设计人员类的实例需要变为持久实例。

由于需要协调的持久性策略,角色:数据库设计人员就负责用持久性框架将持久类映射到数据库。如果项目开发的是持久性框架,则框架开发人员还将负责了解设计类的持久性需求。要向这些人员提供他们所需的信息,此时表明类是持久的,或者(更精确地说)表明类的实例是持久的就足够了。

定义类可视性 回到页首

对于每个类,请确定它所在程序包内的类可视性。公用类可以在包含程序包的外部引用。私有类(或可视性为实施的类)可能只能由同一个程序包内的类引用。

定义操作 回到页首

确定操作

要在设计类上确定操作:

  • 研究每个相对应的分析类的职责,为每个职责创建一个操作。将职责的描述用作操作的初始描述。
  • 研究类 participates 中的用例实现,查看操作如何由用例实现使用。扩展操作,此时为一个用例实现,优化操作、操作的描述、返回类型以及参数。与类相关的每个用例实现的需求都在用例实现的“事件流”中有文字描述。
  • 研究“特别需求”用例,确保您没有遗漏可能在那里有陈述的操作的隐含需求。

要求操作能支持出现在序列图上的消息,因为脚本 - 尚未分配给操作的临时消息规范 - 描述期望类能执行的行为。图 1 是序列图的一个示例。

附带文本中描述的图。

图 1:消息成为确定操作的基础

用例实现不能提供足够的信息来确定所有操作。要查找剩下的操作,请考虑下列问题:

  • 有没有将类的新实例初始化的方法(包括将其连接到它所关联的其它类的实例)?
  • 有没有必要进行测试来查看类的两个实例是否相等?
  • 有没有必要创建类实例的副本?
  • 机制使用的类上有没有要求任何操作?例如,垃圾回收机制可能要求对象能够释放它与所有其它对象的所有引用,以便将所有未使用的资源释放出来。

不要定义仅仅获得并设置公用属性的值的操作(请参阅定义属性定义关联)。通常来讲,这些是由代码生成工具生产的,不要求进行显式的定义。

命名并描述操作

命名操作、返回类型以及参数和它们的类型时,请使用实施语言的命名约定。

对于每个操作,您都应该定义以下内容:

  • 操作名称 - 使名称简短并能描述操作实现的结果。
    • 操作的名称应该遵循实施语言的语法。例如:find_location 对于 C++ 或 Visual Basic 是可以接受的,但对于 Smalltalk(其中不使用下划线)不可接受;能用于所有语言的更好的名称将是 findLocation
    • 请避免使用暗示操作如何执行的名称。例如,Employee.wages()Employee.calculateWages() 要好,因为后者暗示要执行计算操作。操作可能只返回数据库中的一个值。
    • 操作的名称应该清楚地表明其目的。请避免使用不具体的、不描述它们返回的结果的名称,例如 getData。请使用能确切显示期望内容的名称,例如 getAddress。更好的方法是,只要让操作名称是返回或设置的属性的名称就可以了。如果它有参数,它就设置属性。如果它没有参数,它就获取属性。示例:操作 address 返回 Customer 的地址,而 address(aString) 则设置或更改 Customer 的地址。该操作的 getset 本质是操作的特征符中所隐含的。
    • 概念上相同的操作应该有相同的名称,即使定义它们的类不同、即使它们用完全不同的方法实施或者即使它们的参数数量不同也一样。例如,创建对象的操作的名称应该在所有类中都相同。
    • 如果数个类中的操作有相同的特征符,则操作必须返回适合于接收者对象的同种结果。这是多态性概念的一个示例,即不同的对象应该用类似的方法响应相同的消息。示例:操作 name 应该返回对象的名称,而不管名称如何存储或如何得到。遵循这种原则会使模型较易理解。
  • 返回类型 - 返回类型应该是操作返回的对象的类。
  • 简短描述 - 虽然您尽量使操作的名称有意义,但是在尝试了解操作所做的事情时,操作的名称往往只能起很模糊的指示作用。从操作 user's 角度简短地描述操作,由几个句子组成。
  • 参数 - 对于每个参数,请创建一个简短的描述性名称,确定它的类并给它一个简短的描述。在指定参数时,请记住参数越少,可重用性就越高。较少的参数使操作更容易理解,因此更可能找到类似的操作。您可能需要将有很多参数的操作分为数个操作。操作对于那些要使用它的人必须是可以理解的。简短描述应该包含:
    • 参数的意义(如果名称中不明显)
    • 参数是由还是引用传递的
    • 必须提供值的参数
    • 可以选择的参数以及它们的缺省值(如果未提供值)
    • 参数的有效范围(如果适用)
    • 操作中做什么事情
    • 操作更改什么 by reference 参数

一旦定义了操作,就请用关于每条消息调用了什么操作的信息来完成序列图。

有关更多信息,请参阅“指南:设计类”中题为类操作的部分。

定义操作可视性

对于每个操作,请从下列选项确定操作的导出可视性:

  • 公用 - 操作对于类本身以外的模型元素是可视的。
  • 实施 - 操作只在类本身内可视。
  • 受保护 - 操作仅对于类本身、其子类或类的朋友(视语言而定)可视。
  • 私有 - 操作仅对于类本身以及类的朋友可视。

请选择可能最受限制、但仍能实现操作目标的可视性。要做到这一点,请查看序列图,并为每条消息确定消息是来自于接收者程序包的外部类(要求公用可视性)、来自于程序包的内部(要求实施可视性)、来自于子类(要求受保护可视性)还是来自于类本身或朋友(要求私有可视性)。

定义类操作

大部分操作是实例操作;也就是说,它们是在类的实例上执行的。但是,在某些情况下,操作适用于类的所有实例,因此是类范围的操作。类操作接收者实际上是元类的实例 - 类本身的描述 - 而不是类的任何特定实例。类操作的示例包括创建(实例化)新实例的消息,它们返回类的 allInstances,依此类推。

操作字符串加下划线表示,以表明是类范围的操作。

定义方法 回到页首

方法指定操作的实施。如果操作要求的行为由操作名称、描述和参数进行了足够的定义,在许多这样的情况下,方法都是直接在编程语言中实施的。在操作的实施要求使用具体的算法或者要求比操作的描述中更多的信息,就要求有独立的方法描述。方法描述操作如何工作,而不仅仅描述它做什么。

如果方法进行了描述,它就应该讨论:

  • 操作将如何实施
  • 属性将如何实施以及用来实施操作
  • 关系将如何实施以及用来实施操作

需求在各个案例中都不尽相同,但是类的方法规范应该总是说明:

  • 根据需求将做什么
  • 将使用什么其它对象及其操作

更具体的需求可能会涉及:

  • 参数将如何实施
  • 将使用什么特殊的算法(如果有的话)

序列图是这个的重要来源。从这些方面已经能很清楚地了解在执行操作时将在其它对象中使用什么操作。将在其它对象中使用什么操作的规范对于操作的完全实施是必需的。因此,生成完整的方法规范要求您确定所涉及的对象的操作并检查相应的序列图。

定义状态 回到页首

对于一些操作,操作的行为取决于接收者对象所处的状态。状态机是一个工具,它描述对象可以呈现的状态以及使对象从一种状态移动到另一个状态的事件(请参阅指南:状态表图)。状态机对于描述活动类最有用。

图 2 中显示了简单状态机的示例。

附带文本中描述的图。

图 2:加油机的简单状态表图

每个状态转换事件都可能与操作关联。根据对象的状态,操作可能有不同的行为,转换事件描述这种情况如何发生。

关联操作的方法描述应该用特定于状态的信息进行更新,表明操作对每个相关状态应该做什么。状态通常用属性表示;状态表图充当属性确定步骤的输入。

有关更多信息,请参阅指南:状态表图

定义属性 回到页首

在定义方法和确定状态期间,确定类执行其操作所需的属性。属性为类实例提供信息存储,并经常用于代表类实例的状态。类本身保持的任何信息都是通过其属性完成的。对于每个属性,请定义:

  • 它的名称,应该遵循实施语言和项目的命名约定
  • 它的类型,将是受实施语言支持的基本数据类型
  • 它的缺省值或初始值,创建类的新实例时,它被初始化为缺省值或初始值
  • 它的可视性,将是下列值之一:
    • 公用:属性在包含类的程序包的内部和外部都可视
    • 受保护:属性仅对于类本身、其子类或类的朋友(视语言而定)可视
    • 私有:属性仅对于类本身以及类的朋友可视
    • 实施:属性仅对于类本身可视
  • 对于持久类,不管属性是持久的(缺省值)还是临时的 - 即使类本身可能是持久的,并不是类的所有属性都必须是持久的

检查以确保所有的属性都是需要的。属性应该有存在理由 - 属性可以很容易在流程早期添加,并能在它们不再需要后很长时间内都存在(由于眼光短浅而造成的)。额外的属性由于数千个甚至数百万个实例而变得数量繁多,它们对系统的性能和存储需求可能有不利的影响。

有关属性的更多信息,请参阅指南:设计类中题为属性的部分。

定义依赖关系 回到页首

对于每一个要求对象间通信的案例,请提出以下问题:

  • 对接收者的引用是作为参数传递给操作的吗?如果是的话,请在包含发送者类和接收者类的类图中建立这两个类之间的依赖关系。同时,如果使用了交互的通信图形式,那么就请使链接可视性变为合格并将其设置为参数
  • 接收者是全局的吗?如果是的话,请在包含发送者类和接收者类的类图中建立这两个类之间的依赖关系。同时,如果使用了交互的通信图形式,那么就请使链接可视性变为合格并将其设置为全局
  • 接收者是在操作本身过程中创建并销毁的临时对象吗?如果是的话,请在包含发送者类和接收者类的类图中建立这两个类之间的依赖关系。同时,如果使用了交互的通信图形式,那么就请使链接可视性变为合格并将其设置为局部

请注意,用这种方式建模的链接是临时链接,只在协作的特定环境中存在有限的期限 - 从这个意义上来讲,它们是协作中关联角色的实例。但是,如上文所述,类模型中的关系(即,独立于环境)应该是依赖关系。如 [RUM98] 所述,在临时链接的定义中:“将所有这样的链接作为关联进行建模是可能的,但是这样的话,关联的条件就必须陈述得非常宽泛,而且它们在限制对象的组合中失去了许多精确性。”在这种情况下,协作中的关系的建模比依赖关系的建模更重要,因为依赖关系并不完整地描述关系;而仅仅描述它存在而已。

定义关联 回到页首

关联向对象提供互相交流的机制。它们向对象提供消息可以流动的导管。它们还记录类之间的依赖关系,突出强调一个类中的更改可以被许多其它的类感觉到。

检验每个操作的方法描述,了解类的实例如何与其它对象互相交流和协作。要将消息发送给另一个对象,对象就必须有到消息的接收者的引用。通信图(序列图的另一种表示方法)将从链接的角度显示对象通信,如图 3 所示。

附带文本中描述的图。

图 3:通信图的示例

定义关联和聚集

剩下的消息用关联聚集来指定互相交流的两个类的实例之间的关系。有关选择适当的表示方法的信息,请参阅指南:关联指南:聚集。请在通信图中将这两种关联的链接可视性都设置为字段。其它任务包括:

  • 建立关联和聚集的可浏览性。您可以通过考虑它们在交互图上的链接实例化所要求的可浏览性来执行这一操作。因为缺省的情况下,可浏览性为 true,所以您只要找到关联中一个类的所有对象的所有相反链接角色都不要求可浏览性的关联(和聚集)就可以了。在那些情况下,请将类的角色的可浏览性设置为 false
  • 如果关联本身有属性(用关联类表示),请创建带有适当属性的设计类来表示关联类。将该类插入其它两个类之间,并建立关联类和其它的这两个类之间的关联,同时带有适当的多重性。
  • 指定关联关系端是否应该按顺序排列;当与关联另一端上的对象相关联的对象按顺序排列,并且这种顺序必须保留时,就会出现这种情况。
  • 如果关联(或聚集)类仅被当前类引用,请考虑类是否应该嵌套。嵌套类的好处包括更加快速的消息传递以及更简单的设计模型。缺点包括嵌套类占用的空间被固定分配,而不管是否有嵌套类的实例、是否缺乏独立于包含类的对象身份或者是否无法从包含类的外部引用嵌套类实例。

关联和聚集在描述关联类的类图中有最好的定义。类图应该由包含关联类的程序包拥有。图 4 是一个描述关联和聚集的类图示例。

附带文本中描述的图。

图 4:显示类之间的关联、聚集和泛化关系的类图示例

处理分析类之间的预订关联

分析类之间的预订关联用于确定类之间的事件依赖关系。在“设计模型”中,您必须明确地处理这些事件依赖关系,通过使用可用的事件处理程序框架或者通过设计并构造您自己的事件处理程序框架。在一些编程语言中 - 如 Visual Basic - 这是直接了当的;您声明、提出并处理相应的事件。在其它语言中,您可能必须使用一些其它的可重用功能库来处理预订和事件。如果功能无法购买,它就需要进行设计和构造。另请参阅指南:预订关联

定义内部结构 回到页首

一些类可能代表复杂的抽象并拥有复杂的结构。在对类进行建模时,设计人员可能希望代表其内部参与元素以及它们的关系,以确保实施人员将相应地实施在该类内部发生的协作。

在 UML 2.0 中,类被定义为结构化的类,能够拥有内部结构和端口。然后,类可能被分解成互相连接的部分的集合,而这些部分又可以进一步进行分解。可以通过迫使来自外部的通信穿过符合所声明接口的端口来包括类。

当您找到有复杂结构的复杂类时,请为该类创建复合的结构图。对将为该类行为执行角色的部分进行建模。确定如何使用连接器将各部分“连线”。如果要使该类的不同客户端可以访问该类提供的特定部分的行为,请利用带所声明接口的端口。同时请利用端口将该类的内部部分完整地从其环境中分离出来。

有关本主题的更多信息以及复合结构图的示例,请参阅概念:结构化的类

定义泛化关系 回到页首

类可以组织为泛化关系层次结构,以反映通用的行为和通用的结构。可以定义通用的超类子类既可以从中继承行为,也可以从中继承结构。泛化关系是为了表示上的方便,使您能够在一个地方定义通用的结构和行为,并在找到重复行为和结构的地方重复使用这种通用的结构和行为。有关泛化关系的更多信息,请参阅指南:泛化关系

当您找到泛化关系时,请创建一个通用超类来包含通用属性、关联、聚集和操作。将通用行为从将成为通用超类的子类的类中除去。定义从子类到超类的泛化关系

解决用例冲突 回到页首

本步骤的目的是防止两个或更多个用例可能同时、用可能不一致的方式访问设计类的实例时造成的并行冲突。

在设计流程中逐个用例地前进中存在的一个困难是:两个或更多个用例可能会尝试用可能冲突的方式同时对设计对象调用操作。在这些情况下,并行冲突必须明确地进行确定和解决。

如果使用了同步消息传递,那么执行操作将阻止对对象的后续调用,直到操作完成为止。同步消息传递意味着消息处理的“先到先服务”的顺序。这可能会解决并行冲突,特别是当所有消息的优先级都相同或者当每条消息都在相同的执行线程内运行的时候。如果对象可能由不同的执行线程(用活动类表示)访问,就必须使用显式机制来防止或解决并行冲突。

同一个对象上的不同操作被不同的执行线程同时调用而不出现并行冲突是可能的;客户的姓名和地址可以并行修改而不起冲突。只有当两个不同的执行线程尝试修改对象的同一个属性时,才会出现冲突。

请为每一个可能会由不同的执行线程并行访问的对象确定必须受保护以免于同时访问的代码段。在“精化”阶段早期,确定具体的代码段是不可能的;必须受保护的操作就足够了。接着,选择或设计适当的访问控制机制来防止互相冲突的同时访问。这些机制的示例包括消息排队以将访问序列化、使用信号或标记允许一次只能访问一个线程或者其它变化形式的锁定机制。机制的选择倾向于在很大程度上取决于实施,并且一般会随着编程语言和操作环境的变化而变化。

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

“设计类”被优化来处理一般的、非功能的需求中所述。本步骤的重要输入包括分析类的非功能需求,这些需求可能在分析类的特别需求和职责中已经有了说明。这样的需求往往从需要什么样的体系结构(分析)机制来实现类这一方面进行指定;在本步骤中,类然后被优化来合并与这些分析机制相对应的设计机制。

可用的设计机制由软件设计人员中确定和描绘。对于每个需要的设计机制,请使尽可能多的特征合格,在适当的地方给定范围。有关设计机制的更多信息,请参阅活动:确定设计机制概念:分析机制概念:设计和实施机制

在设计类时需要考虑数个一般的设计指南和机制,例如如何:

  • 使用现有的产品和组件
  • 适应编程语言
  • 分发对象
  • 达到可接受的性能
  • 达到某些安全级别
  • 处理错误
  • 等等

评价您的结果 回到页首

在此阶段检查设计模型来验证您的工作正朝着正确的方向前进。没有必要详细复审模型,但是您应该考虑以下检查点:



Rational Unified Process   2003.06.15