简述-访问者模式

介绍

访问模式是23种模式中最复杂的一个。目的在于将数据操作与数据结构进行分离。一个系统由许多对象组成,每个对象都有一个accept操作来接收访问者访问,对象会调用访问者的visit方法传入该对象,使得访问者可以访问处理对象结构中的每个元素。访问者是一个接口,又根据实现不同的访问者来达到对系统类的不同访问实现。

Android中APT(Annotation Processing Tools)则是使用到了该模式,编译的时候编译器检查AbstractProcessor的子类,并且调用该类的process函数,然后将添加注解的元素都传递到process函数中,使得开发人员可以在编译器进行相应处理,例如根据注解生成新的java类,也是我们常用到的ButterKnife的基本原理

定义

封装一些作用于某种数据结构中个元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

UML

组成结构

  • Visitor :接口或抽象类,定义对每个元素(Element)访问行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素个数是一样的。访问者模式要求元素类族要稳定,如果经常添加、移除元素则不适合用这个模式。
  • ConcreteVisitor : 具体的访问者,它需要给出每一个元素访问时所产生的具体行为。
  • Element : 元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,意义是每一个元素都可以被访问者访问。
  • ElementA、ElementB : 具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问元素类的方法。
  • ObjectStructrue : 定义当中所提到的对象结构,对象结构是一个抽象的表述,内部主要是管理了元素集合,并且可以迭代这些元素供访问者访问(被访问者调用)。

事例

利用书中的一个例子,年中工程师、经理需要提供报表给领导看:

  1. CTO关心:
    1. 工程师的:代码行数
    2. 经理的:产品数量
  2. CEO关心:
    1. 工程师的:Kpi
    2. 经理的:kpi和产品数量

那么想想UML的结构,这里就比较适合了,工程师和经理是比较稳定的元素,只是CEO和CTO这关心的不一样,CEO和CTO就可以看作为访问者了。那么我们来实现一下

  1. 创建职员接口,作为Element元素:
/**
 * 职员接口
 * 1. 提供访问者方法
 */
public abstract class Staff {
    public Staff(String name, int kpi) {
        this.name = name;
        this.kpi = kpi;
    }

    /**
     * 职员名称
     */
    protected String name;
    /**
     * 职员kpi
     */
    protected int kpi;

    public abstract void accept(AbsVisitor absVisitor);
}
  1. 创建工程师和经理:
/**
 * 工程师
 */
public class Employee extends Staff {
    private int codeLines;

    public Employee(String name, int kpi) {
        super(name, kpi);
        codeLines = new Random().nextInt(10_000_000);
    }

    /**
     * 接收访问者访问,调用访问者的访问方法
     *
     * @param absVisitor
     */
    @Override
    public void accept(AbsVisitor absVisitor) {
        //调用访问者的访问方法,并传入自己
        absVisitor.visit(this);
    }

    /**
     * 获取工程师代码行数
     */
    public int getCodeLines() {
        return codeLines;
    }
}

/**
 * 经理
 */
public class Manager extends Staff {
    private int products;

    public Manager(String name, int kpi) {
        super(name, kpi);
        products = new Random().nextInt(100);
    }

    /**
     * 接收访问者访问,调用访问者的访问方法
     *
     * @param absVisitor
     */
    @Override
    public void accept(AbsVisitor absVisitor) {
        absVisitor.visit(this);
    }

    /**
     * 获取产品数量
     *
     * @return
     */
    public int getProducts() {
        return products;
    }
}

这里的工程师和经理提供各自对外的方法,然后也实现accept方法,调用访问者的visit方法,访问者接下来定义

  1. 定义访问者,提供visit方法:
/**
 * 访问者
 * CEO、CTO
 */
public abstract class AbsVisitor {
    /**
     * 访问方法,传入管理者
     *
     * @param staffManager
     */
    abstract void visit(Manager staffManager);

    /**
     * 访问方法,传入普通职员
     *
     * @param employee
     */
    abstract void visit(Employee employee);
}

这里没有使用里氏替换来只使用一个方法(参数为Staff),就是出于这个模式要求结构稳定,没必要那样采用,而且那样定参数的话,调用的适合依旧需要判断是哪个对象,会多很多if-else,所以没必要折腾

  1. 定义CTO和CEO,各自实现visit方法:
/**
 * 访问者
 * CEO
 */
public class CEOVisitor extends AbsVisitor {
    /**
     * CEO关心的管理者
     * kpi、产品数量
     *
     * @param staffManager
     */
    @Override
    void visit(Manager staffManager) {
        System.out.println("CTO 关心: manager name:" + staffManager.name + ",kpi:" + staffManager.kpi + ",products:" + staffManager.getProducts());
    }

    /**
     * CEO关心的工程师
     *
     * @param employee
     */
    @Override
    void visit(Employee employee) {
        System.out.println("CEO 关心: employee name:" + employee.name + ",kpi:" + employee.kpi);
    }
}

/**
 * 访问者
 * CTO
 */
public class CTOVisitor extends AbsVisitor {
    /**
     * CTO关心的管理者
     * 产品数量
     * @param staffManager
     */
    @Override
    void visit(Manager staffManager) {
        System.out.println("CTO 关心: manager name:" + staffManager.name + ",products:" + staffManager.getProducts());
    }

    /**
     * CTO关心的工程师
     * 代码行数
     * @param employee
     */
    @Override
    void visit(Employee employee) {
        System.out.println("CTO 关心: employee name:" + employee.name + ",codeLies:" + employee.getCodeLines());
    }
}
  1. 测试:
//先收集工程师和经理信息
        List<Staff> staffs = new ArrayList<>();
        staffs.add(new Employee("王工程师", 5));
        staffs.add(new Employee("李工程师", 9));
        staffs.add(new Employee("大工程师", 10));
        staffs.add(new Manager("周经理", 10));
        staffs.add(new Manager("杨经理", 7));

        //发送给CTO看,一个循环
        System.out.println("===CTO看的报表===");
        CTOVisitor ctoVisitor = new CTOVisitor();
        for (Staff staff : staffs) {
            //利用accept接收访问者,然后在内部调用访问者测访问方法
            staff.accept(ctoVisitor);
        }

        CEOVisitor ceoVisitor = new CEOVisitor();
        System.out.println("===CEO看的报表===");
        for (Staff staff : staffs) {
            staff.accept(ceoVisitor);
        }

这里没有单独去定义UML中的ObjectStructrue,可以在5测试代码中体现出来,要ObjectStructrue的话,就是携带列表,并提供一个遍历方法(传入AbsVisitor)即可。

  1. 输出:
===CTO看的报表===
CTO 关心: employee name:王工程师,codeLies:2767421
CTO 关心: employee name:李工程师,codeLies:2102321
CTO 关心: employee name:大工程师,codeLies:7722955
CTO 关心: manager name:周经理,products:11
CTO 关心: manager name:杨经理,products:89
===CEO看的报表===
CEO 关心: employee name:王工程师,kpi:5
CEO 关心: employee name:李工程师,kpi:9
CEO 关心: employee name:大工程师,kpi:10
CTO 关心: manager name:周经理,kpi:10,products:11
CTO 关心: manager name:杨经理,kpi:7,products:89

使用场景

  • 对象结构比较稳定,但经常需要在对象结构上定义新的操作。
  • 需要对一个对象结构中的对象进行很多偶同的并且不相关的操作,需要避免这些操作“污染”这些对象,也不希望再增加新操作时修改这些类。

优缺点

优点

  • 各角色职责分离,符合单一职责原则。
  • 优秀的扩展性(换访问者很方便)
  • 使得数据结构和作用于结构上的操作解偶,使得操作集合可以独立变化。
  • 灵活性强

缺点

  • 具体元素对访问者公布细节,违反了迪米特原则。
  • 具体元素变更时导致修改成本大。
  • 违反了依赖导致原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象。

总结:访问者模式的思想即是在通过提供访问类来处理一些具有稳定结构元素的访问操作,这样对访问者则是可灵活扩展了。一般情况是不需要用它,如果需要用它的时候,那么可能是真的需要它了。需要根据实际情况考虑是否需要用它,包括其他模式也是一样,根据使用场景和优缺点来看能解决什么问题,然后考虑要不要用它,避免滥用设计模式的情况。