10 行为型模式之 - 访问者模式
九路 618 12

访问者模式介绍

访问者模式是一种将数据操作与数据结构分离的设计模式,它是《设计模式》中23种设计模式中最复杂的一个,但是它的使用频率并不高,正如《设计模式》的作者GOF对访问者模式的描述:大多数情况下,你并不需要使用访问者模式,但是当你一旦需要使用它时,那你就是真地需要它了。

 访问者模式的基本想法是,软件系统中拥有一个由许多对象构成的,比较稳定的对象结构,这些对象的类都拥有一个accept方法,用来接受访问者对象的访问。访问者是一个接口,它拥有一个visit方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果

访问者模式的定义

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

访问者模式使用的场景:

1 对象结构比较稳定,但是经常需要在此对象结构上定义新的操作

2 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类

简单示例:

在年终时,公司都会给员工进行业绩考核,以此来评定该员工的绩效及年终奖,晋升等,这些评定都是由公司高层来负责,但是,不同领域的管理人员对员工的评定是不一样的,为了简单明了地说明问题,我们简单地把员工分为工程师和经理,评定员工的分别为CEO和CTO,我们假定CTO只关注工程师的代码量,经理的产品数量。而CEO关注的是工程师的KPI和经理的KPI以及产品的数量,从中可以看出,CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同的员工类型进行不同的处理,访问者模式此时可以派上用场了,我们看看下面相关代码的实现

员工基类:

/**
 * 员工基类
 */
public abstract class Staff {
    public String name;

    //员工KPI
    public int kpi;

    public Staff(String name){
        this.name = name;
        kpi = new Random().nextInt();
    }

    // 接受visitor的访问
    public abstract void accept(Visitor visitor);
}

Staff类定义了员工的基本信息及一个accept方法,accept方法表示接受访问者的访问,由子类具体实现。下面看看工程师和经理的代码:

/**
 * 工程师类型
 */
public class Engineer extends Staff {
    public Engineer(String name){
        super(name);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    //工程师这一年写的代码数量
    public int getCodeLine(){
        return new Random().nextInt() * 10000;
    }
}
/**
 * 经理类型
 */
public class Manager extends Staff{
    public Manager(String name){
        super(name);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    //经理一年内做的产品的数量
    public int getProducts(){
        return 10;
    }
}

在工程师类中添加了获取代码行数的函数,而在经理类中则添加了获取新产品数量的函数,它们的职责是不一样的,也正是由于它们的差异性才使得访问者模式能够发挥它的作用。 Staff , Engineer , Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。

  然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的showReport函数查看所有员工的业绩。具体代码如下:

/**
 * 员工业务报表
 */
public class BusinessReport {
    List<Staff> mStaffs = new ArrayList<>();

    public BusinessReport(){
        mStaffs.add(new Manager("王经理"));
        mStaffs.add(new Engineer("工程师-小A"));
        mStaffs.add(new Engineer("工程师-小B"));
        mStaffs.add(new Engineer("工程师-小C"));
    }

    /**
     * 为访问者展示报表,公司高层,如CEO,CTO
     */
    public void showReport(Visitor visitor){
        for (Staff staff : mStaffs){
            staff.accept(visitor);
        }
    }
}

下面看看访问者 Visitor类型的定义,Visitor声明了两个visit函数,分别是对工程师和经理的访问函数,具体代码如下:

/**
 * 访问者
 */
public interface Visitor {
    //访问工程师类型
    void visit(Engineer engineer);

    //访问经理类型
    void visit(Manager manager);
}

Visitor接口,该接口有两个visit函数,参数分别为Engineer,Manager,也就是说对于Engineer,Manager的访问会调用两个不同的方法,以此达成区别对待,差异化处理。具体的实现类为CEOVisitor, CTOVisitor,具体代码如下:

/**
 * CEO访问类型,只关注业绩
 */
public class CEOVisitor implements Visitor{
    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程师:"+engineer.name + " KPI : " + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("经理:"+manager.name + " KPI : " + manager.kpi + " , 新产品数量:" + manager.getProducts());
    }
}
/**
 * CTO 类型,只关注技术层面上的贡献
 */
public class CTOVisitor implements Visitor{
    @Override
    public void visit(Engineer engineer) {
        System.out.println("工程师 : "+engineer.name + " , 代码行数:" + engineer.getCodeLine());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("经理 : "+manager.name + " , 产品数量:" + manager.getProducts());
    }
}

在CEO的访问者中,CEO只关注Engineer员工的KPI,而对于Manager类型的员工除了KPI之外还有该Manager本年度新开发产品的数量,两类员工的关注点不同,通过两个visitor方法分别进行处理。而如果不使用访问者模式,只是通过一个visitor函数进行处理,那么就需要在这个visit函数中对不同的员工类进行判断,然后分别处理,代码大致如下:

/**
 * 不使用访问者模式的写法
 */
public class ReportUtils {
    public void visit(Staff staff){
        if(staff instanceof Engineer){
            Engineer engineer = (Engineer) staff;
            System.out.println("工程师:" + engineer.name + " , KPI = "+ engineer.kpi);
        }else if(staff instanceof Manager){
            Manager manager = (Manager) staff;
            System.out.println("经理:"+manager.name + " , KPI : "+manager.kpi + ",新产品数量 : "+ manager.getProducts());
        }
    }
}

这就导致了if else 逻辑的嵌套以及类型的强制转换,难以维护和扩展,当类型较多时,这个ReportUtils就会很复杂,而使用Visitor模式,通过同一个函数对不同的元素类型进行相应处理,使得结构更加清晰,灵活性更高

下面是使用访问者模式的客户端的用法,代码如下:

/**
 *  客户端
 */
public class Client {
    public static void main(String[] args){
        //构建报表
        BusinessReport businessReport = new BusinessReport();
        System.out.println("========== 给CEO看的报表 ============");
        businessReport.showReport(new CEOVisitor());
        System.out.println("========== 给CTO看的报表 ============");
        businessReport.showReport(new CTOVisitor());
    }
}

客户端代码中,首先构建了一个报表对象,该对象中维护了所有员工的集合,然后通过报表类的showReport函数为Visitor对象提供一个访问接口,在这个函数中遍历所有的员工,然后调用员工的accept函数接受访问者的访问,每个访问者对不同类型的员工调用对应的visit函数实现不同的操作

具体输出如下:

========== 给CEO看的报表 ============
经理:王经理 KPI : -521144346 , 新产品数量:10
工程师:工程师-小A KPI : -1619137302
工程师:工程师-小B KPI : -237723895
工程师:工程师-小C KPI : 1468883099
========== 给CTO看的报表 ============
经理 : 王经理 , 产品数量:10
工程师 : 工程师-小A , 代码行数:646642576
工程师 : 工程师-小B , 代码行数:-1775007488
工程师 : 工程师-小C , 代码行数:-1605653216

访问者模式最大的优点就是增加访问者非常容易。我们从代码中可以看到,如果要增加一个访问者,你新创建一个实现了Visitor接口的类,然后实现两个visitor函数来对不同的元素进行不同的操作,从而达到数据对象与数据操作相分离的效果,如果不使用访问者模式,而又想对不同的元素进行不同的操作,那么必定需要使用 if else 和类型转换,这使得代码难以维护和升级,此时,访问者模式的作用就体现出来了。

评论区

索引目录