我知道这是一个经常被人涉足的领域,但我有一个具体的问题......我保证。
我在静态类型、面向对象的世界中花了很少的时间,最近在阅读 制作解释器。虽然我理解这种模式允许在一组定义良好的现有类型(类)上进行可扩展的行为(方法),但我不太明白它的特征双重调度问题的解决方案,至少在没有一些额外假设的情况下。我认为它更多地是对 表达式问题 的权衡,即用封闭类型换取开放类型方法。
在我见过的大多数例子中,你最终会得到这样的结果(无耻地从很棒的 Clojure 设计模式)
public interface Visitor {
void visit(Activity a);
void visit(Message m);
}
public class PDFVisitor implements Visitor {
@Override
public void visit(Activity a) {
PDFExporter.export(a);
}
@Override
public void visit(Message m) {
PDFExporter.export(m);
}
}
public abstract class Item {
abstract void accept(Visitor v);
}
class Message extends Item {
@Override
void accept(Visitor v) {
v.visit(this);
}
}
class Activity extends Item {
@Override
void accept(Visitor v) {
v.visit(this);
}
}
Item i = new Message();
Visitor v = new PDFVisitor();
i.accept(v);
这里我们有一组可能是关闭的或不经常更改的类型(消息和活动),以及一组我们想要的方法开放延期(访客)。现在让我感到困惑的是,在大多数示例中,他们将展示如何在不接触现有类的情况下实现其他访问者,例如这样的东西:
public class XMLVisitor implements Visitor {
@Override
public void visit(Activity a) {
XMLExporter.export(a);
}
@Override
public void visit(Message m) {
XMLExporter.export(m);
}
}
然后手动暗示这是“双重调度”,但它不是。
这里 accept
动态地分派 Item
的子类型,但在 accept
中,visit
方法静态分派给传入的通过方法重载的访问者。因此,我们对 Item
进行单一调度,然后 accept
中的“第二个”静态调度实际上是选择一个行为(方法)来调用该 Item< /代码> 类型。只有一种“类型”被调度,而不是两种 - 第二种是行为。
当我想到双重分派时,我想到的是根据两个参数的类型分派的函数。一种行为,两种类型。
export(Activity,XML)
export(Activity,PDF)
export(Message,XML)
export(Message,PDF)
对我来说,这与访问者模式略有不同,访问者模式允许将任何行为集扩展到现有类,但这些行为不一定都代表相同的行为,如上面的四个 export
示例 -它们可以是任何东西。如果我们添加另一个访客,它可能代表导出,但也可能不代表导出。从 API 层,您只需调用 accept
方法并相信传入的 Visitor 会执行您想要的操作,无论是什么。
我是否以错误的方式看待这个问题?
I know this is well trodden territory but I have a specific question... I promise.
Having spent very little time in the statically typed, object oriented world, I recently came across this design pattern while reading Crafting Interpreters. While I understand this pattern allows for extensible behavior (methods) on a set of well defined existing types (classes), I don't quite get the characterization of it as a solution to the double dispatch problem, at least not without some additional assumptions. I see it more as making a tradeoff to the expression problem, where you trade closed types for open methods.
In most of the examples I've seen, you end up with something like this (shamelessly stolen from the awesome Clojure Design Patterns)
public interface Visitor {
void visit(Activity a);
void visit(Message m);
}
public class PDFVisitor implements Visitor {
@Override
public void visit(Activity a) {
PDFExporter.export(a);
}
@Override
public void visit(Message m) {
PDFExporter.export(m);
}
}
public abstract class Item {
abstract void accept(Visitor v);
}
class Message extends Item {
@Override
void accept(Visitor v) {
v.visit(this);
}
}
class Activity extends Item {
@Override
void accept(Visitor v) {
v.visit(this);
}
}
Item i = new Message();
Visitor v = new PDFVisitor();
i.accept(v);
Here we have a set of types (Message and Activity) which are presumably closed or infrequently changing, and a set of methods which we want to be open for extension (the Visitors). Now where I get confused is that in most examples, they will show how you can implement other visitors without touching existing classes, e.g. something like this:
public class XMLVisitor implements Visitor {
@Override
public void visit(Activity a) {
XMLExporter.export(a);
}
@Override
public void visit(Message m) {
XMLExporter.export(m);
}
}
and then make some hand-waivy allusion to this being "double dispatch", which it is not.
Here accept
dynamically dispatches on the subtype of Item
, but within accept
the visit
methods statically dispatch to the passed in visitor via method overloading. So we have single dispatch on Item
, and then the "second" static dispatch within accept
is really about selecting a behavior (method) to call with that Item
type. There is only one "type" being dispatched on, not two - the second is a behavior.
When I think of double dispatch, I think of a function that dispatches on the type of two arguments. One behavior, two types.
export(Activity,XML)
export(Activity,PDF)
export(Message,XML)
export(Message,PDF)
To me this is subtly different to the visitor pattern which allows any set of behaviors to be extended to existing classes, but those behaviors don't necessarily all represent the same behavior like in the four export
examples above - they can be anything. If we add another Visitor it may represent exporting, but it could just as well not. From the API layer you're just calling accept
methods and trusting that the passed in Visitor does what you want, whatever that may be.
Am I looking at this the wrong way?
发布评论
评论(1)
@user207421 的评论很到位。如果一种语言本身不支持双重调度,则任何设计模式都无法改变该语言以使其支持。模式仅提供一种替代方案,可以解决双分派在另一种语言中应用的一些问题。
已经了解双重调度的学习访问者模式的人可能会得到诸如“访问者解决了与双重调度解决的类似问题”之类的解释的帮助。不幸的是,这种解释常常被简化为“访问者实现双重调度”,这是不正确的。
您认识到这一点意味着您已经对这两个概念有了深入的了解。
The comment from @user207421 is spot on. If a language does not natively support double dispatch, no design pattern can alter the language to make it so. A pattern merely provides an alternative which may solve some of the problems that double dispatch would be applied to in another language.
People learning the Visitor Pattern who already have an understanding of double dispatch may be assisted by explanations such as, "Visitor solves a similar set of problems to those solved by double dispatch". Unfortunately, that explanation is often reduced to, "Visitor implements double dispatch" which is not true.
The fact you've recognized this means you have a solid understanding of both concepts already.