虽然看起来有些复杂,但其目的很常见。
本质源于面向对象编程中,对抽象的权衡取舍,或者说一种优雅的平衡方式。
我们先理解这样一种直观:写代码的时候,为了减少重复,我们主体代码通常希望使用高层接口;但为了支持灵活性,底层实现需要多种实现。
那么,如何在高层接口的基础上,访问到具体底层对象的方法呢?
当然,可以 instanceof + cast
,但太不优雅。
访问者模式就从这里介入。
我们引入一个更具体的例子。
假设我们要考虑一个树结构,其中每个节点可由不同的内存布局,或者说有不同的物理实现方式,甚至可能某个逻辑节点时由多个对象构成的——那么显然我们应该将逻辑节点抽象为接口,不妨将其设计为 INode
。我们假设逻辑节点之间的映射是通过字符串实现的,那么至少有两个接口如下
public interface INode {
public INode getChild(String key);
public void addChild(String key, INode child);
}
然而,为了实现灵活性,有子接口分别以 byte[]
和 byte
作为映射子节点的参数。
public interface INodeA implements INode {
public INode getChildA(byte[] k);
}
public interface INodeB implements INode {
public INode getChildB(byte k);
}
在树结构的主体代码中,我们显然希望将 INode
作为主要引用,如
public long searchOnTree(String[] keys) {
// do something about INode
INode cur = root;
while(!cur.isLeaf()) {
...
}
}
但是,对某些情况下,我们需要对 INode 引用的对象,调用 getChildA/getChildB
。
如前文,显然可以 instanceof + cast
,但太不优雅。
因为这里只考虑 INode 这个接口的多样性,那么我们可以假设某个静态实例 VISITOR
作为访问者。
设计如下接口
public interface INode {
public INode accept(Visitor v);
}
考虑两个实现不同接口的类有
public class Node implements INode {
public INode accept(Visitor v) {
// call getChild(String);
}
}
public class NodeA implements INodeA {
public Inode accept(Visitor v){
// call getChildA(byte[]);
}
}
public class NodeB implements INodeB {
public INode accept(Visitor v) {
// call getChildB(byte);
}
}
通过 accept
的动态绑定,可以从 INode
的 accept
接口出发,最终调用多样化的实现方法(getChild/getChildA/getChildB
)。
事实上, 对于完整的访问者模式,visitor
也可以实现多态——在执行 visitor.visit([INode|INodeA|INodeB])
时,根据不同的 visitor 对象类型来实现不同的逻辑。
小结,这种做法可以:
- 在主体代码中围绕高层引用编程,减少代码重复;
- 为不同的实现类型,执行不同的方法,即使这些方法并没有在高层接口中暴露出来;
- 实现上述基础的情况下,避免使用
instanceof + cast
命令。
一句话:其实就是“包了一层”,用方法动态绑定代替了类型判断、转换。
事实上,还有一种方法可以替换上述做法,可以对比加深理解。
即,将各子类方法并集到公共接口中,同时,为各个子类方法实现 default 直接抛出 UnsupportedOperation 即有
public interface INode {
public INode getChild(String key);
default public INode getChildA(byte[] key) {throw new UnsupportedOperation();}
default public INode getChildB(byte key) {throw new UnsupportedOperation();}
}
仅在对应类中对 getChildA/getChildB
做覆盖 override,在主体代码中直接用公共接口的引用调用对应方法即可。
通过代码逻辑语义避免抛出异常。