1 解决的问题

如何用设计模式吃自助餐? - 知乎

2 访问者模式

访问者模式是一种行为设计模式,它能将算法与其所作用的对象隔离开来。

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

3 实现代码

以汽车结构为例,里面包含了引擎,车身等,不同角色的人可以对这些结构进行不同的访问,如:

  • 司机 → 查看
  • 洗车佬 → 清洁
  • 维修佬 → 检查维修

不用访问者,代码实现一波:

public abstract class AbstractVisitor {
    protected String name;
    public AbstractVisitor(String name) { this.name = name; }
    abstract void visitorEngine();
    abstract void visitorBody();
}
 
public class DriverVisitor extends AbstractVisitor {
    public DriverVisitor(String name) { super(name); }
    @Override void visitorEngine() { System.out.println(name + " → 查看引擎"); }
    @Override void visitorBody() { System.out.println(name + " → 查看车身"); }
}
 
public class CleanerVisitor extends AbstractVisitor {
    public CleanerVisitor(String name) { super(name); }
    @Override void visitorEngine() { System.out.println(name + " → 清洗引擎"); }
    @Override void visitorBody() { System.out.println(name + " → 清洗车身"); }
}
 
public class RepairVisitor extends AbstractVisitor {
    public RepairVisitor(String name) { super(name); }
    @Override void visitorEngine() { System.out.println(name + " → 修理引擎"); }
    @Override void visitorBody() { System.out.println(name + " → 修理车身"); }
}
 
// 测试用例
public class Test {
    public static void main(String[] args) {
        List<AbstractVisitor> visitors = new ArrayList<>();
        visitors.add(new DriverVisitor("杰哥"));
        visitors.add(new CleanerVisitor("老王"));
        visitors.add(new RepairVisitor("大锤"));
        for(AbstractVisitor visitor: visitors) {
            visitor.visitorEngine();
            visitor.visitorBody();
        }
    }
}

代码运行结果如下

可以,但存在问

  • 想添加功能,如支持访问轮胎,那么所有类都要改动,违反了开闭原则;
  • 访问逻辑都耦合到访问者类中,导致它们的职责不够单一,成了大杂化;

拆分解耦,把业务操作与具体的数据结构解耦,设计成独立的类,用访问者模式对象上述代码重构:

// 抽象访问者
public abstract class AbstractVisitor {
    protected String name;
    public AbstractVisitor(String name) { this.name = name; }
}
 
// 具体访问者
public class DriverVisitor extends AbstractVisitor {
    public DriverVisitor(String name) { super(name); }
}
 
public class CleanerVisitor extends AbstractVisitor {
    public CleanerVisitor(String name) { super(name); }
}
 
public class RepairVisitor extends AbstractVisitor {
    public RepairVisitor(String name) { super(name); }
}
 
// 对象结构
public class Car {
    public void visitorEngine(DriverVisitor visitor) {
        System.out.println(visitor.name + " → 查看引擎");
    }
 
    public void visitorEngine(CleanerVisitor visitor) {
        System.out.println(visitor.name + " → 清洗引擎");
    }
 
    public void visitorEngine(RepairVisitor visitor) {
        System.out.println(visitor.name + " → 修理引擎");
    }
 
    public void visitorBody(DriverVisitor visitor) {
        System.out.println(visitor.name + " → 查看车身");
    }
 
    public void visitorBody(CleanerVisitor visitor) {
        System.out.println(visitor.name + " → 清洗车身");
    }
 
    public void visitorBody(RepairVisitor visitor) {
        System.out.println(visitor.name + " → 修理车身");
    }
}
 
// 测试用例
public class Test {
    public static void main(String[] args) {
        List<AbstractVisitor> visitors = new ArrayList<>();
        visitors.add(new DriverVisitor("杰哥"));
        visitors.add(new CleanerVisitor("老王"));
        visitors.add(new RepairVisitor("大锤"));
        Car car = new Car();
        for(AbstractVisitor visitor: visitors) {
            // 此处编译不通过
            car.visitorEngine(visitor);
            car.visitorBody(visitor);
        }
    }
}
 

上述代码理论上是可行,利用函数重载,根据参数类型调用对应方法,实际上却是编译都过不了!

原因是:

Java 中的 函数重载 是一种 静态绑定,编译时并不能获取对象的 实际类型,而是根据 声明类型 执行声明类型对应的方法。

解法就是:将行为/业务抽象成单独的类,函数传入不同的访问者,根据不同的访问者入参执行对应操作,访问者从主动变成被动,以此规避了编译失败问题。接着代码实现一波:

访问角色,引擎和车身,传入不同的访问者,执行不同的操作:

// 抽象访问角色类
public interface Visit {
    void visit(DriverVisitor visitor);
    void visit(CleanerVisitor visitor);
    void visit(RepairVisitor visitor);
}
 
// 访问角色具体实现类
public class Engine implements Visit {
    @Override public void visit(DriverVisitor visitor) {
        System.out.println(visitor.name + "→ 查看引擎");
    }
 
    @Override public void visit(CleanerVisitor visitor) {
        System.out.println(visitor.name + "→ 清洗引擎");
    }
 
    @Override public void visit(RepairVisitor visitor) {
        System.out.println(visitor.name + "→ 修理引擎");
    }
}
 
public class Body implements Visit {
    @Override public void visit(DriverVisitor visitor) {
        System.out.println(visitor.name + "→ 查看车身");
    }
 
    @Override public void visit(CleanerVisitor visitor) {
        System.out.println(visitor.name + "→ 清洗车身");
    }
 
    @Override public void visit(RepairVisitor visitor) {
        System.out.println(visitor.name + "→ 修理车身");
    }
}
 

再接着是 访问者,定义所有 访问角色 的访问操作:

// 抽象访问者类
public abstract class AbstractVisitor {
    protected String name;
    public AbstractVisitor(String name) { this.name = name; }
    abstract void visitorEngine(Engine engine);
    abstract void visitorBody(Body body);
}
 
// 具体访问者类
public class DriverVisitor extends AbstractVisitor {
    public DriverVisitor(String name) { super(name); }
    @Override void visitorEngine(Engine engine) { engine.visit(this); }
    @Override void visitorBody(Body body) { body.visit(this); }
}
 
public class CleanerVisitor extends AbstractVisitor {
    public CleanerVisitor(String name) { super(name); }
    @Override void visitorEngine(Engine engine) { engine.visit(this); }
    @Override void visitorBody(Body body) { body.visit(this); }
}
 
public class RepairVisitor extends AbstractVisitor {
    public RepairVisitor(String name) { super(name); }
    @Override void visitorEngine(Engine engine) { engine.visit(this); }
    @Override void visitorBody(Body body) { body.visit(this); }
}
 

接着上测试用例:

public class Test {
    public static void main(String[] args) {
        List<AbstractVisitor> visitors = new ArrayList<>();
        visitors.add(new DriverVisitor("杰哥"));
        visitors.add(new CleanerVisitor("老王"));
        visitors.add(new RepairVisitor("大锤"));
        // 实例化访问角色
        Engine engine = new Engine();
        Body body = new Body();
        for(AbstractVisitor visitor: visitors) {
            visitor.visitorEngine(engine);
            visitor.visitorBody(body);
        }
    }
}
 

代码运行结果同上,可以,但还存在添加新业务,每个访问者都要改动的问题,还要再改改~

// 抽象访问者类
public class RepairVisitor extends AbstractVisitor {
    public RepairVisitor(String name) { super(name); }
    @Override void accept(Visit visit) { visit.visit(this); }
}
 
// 具体访问者类
public class DriverVisitor extends AbstractVisitor {
    public DriverVisitor(String name) { super(name); }
    @Override void accept(Visit visit) { visit.visit(this); }
}
 
public class CleanerVisitor extends AbstractVisitor {
    public CleanerVisitor(String name) { super(name); }
    @Override void accept(Visit visit) { visit.visit(this); }
}
 
public class RepairVisitor extends AbstractVisitor {
    public RepairVisitor(String name) { super(name); }
    @Override void accept(Visit visit) { visit.visit(this); }
}
 
// 测试用例
public class Test {
    public static void main(String[] args) {
        List<AbstractVisitor> visitors = new ArrayList<>();
        visitors.add(new DriverVisitor("杰哥"));
        visitors.add(new CleanerVisitor("老王"));
        visitors.add(new RepairVisitor("大锤"));
        // 实例化访问角色
        Engine engine = new Engine();
        Body body = new Body();
        for(AbstractVisitor visitor: visitors) {
            visitor.accept(engine);
            visitor.accept(body);
        }
    }
}

改完后,此时我们要增加一个访问轮胎的业务,只需让其实现 Visit 接口,实例化后直接 accept()即可,不用改动访问者角色代码,妙啊!!!

4 优缺点

优点

  • 满足开闭原则和单一职责原则;
  • 可扩展性,增加新的访问操作及访问者非常方便;

缺点

  • 不适用于结构经常变化的场景;
  • 具体访问角色变更时需修改代码,易引入问题;

5 应用场景

  • 对象数据结构相对稳定,而操作却经常变化;
  • 需将数据结构与不常用操作进行分离;
  • 需在运行时动态决定使用哪些对象和方法;

6 单分派和双分派概念

6.1 单分派

  • 执行 哪个对象的方法,根据 对象的运行时类型 决定;
  • 执行 对象的哪个方法,根据 方法参数的编译时类型 决定;

6.2 双分派

  • 执行 哪个对象的方法,根据 对象的运行时类型 决定;
  • 执行 对象的哪个方法,根据 方法参数的运行时类型 决定;

Java 函数重载,调用哪个重载函数,取决于入参的声明类型(编译器时类型),所以它只支持单分派。

6.3 经典应用例子

访问者不是常用的设计模式, 因为它不仅复杂, 应用范围也比较狭窄。

这里是 Java 程序库代码中该模式的一些示例: