Logo
Section 1 / 5

反向引用

让我们来学习类模型中的反向引用属性。

导入

反向引用

让我们来学习类模型中的反向引用属性。

在面向对象建模与编程中,双向关联通过一对相互逆向的引用属性来表示,这些属性支持双向导航,也称为对象访问。

以下模型作为我们的示例。它包含两个双向关联,如关联两端的拥有者实心圆点所示:

...
Publisher-Book-Author信息设计模型,包含两个双向关联

逆向引用属性

为了方便检索由某位俱乐部成员担任主席或共同主席的委员会,我们可以在 Committee-ClubMember 示例模型中添加两个引用属性。这两个属性分别是:某位俱乐部成员作为委员会的主席(ClubMember::chairedCommittee)和某位俱乐部成员作为委员会的共同主席(ClubMember::coChairedCommittee)。我们假设任何一位俱乐部成员最多只能担任一个委员会的主席或共同主席(该限制是非排他的)。因此,我们得到如下模型:

...
Committee 和 ClubMember 模型

Committee::chair 和 ClubMember::chairedCommittee 这两个引用属性之间存在紧密对应关系,它们互为逆向。当俱乐部成员汤姆担任预算委员会的主席时,用元组表示为 ["预算委员会", "汤姆"],那么预算委员会就是由汤姆担任主席的委员会,其逆向元组表示为 ["汤姆", "预算委员会"]。为了在图中表达这种逆向关系,我们在 ClubMember::chairedCommittee 属性声明后附加一个逆向属性约束(即 {inverse of chair}),同样地,我们也对 Committee::chair 属性进行类似标注,如下图所示:

...
Committee 和 ClubMember 模型,带有逆向属性约束

使用面向对象语言的引用路径语法,若变量 c 引用一个 Committee 对象,则可得等式:

c.chair.chairedCommittee=cc.chair.chairedCommittee = c

或者,若变量 m 引用一个 ClubMember 对象,则可得:

m.chairedCommittee.chair=mm.chairedCommittee.chair = m

当属性 p2p_{2} 是属性 p1p_{1} 的逆向时,意味着 p1p_{1} 也是 p2p_{2} 的逆向。因此,当我们声明 ClubMember::chairedCommittee 为 Committee::chair 的逆向时,Committee::chair 也就自动成为 ClubMember::chairedCommittee 的逆向。因此,我们将 Committee::chair 和 ClubMember::chairedCommittee 称为一对相互逆向的引用属性。如果模型中存在这样的一对属性,则意味着存在冗余,因为两个引用属性中的任何一个都可以通过逆向操作从另一个推导出来。支持双向高效对象访问的双向导航虽然便利,但带来了数据存储和更新开销。

为了维护相互逆向引用属性对中的重复信息,通常将其中一个属性视为主属性(leader),另一个视为从属属性(follower)。在受影响的模型类的变更方法(如属性设置器)代码中必须牢记这一区分。在模型图中,我们通过在从属属性名称前加上斜杠(/)的UML符号来标明其为从属属性。下图展示了一个示例:

...
Committee 和 ClubMember 模型,带有主属性和从属属性

现在,ClubMember 中的 chairedCommittee 属性已标记为派生属性,这由其名称前的斜杠表示。其注释 {inverse of chair} 定义了推导规则:当对 Committee::chair 属性进行逆向操作时,即可生成该属性值。

属性的推导方式

属性的推导有两种实现方式:一种是在读取时按需计算(on read),另一种是在更新时即时计算(on update),即每当推导表达式中涉及的变量(通常是其他属性)发生变化时,就执行一次更新计算。

后一种情况相当于数据库中的物化视图。仅在读取时推导的引用属性可能无法保证高效的导航,因为读取时的计算可能导致不可接受的延迟。而基于更新推导的属性则能提供高效的导航性能。

当一个逆向引用属性在名称前加上斜杠(/)时,表示它是通过更新推导的。例如,在上述示例中,/chairedCommittee 属性是在更新时根据 chair 属性推导得出的。

生命周期依赖

当存在派生引用属性时,必须处理相关模型类之间的生命周期依赖,这需要基于关联功能类型(一对一、多对一、多对多)采用特殊的变更管理机制。

我们以派生的逆向引用属性 ClubMember::chairedCommittee 为例,它是一个单值且可选的属性,这意味着:

  1. 每当创建一个新的委员会对象(必须指定 chair),对应的 ClubMember::chairedCommittee 属性也应相应赋值;
  2. 每当 chair 属性被更新(例如,为某个委员会指派新主席),原主席对应的 ClubMember::chairedCommittee 属性应被清空,新主席的属性应被设置;
  3. 每当一个委员会对象被销毁时,其对应的 ClubMember::chairedCommittee 属性也应被清空。

如果一个派生的逆向引用属性是多值的,而其基础属性是单值的,则生命周期依赖意味着:

  1. 每当创建一个新的基础对象时,对应的逆向属性也应更新。此时,我们将新基础对象的引用加入其值集合中。例如,若基础对象是书籍,则可以将新书对象的引用添加到 Publisher::publishedBooks 中;
  2. 当基础属性被更新(例如,为某本书分配新出版商)时,对应的逆向属性 Publisher::publishedBooks 也必须同步更新。这可以通过从其值集合中移除旧的引用并添加新的引用实现;
  3. 当基础对象被销毁时,对应的逆向属性也应更新。这可通过从其值集合中移除对基础对象的引用实现,例如从 Publisher::publishedBooks 中移除要销毁的书籍对象的引用。

从纯计算角度看,我们可以自由选择两个相互逆向的引用属性中的任意一个作为主属性。例如,Book::authors 与 Author::authoredBooks 都是合适的选择。

然而,在许多情况下,关联代表了不对称的本体论存在依赖关系,从而决定了哪一方应作为主属性。例如,Book 与 Author 之间的作者关系体现了本体论上的存在依赖:一本书的存在依赖于其作者,而作者的存在并不依赖于其任何作品。因此,Book 与 Author 之间的对象生命周期依赖表明,应将 Book::authors 中的作者引用作为主属性,这是维护双向关联的自然选择。而 Author::authoredBooks 则作为从属属性,从 Book::authors 推导而来。

实现单向非功能关联

创建一个面向对象的类模型