3.2 对象之间的关系
一个对象本身是非常无趣的。对象通过与其他对象协作,为系统的行为做出贡献。“我们没有一个钻头研磨机(bit-grinding processor)来掠夺和打劫数据结构,但我们有许多行为良好的对象,它们有礼貌地互相询问,实现它们不同的愿望。”[13]例如,考虑一个飞机的对象结构,它被定义为“一组部件,它们都有落向地面的自然趋势,需要不断努力和管理才能力争取得成果”[14]。只有所有组成对象共同努力,飞机才能飞行。
两个对象之间的关系包括了一个对另一个所做的假定,即包括可以执行哪些操作以及将导致怎样的行为。我们发现,在面向对象分析和设计中有两种对象关系特别有趣,它们是:
■ 链接;
■ 聚合。
3.2.1 链接
链接(link)这个术语来自Rumbaugh等人,他们将它定义为“两个对象之间物理上或概念上的联系”[16]。一个对象通过它与其他对象的链接,与其他对象进行协作。换言之,链接代表了具体的关联,通过这种关联,一个对象(客户)请求另一个对象(服务提供者)的服务,或者通过这种关联从一个对象导航到另一个对象。
图3-5展示了几种不同的链接。在这个图中,两个对象图标之间的连线代表这两个对象之间存在链接,意味着消息可以通过这个途径传递。消息用小箭头表示,它代表消息的方向,带有一个标签为消息本身命名。例如,图3-5中展示了一个简化的水流控制系统的一部分,这可能是在一个制造工厂中控制管道水流的。可以看到,FlowController对象(水流控制器)有一个到Valve对象(阀门)的链接。Valve对象有一个到DisplayPanel对象(显示面板)的链接,DisplayPanel将显示它的状态。只有通过这些链接,一个对象才能向另一个对象发送消息。
在两个对象之间发送消息通常是单向的,虽然偶尔也会出现双向的情况。在我们的例子中,FlowController对象调用了Valve对象上的操作(目的是改变它的设置)和DisplayPanel对象上的操作(目的是改变它显示的东西)。这种关注点分离在结构良好的面向对象系统中是很常见的。另外请注意,虽然消息传递是由客户发起的(如FlowController),指向服务提供者(如Valve对象),但是数据可以通过链接双向流动。例如,当FlowController调用Valve对象上的adjust操作时,数据(即要改变的设置)从客户流向了服务提供者。然后,如果FlowController调用了Valve对象上的另一个操作isClosed,结果(即阀门是否处于完全关闭的位置)将从服务提供者传回到客户。
图3-5 链接
作为链接的参与者,一个对象可能扮演以下三种角色之一。
■ 控制器:这个对象可以操作其他对象,但不会被其他对象操作。在某些地方,“主动对象”和“控制器”这两个术语是互换使用的。
■ 服务器:这个对象不操作其他对象,它只被其他对象操作。
■ 代理:这个对象既可以操作其他对象,也可以被其他对象操作。创建代理通常是为了表示问题域中的一个真实对象。
在如图3-5所示的例子中,FlowController是一个控制器对象,DisplayPanel是一个服务器对象,Valve是一个代理。示例3-3展示了这些职责如何恰当地被分配在一组协作的对象中。
示例3-3
在许多不同类型的工业过程中,某些反应要求一定的温度坡度,即提升某种物质的温度,让它在某个温度上保持一段时间,然后将它冷却到环境温度。不同的过程需要不同的温度曲线:某些对象(如望远镜中的镜子)必须慢慢冷却,而另一些物质(如钢)必须迅速冷却。这种温度坡度具有足够的定义良好的行为,可以创建为一个类。因此我们提供了TemperatureRamp类,它在概念上是一种时间/温度的映射关系(参见图3-6)。
图3-6 聚合
实际上,这种抽象的行为不只是简单的时间/温度映射关系。例如,可以设置一种温度坡度,要求在60分钟时达到250°F(进入温度坡度1小时后),在180分钟时达到150°F(进入温度坡度3小时后),然后我们想知道在120分钟时温度是多少。这需要线性插值,这就是我们希望的这种抽象的另一个行为(即插值)。
有一个行为显然不是这种抽象需要具备的,即控制加热器来实现特定的温度坡度。我们喜欢更好的关注点分离,让这种行为通过3个对象的协作来实现:一个温度坡度实例、一个加热器和一个温度控制器(参见图3-6)。process操作提供了这种抽象的主要行为,它的目标是利用指定位置的加热器实现指定的温度坡度。
对于我们的风格,有一点说明。第一眼看上去,似乎我们设计了一种抽象,其目的只是将一种功能分解包装在一个类中,使它看起来显得高贵而面向对象。但schedule操作表明并非如此。TemperatureController类的对象拥有足够的知识,可以决定何时应该安排哪一种具体控制,所以我们将这个操作作为抽象的另一个行为。在某些高能耗的工业过程中,加热物质是成本很高的事情,考虑前一个过程遗留下来的热量是很重要的,另外也要考虑未参与的加热器的正常冷却。由于存在schedule操作,客户可以查询TemperatureController对象,以确定处理特定温度坡度的下一个最优时间。
1.可见性
考虑两个对象A和B,它们之间存在一个链接。为了让A能向B发送一条消息,B必须以某种方式让A能看到它。在对问题分析的过程中,基本上可以忽略可见性的问题,但当开始设计具体的实现时,就必须考虑跨越链接的可见性,因为,我们此时的考虑决定了链接两端对象的范围的访问。本章稍后将会详细讨论这一点。
2.同步
当一个对象通过链接向另一个对象发送一条消息时,这两个对象就称为同步了。在完全串行式的应用中,这种同步通常是通过简单的方法调用来完成的。但是,在存在多控制线程的情况下,对象需要更复杂的消息传递机制来处理并发系统中可能出现的互斥问题。前面曾提到,主动对象拥有自己的控制线程,所以我们期望它们的语义在其他对象存在时仍然得到保证。但是,当一个主动对象与一个被动对象之间有链接时,我们必须选择以下三种同步方式之一。
■ 顺序:只有在某一时刻只存在一个主动对象时,被动对象的语义才得到保证。
■ 守卫:在多个控制线程的程序下,被动对象的语义也能保证,但主动的客户之间必须协作,以实现互斥访问。
■ 并发:在多个控制线程的程序下,被动对象的语义也能保证,服务提供者保证互斥。
3.2.2 聚合
链接表明了一种端到端的关系或客户/服务提供者的关系,而聚合则表明了一种整体/部分层次结构,提供了从整体(也称为聚合体)导航到它的部分的能力。例如,如图3-6所示,TemperatureController对象拥有到TemperatureRamp对象的链接,也有到Heater对象的链接。因此TemperatureController对象是整体,Heater是它的部分。聚合关系的表示方法将在第5章中进一步讨论。
如果一个对象是另一个对象的一部分,就意味着它到它的聚合体有一个链接。通过这个链接,聚合体可以向它的部分发送消息。对于TemperatureController对象,可以找到它对应的Heater。对于Heater这样的对象,当且仅当Heater的状态包括它的包装对象(也称为它的容器)的知识时,才有可能导航到它的包装对象。
聚合可以代表物理上的包含,也可以不代表。例如,一架飞机由机翼、引擎、起落架等组成,这是物理上包含的例子。与此不同,股票持有人及其持有股票之间的关系则是不需要物理上包含的聚合关系。股票持有人拥有股票,但这些股票不是股票持有人的物理组成部分。这种整体/部分的关系更多的是概念上的,因此不太直接,不像构成飞机的各部分那样的物理聚合。
显然,在链接和聚合之间需要折中。有时候聚合更好,因为它将各个部分封装为整体的秘密;有时候链接更好,因为它们允许对象之间较松的耦合。明智的工程决定需要仔细权衡这两方面的因素。