以Visitor设计模式应对需求变化
网络上大多数的设计模式文章,都是采用了一些抽象的例子,今天我以实际的工作经验来描述一下在客户需求变化下如何通过良好的模式来应对。假设我们已经有一个成熟的version或者milestone,这个版本通过了全部的unit test,qa test, cvs或者svn上已经tag过并且冻结,这时候,REQ提出一个新的需求,要求DEV增加,这时候我们有两个选择,一,checkout部分源码Class,修改后commit,产生这个class的一个新的revision;二,不改动任何现存Java代码,只增加一些类,修改一下XML配置文件。哪种方式更优越呢?显然,第二种方案不需要触动已有的测试封装过的类,是更高质量的做法。但是,不改动任何类就想增加新的功能,这是天方夜谭么?
如果我们采用了visitor pattern,就可以实现,见图一
当我们在开发一个包含logic的implementation class的时候,只需要让它继承一个
abstract class Visitable
public void accept(Visitor visitor)
{
visitor.visit(this);
}
这里相当于给现有的classl留了一道小门,为的就是应对将来可能的变化
那么,在最初的版本里面,client是这样调用logic class的
ConcreteClass c=new ConcreteClass ();
c.methodA();
现在,需求增加了,要求多一个methodB,传统的方式是修改ConcreteClass的源码,增加一个method叫methodB,现在我们不动ConcreteClass
ConcreteClass extends Visitable,所以我们用一个新的Visitor,通过预留的那个后门,来增加新的逻辑,这里就好像你是房子的主人,今天需要打扫房间,可是你今天有事要出门,那么你就请一个有你房子后门钥匙的朋友来替你打扫。
(注,由于java不能多继承,如果你的ConcreteClass已经继承了其他类,可以采用composite一个Visitable的子类的方式来变相完成这个多继承)
public class MethodBVisitor implements Visitor
public void visit(Visitable visitable)
{
ConcreteClass c=(ConcreteClass)visitable;
methodB(c);
}
private void methodB(ConcreteClass c)
{
//call public method in ConcreteClass, here is the new logic!
}
client的调用方式改为
ConcreteClass c=new ConcreteClass ();
c.methodA();
Visitor v=new MethodBVisitor();
c.accept(v);//methodB implemented in MethodBVisitor instead of in ConcreteClass
需要注意的是,如果要让Visitor能充分的实现新的功能,需要ConcreteClass把所有Visitor可能需要用到的method和property(gettter, setter)都设成public或者friendly的,这就好比你请朋友来帮你打扫,如果卧室也需要打扫的话你还要给他卧室钥匙。
最后,再进一步,把Visitor的创建,改成基于xml的(或者采用Spring的BeanFactory)
<Visitors>
<Visitor class="com.***.***.MethodBVisitor">
<Visitor class="com.***.***.MethodCVisitor">
<Visitors>
Client调用代码改为
ConcreteClass c=new ConcreteClass ();
c.methodA();
List<Visitor> vList=XMLConfigFactory.load(VISITOR_CLASS_LIST); //create object base on reflection, similiar to Spring IoC
for(Visitor v:vList) c.accept(v);
修改后,如果需要增加一个visitor,只需要在xml配置里增加一行,class名设为新增加的visitor,我们就可以在只修改一个xml配置文件和增加一个class的情况下,完成一个新的方法,确保不影响现存代码的质量。这里体现了一个重要的OOAD原则,扩展而不是修改。
[ 本帖最后由 hoopoos 于 30-4-2009 12:12 编辑 ] LZ举的例子很清晰易懂,我的感觉就是Visitor模式适合用在有一个稳定的接口或者数据结构的前提下,同时又有很多围绕使用这个接口/数据结构的相似行为的场景下。 比如,IDE中有一个版本控制的接口, 我们就可以通过Visitor来提供各种具体版本控制的实现。还有比如一些音频/视频解码器plugin的实现等等,用Visitor也很自然。
以前使用Ice的时候阅读Ice的Slice Parser源代码,大量使用了Visitor模式用来从一个统一的IDL解析器中扩展生成面向各种语言的Mapping, 每个具体的语言都有一组Visitors, 每个语法成为都有一个对应的Visitor接口, 比如ClassVisitor, InterfaceVisitor, 对应于Java的代码生成会有对应的JavaClassVisitor, JavaInterfaceVisitor。 整体代码阅读起来的确非常清晰, 更大的好处还有可以非常方便地扩展出来对新的语言的支持。 单分派做(双)多分派......使调用和被调用者解耦
另一个好处是使一方类结构稳定..... MethodBVisitor中methodB的逻辑如果不能完全由ConcreteClass已有的public的method组合而来,则必然有部分应该属于ConcreteClass的逻辑写到了methodB中。
理想的情况是
private void methodB(ConcreteClass c)
{
//call public method in ConcreteClass, here is the new logic!
c.method1();
c.method2();
... ...
c.methodN();
}
但是如果可以这样的话,引入visitor意义就不大了,client中一样可以做到,因为新的逻辑只是已有逻辑的组合。
现实的情况往往是
private void methodB(ConcreteClass c)
{
//call public method in ConcreteClass, here is the new logic!
c.method1();
method1(c); // ignore private implementation
c.method2();
method2(c); // ignore private implementation
... ...
c.methodN();
methodN(c); // ignore private implementation
}
这样的话,原本应该面向对象地实现到ConcreteClass中的逻辑被面向过程地实现到了MethodBVisitor的方法中。
如果我说的是事实,那么这种使用仅仅是权宜之计而已。以后有机会还是要重构这些失散的逻辑,使之回到ConcreteClass。
不仅如此,配合这个模式,单元测试覆盖到的methodB以及其依赖的mehthod1到N也将在以后的重构中被修改,而且要增加新的单元测试案例覆盖那些刚回家的逻辑。
只有这样,代码才不会因年久失修而腐烂。
如果可以预见上述的工作未必能够完成,我建议还是直接解冻代码做修改,或者推迟这个需求到下一个release。 2楼的例子更像是策略,而非访问者。 原则上说,3楼说的非常正确
但是不适合评价本例
从这部分代码可以看出
ConcreteClass c=new ConcreteClass ();
c.methodA();
List<Visitor> vList=XMLConfigFactory.load(VISITOR_CLASS_LIST);
for(Visitor v:vList) c.accept(v);
这个模式是通过外部的扩展使新的需求通过松耦的方式实现,原有的紧耦还是存在的。 原帖由 yuba 于 30-4-2009 13:06 发表 http://www.freeoz.org/forum/images/common/back.gif
MethodBVisitor中methodB的逻辑如果不能完全由ConcreteClass已有的public的method组合而来,则必然有部分应该属于ConcreteClass的逻辑写到了methodB中。
理想的情况是
private void methodB(ConcreteClass c)
{ ...
Agree! 这回仔细看了,呵呵......
而且还有个问题, 如果仅仅是methodB 引用ConcreteClass c那还好, 如果不仅如此,就不好玩了.... 原帖由 yuba 于 30-4-2009 13:14 发表 http://www.freeoz.org/forum/images/common/back.gif
原则上说,3楼说的非常正确
但是不适合评价本例
从这部分代码可以看出
ConcreteClass c=new ConcreteClass ();
c.methodA();
List vList=XMLConfigFactory.load(VISITOR_CLASS_LIST);
for(Visitor v:vList ...
赫赫,谢谢。
我觉得这个例子不适合做VISITOR,没有解到耦,只是一个权宜之计, 长远看, 会把代码搞乱...., 所以这么一说....:-) 逻辑分散。vistor 权力过大,不过怎么说呢,管用就行。现在还有什么面向方向编程,动态语言也有点流行,在变动的世界中,管用的就是好办法。
补充一点,个人以为除非应用是那种plugin的架构, 如果只是CR,直接改代码,增加个新方法,看起来是个傻办法实际上最简单。除非不得已不要弄那么复杂,逻辑还分散,以后没法查。
[ 本帖最后由 black_zerg 于 30-4-2009 14:08 编辑 ]
回复 #4 yuba 的帖子
"如果要让Visitor能充分的实现新的功能,需要ConcreteClass把所有Visitor可能需要用到的method和property(gettter, setter)都设成public或者friendly的"这句话已经表明,新的Visitor能够在完全利用ConcreteClass现存方法和属性的基础上,扩展新的功能,而这些扩展的功能,完全可以脱离ConcreteClass而存在,和ConcreteClass没有什么耦合的关系(ConcreteClass的所有方法所有属性Visitor都能访问),并且不局限于ConcreteClass的现有方法的组合。如果ConcreteClass实现的得当,需求只增加而不变化,这个类可以永远的冻结起来。
Visitor在这里应用的一个重要前提是:需求只增加而不变化。
“这样的话,原本应该面向对象地实现到ConcreteClass中的逻辑被面向过程地实现到了MethodBVisitor的方法中。
如果我说的是事实,那么这种使用仅仅是权宜之计而已。以后有机会还是要重构这些失散的逻辑,使之回到ConcreteClass。”
如果如你所说的这样,“原本应该面向对象地实现到ConcreteClass中的逻辑”,这个就不符合我在上面提到的背景,举个例子,你有一部手机,你现在想增加一个GPS功能,你可以把手机拆了,装个GPS芯片进去,你也可以用一根线或者蓝牙,连一个GPS模块,假设你对拆手机装手机没把握的话,是不是后面的方案更好呢? 原帖由 black_zerg 于 30-4-2009 13:56 发表 http://www.freeoz.org/forum/images/common/back.gif
逻辑分散。vistor 权力过大,不过怎么说呢,管用就行。现在还有什么面向方向编程,动态语言也有点流行,在变动的世界中,管用的就是好办法。
vistor 权力过大
~~~~~~~~~~~~~
Yes....
面向方向编程/ AspectJ是个有趣的话题,呵呵.... 原帖由 hoopoos 于 30-4-2009 14:09 发表 http://www.freeoz.org/forum/images/common/back.gif
"如果要让Visitor能充分的实现新的功能,需要ConcreteClass把所有Visitor可能需要用到的method和property(gettter, setter)都设成public或者friendly的"
这句话已经表明,新的Visitor能够在完全利用ConcreteClas ...
关键是你是"手机"设计者,而非使用者.... 原帖由 black_zerg 于 30-4-2009 13:56 发表 http://www.freeoz.org/forum/images/common/back.gif
逻辑分散。vistor 权力过大,不过怎么说呢,管用就行。现在还有什么面向方向编程,动态语言也有点流行,在变动的世界中,管用的就是好办法。
补充一点,个人以为除非应用是那种plugin的架构, 如果只是CR,直接改代 ...
如果你有5000个类,以及相应5000个test case,直接修改代码提交,意味着5000个类要重新编译,5000个test case要重新运行,application 重新deploy, server 重启,ant可能会运行几个小时。
如果你只是增加几个类,你可以把这几个class编译出来的.class更新到jar里面,只运行几个test case,也许10分钟,你的server已经更新了,带着新的功能。
假如你是在一个小team里面做一个小项目,那么别管什么模式框架了,管用就行。但是如果你在一个上百人的team里做一个周期长达几年的产品,你必须为很多事情考虑,这也是最需要设计模式的场合。
再着重说明一下,Visitor模式适用于,以新的附加功能,扩展已经测试封装过的模块。如果新的功能是本来就属于旧模块的核心功能,只能说明最初的需求和设计是失败的,不完整的。仍然是上面那个手机的例子,如果要给一个手机增加一个发短信的功能,能用Visitor么?这本来就是手机的基本功能! The OpenClosedPrinciple (OCP): "A reusable class should be open for extension, but closed for modification."
Design Pattern体现了OO Design Principles,但是很多时候开发者不能接受,因为他们觉得小题大做,我建议不论是不是接受Design Pattern,在自己的设计里面体现Design Principles还是很重要的,你可以有自己的模式,但是你最好能尊重这些Principles,有一天你加入一个高水平的大型团队,你就体会到尊重这些Principles给你和你的团队带来的收益。 LZ挺defensive的.....这样做技术就很无趣了.......
完.......... 不明白为什么要重编译5000个类。你的新逻辑全放一个class里也没问题,只不过在切入的时候,直接修改原先的代码,切入这个新类。这就是基于一个change request.需要做的事情.
如果用这个vistor的模式来做需求变化,因为你的主类不知道那些将来会变,只能暴露很多内部方法给vistor,逻辑就很分散。越是大的项目,这么做的后果就越严重,因为你根本不知道那个vistor,谁做的vistor会破坏你的逻辑甚至整体的逻辑。
如果你的应用程序本身要求就是可扩充的,设计成plugin-in的架构,考虑到了逻辑暴露的问题,那就用这个也无妨。
关于test case,本身我对这个不怎么有爱,但是理论上每次新版本,你就是必须全部跑,没什么好折中,特别vistor这种,感觉有逻辑侵入风险,更必须全跑。
所以我的结论和你是相反的,就需求变化来说, 这vistitor模式比较适合快速开发。优点是一次更新的逻辑比较集中。但长期看会搅乱整个程序逻辑。做一次变动就造几个vistor,而且得确定这样能适合所有的变动需求,并不是那么有意义。
总之我觉得这个东西适合于插件系统,不适合用来做CR。理由就是cr的可能涉及面很广,而且可能次数很多,你用这个vistor以后根本维护不过来,最后代码逻辑很可能七零八落。
[ 本帖最后由 black_zerg 于 30-4-2009 14:54 编辑 ] 原帖由 sliuhao 于 30-4-2009 14:42 发表 http://www.freeoz.org/forum/images/common/back.gif
LZ挺defensive的.....这样做技术就很无趣了.......
完..........
呵呵,用a matter of fact说话,也是defensive么?如果你不同意我的看法,可以用一些事实来支持。我尽量清楚的表述前因后果,场景场合,以免误解。
做技术么,就是很客观的事情,如果我要defend我的观点,我必须用事实说话,"A reusable class should be open for extension, but closed for modification." ,why? 因为实践出真知。 基本上,LZ对Visitor是怎么回事解释的很清楚,但是举的例子不是体现Visitor优势的好例子。LS几位不需用放大镜盯着例子看,那样就更没趣了:P
tips:
讨论问题注意风度,避免文人相轻的坏习气 原帖由 black_zerg 于 30-4-2009 14:48 发表 http://www.freeoz.org/forum/images/common/back.gif
总之我觉得这个东西适合于插件系统,不适合用来做CR
说到点子上了,LZ的原理解释方面可以打8分,例子只能打3分,因为有误导之嫌:P 原帖由 black_zerg 于 30-4-2009 14:48 发表 http://www.freeoz.org/forum/images/common/back.gif
不明白为什么要重编译5000个类。你的新逻辑全放一个class里也没问题,只不过在切入的时候,直接修改原先的代码,切入这个新类。这就是基于一个change request.需要做的事情.
如果用这个vistor的模式来做需求变化, ...
一般来说,在大的team里面,做编译build的,是单独的一个人,他可能根本不知道你改了什么,他只知道,你改的这个东西,别的地方有调用,他不看代码的,所以他只能把所有的类全部编译测试。如果你用visitor,增加一个新的类,这个类只调用那个现存的测试封装过的类,我可以不可以假设,要编译测试的,只是新的这个visitor呢?因为无论这个visitor做了什么,它都不会影响现有的类,它就算全是bug,甚至编译都通不过,也只是它一个类的问题。
另外,visitor毕竟只是客人,他不能代替主人的事情,把主人该做的事情放到客人那里,然后责怪这个visitor模式造成逻辑分散,这合理么?
总的来说,模式合理的运用,是基于对场合的正确理解和分析,不分场合用模式,然后把自己陷于一个两难的境地,是不可取的,如果因为这个原因低估模式的价值,那更不取。 原帖由 yuba 于 30-4-2009 13:09 发表 http://www.freeoz.org/forum/images/common/back.gif
2楼的例子更像是策略,而非访问者。
Visitor是双分派,Strategy是单分派。Visitor从概念的范围讲可以涵盖Strategy,同一类Visitor接口的不同实现可以采用Strategy。 原帖由 coredump 于 30-4-2009 14:57 发表 http://www.freeoz.org/forum/images/common/back.gif
说到点子上了,LZ的原理解释方面可以打8分,例子只能打3分,因为有误导之嫌:P
我用了两个例子,手机GPS模块和主人请客人来帮忙打扫,不知道为什么会有误导之嫌? 请指正。 如果我没记错,ant根本不会编译你没改过的。
测试的话,要么别跑,要跑就得全跑。
打包的话都是做ant或者mvn脚本,跟人也没关系,谁打包,什么时候打包都一样逻辑啊。
[ 本帖最后由 black_zerg 于 30-4-2009 15:07 编辑 ] 原帖由 hoopoos 于 30-4-2009 15:05 发表 http://www.freeoz.org/forum/images/common/back.gif
我用了两个例子,手机GPS模块和主人请客人来帮忙打扫,不知道为什么会有误导之嫌? 请指正。
说例子不恰当是指你1楼的例子,后面的例子还是很恰当的。
只是新的这个visitor呢?因为无论这个visitor做了什么,它都不会影响现有的类,它就算全是bug,甚至编译都通不过,也只是它一个类的问题。
当这样的类累计起来后就不是一个类的问题了。 你强调的是一次的行为, black_zerg关注的是这种的行为的累积后果。 原帖由 coredump 于 30-4-2009 12:04 发表 http://www.freeoz.org/forum/images/common/back.gif
LZ举的例子很清晰易懂
原帖由 coredump 于 30-4-2009 14:57 发表 http://www.freeoz.org/forum/images/common/back.gif
例子只能打3分,因为有误导之嫌:P
一针见血
我就是因为很清晰易懂才被误导的 “假设我们已经有一个成熟的version或者milestone,这个版本通过了全部的unit test,qa test, cvs或者svn上已经tag过并且冻结,这时候,REQ提出一个新的需求,要求DEV增加,”
我想我已经把场合说的很清楚了
The OpenClosedPrinciple (OCP): "A reusable class should be open for extension, but closed for modification."
这个原则极致化的结果就是,不论是需求增加还是需求修改,永远只增加类,不会修改类,逻辑分散是一件坏事情么?Head First In Design Pattern里的第一个例子, 一个Duck类,飞的逻辑也分出去了,叫的逻辑也分出去了
这个讨论很好,大家都理解怎么回事的,不过个人的工作领域不一样,不一定能产生共鸣的。 这个原则极致化的结果就是,不论是需求增加还是需求修改,永远只增加类,不会修改类
这个极至化很傻 原帖由 coredump 于 30-4-2009 15:35 发表 http://www.freeoz.org/forum/images/common/back.gif
这个极至化很傻
目前如果有人想做到这个极致化,确实很傻,但是,The OpenClosedPrinciple (OCP): "A reusable class should be open for extension, but closed for modification." (我已经记不清这是第几次引用这个了),能够在OO的领域提出这样的一个原则,说明这个极致化,是符合OO精神的,代表着更高质量的代码。无论是修改还是增加新功能,只增加新的类,不修改旧的类,旧的类一旦单元测试通过就永久冻结,新增加的类测试通过后,也冻结,这样增量的开发模式,使得推进的步伐更坚定,软件的质量更可靠。当然,极致化的前提是,架构的水平足够高,设计的水平足够高,需求分析的水平足够高。 一直以为主人只是不在家
是等他回来呢好呢?还是找有钥匙的朋友好呢?
现在才知道,主人不是不在家,是不在人世了