分类:
2010-03-28 01:02:48
相互引用是指两个类之间相互引用了对方。从类图上看,引用箭头是双向的。相互引用是一种不够良好的设计。从高层次(设计)来说,类之间的引用关系应该是单向的,如果类A了解类B,说明在某种程度上类A比类B的层次要高一些,如果同时类B也了解类A,则说明类的设计有问题,两个类有混杂的部分,比如可能类的职责不够单一,或者类的职责不够明确,总之类的设计者没有把二者的关联分析清楚。应考虑重新设计一下,比如再提取一个新类,来去除这种高耦合关系。
从低层次(代码实现)来说,相互引用可能会带来编译问题,Delphi的帮助文档中,对单元引用是这么说的:
uses子句列出了program、library、unit所使用的单元文件。uses子句可能出现在以下地方:
Ø program或者library的工程文件中;
Ø unit的interface部分;
Ø unit的implementation部分。
若单元之间有直接或间接引用,则称这些单元之间是相互依赖的。只要在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;
...
为了减少循环引用,应尽可能把引用放到implementation的uses子句中,只有在interface部分确实需要的,才把它放到interface部分的uses子句中。
我认为,即使放在implementation可以解决问题,还是应该避免循环引用。它会带来下列问题:
Ø 设计不清楚,职责不明确
这在一开始就提到了。一般这种设计会违反类的单一职责原则,不利于对类的理解。
比如TClientForm和TClientController两个类,从名字上可以看设计者的原意,前者应该负责界面显示,后者应该是负责逻辑控制,TClientForm直接聚合TClientController即可,不用反向引用。但实际上二者是双向引用的,代码中,TClientController负责了一部分界面显示的职责,而TClientForm负责了一部分业务逻辑的职责。
Ø 代码逻辑纠缠不清
与上一条在本质上是一致的。由于设计不明确,在实现上就表现为代码上的纠缠,比如本来应该是类B的职责,就可能由类A来完成了,或者二者都实现了其中一部分,还可能出现重复实现的问题。假如出了bug,也难以定位和修改。
比如TUserSettingController类,原意是负责处理文件的读/写,它与业务逻辑是完全独立的,但是实现上,它与高层代码之间有双向的引用关系,之所以需要引用其它单元,是因为在它里面有一些业务逻辑的处理(如权限判断),这就使得代码有点混乱,而且多出了一些与业务逻辑相关的方法。这种情况下如果出现了bug,就无法把关注点集中到一个类上,而需要全面考虑,增加了复杂度。
Ø 扩展性,移植性差
比如GCM QQ中,TQQMessage是一个公共的类,在客户端和服务器端都要使用。后来服务器因为要添加消息列表的功能,添加了TQQMessageElem类。该类对TQQMessage做了一层封装。在设计上,TQQMessageElem和TQQMessage是双向引用的。实现上也没有出现问题,因为是在implementation部分引用的。问题在于,客户端受到了影响:编译失败了,因为客户端工程中找不到TQQMessageElem所在单元。一个解决方案是把TQQMessageElem的单元文件的目录加入客户端搜索路径,但是这种办法实为下策,因为TQQMessageElem是服务器才用的,客户端根本不应该知道它。即使加了它,又必须要加其它所需文件,像滚雪球一样越来越大。(GCM中也有这个问题,比如编译GCMUtils.pas时,同时要编译客户端一些Form和Frame的基类。)以后如果别的工程要用到TQQMessage,还必须多引用一系列相关单元文件,不但冗余,而且容易出错。
去除双向引用的常用方法:
Ø 添加新类
再提取一个新类,让它来负责两个类的公共部分。如下图所示:
Ø 继承
如下图所示:
我们GCM中使用策略(Strategy)模式时,经常采用这种方法。
Ø 回调
其应用场景为:一个(称为A)知道做什么,另一个(称为B)知道什么时候做。比如A知道B,现在B要在某一时机(比如点击按钮,Timer触发)对A进行处理,双向引用是一种解决办法,即在B的ButtonClick事件中调用A.XX方法。但是更好的办法是不需要了解A,B只需声明一个函数指针,该函数由A实现,并由A将B的函数指针指向自己的函数实现。这样就降低了耦合性,增强了扩展性。B只知道这个时机需要做事,但是并不关心做的是什么事情,因为业务逻辑是由A来负责的。同时B是可移植的,其他类也可以用B,只需要将其函数指针指向自己的实现即可。
Ø 一定要理解类所实现的抽象是什么
头脑要清醒,类的设计中,应该职责明确且单一。
Ø 尽可能地限制类和成员的可访问性
能隐藏的东西尽量隐藏,包括复杂度,变化源等。事实上可以反向思维一下:不是考虑应该隐藏哪些东西,而是考虑外界有必要看到哪些东西。
Ø 应尽量减少类与类之间相互合作的范围
即高内聚,低耦合。从N年前就在说了,不再赘述。
Ø Demeter法则
若A对象引用了B对象,则A可以调用B的任何public方法,但A应避免再调用由B对象所提供的C对象中的东西。这是为了避免A和C产生耦合。例如C变化了,A应该完全不受影响。
Ø 高扇入,低扇出
高扇入是指让尽量多的类来使用本类,低扇出是指让本类尽量少依赖于其它类。这两点应该很容易理解。
Ø 避免万能类,避免用动词命名的类
万能类很难做到职责明确且单一。用动词命名的类往往只有行为,没有数据,可能并不应该是一个独立的类,也许只是其它类的一部分。