访问者模式
访问者模式(Visitor), 表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
这个模式相对比较复杂, 而又很少能被用上, 拿GOF作者的话'大多数时候你并不需要访问者模式, 但当你一旦需要它, 那就是真正的需要'
访问者模式的本质就是解数据结构和结构上的操作之间的耦合, 使操作集合可以自由的演化.
看上去很美好, 不过有个比较苛刻的条件, 就是数据结构的类层次是稳定的, 不变的, 才可以使用.
如下图, Element的子类只有ConcreteElement1和ConcreteElement2, 不会变化, 举个比较典型的例子, 人的性别, 只有男,女, 不会变的很稳定, 这样就可以使用访问者模式. 所以能满足这种条件的很少, 于是这个模式很少被用到.
场景, 为数据结构添加新的操作newfunc, 即Element添加新的成员函数newfunc()
正规的做法就是, 修改Element, ConcreteElement1, ConcreteElement2来添加newfunc. 这样做违反开闭原则, 而且当这个数据结构的操作变化频繁的时候, 就很麻烦了.
这个模式的做法, 既然操作频繁变化, 就把操作独立出来, 对于每个操作生成一个Visitor子类. 对于一个操作而言, 对ConcreteElement1, ConcreteElement2的具体实现是不同的, 所以在ConcreteVisitor1类中, 要分别为ConcreteElement1, ConcreteElement2定义不同的操作函数.
这儿就解释了为什么, Element的类层次必须稳定, 如果这儿增加一个ConcreteElement3, 那么在所有的Visitor中都必须添加相应的处理函数, 这就回到了上面的问题. 所以只有Element的类层次不变, 我们这样做才又意义.
这个模式的好处就是, 在增加操作时, 不需要改变数据结构类, 只需要定义一个新的操作子类即可, 符合开闭原则.
public abstract class Visitor //把操作从数据结构中抽象出来 { public abstract void VisitConcreteElementA(ConcreteElementA concreteElementA); public abstract void VisitConcreteElementB(ConcreteElementB concreteElementB); } public class ConcreteVisitor1 : Visitor //新增加操作, 只需要添加Visitor子类 { public override void VisitConcreteElementA(ConcreteElementA concreteElementA) { //新操作对A的实现 } public override void VisitConcreteElementB(ConcreteElementB concreteElementB) { //新操作对B的实现 } } public class ConcreteElementA : Element { //所谓的双分派技术, 即先把操作类作为参数传入, 再把this作为参数传给操作类 //把具体操作细节给屏蔽了, 无论什么操作, 代码都不用变 public override void Accept(Visitor visitor) { visitor.VisitConcreteElementA(this); } } //高层枚举类, 用来遍历执行Visitor操作 public class ObjectStructure { IList<Element> elements=new List<Element>(); public void Add(Element e) { elements.Add(e); } public void Remove(Element e) { elements.Remove(e); } public void Accept(Visitor visitor) { foreach (Element e in elements) { e.Accept(visitor); } } } //客户代码 ObjectStructure os = new ObjectStructure(); os.Add(new ConcreteElementA()); os.Add(new ConcreteElementB()); ConcreteVisitor1 cv1 = new ConcreteVisitor1(); ConcreteVisitor2 cv2 = new ConcreteVisitor2(); //对所有的element分别执行cv1和cv2两种操作 os.Accept(cv1); os.Accept(cv2);
其实访问者模式要解决的表达式问题, 参考Protocol and DataType
对于表达式问题的两个子问题,
1. 将已存在的方法扩展到新的类型, 比较容易实现, 通过继承可以实现
2. 为已存在的类型扩展新的方法,
对于这个问题, 面向对象是比较难于解决的和比较麻烦, 对于一般情况, 需要去每个类里面去添加该方法...
可以说, 访问者模式就是用于解决这个问题的, 当然是有代价的, 代价就是不支持1, 理由上面已经讲了, 除非这种操作对于所有的类的逻辑都是一样的, 但这种情况不太常见
所以, 面向对象无法完美的解决表达式问题, 就算采取访问者模式解决了子问题2, 但是代价就是牺牲了1
而可以看到, clojure就可以比较完美的解决这个问题
本文章摘自博客园,原文发布日期: 2013-08-13