访问者模式是设计模式中相对比较复杂的设计模式,在Android源码中的UnifiedEmail项目中的HtmlDocument就使用了访问者模式,Email中的内容可以是Html文档,而Html文档需要解析保存,HtmlDocument就是html节点的一个容器,本文将介绍HtmlDocument与访问者模式。文中对访问者模式介绍的会相对简单,如果想理解好,建议去看一下Gof的描述,更侧重于与Android源码的结合(源码基于6.0.1_r30)。原文发表在我的博客。
访问者模式
首先看一下访问者模式相关内容。
意图
表示一个作用于对象内部结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
UML图
上面就是访问者模式的简单UML图,通过定义一个外在的Visitor来访问ObjectStructor对象结构中的Element,这样可以变化访问者来访问对象结构中的元素,但又不会影响元素内部结构。
代码示例
abstract class Visitor{
public abstract void visitorElement1(ConcreteElement1 element);
public abstract void visitorElement2(ConcreteElement2 element);
}
class ConcreteVisitor1 extends Visitor{
public abstract void visitorElement1(ConcreteElement1 element){
element.operate1();
// do other things.
}
public abstract void visitorElement2(ConcreteElement2 element){
element.operate2();
// do other things.
}
}
abstract class Element{
public abstract void accept(Visitor visitor);
}
class ConcreteElement1 extends Element{
public void accept(Visitor visitor){
visitor.visitorElement1(this);
}
}
class ConcreteElement2 extends Element{
public void accept(Visitor visitor){
visitor.visitorElement2(this);
}
}
public class ObjectStructor{
List<Element> elements = new ArrayList<Element>();
elements.add(new ConcreteElement1());
elements.add(new ConcreteElement2());
public void accept(Visitor visitor){
for(Element element: elements){
element.accept(visitor);
}
}
public static void main(String [] args){
ConcreteVisitor1 visitor = new ConcreteVisitor1();
ObjectStructor stru = new ObjectStructor();
stru.accept(visitor);
}
}
上面就是一段简单的访问者模式的代码,访问者模式有几个特点:对象结构内部的元素不会变化,如果变化了,那么Visitor又得变了;对象结构内部元素有不同的接口;需要对对象内部元素有不同的不相关访问,而放在元素内部,又会”污染“元素。
UnifiedEmail 中的HtmlDocument
上面就是整个HtmlDocument的观察者UML图,它是一个非常经典的观察者模式。HtmlDocument表示html文档,里面包含了Html的组成内容,它包含了Comment(注释),Text(文本),Tag(标签),EndTag(结束标签)这些元素。
类介绍
所有的类都在UnifiedEmail项目的com.google.android.mail.common.html.parser包中。
HtmlDocument
表示Html文档,它是Html节点的容器,包含了一个List<Node> node
属性,这个属性就是包含了html的所有节点。另外它也包含了一些将html转换为String的接口:
- toHTML
- toXHTML
- toOriginalHTML
它是Node元素的容器,访问者就是直接与HtmlDocument打交道。另外HtmlDocument是一个非常大的类,Node, Comment, Text, Tag, EndTag, Visitor都是HtmlDocument的静态内部类。并且它还包含了Comment,Text,Tag的静态创建方法。
Visitor
观察者基类,它有几个实现者:
- Builder: 这个是用来创建HtmlDocument的
- HtmlTreeBuilder: 这个类是用来创建HtmlTree的,通过访问所有的节点元素,创建一个HtmlTree。
- DebugPrinter: 这个使用来调试的,只是打印了节点元素的内容。
- VisitorWrapper: 这是一个Visitor的装饰类。
通过变化访问者(或创建新的访问者)来操作HtmlDocument的节点元素,而不是直接修改HtmlDocument节点元素,这样避免了修改节点元素内部内容。
Node
基础的节点元素类,它是所有节点元素的父类,表示Html文档中的相关节点元素(标签,文本,注释等等)。在HtmlDocument中保存在一个List列表里面。
HtmlTreeBuilder
HtmlTreeBuilder是一个访问者,HtmlTreeBuilder的作用是通过访问HtmlDocument中的节点来创建HtmlTree。HtmlTree是一个表示解析好了的,有较好的格式化了的html文本的对象。它提供了方法直接将html转换为文本,并且提供了查找文本标签的方法,比HtmlDocument格式化要更好些。
在HtmlTreeBuilder中有一段对节点元素的访问测试代码:
/** For testing */
public static void main(String[] args) throws IOException {
logger.setLevel(Level.FINEST);
String html = new String(ByteStreams.toByteArray(System.in));
HtmlParser parser = new HtmlParser();
HtmlDocument doc = parser.parse(html);
HtmlTreeBuilder builder = new HtmlTreeBuilder();
doc.accept(builder);
String outputHtml = builder.getTree().getHtml();
System.out.println(outputHtml);
}
HtmlParser
在com.google.android.mail.common.html.parser包里面还有HtmlParser类,它是用来解析Html文档的,把Html文档文本解析为HtmlDocument。
设计思考
访问者模式适合结构对象中的元素不会经常变化,每个元素的接口又不会很一致,但是需要对针对每一种元素进行操作,而对这些元素又经常需要变化操作。而Html的节点就很适合这些特征,每个节点元素的接口其实是不一致的,但是他们总共也就那几种元素(Comment, Tag, EndTag,Text),而有时候又需要添加一些新的操作。
如果说元素之间的接口都是一致的话,那么可以直接使用迭代器模式,迭代器模式代码结构更加简单。如果经常需要增加新的节点类型,那么对节点操作最好直接放在节点内部,因为增加新的节点类型,意味着访问者也要增加一种新的接口。
总结
在UnifiedEmail中还有其他的使用了访问者模式的,比如org.apache.james.mime4j.field.address.parser.AddressListParserVisitor
,com.android.mail.utils.DequeMap.Visitor
。通过具体使用的源码来学习设计模式会更加有效率,当然最好是自己在实践中使用,通过设计模式来看代码结构会更加明朗,清晰,也能更好地看细节。