前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >018.访问者模式

018.访问者模式

作者头像
CoderJed
发布2021-01-06 17:59:22
3550
发布2021-01-06 17:59:22
举报
文章被收录于专栏:Jed的技术阶梯

现在有这样一个需求,我要把公司中的所有人员信息都打印汇报上去,每一个员工都有这些信息:名字、性别、薪水,我们来看类图:

这个类图还是比较简单的,使用了一个模版方法模式,把所要的信息都打印出来,我们先来看一下抽象类:

代码语言:javascript
复制
/**
 * @description 在一个单位里谁都是员工,甭管你是部门经理还是小兵
 */
public abstract class Employee {

    // 0代表男性
    public static final int MALE = 0;
    // 1代表女性
    public static final int FEMALE = 1;

    private String name;
    private int salary;
    private int sex;

    public final void report() {
        System.out.println("姓名: " + name + "\t性别: " + (sex == FEMALE ? "女" : "男") + "\t薪水: " + salary + "\t" + getOtherInfo());
    }

    protected abstract String getOtherInfo();

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }
}

再看小兵的实现类:

代码语言:javascript
复制
/**
 * @description 普通员工,也就是最小的小兵
 */
public class CommonEmployee extends Employee {

    // 工作内容
    private String job;

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    @Override
    protected String getOtherInfo() {
        return "工作: " +job + "\t";
    }
}

再来看领导阶层:

代码语言:javascript
复制
/**
 * @description 经理级人物
 */
public class Manager extends Employee {

    // 业绩
    private String performance;

    public String getPerformance() {
        return performance;
    }

    public void setPerformance(String performance) {
        this.performance = performance;
    }

    @Override
    protected String getOtherInfo() {
        return "业绩: " + performance + "\t";
    }
}

然后我们来看一下我们的invoker类:

代码语言:javascript
复制
public class Client {

    public static void main(String[] args) {

        for (Employee employee : mockEmployee()) {
            employee.report();
        }

    }

    public static List<Employee> mockEmployee() {
        List<Employee> empList = new ArrayList<>();

        CommonEmployee zhangSan = new CommonEmployee();
        zhangSan.setJob("Java开发");
        zhangSan.setName("张三");
        zhangSan.setSalary(1800);
        zhangSan.setSex(Employee.MALE);

        CommonEmployee liSi = new CommonEmployee();
        liSi.setJob("前端开发");
        liSi.setName("李四");
        liSi.setSalary(1900);
        liSi.setSex(Employee.FEMALE);

        Manager wangWu = new Manager();
        wangWu.setPerformance("基本都是负值");
        wangWu.setName("王五");
        wangWu.setSalary(18750);
        wangWu.setSex(Employee.FEMALE);

        empList.add(zhangSan);
        empList.add(liSi);
        empList.add(wangWu);

        return empList;
    }

}

程序运行结果:

代码语言:javascript
复制
姓名: 张三  性别: 男   薪水: 1800    工作: Java开发  
姓名: 李四  性别: 女   薪水: 1900    工作: 前端开发    
姓名: 王五  性别: 女   薪水: 18750   业绩: 基本都是负值

接下来改进我们的代码:

每个普通员工类和经理类都一个report()方法,它们要实现的内容不相同,而且还有可能会发生变动,那我们就让其他类来实现这个 report()方法:

看代码实现:

代码语言:javascript
复制
public interface IVisitor {

    // 定义可以访问普通员工
    void visit(CommonEmployee commonEmployee);

    // 定义可以访问部门经理
    void visit(Manager manager);

}

public class Visitor implements IVisitor {

    // 组装基本信息
    private String getBasicInfo(Employee employee) {
        return "姓名: " + employee.getName() + "\t性别: " +
                (employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t薪水: " +
                employee.getSalary() + "\t";
    }

    // 组装部门经理的信息
    private String getManagerInfo(Manager manager) {
        return getBasicInfo(manager) + "业绩: " + manager.getPerformance() + "\t";
    }

    // 组装普通员工的信息
    private String getCommonEmployeeInfo(CommonEmployee commonEmployee) {
        return getBasicInfo(commonEmployee) + "工作: " + commonEmployee.getJob() + "\t";
    }

    @Override
    public void visit(CommonEmployee commonEmployee) {
        System.out.println(getCommonEmployeeInfo(commonEmployee));
    }

    @Override
    public void visit(Manager manager) {
        System.out.println(getManagerInfo(manager));
    }

}

public abstract class Employee {

    // 0代表男性
    public static final int MALE = 0;
    // 1代表女性
    public static final int FEMALE = 1;

    private String name;
    private int salary;
    private int sex;

    // 一个访问者过来访问
    public abstract void accept(IVisitor visitor);

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSalary() {
        return salary;
    }

    public void setSalary(int salary) {
        this.salary = salary;
    }

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }
}

public class CommonEmployee extends Employee {

    // 工作内容
    private String job;

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

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

public class Manager extends Employee {

    // 业绩
    private String performance;

    public String getPerformance() {
        return performance;
    }

    public void setPerformance(String performance) {
        this.performance = performance;
    }

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

public class Client {

    public static void main(String[] args) {

        for (Employee employee : mockEmployee()) {
            employee.accept(new Visitor());
        }

    }

    public static List<Employee> mockEmployee() {
        List<Employee> empList = new ArrayList<>();

        CommonEmployee zhangSan = new CommonEmployee();
        zhangSan.setJob("Java开发");
        zhangSan.setName("张三");
        zhangSan.setSalary(1800);
        zhangSan.setSex(Employee.MALE);

        CommonEmployee liSi = new CommonEmployee();
        liSi.setJob("前端开发");
        liSi.setName("李四");
        liSi.setSalary(1900);
        liSi.setSex(Employee.FEMALE);

        Manager wangWu = new Manager();
        wangWu.setPerformance("基本都是负值");
        wangWu.setName("王五");
        wangWu.setSalary(18750);
        wangWu.setSex(Employee.FEMALE);

        empList.add(zhangSan);
        empList.add(liSi);
        empList.add(wangWu);

        return empList;
    }

}

运行结果也完全相同,那回过头我们来看看这个程序是怎么实现的:

  • 首先通过循环遍历所有元素;
  • 其次,每个员工对象都定义了一个访问者;
  • 再其次,员工对象把自己做为一个参数调用访问者visit()方法;
  • 然后,访问者调用自己内部的计算逻辑,计算出相应的数据和表格元素;
  • 最后,访问者打印出报表和数据;

这样一来,如果打印的信息格式发生变化了,我只要修改Visitor的实现或者再产生一个Visitor就可以产生一个新的报表格式,而其他的类都不用修改。

以上讲的就是访问者模式,这个模式的通用类图如下:

看了这个通用类图,大家可能要犯迷糊了,这里怎么有一个ObjectStruture这个类呢?你刚刚举得例子就没有呢?真没有吗?我们不是定义了一个List了吗?这就是一个ObjectStruture,我们来看这几个角色的职责:

  • 抽象访问者(Visitor):抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit()方法的参数定义哪些对象是可以被访问的;
  • 具体访问者(ConcreteVisitor):访问者访问到一个类后该怎么干,要做什么事情;
  • 抽象元素(Element):接口或者抽象类,声明接受那一类型的访问者访问,程序上是通过accept()方法中的参数来定义;
  • 具体元素:(ConcreteElement):实现 accept 方法,通常是 visitor.visit(this),基本上都形成了一 个套路了;
  • 结构对象(ObjectStruture):容纳多个不同类、不同接口的容器,比如ListSetMap等,在项目中,一般很少抽象出来这个角色;

接下来我们来思考一下,访问者可以用在什么地方。在这种地方你一定要考虑到使用访问者模式:业务规则要求遍历多个不同的对象。这本身也是访问者模式出发点,迭代器模式只能访问同类或同接口的数据,(当然了,你使用instanceof的话,能访问所有的数据,这个不争论),而访问者模式是对迭代器模式的扩充,可以遍历不同的对象,然后执行不同的操作,也就是针对访问的对象不同,执行不同的操作。访问者模式还有一个用途,就是充当拦截器(Interceptor)角色,这个我们在后边来讲。

访问者模式有哪些优点呢?

  • 首先是符合单一职责原则,具体元素角色也就是Employee这个类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确的分离开来,各自演绎而变化;
  • 其次,由于职责分开,继续增加对数据的操作是非常快捷的,例如现在要增加一个给最大老板的一份报表,这份报表格式又有所不同,容易处理吧,直接在Visitor中增加一个方法,传递过来数据后进行整理打印;
  • 最后,数据汇总,就以刚刚我们说的Employee的例子,如果我现在要统计所有员工的工资之和,怎么计算?把所有人的工资for循环加一遍?是个办法,那我再提个问题,员工工资*1.2,部门经理工资*1.4,总经理*1.8,然后把这些工资加起来,你怎么处理?使用 for循环,然后使用instanceof来判断是员工还是经理?可以解决,但不是个好办法,好办法是通过访问者模式来实现,把数据扔给访问者,由访问者来进行统计计算。

访问者模式的缺点也很明显:

  • 访问者要访问一个类就必然要求这个类公布一些方法,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的;
  • 还有一个缺点就是,具体角色的增加删除修改都是比较苦难的,就上面那个例子,你想想,你要是想增加一个成员变量,比如年龄 ageVisitor就需要修改,如果Visitor是一个还好说,多个呢?业务逻辑再复杂点呢?

访问者模式是有缺点的,是事物都有缺点,但是这仍然掩盖不了它的光芒,访问者模式结合其他模式比如模版方法模式、状态模式、解释器模式、代理模式等就会非常强大,这个我们放在模式混编中来讲解。

在这里我提出三个扩展的功能共大家参考:

  • 统计功能。在访问者模式中的使用中我也提到访问者的统计功能,汇总和报表是金融类企业非常常用的功能,基本上都是一堆的计算公式,然后出一个报表,很多项目是采用了数据库的存储过程来实现,这个我不是很推荐,除非海量数据处理,一个晚上要上亿、几十亿条的数据跑批处理,这个除了存储过程来处理没有其他办法的,你要是用应用服务器来处理,连接数据库的网络就是处于100%用状态,一个晚上也未必跑得完这批数据!除了这种海量数据外,我建议数据统计和报表的批处理通过访问者模式来处理会比较简单。好,那我们来统计一下公司人员的工资,先看类图:

看代码实现:

代码语言:javascript
复制
public interface IVisitor {

    // 定义可以访问普通员工
    void visit(CommonEmployee commonEmployee);

    // 定义可以访问部门经理
    void visit(Manager manager);

    // 统计所有员工工资总和
    int getTotalSalary();

}

public class Visitor implements IVisitor {

    // 部门经理的工资系数是5
    private static final int MANAGER_COEFFICIENT = 5;
    // 员工的工资系数是2
    private static final int COMMON_EMPLOYEE_COEFFICIENT = 2;

    // 普通员工的工资总和
    private int commonTotalSalary = 0;
    // 部门经理的工资总和
    private int managerTotalSalary = 0;

    // 组装基本信息
    private String getBasicInfo(Employee employee) {
        return "姓名: " + employee.getName() + "\t性别: " +
                (employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t薪水: " +
                employee.getSalary() + "\t";
    }

    // 组装部门经理的信息
    private String getManagerInfo(Manager manager) {
        return getBasicInfo(manager) + "业绩: " + manager.getPerformance() + "\t";
    }

    // 组装普通员工的信息
    private String getCommonEmployeeInfo(CommonEmployee commonEmployee) {
        return getBasicInfo(commonEmployee) + "工作: " + commonEmployee.getJob() + "\t";
    }

    // 计算部门经理的工资总和
    private void calManagerSalary(int salary) {
        this.managerTotalSalary += salary * MANAGER_COEFFICIENT;
    }

    // 计算普通员工的工资总和
    private void calCommonSalary(int salary) {
        this.commonTotalSalary += salary * COMMON_EMPLOYEE_COEFFICIENT;
    }

    @Override
    public int getTotalSalary() {
        return managerTotalSalary + commonTotalSalary;
    }

    @Override
    public void visit(CommonEmployee commonEmployee) {
        System.out.println(getCommonEmployeeInfo(commonEmployee));
        // 计算普通员工的薪水总和
        calCommonSalary(commonEmployee.getSalary());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println(getManagerInfo(manager));
        // 计算部门经理的薪水总和
        calManagerSalary(manager.getSalary());
    }

}

public class Client {

    public static void main(String[] args) {
        IVisitor visitor = new Visitor();
        for (Employee employee : mockEmployee()) {
            employee.accept(visitor);
        }
        System.out.println("工资总额: " + visitor.getTotalSalary());
    }

    public static List<Employee> mockEmployee() {
        List<Employee> empList = new ArrayList<>();

        CommonEmployee zhangSan = new CommonEmployee();
        zhangSan.setJob("Java开发");
        zhangSan.setName("张三");
        zhangSan.setSalary(1800);
        zhangSan.setSex(Employee.MALE);

        CommonEmployee liSi = new CommonEmployee();
        liSi.setJob("前端开发");
        liSi.setName("李四");
        liSi.setSalary(1900);
        liSi.setSex(Employee.FEMALE);

        Manager wangWu = new Manager();
        wangWu.setPerformance("基本都是负值");
        wangWu.setName("王五");
        wangWu.setSalary(18750);
        wangWu.setSex(Employee.FEMALE);

        empList.add(zhangSan);
        empList.add(liSi);
        empList.add(wangWu);

        return empList;
    }

}

// Employee及其两个子类是没有任何变化的
  • 多个访问者 在实际的项目中,一个对象,多个访问者的情况非常多。其实我们上面例子就应该是两个访问者,为什么呢?报表分两种,一种是展示表,通过数据库查询,把结果展示出来,这个就类似于我们的那个列表;第二种是汇总表,这个是需要通过模型或者公式计算出来的,一般都是批处理结果,这个类似于我们计算工资总额,这两种报表格式是对同一堆数据的两种处理方式,从程序上看,一个类就有个不同的访问者了,那我们修改一下类图:

代码如下:

代码语言:javascript
复制
public interface IVisitor {

    // 定义可以访问普通员工
    void visit(CommonEmployee commonEmployee);

    // 定义可以访问部门经理
    void visit(Manager manager);


}

public interface IShowVisitor extends IVisitor {

    // 展示报表
    void report();

}

public interface ITotalVisitor extends IVisitor {

    // 统计所有员工工资总和
    void totalSalary();

}

public class ShowVisitor implements IShowVisitor {

    private String info = "";

    // 组装基本信息
    private String getBasicInfo(Employee employee) {
        return "姓名: " + employee.getName() + "\t性别: " +
                (employee.getSex() == Employee.FEMALE ? "女" : "男") + "\t薪水: " +
                employee.getSalary() + "\t";
    }

    @Override
    public void report() {
        System.out.println(info);
    }

    @Override
    public void visit(CommonEmployee commonEmployee) {
        this.info += getBasicInfo(commonEmployee) + "工作: " + commonEmployee.getJob() + "\t\n";
    }

    @Override
    public void visit(Manager manager) {
        this.info += getBasicInfo(manager) + "业绩: " + manager.getPerformance() + "\t\n";
    }

}

public class TotalVisitor implements ITotalVisitor {

    // 部门经理的工资系数是5
    private static final int MANAGER_COEFFICIENT = 5;
    // 员工的工资系数是2
    private static final int COMMON_EMPLOYEE_COEFFICIENT = 2;

    // 普通员工的工资总和
    private int commonTotalSalary = 0;
    // 部门经理的工资总和
    private int managerTotalSalary = 0;

    // 计算部门经理的工资总和
    private void calManagerSalary(int salary) {
        this.managerTotalSalary += salary * MANAGER_COEFFICIENT;
    }

    // 计算普通员工的工资总和
    private void calCommonSalary(int salary) {
        this.commonTotalSalary += salary * COMMON_EMPLOYEE_COEFFICIENT;
    }

    @Override
    public void totalSalary() {
        System.out.println("总工资: " + (managerTotalSalary + commonTotalSalary));
    }

    @Override
    public void visit(CommonEmployee commonEmployee) {
        // 计算普通员工的薪水总和
        calCommonSalary(commonEmployee.getSalary());
    }

    @Override
    public void visit(Manager manager) {
        // 计算部门经理的薪水总和
        calManagerSalary(manager.getSalary());
    }

}

public class Client {

    public static void main(String[] args) {
        IShowVisitor showVisitor = new ShowVisitor();
        ITotalVisitor totalVisitor = new TotalVisitor();
        for (Employee employee : mockEmployee()) {
            employee.accept(showVisitor);
            employee.accept(totalVisitor);
        }
        showVisitor.report();
        totalVisitor.totalSalary();
    }

    public static List<Employee> mockEmployee() {
        ......
    }

}

// Employee及其两个子类是没有任何变化的
  • 拦截器 拦截器的核心作用是“围墙”作用,拦截器对被拦截的对象进行检查,符合规则的对象则开门放进去,继续执行下一个逻辑,不符合规则的则弹回(其实这也是过滤器的作用);拦截器还有一个作用是修改数据,对于符合规则数据可以进行修改,以便继续后序的逻辑。具备了这两个功能,拦截器的雏形就有了,访问者模式就可以实现简单的拦截器角色,我们来看类图:

着是不是和访问者模式的通用类图很类似?两个accept()方法,其中参数为List类型的则实现了拦截器栈的作用,DynamicProxy类使用了动态代理和反射模式。拦截器实现起来也不复杂,今天就不实现了,这个作为作业,请大家自己来实现。计划在混编模式中一起探讨。

本文原书:

《您的设计模式》 作者:CBF4LIFE

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据库
云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档