https://en.wikipedia.org/wiki/Visitor_pattern

下面的链接是我见过比较好地讲解Visitor模式的文章, 摘录于此, 但调整了结构, 并做了一些补充.
https://manski.net/2013/05/the-visitor-pattern-explained/
double dispatch
visitor模式主要用来解决”double dispatch”问题.
大多数面向对象语言都支持single dispatch, 即virtual methods1
2
3
4
5
6public class SpaceShip {
public virtual string GetShipType() { return "SpaceShip"; }
}
public class ApolloSpacecraft : SpaceShip {
public override string GetShipType() { return "ApolloSpacecraft"; } }
执行下列代码:1
2SpaceShip ship = new ApolloSpacecraft();
Console.WriteLine(ship.GetShipType());
输出ApolloSpacecraft
在运行时, 具体调用哪一个GetShipType取决于ship这一个对象的真实类型. 所以叫做single dispatch. 类推一下, double dispatch可以理解为某个方法的调用取决于两个对象的真实类型. 下面的示例代码演示了double dispatch 问题1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class Asteroid {
public virtual void CollideWith(SpaceShip ship) {
Console.WriteLine("Asteroid hit a SpaceShip");
}
public virtual void CollideWith(ApolloSpacecraft ship) {
Console.WriteLine("Asteroid hit an ApolloSpacecraft");
}
};
public class ExplodingAsteroid : Asteroid {
public override void CollideWith(SpaceShip ship) {
Console.WriteLine("ExplodingAsteroid hit a SpaceShip");
}
public override void CollideWith(ApolloSpacecraft ship) {
Console.WriteLine("ExplodingAsteroid hit an ApolloSpacecraft");
}
};
执行下列代码1
2
3
4
5
6
7
8
9Asteroid theAsteroid = new Asteroid();
ExplodingAsteroid theExplodingAsteroid = new ExplodingAsteroid();
SpaceShip theSpaceShip = new SpaceShip();
ApolloSpacecraft theApolloSpacecraft = new ApolloSpacecraft();
theAsteroid.CollideWith(theSpaceShip);
theAsteroid.CollideWith(theApolloSpacecraft);
theExplodingAsteroid.CollideWith(theSpaceShip);
theExplodingAsteroid.CollideWith(theApolloSpacecraft);
输出:
Asteroid hit a SpaceShipAsteroid hit an ApolloSpacecraftExplodingAsteroid hit a SpaceShipExplodingAsteroid hit an ApolloSpacecraft
没有问题
但如果是1
2
3// Note the different data types!
Asteroid theExplodingAsteroidRef = new ExplodingAsteroid();
SpaceShip theApolloSpacecraftRef = new ApolloSpacecraft(); theExplodingAsteroidRef.CollideWith(theApolloSpacecraftRef);
期望的输出是ExplodingAsteroid hit an ApolloSpacecraft
但实际的输出是ExplodingAsteroid hit a SpaceShip
这是因为调用哪一个CollideWith方法, 只是基于ExplodingAsteroidRef的类型, 而不是基于ExplodingAsteroidRef和 theApolloSpacecraftRef的类型(double dispatch).
More Effective C++ item 31对于这个例子有更详细的讲解.
解决这个问题的经典方式是visitor模式, 另外C# 4.0引入了dynamic关键字, 能更简便地解决这个问题, 参考C# in a nutshell
visitor 模式
wiki: In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existent object structures without modifying the structures. It is one way to follow the open/closed principle.
In essence, the visitor allows adding new virtual functions to a family of classes, without modifying the classes. Instead, a visitor class is created that implements all of the appropriate specializations of the virtual function. The visitor takes the instance reference as input, and implements the goal through double dispatch.
简单的讲visitor模式将对象和操作对象的方法分开. 作者用一个文本解析器为例, 文本解析器可以将文本输出为html, LaTeX或其他的格式, 文本Document 包含以下几个部分:
1 | public abstract class DocumentPart { |
先用single dispatch(virtual methods), 看看会出现什么问题
将文本输出为HTML格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public abstract class DocumentPart {
public string Text { get; private set; }
public abstract string ToHTML();
}
public class PlainText : DocumentPart {
public override string ToHTML() { return this.Text; }
}
public class BoldText : DocumentPart {
public override string ToHTML() { return "<b>" + this.Text + "</b>"; }
}
public class Hyperlink : DocumentPart {
public string Url { get; private set; }
public override string ToHTML() { return "<a href=\"" + this.Url + "\">" + this.Text + "</a>"; }
}
public class Document {
private List<DocumentPart> m_parts;
public string ToHTML() {
string output = "";
foreach (DocumentPart part in this.m_parts) {
output += part.ToHTML();
}
return output;
}
}将文本输出为LaTeX格式和纯文本格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46public abstract class DocumentPart {
public string Text { get; private set; }
public abstract string ToHTML();
public abstract string ToPlainText();
public abstract string ToLatex();
}
public class PlainText : DocumentPart {
public override string ToHTML() { return this.Text; }
public override string ToPlainText() { return this.Text; }
public override string ToLatex() { return this.Text; }
}
public class BoldText : DocumentPart {
public override string ToHTML() { return "<b>" + this.Text + "</b>"; }
public override string ToPlainText() { return "**" + this.Text + "**"; }
public override string ToLatex() { return "\\textbf{" + this.Text + "}"; }
}
public class Hyperlink : DocumentPart {
public string Url { get; private set; }
public override string ToHTML() { return "<a href=\"" + this.Url + "\">" + this.Text + "</a>"; }
public override string ToPlainText() { return this.Text + " [" + this.Url + "]"; }
public override string ToLatex() { return "\\href{" + this.Url + "}{" + this.Text + "}"; }
}
public class Document {
private List<DocumentPart> m_parts;
public string ToHTML() {
string output = "";
foreach (DocumentPart part in this.m_parts) {
output += part.ToHTML();
}
return output;
}
public string ToPlainText() {
string output = "";
foreach (DocumentPart part in this.m_parts) {
output += part.ToPlainText();
}
return output;
}
public string ToLatex() {
string output = "";
foreach (DocumentPart part in this.m_parts) {
output += part.ToLatex();
}
return output;
}
}
这种实现方式将DocumentPart以及操作DocumentPart的方法捆绑在一起, 带来了两个问题:
- 如果需要添加新的格式, 需要修改DocumentPart以及它的所有子类
- 每个DocumentPart都需要知晓格式的细节
而visitor模式是这样处理该问题的.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49public interface IVisitor {
void Visit(PlainText docPart);
void Visit(BoldText docPart);
void Visit(Hyperlink docPart);
}
public class HtmlVisitor : IVisitor {
public string Output { get { return this.m_output; } }
private string m_output = "";
public void Visit(PlainText docPart) { this.Output += docPart.Text; }
public void Visit(BoldText docPart) { this.m_output += "<b>" + docPart.Text + "</b>"; }
public void Visit(Hyperlink docPart) { this.m_output += "<a href=\"" + docPart.Url + "\">" + docPart.Text + "</a>"; }
}
public class LatexVisitor : IVisitor {
public string Output { get { return this.m_output; } }
private string m_output = "";
public void Visit(PlainText docPart) { this.Output += docPart.Text; }
public void Visit(BoldText docPart) { this.m_output += "\\textbf{" + docPart.Text + "}"; }
public void Visit(Hyperlink docPart) { this.m_output += "\\href{" + docPart.Url + "}{" + docPart.Text + "}"; }
}
public abstract class DocumentPart {
public string Text { get; private set; }
public abstract void Accept(IVisitor visitor);
}
public class PlainText : DocumentPart {
public override void Accept(IVisitor visitor) {
visitor.Visit(this);
}
}
public class BoldText : DocumentPart {
public override void Accept(IVisitor visitor) {
visitor.Visit(this);
}
}
public class Hyperlink : DocumentPart {
public string Url { get; private set; }
public override void Accept(IVisitor visitor) {
visitor.Visit(this);
}
}
public class Document {
private List<DocumentPart> m_parts;
public void Accept(IVisitor visitor) {
foreach (DocumentPart part in this.m_parts) {
part.Accept(visitor);
}
}
}
具体输出哪种格式可以这样做1
2
3
4Document doc = ...;
HtmlVisitor visitor = new HtmlVisitor();
doc.Accept(visitor);
Console.WriteLine("Html:\n" + visitor.Output);
如此, DocumentPart和具体的格式是解耦的.
为什么说visitor 模式可以解决double dispatch问题? 看一下Document的Accept方法1
2
3
4
5public void Accept(IVisitor visitor) {
foreach (DocumentPart part in this.m_parts) {
part.Accept(visitor);
}
}
part.Accept(visitor), 具体调用哪一个Accept方法, 由part的真实类型决定. 这是single dispatch(override)
Accept方法内部是visitor.Visit(this), 具体调用哪一个Visit方法由this来决定, 这是overload















