您好,欢迎访问三七文档
第13章实现策略从本书前几章考虑的例子可以清楚地看到,UML使用的设计模型的许多特征可以用面向对象程序设计语言直接实现。例如,类图中的类可以作为Java的类实现,泛化可以用继承实现等等。许多CASE工具通过实现像这样一些规则,提供了代码生成能力。从设计模型到代码的这种直接转换是面向对象设计的重要优势,但是设计模型的某些特征仍然不能直接映射为程序设计语言的结构。本章将考虑这些特征中最突出的特征,并讨论实现它们的不同策略。关联是类图最重要的特征,而在程序设计语言中却没有直接类似物。第7章的餐馆预约系统给出了通过引用实现关联的几个简单例子,这种方式将在13.1节和随后几节详细加以描述。本章也描述了可以实现更复杂类型的关联的方法,譬如限定关联和关联类。一个应用的动态模型包含的信息,不是反映在实现的声明结构中,而是反映在程序中的类的各个方法中。对象交互图描述了操作执行中发送消息的次序,这些信息自然可以用于指导各个操作的实现。另一方面,状态图描述的约束是适用于一个类的所有操作的约束,因而可以影响到该类的所有方法。因而采用一致的策略,保证这些约束能够正确地反映在该类的方法的实现中,是一个好的思想。13.7节讨论了实现状态图的各种方式。13.1实现关联关联描述了一个系统运行时对象之间存在的链接的特性。从一个对象到另一个对象的链接,使每个对象知道另一个对象的本体或者位置,除了其他作用,这还使对象可以用链接作为通信通道,相互发送信息。无论选择什么方式实现链接,必须支持链接的这些特性。正如在7.6节阐述过的,引用提供了支持链接的机能,而实现简单关联最常用的方式是把引用用到链接的对象。链接和引用的区别主要在于,链接是对称的,而引用只适用于一个方向。如果两个对象链接,一个链接作为通道可以在两个方向中任何一个方向发送信息,然而使用引用,一个对象可以向另一个对象发送信息,但另一个对象却不知道引用它的对象,也就没有办法发送消息返回给该对象。这就意味着,如果一个链接必须支持双向传递消息,那么就需要用一对引用来实现该链接,每个方向上一个。使用两个引用会导致相当大的实现开销,而一致地维护反向引用又是实现正确性的关键。然而在许多情况下,特定的链接只需要沿一个方向遍历。在这种情况下,关联通常用导航性约束标志。这就在很大程度上简化了关联的实现:如果一个关联只是在一个方向是可导航的,就可以用指向遍历方向的单个引用实现。只在一个方向实现链接,可以相当大地简化链接的实现。另一方面,如果将来对这个系统的修改,要求沿着另一个方向遍历一个关联,又可能需要对程序和数据格式进行重大改变。决定是否只在一个方向上实现关联,涉及到在实现的简单性和未来修改关联的可能性之间的平衡,这个决定只能根据具体情况做出。13.2节讨论当决定只在一个方向上维护关联时如何实现关联。这种实现称为单向实现。13.3节讨论双向实现,这里已做出了必须在两个方向上维护关联的决定。一般地,关联的实现有两个不同的方面。第一,必须定义数据声明存储链接的细节。通常这个声明由在一个类中定义的数据成员组成,这些数据成员可以存储对关联的类的对象的引用。第二,必须考虑该应用的其余部分可以操纵这些指针的手段。一般地,构成该关联的实现基础的细节对客户代码应当是隐蔽的,这就意味着,参与关联的每个类应当定义适当范围的接口操作,以维护类图所定义的关联的语义。13.2单向实现本节讨论的情况是,已经做出决定,只需要支持在一个方向的关联。这个设计决策可以用在关联上画一个导航箭头,指明需要遍历的方向,表示在类图上。依赖于箭头所指关联端点的重数,实现也不同;箭尾端的重数对关联的实现没有影响。下面将讨论单向关联的重数为可选的(Optional),”只有1”,和”多”的情况。13.2.1可选关联图13.1表示的是一个只须单方向实现的关联,每个账户可以发出一个借记卡使用该账户,但因为这是该银行提供的全新功能,可以设想许多账户持有者不会立即有机会有这样一个借记卡。图13.1可选关联这个关联,如下所述,可以使用一个简单的引用变量实现。它允许一个账户对象至多持有对一个借记卡对象的引用。一个账户没有链接到借记卡的情况,用允许该引用变量有一个空引用表示。因此,这个实现准确地提供了该关联指明的重数要求。为了维护这个链接应该提供哪些操作,实际上需要对相关应用进行更详细的考察。上述代码并没有假定在创建账户时就提供一个借记卡。此外,还提供了改变链接到一个账户的借记卡,或者完全解除对一个卡的链接的操作。这种实现允许在一个账户生存期的不同时间,将不同的借记卡链接到该账户。具有这种特性的关联有时称为可变关联。另一方面,不变关联是到一个对象的链接,不可能用到另一个不同对象的链接替换。这种情况对应的需求是,对一个特定账户只能发出一张借记卡。如果账户和借记卡之间的关联是不变的,下面给出的该账户类的另一个声明可能更合适。它提供了增加一个卡到一个账户,并且只有当该账户没有持有卡时才允许增加一个卡。如果该账户已经分配了一个卡,就不能再改变或解除,所以相关的操作也从接口中去掉。13.2.2一对一关联这个不变关联的例子表明,一般地,在相关的类中通过提供合适的数据成员声明,能够直接实现的只是关联的某些特性。关联的另一些语义特征,可以通过在类的接口中只提供受限制的操作类别,或者将保证维护所需要约束的代码包括在成员函数的实现中实施。考虑图13.2所示的关联,这个关联描述了银行账户必须有担保人,该担保人愿意承担由账户持有人引起的欠款的情况。由于需要经常查明账户担保人的详细描述,但一般并不需要找出一个担保人所担保的一个或多个账户。因此,决定只在从账户到担保人这一个方向上实现该关联。图13.2一对一关联前面的例子说明,保存一个引用的变量有重数0或1是合乎情理的,因为它可以保存空引用。如果要求重数只有1,就必须增加对运行时出现的空引用进行检查的代码。在该账户类的下述实现中,如果对担保人对象提供了一个空引用,构造函数就抛出一个异常,在该类的接口中也没有提供更新已有引用的操作。这个代码是将账户对象和担保人对象之间的关联作为不变关联实现的。如果这个关联是可变的,那么该账户的担保人就可能被改变,可以把适当的函数,像构造函数,增加到提供这个功能的类中,由该函数核实这个对新担保人的引用是非空的。13.2.3重数为多的关联图13.3表示的是一个要求重数为多的关联的单向实现。该银行的每位管理者负责管理若干个账户,但该模型假定没有必要直接查找一个特定账户的管理者是谁。这个关联的新特征是管理者对象可以链接到不止一个账户,即可能是许多账户对象。为了实现这个关联,管理者对象必须维护多个指针,每个指针指向它所链接的每一个账户,因此必须使用某个适当的数据结构存储所有这些指针。此外,该管理者类的接口还应当提供维护这个指针聚集的操作,例如增加或去掉一个特定账户。图13.3重数为多的关联实现这种关联的最简单和最可靠的方法是使用类库中的适当的容器类。为达到此目的,对Java中的简单实现,Vector类是合理的选择。下面给出的是使用向量实现管理者类的要点。这个类声明一个账户向量作为私有数据成员,向该聚集增加或从该聚集除去账户的操作,只须调用该向量类接口中定义的函数。像图13.3中的类图的语义部分是,在一位管理者和特定账户间至多只能有一个链接。然而在这个实现中却没有理由说明,为什么由该管理者持有的向量中不能存储指向同一个账户的多个指针。这是由于程序设计语言不可能用声明把用UML类图中可能表达的每一个约束都转换成计算机可以记录的形式的一个例子。因此,“addAccount”函数的正确实现,应当检查增加的账户不是已经链接到该管理者对象的账户。13.3双向实现如果一个关联需要双向实现,如上面讨论的,每个链接可以用一对引用实现。为了支持这样的实现需要的声明和代码,在本质上与上面讨论的是一样的,仅有的不同是在参与此关联的两个类中都必须声明符合要求的域。处理双向实现中增加的复杂性是由于必须保证运行时实现一个链接的两个指针要保持一致而引起的。这种特性常称为引用完整性。图13.4(a)表示的是所希望的情况,这里一对”相等又相反”的指针实现了一个链接。图13.4(b)则违反了引用完整性。图13.4引用完整性和它的违反对于账户和担保人之间的关联,所要求的特性可以非形式地表述为”一个账户的担保人担保的必须是他所担保的那个账户”。图13.4(b)违反了这个表述:上边的账户对象持有对一个担保人对象的引用,而这个担保人对象持有的却是对完全不同的账户的引用。这两个引用不能被理解为是同一个链接的实现。这个例子清楚说明,用在相关类中简单地给出数据成员的合适定义,不可能保证引用完整性。这些声明只是断言持有一个或多个引用,而没有给出所引用的对象的性质,或者不同引用之间保持的关系的信息。正像单向实现的某些情况一样,双向实现的相关约束,也只能够在这些链接的操作代码中维护。像单向实现一样,不需要每一个关联都支持所有可能形式的链接操纵。需要支持什么行为依赖于各个应用的详细说明,并由参与该关联的类的操作接口来定义。本节将考虑几种有代表性的情况,引起对双向实现需要牢记的问题的注意。13.3.1一对一和可选关联图13.5表示的是图13.1的关联,但在关联的两端都增加了重数。假定我们现在需要提供这个关联的双向实现,我们也假定该关联在借记卡到账户方向是不可变的,或者说,一个借记卡一但被链接到一个账户,该卡就必须保持对该账户的链接,直到其生命期结束。另一方面,一个账户在不同时间,可以有不同的卡与该账户链接,以满足该账户持有者丢失卡的情况。图13.5双向一对一关联这个关联可以作为在从左到右方向是可变关联和可选关联的组合,另一个方向是不变关联来考虑。实现这个关联的简单方式,如下所述,是13.2节给出的实现的组合。为了简单,略去了这些类中的方法体。这个实现肯定要提供所必须的数据成员存储双向链接,但是,用于链接的两个方向的方法却是独立地维护的。例如,从该账户到该卡的指针,是在该账户构造函数中设置的,而从卡到账户的指针,是在该卡的构造函数中设置的。这种分工使得维护这个关联的引用完整性比所需要的更困难。例如,建立一个新的借记卡和一个账户之间的链接,需要两个分离的操作,第一要创建该借记卡,第二要把它链接到该账户。当该卡本身被创建后,再建立从卡到账户的链接。实现这个关联的代码可以如下所述。为了保证引用完整性得以维护,必须保证这两个操作总是一起执行。然而,由于需要两个语句,实际上有可能一个语句被遗漏了,或者提供了一个错误的参数,产生一个不一致的数据结构。如在下述例子中的借记卡,就被用一个错误的账户初始化了。一个较好的解决方案是明确地将维护该关联的责任只交付给两个类中的一个类。这样,两个对象之间的链接就可以用一个函数调用方式建立,并且可以使用封装确保只有可信赖的函数才能直接操纵这些链接。选择哪个类负责维护关联,通常是由整个设计的其他方面合理地提出的。在当前情况,由账户类的一个操作创建该账户的新的借记卡看来是合适的,这会提供一个高效的变元,使该账户类负责维护该关联。这样,该账户类和借记卡类可以定义如下:现在,用该账户类中的”addCard”操作,实际上就可以创建借记卡。这个操作实现,动态地将此当前账户的地址作为初始化参数,传送给借记卡类,创建一个新的借记卡。借记卡类的构造函数,使用这个初始化参数设置一个指针,指向创建此卡的账户。这样,一个调用增加卡的单个操作就保证正确地建立了一个双向链接。为了保证没有其他路径建立链接,借记卡类的构造函数声明为非公有的。这样就可以把建立借记卡的类限制在同一个包内,为防止任意建立借记卡提供了某种程度的保护。在C++中等效的“friend”机制可以用来确保借记卡只能被该账户类的函数建立。由于这个关联在一个方向上被声明为不可变的,这个例子特别简单。一般地,如果一个关联在两个方向上都是可变的,链接可能变动的各种情况都可能发生,链接的正确实现必须保证这些问题都能正确地得到处理。例如,假定一位储户可能持有多个账户,但只有一张借记卡,当使用该卡时才指明从哪个账户借记。这样,账户和借记卡之间的这种
本文标题:第13章实现策略
链接地址:https://www.777doc.com/doc-2243005 .html