Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1042639
  • 博文数量: 264
  • 博客积分: 6005
  • 博客等级: 大校
  • 技术积分: 2798
  • 用 户 组: 普通用户
  • 注册时间: 2007-08-08 20:15
文章分类

全部博文(264)

文章存档

2011年(42)

2010年(213)

2009年(4)

2008年(2)

2007年(3)

分类:

2010-03-28 01:02:48

消除类之间的相互引用

消除类之间的相互引用

理论

相互引用是指两个类之间相互引用了对方。从类图上看,引用箭头是双向的。相互引用是一种不够良好的设计。从高层次(设计)来说,类之间的引用关系应该是单向的,如果类A了解类B,说明在某种程度上类A比类B的层次要高一些,如果同时类B也了解类A,则说明类的设计有问题,两个类有混杂的部分,比如可能类的职责不够单一,或者类的职责不够明确,总之类的设计者没有把二者的关联分析清楚。应考虑重新设计一下,比如再提取一个新类,来去除这种高耦合关系。

从低层次(代码实现)来说,相互引用可能会带来编译问题,Delphi的帮助文档中,对单元引用是这么说的:

uses子句列出了programlibraryunit所使用的单元文件。uses子句可能出现在以下地方:

Ø         program或者library的工程文件中;

Ø         unitinterface部分;

Ø         unitimplementation部分。

若单元之间有直接或间接引用,则称这些单元之间是相互依赖的。只要在interface部分的uses子句中没有循环路径,相互依赖就是没有问题的。因此出现这个问题时,只要把某个或某些单元引用移到implementation部分的uses子句中即可。

例如,下面的代码导致编译错误:

unit Unit1;
interface
uses Unit2;
...
unit Unit2;
interface
uses Unit1;
...

而下面的代码是合法的:

unit Unit1;
interface
uses Unit2;
...
unit Unit2;
interface
...
implementation
uses Unit1;
...

为了减少循环引用,应尽可能把引用放到implementationuses子句中,只有在interface部分确实需要的,才把它放到interface部分的uses子句中。

分析

我认为,即使放在implementation可以解决问题,还是应该避免循环引用。它会带来下列问题:

Ø         设计不清楚,职责不明确

这在一开始就提到了。一般这种设计会违反类的单一职责原则,不利于对类的理解。

比如TClientFormTClientController两个类,从名字上可以看设计者的原意,前者应该负责界面显示,后者应该是负责逻辑控制,TClientForm直接聚合TClientController即可,不用反向引用。但实际上二者是双向引用的,代码中,TClientController负责了一部分界面显示的职责,而TClientForm负责了一部分业务逻辑的职责。

Ø         代码逻辑纠缠不清

与上一条在本质上是一致的。由于设计不明确,在实现上就表现为代码上的纠缠,比如本来应该是类B的职责,就可能由类A来完成了,或者二者都实现了其中一部分,还可能出现重复实现的问题。假如出了bug,也难以定位和修改。

比如TUserSettingController类,原意是负责处理文件的读/写,它与业务逻辑是完全独立的,但是实现上,它与高层代码之间有双向的引用关系,之所以需要引用其它单元,是因为在它里面有一些业务逻辑的处理(如权限判断),这就使得代码有点混乱,而且多出了一些与业务逻辑相关的方法。这种情况下如果出现了bug,就无法把关注点集中到一个类上,而需要全面考虑,增加了复杂度。

Ø         扩展性,移植性差

比如GCM QQ中,TQQMessage是一个公共的类,在客户端和服务器端都要使用。后来服务器因为要添加消息列表的功能,添加了TQQMessageElem类。该类对TQQMessage做了一层封装。在设计上,TQQMessageElemTQQMessage是双向引用的。实现上也没有出现问题,因为是在implementation部分引用的。问题在于,客户端受到了影响:编译失败了,因为客户端工程中找不到TQQMessageElem所在单元。一个解决方案是把TQQMessageElem的单元文件的目录加入客户端搜索路径,但是这种办法实为下策,因为TQQMessageElem是服务器才用的,客户端根本不应该知道它。即使加了它,又必须要加其它所需文件,像滚雪球一样越来越大。(GCM中也有这个问题,比如编译GCMUtils.pas时,同时要编译客户端一些FormFrame的基类。)以后如果别的工程要用到TQQMessage,还必须多引用一系列相关单元文件,不但冗余,而且容易出错。

解决办法

去除双向引用的常用方法:

Ø         添加新类

再提取一个新类,让它来负责两个类的公共部分。如下图所示:

 

 

Ø         继承

如下图所示:


我们GCM中使用策略(Strategy)模式时,经常采用这种方法。

Ø         回调

其应用场景为:一个(称为A)知道做什么,另一个(称为B)知道什么时候做。比如A知道B,现在B要在某一时机(比如点击按钮,Timer触发)对A进行处理,双向引用是一种解决办法,即在BButtonClick事件中调用A.XX方法。但是更好的办法是不需要了解AB只需声明一个函数指针,该函数由A实现,并由AB的函数指针指向自己的函数实现。这样就降低了耦合性,增强了扩展性。B只知道这个时机需要做事,但是并不关心做的是什么事情,因为业务逻辑是由A来负责的。同时B是可移植的,其他类也可以用B,只需要将其函数指针指向自己的实现即可。

附《代码大全》中有关类的设计的一些思想

Ø         一定要理解类所实现的抽象是什么

头脑要清醒,类的设计中,应该职责明确且单一。

Ø         尽可能地限制类和成员的可访问性

能隐藏的东西尽量隐藏,包括复杂度,变化源等。事实上可以反向思维一下:不是考虑应该隐藏哪些东西,而是考虑外界有必要看到哪些东西。

Ø         应尽量减少类与类之间相互合作的范围

即高内聚,低耦合。从N年前就在说了,不再赘述。

Ø         Demeter法则

A对象引用了B对象,则A可以调用B的任何public方法,但A应避免再调用由B对象所提供的C对象中的东西。这是为了避免AC产生耦合。例如C变化了,A应该完全不受影响。

Ø         高扇入,低扇出

高扇入是指让尽量多的类来使用本类,低扇出是指让本类尽量少依赖于其它类。这两点应该很容易理解。

Ø         避免万能类,避免用动词命名的类

万能类很难做到职责明确且单一。用动词命名的类往往只有行为,没有数据,可能并不应该是一个独立的类,也许只是其它类的一部分。

阅读(851) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~