访问者模式(visitor pattern)允许我们在不修改对象结构的情况下向其添加新操作。
问题提出
假设我们创建一个HTML编辑器。
HTML是由各个节点组成,比如h1, a 等。
创建接口HtmlNode用于表示各节点共同的特性。假设我们希望高亮显示各节点标记,HtmlNode接口定义了highlight()方法。
创建类HeadingNode和类AnchorNode,实现了HtmlNode接口。
接着创建HtmlDocument类,它有nodes列表代表各个节点,add(HtmlNode node)方法,以及highlight方法。在HtmlDocument的highlight方法中,遍历每一个node调用其highlight方法。
再到main方法中测试。
目前我们节点的highlight方法能正确地执行,但明天我们如果添加其他方法,比如添加转变成纯文本的方法plainText,我们就需要修改HtmlNode接口、HeadingNode类、AnchorNode类以及HtmlDocument类的实现,这就违反了开放闭合原则。
在当前的实现下,我们每次为HtmlNode引进新的功能就需要修改我们的结构中每一个类型。非常麻烦。
访问者模式就是用于解决这个问题。
解决方案
我们当前的类图是这样的。
这个设计有些问题。
首先,违反了开放封闭原则,每次我们要支持新的操作时,都必须更改此结构中的所有类型;其次,每个操作的逻辑分散在不同的类中,比如高亮操作(highlight)的实现就分散在HeadinNode类和AnchorNode类中。
为了解决这些问题,我们需要使用接口表示每个操作,我们引入了一个名为 Operation 的接口,它有两个apply方法 ,一个接受Heading对象,另一个接受Anchor对象,这种用法叫做方法重载,表示方法名称相同,但是有不同的参数。接着我们就可以创建具体的操作,例如 HighlightOperation 和 PlainTextOperation。
有了这种结构,给定操作的所有逻辑都将集中在一个地方。在我们的 highlightOperation 类中,我们将有两个apply方法,分别是高亮显示Heading对象和高亮显示Anchor对象的逻辑,因此此操作(hightlight)的所有逻辑都将集中在一个地方。
此结构的另一个好处是它遵循开放封闭原则。如果明天我们想要支持新操作,我们只需创建一个实现Operation接口的新类。
但是,使用此解决方案是有条件的,那就是我们的对象结构要稳定。
目前我们有两种节点类型:HeadingNode和AnchorNode,operation接口也就定义了两个apply方法,一个接受heading对象,另一个接受anchor对象。 如果我们的对象结构不稳定经常会有新类型,那么每次我们引入新类型时,我们都必须修改Operation接口以及实现此接口的所有类。
因此,在我们的对象结构稳定,同时又经常支持新操作,我们才应该使用这种方法。 HTML 文档是一个很好的例子,因为在 HTML 中,大约 有20 到 30 种类型的节点,一般不会增加,但是却经常需要支持新的操作,例如,我们想添加一个新操作来提取 HTML文档中的图像或链接。因为我们的对象结构相当确定,我们提前无需知道会有什么操作,在后面按需添加即可。
现在让我们看看如何将Operation接口引入我们当前的结构。
这是当前的结构。
HtmlNode 接口有两个方法,highlight 和 plainText。HeadingNode和AnchorNode是实现HtmlNode接口的两个类。
我们要摆脱这些特定的方法,用一个通用方法替换,比如叫execute,该方法接受operation对象。
HtmlNode对Operation接口有依赖关系。
每一个HtmlNode具体类比如HeadingNode、AnchorNode需要实现execute方法。你认为在execute方法里应该有什么?
execute方法接受一个Operation对象,这个Operation对象有两个apply方法,一个接受Heading对象,一个接受Anchor对象,我们传递本身this即可。
operation.apply(this)
这就是访问者模式(visitor pattern)。
你可能奇怪,为什么叫访问者模式,没有访问者啊?
查看四人帮的著作,我们就清楚了,原图是这样的。
四人帮的Visitor相当我们的Operation,visit就是apply。我们的HtmlNode就是Element,execute方法相当于他们的accept方法。
前面我们已提过多次,不一定要命名四人帮一样的名字。根据项目恰当地命名更好。
总之,访问者模式就是允许我们在不修改对象结构的情况下向其添加新操作。
代码实现
创建接口Operation,它有两个apply方法,一个接受HeadingNode对象,另一个接受AnchorNode的对象。
创建类HighlightOperation,它实现了接口Operation。
修改接口HtmlNode,去掉之前定义的highlight方法,添加execute方法。
修改HtmlNode接口的实现类AnchorNode和HeadingNode,同样删除highlight方法,实现execute方法。在execute方法里,调用operation对象的apply方法。
operation.apply(this);
再到HtmlDocument类,同样用execute方法代替highlight方法。execute方法里,遍历每个HtmlNode并调用HtmlNode的execute方法。
HtmlDocument是可以扩展的,我们可以传递新的操作给它而不用作任何修改。这就是访问者模式的好处。
最后到main方法测试。
后续我们如果需要添加一个提取文本信息的操作,可以简单添加类PlainTextOperation并实现Operation接口即可,HtmlDocument、AnchorNode、HeadingNode等类型均不用作任何修改,完美符合开放闭合原则。
再回到main测试。
小结
访问者模式允许我们在不修改对象结构的情况下向其添加新操作。
在本系列第1篇文章中,我们说过设计模式分为3个类型,分别是创造类型模式、结构类型模式以及行为类型模式,到此,我们已学习了所有行为类型的设计模式,下一篇开始学习结构类型的模式。