从代码到模式(三) 对象间关系

bfzhao 2010-08-20 11:23:41
对象间的交互关系构成了我们业务逻辑的基础。所以对象间的交互关系的复杂性决定了我们最终代码的复杂性。我们现在讨论对象交互可能遇到的问题。

对于两个对象包,他们中间可能有着复杂的相互调用关系。如果让每个包中的对象直接和另一个包中的对象交互,那么这两个包的接口会激起复杂而且难于扩展。俗话说,家有千口,主事一人。我们应该把关外部接口交互相关的事情都集中到一个单独的对象中,就好象所有的外事关系大都归外交部管一样。所以我们会在对象包之间加入一个专门用于接口协调以及转发的对象。该对象代理了所有包内部需要导出的接口(注意,保护代理的应用),另一个包只需要和该对象交互就可以了,而不需关心其内部的实现。理想的情况是每一个包都有一个这样的接口对象,但是在小型的对象系统中,往往是两个包共用一个接口对象。这种使用方法,就是Mediator模式

对于上面的对象包接口对象,它并不关心实际的处理具体消息的对象是什么,它只是把特定的消息路由到合适消息处理接口,而由实现了该接口的对象处理。如果这个对象本身也是对象聚合的话,它可能把消息进一步分发给它认为应该处理该消息的对象。如此这样下去,直到有一个对象处理了消息(当然有可能到了最后也没有任何对象处理该消息,出现这种情况,意味着该消息被忽略了,可能是实现缺陷)。宏观上看,这是一个自里向外,从上倒下的过程,根据对象的职责来寻找最合适的处理该消息的对象。GoF把这种模式称为Chain of Responsibility模式

对象组合技术是OO技术的基础。一般来说,内部对象和外部对象他们本身提供的外部接口是不同的,亦即,他们并没有同时实现某种特定的型。然而,在某些时候,要求内部对象和外部对象都提供相同的型的实现,这里是两个例子:

1. 常见的窗口系统中,一个窗口可以是另一个窗口的子窗口(编辑框可以是对话框的子窗口),同时他们本身又都是窗口(实现了抽象的窗口接口)

2. 层次文件系统中,目录和文件都是文件系统中的一个接口,实现了相同的接口;与此同时,目录对象又可以包含文件对象。

这种把同类对象以相互嵌套的方式组织对象以及在需要的时候把收到的消息转发给子对象(可以接受同样的消息是因为他们实现了相同的型)的方式,GoF命名为Composite模式。组合模式和职责链模式看起来有点类似,都是把一个消息发送到一系列对象中。职责链强调的是找到一个仅且一个最合适的对象处理该消息,这很像在链表中查找;而组合模式强调的是所有的对象都以一致的方式处理同一个消息,而这个对象可能以相互包含的形式存在。这更像在树中做深度遍历。从实际的实现结构上看,而这可能非常类似。

对象间的消息传递,我们往往容易忽略消息的发送者。之所以如此,是因为大部分的情况下,消息的接收者不关心消息是从谁发送过来的。如果消息的接收者需要消息的发送者信息才能工作,一般的做法是把消息发送者的标志嵌入到消息中,然后消息接收者根据对方标识来作出回应。这是典型的switch/case语句可以处理的情况。你可能会说了,不是说OO可以使用强类型的办法消除这些难看的switch/case调用吗?确实如此,不过由于C++支持的限制(不能通过消息发送者来重载),我们只能想一个间接办法。

假设S1和S2都继承与S,而R1和R2都继承与R。s是对象S1或者S2的指针,而r是对象R1或者R2的指针。现在我们需要发送消息让s发送消息m给r,然后期望根据S1,S2,R1,R2组合的不同,可以给出不同的结果。

由于C++仅仅支持接收方的重载,所以我们需要现在接收方做一次,然后返回到发送方在做一次。那就意味着不仅仅R要支持消息m,S也需要。因为r要把消息重新传回给s,所以s一定要把自己也作为参数传给r。所以代码如下
r->m(s);

而m可以实现为
void R1::m(S* s) { s->m(this); }
void R2::m(S* s) { s->m(this); }

对应的S1和S2的实现分别是:
void S1::m(R1* r) {}
void S1::m(R2* r) {}
void S2::m(R1* r) {}
void S2::m(R2* r) {}

到这里,我们已经利用多态技术把对象分开了,尽管很难看。谁叫语言支持不好呢?类似,我们可以实现三元组以及更多元组对象的重载,但是复杂性是指数增长的。二元组重载已经可以应付绝大多数的情况。如果你真的需要更高元组的对象重载机制,换一种语言把,如SmallTalk。
如果把其中R对应的消息换成更通用的accept,把S对应的消息换成visit,并且把第二次的分发使用专门的对象表达,使之更加通用,我们得到如下代码:
void R1::accept(S* s) { s->visit(this); }
void R2::accept(S* s) { s->visit(this); }

struct S {
virtual void visit(R1*) = 0;
virtual void visit(R2*) = 0;
}

struct S1 : S {
virtual void visit(R1*) {}
virtual void visit(R2*) {}
}

struct S2 : S {
virtual void visit(R1*) {}
virtual void visit(R2*) {}
}

这样,不仅仅是S1和S2,任何需要和R做如此交互的类只需要实现S接口接口。这样S非常像是R类层次结构的一个亲朋密友,对R层次结构中的每一个类都有直接关系。这种结构,就是GoF中说的Visit模式。其中S被叫做Visitor。这可能是这些设计模式中最不好理解的一个了。

对象间的消息传递机制一般都是一对一的,如果需要广播的情况则处理起来就困难了。其一是我们不想把消息源和消息接收对象绑定起来,以及消息源最好不要直接引用消息接收对象;其二是我们可以方便地增删消息接收对象。想想我们先前提到的针对接口编程的箴言,我们就会把消息处理对象的公共接口抽象出来,那就是专门处理该消息的接口。然后所有的需要接受此广播消息的对象都实现此接口。而在消息源方面,我们需要维护一个这个实现这个接口的对象的动态序列,并且提供增删函数,可以让一个对象在需要处理该消息的时候可以把它自己添加到其中。这样,当需要发送消息的时候,我们只要遍历该注册队列,依次发送即可。这就是转说中的有诸如反向依赖,订阅/通知等漂亮名字的Observer模式

大部分的服务对象都处在这样一种过程中:等待,接收到请求后处理,然后继续等待。在这两个等待阶段,一般来说对象都处于相同的状态。而且要处理事情也是相对独立。假设我们要构建一个HTTP服务器,客户发来请求,根据请求的不同(根据请求结构中的某些字段来判断),转发给不同的子模块处理。这是最简单的情况;更复杂一点的情况是,一个子模块调用完毕之后,还不能立即把结果返回给客户,而需要进一步的处理。这时候,框架代码需要在把这些中间结果转发给另外的模块,直到处理完毕之后,把结果返回给用户。按照这样的处理方式,框架待续需要知道各种请求的处理逻辑。更糟糕的是,当我们要新增加一个请求是,必须修改框架代码才可以。怎么可以避免这些缺点?答案很简单,把这些不同的处理逻辑都封装成对象,使之有相同的接口(这意味它们实现了相同的型)。因为每一个处理逻辑都很清楚其后续处理逻辑,返回处理结果的时候,直接构造返回需要后续处理的对象即可。

这就是State模式,把相对独立的业务逻辑表达为特定的对象,并且把它们串联起来。当一个对象调用完毕后,立即返回需要处理的下一个对象的应用;这样框架代码只需要构造最开始的对象,调用之,然后重复地调用对象的类似接口就可以了,直到返回对象为空。

常见的对象间交互的模式就是这些,剩下的GoF模式我们下节再谈。

http://blog.csdn.net/bfzhao/archive/2010/08/19/5824435.aspx
...全文
108 3 打赏 收藏 转发到动态 举报
写回复
用AI写文章
3 条回复
切换为时间正序
请发表友善的回复…
发表回复
weixiaoshashou 2010-08-20
  • 打赏
  • 举报
回复
支持下。
bfzhao 2010-08-20
  • 打赏
  • 举报
回复
是的,这里介绍的是GoF经典23个模式。
先学习基本的OO设计,然后在考虑怎么归纳总结其中的模式。就像首先学习怎么使用C++的基本语法规则,然后才能学习如果使用C++做OO设计。

如果专门是学习设计模式,从Java入门比从C++入门简单。Head First系列适合初学者,有中文版发行。
大拙男 2010-08-20
  • 打赏
  • 举报
回复
这是设计模式里面的东西么?
我记得做一个软件的时候里面有"redo"和"undo"的功能,要用到设计模式
但是我现在还没懂得什么叫设计模式
大侠,问下,我要怎么深入了解模式设计?怎么入门?有什么好的书籍推荐么?

64,646

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧