指南:设计包
设计包是用来对设计模型进行分区的构造。本指南说明如何确定和指定设计包。
关系
主要描述

简介

可以将设计模型构造成较小的单元,以使其更易于理解。通过将设计模型元素分组成包和子系统,然后显示这些组如何互相关联,可以更容易理解模型的整体结构。注意, 设计子系统建模为一个实现一个或多个接口的组件;关于更多信息,请参阅工作产品:设计子系统工作产品指南:设计子系统。另一方面,设计包只是用于分组。

包内容可视性

包中包含的类可以是公共或私有的。所有其他类都可以和公共类相关联。私有类只可以和包中包含的类相关联。

包接口由包的公共类组成。包接口(公共类)隔离其他包并实施与其他包的依赖关系。使用此方法可以简化并行开发,因为可以在早期建立接口,而开发者只需要知道其他包的接口中的更改。

包分区条件

会因为许多原因而将设计模型分区:

  • 当系统完成时,可以使用包和子系统作为命令、配置或交付单元。
  • 由于资源分配以及不同开发团队的能力不同,可能要求在处于不同位置的不同组之间划分项目。 子系统和良好定义的接口一起,提供了以受控、协调方式在不同团队之间划分工作的一种方法,使设计和实施能够并行进行。
  • 可以使用子系统以反映用户类型的方法构造设计模型。许多更改需求都源自用户;子系统确保特定用户类型的更改只会影响对应于该用户类型的系统部件。
  • 在某些应用程序中,某些信息应只能由少数人访问。子系统使您能够在需要隐私的区域保护隐私。
  • 如果正在构建支持系统,则可以使用子系统和包使其获得与要支持的系统结构类似的结构。以此方法,可以同步两个系统的维护。
  • 子系统用于表示系统使用的现有产品和服务(例如,COTS 产品和库),如下面几节中所说明。

封装边界类

当将边界类分发到包时,可以应用两种不同的策略,选择哪一个策略取决于是否会在将来大幅更改系统接口。

  • 如果有可能会替换系统接口,或进行大量更改,则应将该接口与设计模型的其余部分分离。更改用户接口时,只影响这些包。此类重大更改的示例是从面向命令行的接口切换至面向窗口的接口。

附带文本中描述的图。

如果主要目标是简化重大接口更改,则应将边界类放置在一个(或几个)单独的包中。

  • 如果没有打算进行重大接口更改,则应当将对系统服务的更改(而非对接口的更改)作为指导原则。然后应将边界类和与其功能相关的实体类和控制类放在一起。这样,就能够容易地看到在更改某实体类或控制类的情况下将影响哪些边界类。

附带文本中描述的图。

为简化对系统服务的更改,将边界类和与其功能相关的类封装在一起。

对于在功能上与任何实体类或控制类都不相关的必需边界类,应将它们和属于同一接口的边界类放置在单独的包中。

如果某边界类与可选服务相关,则应将它与共同协作提供服务的类分组到单独的子系统中。该子系统应映射到一个可选组件,在定购可选功能时提供该组件。

封装功能相关的类

应为功能相关的每组类确定一个包。当判断两个类是否功能相关时,可以应用几个实践条件。按重要性递减顺序,这些条件是:

  • 如果一个类的行为和/或结构中的更改使另一个类中也有必要更改,则这两个类在功能上相关。

示例

如果将新的属性添加到实体类订单,则很可能有必要更新控制类订单管理员。因此,它们属于相同的包订单处理。

  • 可以通过以一个类(例如,实体类)开始并检查从系统中删除它会有什么影响,来判断这个类是否与另一个类在功能上相关。所有由于删除某个类而变得多余的类都与被删除的类有某些联系。多余性表示只有被删除的类才使用该类,或该类自身依赖于被删除的类。

示例

仓库处理系统中有一个包订单处理,包含两个控制类订单管理员订单登记员。这两个控制类都对有关仓库中订单处理的服务建模。实体类订单存储所有订单属性和关系,该类仅用于订单处理。如果除去了该实体类,则不再需要 订单管理员订单登记员,因为仅当订单存在时它们才有用。因此,实体类订单应与这两个控制类包含在同一个包中。

附带文本中描述的图。

订单管理员订单登记员订单同属一个包,因为如果从系统中除去订单,则这两个类将变得多余。

  • 如果两个对象使用大量消息交互或有其他方式的复杂的相互通信,则它们可以是功能相关的。

示例

控制类任务执行者将许多消息发送至传输者接口,并从传输者接口接收很多消息。这是应将它们包含在同一个包任务处理中的另一原因。

  • 如果边界类的功能是显示特定实体类,则边界类可与该特定实体类功能相关。

示例

仓库处理系统中的边界类托板形式,用于向用户显示实体类托板的实例。每个托板使用屏幕上的标识号来表示。如果更改了有关某个托板的信息(例如,还为该托板指定了名称),则可能还必须更改边界类。因此 托板形式应与托板包含在同一个包中。

  • 如果两个类与同一参与者交互或受到同一参与者中的更改的影响,则这两个类可以是功能相关的。如果两个类不涉及相同参与者,则它们不应在相同的包中。当然可以由于更重要的原因而忽略最后一个规则。

示例

仓库处理系统中有一个包任务处理,该包除了其他内容,还包含控制类任务执行者。这是参与者传输者(可以在仓库中传输托盘的物理传输者)中涉及的仅有的一个包。该参与者通过边界类传输者接口与控制类 任务执行者交互。因此应在任务处理包中包含该边界类。

附带文本中描述的图。

传输者接口任务执行者属于相同的包,因为它们都受传输者参与者的更改的影响。

  • 如果两个类之间有关系(关联、聚集等),则这两个类可以是功能相关的。当然,不能盲目地遵循该条件,但当没有其他条件适用时,可以使用它。
  • 某个类可以与创建该类实例的类功能相关。

以下两个条件可以用于确定何时应将两个类放到相同的包中:

  • 与不同参与者相关的两个类不应放到相同的包中。
  • 不应将可选类和强制类放到相同的包中。

评估包聚集

首先,包中的所有元素必须有相同的可选性:没有在强制包中的可选模型元素。

示例

强制实体类物品类型除了其他内容,还有名为重新进货阈值的属性。但重新进货功能在系统中是可选的。因此,应将物品分成两个实体类:可选类与必需类,它们是相关的。

认为是强制类型的包不应依赖于任何认为是可选类型的包。

作为一项规则,两个不同的参与者不能使用单个包。此规则的原因是更改一个参与者的行为不应影响到其他参与者。但该规则有例外情况,例如对于组成可选服务的包。无论有多少参与者在使用该类型的包,都不应对该类型的包进行划分。因此,可以拆分任何由几个参与者使用的包或类,除非包是可选的。

同一包中的所有类必须是功能相关的。如果遵循了『从功能相关的类查找包』一节中的条件,则一个包中的类之间将是功能相关的。但是,特定类可能自身就包含“过多”不属于该类的行为或关系。应将该类部分删除,以使之成为一个全新的类或可能属于另一个包的其他类。

示例

一个包中的控制类 A 的行为不应过分依赖于另一个包中的类 B。要隔离特定于 B 的行为,必须将控制类 A 分成两个控制类 A'A"。将特定于 B 的行为放在一个新的控制类 A" 中,该类在与 B 相同的包中。新类 A" 与原对象 A' 也有关系(例如泛化关系)。

附带文本中描述的图。

要隔离特定于 B 的行为,应将缺少相同性质的控制类 A 分成两个控制类 A'A''

描述包依赖关系

如果一个包中的类与不同包中的类有关联,则这些包互相依赖。应使用包之间的依赖关系对包依赖关系建模。 依赖关系帮助我们评估更改结果:许多包所依赖的包比没有包依赖的包更难以更改。

因为将在包指定期间发现与此类似的一些依赖关系,因此必须在工作时更改这些关系。依赖关系的描述可包含关于哪些类关系引起了依赖关系的信息。因为这引入了难以维护的信息,因此仅当信息是相关的并且是值时才应完成它。

示例

仓库处理系统中,从包订单处理到包商品处理存在依赖关系。之所以发生此关联,是因为订单处理中的实体类订单与另一个包中的实体类商品类型有关联。

附带文本中描述的图。

订单处理依赖于商品处理,因为在这两个包中的两个类之间存在关联。

评估包耦合

包耦合有好处和坏处:好处是因为耦合代表重用,坏处是因为耦合代表使系统难于更改和演化的依赖关系。可遵循一些常规原理:

  • 不应交叉耦合(即交叉依赖)包:例如,两个包不应互相依赖。

附带文本中描述的图。

在这些情况中,需要将包重新组织以除去交叉依赖关系。

  • 下层中的包不应依赖于上层中的包。包应仅依赖于同一层和次下层中的包。

附带文本中描述的图。

在这些情况中,需要将功能重新分区。一种解决方案是按照接口声明依赖关系,并组织下层中的接口。

  • 通常情况下,除非依赖行为在所有层之间是公共的,否则依赖关系不得跳层,另一可选方法是简单地在各层上传递操作调用。
  • 包不应依赖于子系统,仅应依赖于其他包或接口。