Java设计模式-访问者模式
访问者(Visitor)模式,在23种设计模式中属于对象行为型模式,也是Java语言模拟双分派机制的一种实现方式。
访问者模式表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
引言
在GOF定义的23中设计模式中,除去本文所介绍的一种模式之外,还剩解释器模式、迭代器模式以及原型模式。其实,本来想整理一下其它方向的内容,但是后来想想,已经整理了许多种类型设计模式了,目前还剩下这么三四种类型,不如一起梳理了,这样也可以对设计模式有一个阶段性的总结。
在23种设计模式中,行为型模式占了11种,接近一半的设计模式都是涉及对象或类的行为,在还未梳理的几种设计模式中,除原型模式是创建型模式之外,其它的模式都是对象行为型模式。迭代器模式还是比较容易理解的一种模式,但是访问者模式以及解释器模式,在行为模式中,可以说都是相对比较复杂的设计模式,而且这两种模式,在平常的应用软件开发中也不常见。
访问者模式,从模式名称可以知道就是如何定义一种方式用于访问某些元素。遍历其实就是访问的一般形式,但是如果想要差异化的访问某些类型的元素,这时候可能需要借助于多个if-else条件语句,但是借助于访问者模式,我们可以按照面向对象的方式去访问某些元素,而且还是在不用修改元素类行为的前提下。
如下是HTMLParser用于访问获取所有文本节点的代码示例:
Parser parser = Parser.createParser(html,"UTF-8"); TextExtractingVisitor visitor = new TextExtractingVisitor(); parser.visitAllNodesWith(visitor); String text=visitor.getExtractedText();
适用性
- 一个对象结构包含多种类型的对象,希望对这些对象采取一些依赖具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
- 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
- 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
Visitor模式通用示例
// 抽象元素类 public interface Element { void accept(Visitor visitor); } // 抽象访问者类 public interface Visitor { void visit(ConcreteElementA elementA); void visit(ConcreteElementB elementB); } // 具体元素类 public class ConcreteElementA implements Element { @Override public void accept(Visitor visitor) { visitor.visit(this); } } // 具体访问者类 public class ConcreteVisitor implements Visitor { @Override public void visit(ConcreteElementA elementA) { System.out.println("Visitor_"+elementA.getClass().getSimpleName()); } @Override public void visit(ConcreteElementB elementB) { System.out.println("Visitor_"+elementB.getClass().getSimpleName()); } } // 对象结构类 public class ObjectStructure { private Listelements = new ArrayList<>(); public void accept(Visitor visitor) { for (Element element : elements) { element.accept(visitor); } } public void addElement(Element element) { elements.add(element); } public void removeElement(Element element) { elements.remove(element); } } // 客户端测试类 public class Client { public static void main(String[] args) { ObjectStructure structure=new ObjectStructure(); structure.addElement(new ConcreteElementA()); structure.addElement(new ConcreteElementB()); Visitor visitor=new ConcreteVisitor(); structure.accept(visitor); } }
示例执行结果如下:
Visitor_ConcreteElementA Visitor_ConcreteElementB
仿写HTMLParser访问文本节点示例
// 抽象元素节点类 public interface Element { String getName(); void accept(Visitor visitor); } // 抽象访问者类 public interface Visitor { void visitHeader(Element header); void visitBody(Element body); void visitFooter(Element footer); } // HTML Body节点类 public class BodyElement implements Element { @Override public void accept(Visitor visitor) { visitor.visitBody(this); } @Override public String getName() { return "Body"; } } // Body节点访问者类 public class BodyVisitor extends AbstractVisitor { private StringBuffer buffer = new StringBuffer(); private int index; // 获取所有Body节点信息 public String getVisitorBody() { return buffer.toString(); } @Override public void visitBody(Element body) { buffer.append("Visitor_" + body.getName() + "_" + index + "\n"); index++; } } // 客户端测试类 public class Client { public static void main(String[] args) { ObjectStructure structure = new ObjectStructure(); structure.addElement(new HeaderElement()); structure.addElement(new BodyElement()); structure.addElement(new BodyElement()); structure.addElement(new FooterElement()); BodyVisitor visitor = new BodyVisitor(); structure.accept(visitor); System.out.println(visitor.getVisitorBody()); } }
示例执行结果如下:
Visitor_Body_0 Visitor_Body_1
模式分析
Visitor模式类图如下:
- Visitor抽象访问者:可以定义为接口或者抽象类形式,它会为每一个具体元素ConcreteElement声明一个访问操作,其实就是对应的一个visit方法,从这个方法的名称或者入参类型就可以知道所需要访问元素的具体类型,具体访问者需要实现这些方法,定义对这些元素的访问操作。
- ConcreteVisitor具体访问者:实现抽象访问者中声明的操作,每一个操作作用于访问对象结构中一种类型元素。
- Element抽象元素:可以定义为接口或者抽象类,除了元素自身方法外,它还会为Visitor提供一个accept()方法,accept()方法的入参通常为一个抽象访问者Visitor。
- ConcreteElement具体元素:具体实现Element中声明的accept()方法,在accept()方法中调用访问者的visit()方法以便完成对某个元素的操作。
- ObjectStructure对象结构:对象结构是一个元素的集合,用于存放元素对象,并且提供遍历其内部元素的方法。它可以使一个组合对象,或者是一个集合类型对象,如Set对象或者List对象。
在访问者模式中对象结构存储了不同类型的元素,以供不同的访问者访问。从类图结构也可以看出,访问者模式中包括两个层次结构:一个是访问者层次结构,提供了抽象访问者和具体访问者,另外一个是元素层次结构,提供了抽象元素和具体元素。
在访问者模式中,两个层级结构种哪种层次的变动对访问者模式的影响更大呢?元素层级结构,因为访问者层次结构中,所有的设计的访问操作的都是针对的每个具体的元素,一旦元素类型有变动,需要更新每一个访问者类。所以访问者模式常应用在元素结构类型基本不变的场景中,如HTML、XML以及Java类文件解析器中。
在访问者模式中,抽象访问者会针对每种具体元素定义一个对应的方法。一般方法的命名有两种形式:一种是方法名直接标注出访问的具体元素的名称,如visitElementA();另外一种是统一命名为visit()方法,但是入参类型不同,入参为具体的元素对象。在上文示例中刚好两种命名方式都有涉及。
访问者模式使用过程中,元素结构层次相对简单,只需要在元素类中定义一个accept(Visitor visitor)方法,然后在方法体中直接使用visitor.visit(this)即可。但是在访问者层级结构中,相比较而言复杂一些,想要访问的具体元素ConcreteElement已经通过方法的入参传入,这时候ConcreteVisitor所要执行的逻辑就跟具体业务场景强相关了。一般将从Element调用accept()方法开始到Visitor调用visit()方法的过程称为Java的双分派机制。有关双分派机制的相关内容就不在本文中扩展开来了,如果有兴趣做进一步的了解,可以直接在网上搜索相关内容。
模式优点
增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。
访问者集中相关的操作而分离了无关操作。类的职责更加清晰,有利于元素对象的复用。比如想访问所有的文本节点,我们只需要在相应的文本节点访问者类中进行操作即可,而无需关心其它节点。想访问所有的超链接节点,也只需要在超链接的访问者类中操作,由于超链接节点中也有文本节点,这样文本节点既可以应用在文本访问者中也可以应用在超链接访问者中。
模式缺点
增加新的ConcreteElement比较困难。如果需要新增一个ConcreteElement,那么需要在抽象访问者中新增一个新的访问操作,并在每一个ConcreteVisitor中实现该访问操作。
破坏了封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
与其它模式关系
在一些非线性的集合结构中,可以借助于迭代器模式迭代元素列表,然后将访问者传入,或者直接使用迭代器进行访问操作。
访问者可以用于对一个由组合模式定义的对象结构进行操作。
结束语
学习了访问者模式之后,相信很多开发者或多或少都有些疑问,就是如何可以将该设计模式应用在实际的开发中。其实在查找相关资料的过程中,我也一直都在找一个相对更具有说服力的示例,后来发现HTMLParser这个库中恰好有对该模式的应用,虽然HTMLParser库已经很久没有更新了,可是并不影响我们学习研究,特别是对Visitor模式而言绝对是一个非常典型的应用示例。
另外一个典型的应用场景就在JDK类库中,如果有接触过编译时使用注解生成代码,相信对于TypeElement这个类型并不陌生,TypeElement继承自Element接口,在Element接口中有如下方法:
/** * Applies a visitor to this element. * * @paramthe return type of the visitor's methods * @param the type of the additional parameter to the visitor's methods * @param v the visitor operating on this element * @param p additional parameter to the visitor * @return a visitor-specified result */
R accept(ElementVisitor v, P p); 然后看一下ElementVisitor的定义: public interface ElementVisitor { /** * A convenience method equivalent to {@code v.visit(e, null)}. * @param e the element to visit * @return a visitor-specified result */ R visit(Element e); /** * Visits a package element. * @param e the element to visit * @param p a visitor-specified parameter * @return a visitor-specified result */ R visitPackage(PackageElement e, P p); /** * Visits a type element. * @param e the element to visit * @param p a visitor-specified parameter * @return a visitor-specified result */ R visitType(TypeElement e, P p); //... }
示例源代码下载,提取码:w0s2