瞅瞅我的新 Git 课! 嘿!瞅瞅我的新 Git 课! 嘿!GitByBit.com 上有我的新 Git 课,瞅瞅! 嘿!想来点 Git 的爽复习?GitByBit.com 上我的新课,瞅瞅!

访问者和双分派

让我们看看下面几何图形类的层次结构 注意伪代码

interface Graphic is
    method draw()

class Shape implements Graphic is
    field id
    method draw()
    // ……

class Dot extends Shape is
    field x, y
    method draw()
    // ……

class Circle extends Dot is
    field radius
    method draw()
    // ……

class Rectangle extends Shape is
    field width, height
    method draw()
    // ……

class CompoundGraphic implements Graphic is
    field children: array of Graphic
    method draw()
    // ……

这些代码运行正常且程序处于开发阶段 但某天你决定开发导出功能 如果将导出功能的代码放入这些类中 它们看上去会很奇怪 因此 你决定不在层次结构里的类中添加导出功能 而是在层次结构外创建一个包含所有导出逻辑的新类 该类将包含将每个对象的公有状态导出为 XML 字符串的方法

class Exporter is
    method export(s: Shape) is
        print("导出形状")
    method export(d: Dot)
        print("导出点")
    method export(c: Circle)
        print("导出圆形")
    method export(r: Rectangle)
        print("导出矩形")
    method export(cs: CompoundGraphic)
        print("导出组合图形")

这些代码看上去不错 让我们运行试试

class App() is
    method export(shape: Shape) is
        Exporter exporter = new Exporter()
        exporter.export(shape);

app.export(new Circle());
// 不幸的是,这里将输出“导出形状”。

等等 为什么

像编译器一样思考

注意 下面的内容对于绝大多数面向对象编程的现代语言 Java C# 和 PHP 等 来说都是成立的

后期/动态绑定

假设你是一个编译器 你必须决定如何编译下面的代码

method drawShape(shape: Shape) is
    shape.draw();

让我们看看 Shape形状类中定义了  draw绘制方法 稍等 还有四个子类重写了该方法 我们能否有把握地决定调用哪个实现呢 看上去不太可能 确认的唯一方式是启动程序并检查传递给该方法的对象所属的类 我们只知道一件事情 该对象将包含 draw方法的实现

因此 最终的机器代码将检查 s参数的类并且从合适的类中选择 draw方法的实现

这种动态类型检查被称为后期 或动态 绑定

  • 后期 是因为我们在编译后和运行时才将对象及其实现链接起来
  • 动态 是因为每个新对象都可能需要链接到不同的实现

前期/静态绑定

现在 让我们来 编译 以下代码

method exportShape(shape: Shape) is
    Exporter exporter = new Exporter()
    exporter.export(shape);

第二行代码很清楚 Exporter类没有构造方法 因此我们仅能将对象初始化 那么对 export导出方法的调用呢 Exporter有五个同名但参数不同的方法 调用哪一个呢 看来我们在这里也需要动态绑定

但还有另一个问题 如果 导出器类中有一个图形类没有相应的 export方法怎么办 例如 一个 Ellipse椭圆对象 编译器不能确保存在适当的与重写后的方法相对应的重载方法 编译器无法应对这种模凌两可的情况

因此 编译器开发者会选择安全的方式 使用前期 或静态 绑定来处理重载方法

  • 前期 是因为它发生在运行程序前编译的时候
  • 静态 是因为它无法在运行时更改

让我们回到之前的示例 我们可以确定传递过来的参数类型属于 Shape类层次结构中 要么是 Shape 要么是它的子类 我们还知道 Exporter类包含支持 Shape类的导出功能基础实现 export(s: Shape) 

这是唯一能够安全链接当前代码而不会造成模凌两可情形的实现 因此尽管我们将 Rectangle对象传递给了 export­Shape  导出类仍将调用 export(s: Shape)方法

双分派

双分派是一个允许在重载时使用动态绑定的技巧 下面是其实现方式

class Visitor is
    method visit(s: Shape) is
        print("访问形状")
    method visit(d: Dot)
        print("访问点")

interface Graphic is
    method accept(v: Visitor)

class Shape implements Graphic is
    method accept(v: Visitor)
        // 编译器明确知道 `this` 的类型是 `Shape`。
        // 因此可以安全地调用 `visit(s: Shape)`。
        v.visit(this)

class Dot extends Shape is
    method accept(v: Visitor)
        // 编译器明确知道 `this` 的类型是 `Dot`。
        // 因此可以安全地调用 `visit(s: Dot)`。
        v.visit(this)


Visitor v = new Visitor();
Graphic g = new Dot();

// `accept` 方法是重写而不是重载的。编译器可以进行动态绑定。
// 因此在对象调用某个方法时,将执行其所属类中的 `accept`
// 方法(在本例中是 `Dot` 类)。
g.accept(v);

// 输出:"访问点"

后记

尽管访问者模式基于双分派的原则创建 但这并不是其主要目的 访问者的目的是让你能为整个类层次结构添加 外部 操作 而无需修改这些类的已有代码