设计模式visitor——访问者
http://www.ibm.com/developerworks/cn/java/l-dpstruct/part1/
?
把对象本身和对象的操作分离开
?
最终的代码示例
在这个类图中,我增加了几条依赖线,即ErrorHandler是依赖于DbError和CommError的。此时我们可以看到:ErrorBase依赖于ErrorHandler,ErrorHandler依赖于ErrorBase的派生类(DbError和CommError),而ErrorBase的派生类又要依赖于ErrorBase本身。这就形成了一个循环的依赖过程,这样的结果就是ErrorBase传递地依赖于它的所有派生类。
这种循环依赖关系会带来严重的问题,一旦ErrorBase新增了一个派生类,那么ErrorHandler类必须要被修改,由于ErrorBase又依赖于ErrorHandler,那么依赖于ErrorBase的所有类都需要重新编译。这就意味着ErrorBase的所有派生类以及所有这些派生类的使用者都要重新编译,这种大规模的重新编译在开发一个分布式系统时会导致非常大的工作量,因为要重新分布每一个重新编译过的类,如果在重新分布时出现一些差错(如:忘记替换一些类),就会导致微妙的错误,而且很不容易查找出来。
另外,在该模式中存在一个假设,就是默认任意一个错误处理者要处理所有的错误类型,这个假设在某些情况下是不成立的,比如:如果对于LogicError我们不打算通知LOGSys进行处理会怎样呢?我们不得不要写一个处理该错误的空函数(当然你可以在ErrorHandler中写一个缺省的实现)。如果ErrorBase的类层次架构越来越大,并且它们要求的处理方法也有很多的不同,就会导致ErrorHandler接口中的方法集庞大,并且任何一个ErrorBase的派生类的改变,都会导致大规模的重新编译(甚至是毫无关系的类也要重新编译),重新分布,如果这种改变比较频繁,结果当然是无法忍受的。
通过上图可以看出,ErrorHandler中没有任何方法了,已经退化为一个空的接口,所以也就不可能再依赖于任何ErrorBase的派生类了。和visitor模式不同,这个方案中针对每一个特定的ErrorBase的派生类定义一个相应的处理接口,在每个派生类的handle方法实现中,运用RTTI技术进行相应的类型转换(把ErrorHandler转换为自己对应的错误处理接口,比如:在DbError的handle方法中,就把ErrorHandler转换为DbErrorHandler。Java在这方面做得不错,可以进行比较安全的类型转换),想要处理该错误的实体不仅要实现ErrorHandler接口,而且还要实现相应的针对具体错误类的处理接口,比如:GUISys就实现了三个接口(ErrorHandler、DBErrorHandler以及CommErrorHandler),从而也就实现了对DbError和CommError的处理。
这种方法打断了类之间的循环依赖关系,使得增加新的错误类型变得容易,并且也避免了可能出现的大规模的重新编译以及类的重新分布。并且,你也可以有选择地进行错误的处理,比如:如果GUISys不想处理DbError的话,很简单,不要实现DbErrorHandler接口即可,使得程序的结构非常清晰。可见这个方案克服了原始的visitor设计模式的不足。
通过对比上下两个类图可以看出,下面的要比上面的复杂,这也是该方案的一个缺点。如果问题的规模不大,重新编译以及重新分布没有多少工作量的话,还是可以使用原始的visitor模式的。仅仅当问题的规模扩大到visitor模式不能适用的地步时,可以考虑使用该方案。另外,由于改进的方案中使用了RTTI技术,会导致性能上的损失以及不可预测性,在使用时也要特别注意。
我们做如下的类比以便于更好地理解原始的visitor模式和改进后的方案。原始的visitor模式好像一个矩阵,在X方向上是一个个具体的错误类型,在Y方向上是一个个可以处理错误的实体,每一个交叉点上是具体的处理方法,矩阵中的每一个位置上都必须有一个处理方法。而改进后的方案象一个稀疏矩阵,仅仅在需要的位置上才有具体的处理方法,从而减少了很多冗余。
下面给出关键的代码片断:
interface ErrorBase{ public void handle(ErrorHandler handler);}class DBError implements ErrorBase{ public void handle(ErrorHandler handler) { try { DbErrorHandler dbHandler = (DbErrorHandler)handler; dbHandler.handle(this); } catch(ClassCastException e) { } }}class CommError implements ErrorBase{ public void handle(ErrorHandler handler) { try { CommErrorHandler commHandler = (CommErrorHandler)handler; commHandler.handle(this); } catch(ClassCastException e)\ { } }}interface ErrorHandler {}interface DbErrorHandler{ public void handle(DBrror dbError);}interface CommErrorHandler{ public void handle(CommError commError);}class GUISys implements ErrorHandler, DbErrorHandler, CommErrorHandler{ public void announceError(ErrorBase error) { error.handle(this); } public void handle(DBError dbError) { /* 通知用户界面进行有关数据库错误的处理 */ } public void handle(CommError commError) { /* 通知用户界面进行有关通信错误的处理 */ }}class LogSys implements ErrorHandler, DbErrorHandler, CommErrorHandler{ public void announceError(ErrorBase error) { error.handle(this); } public void handle(DBError dbError) { /* 通知日志系统进行有关数据库错误的处理 */ } public void handle(CommError commError) {/* 通知日志系统进行有关通信错误的处理 */ }}
?
回页首
5、 结论
本文从另一个方面,设计模式的适用性,探讨了在进行程序重构时该如何选择重构的目标,以及如何对现有的设计模式进行改进使之符合我们的目标。本文的主要目的是想说明,在进行设计模式的选择时,不仅要关注设计模式是解决什么问题的,还要关注使用该设计模式解决问题时会受到什么约束和限制。这样就可以帮助你更好地理解问题,做出合理的取舍,或者你可以根据自己的需求对已有的设计模式进行修改使之满足你的要求,如果你这样做了,你就很可能创造出了一种新的设计模式。