018.访问者模式

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

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

/**
 * @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;
    }
}

再看小兵的实现类:

/**
 * @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";
    }
}

再来看领导阶层:

/**
 * @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类:

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;
    }

}

程序运行结果:

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

接下来改进我们的代码:

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

看代码实现:

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%用状态,一个晚上也未必跑得完这批数据!除了这种海量数据外,我建议数据统计和报表的批处理通过访问者模式来处理会比较简单。好,那我们来统计一下公司人员的工资,先看类图:

看代码实现:

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及其两个子类是没有任何变化的
  • 多个访问者

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

代码如下:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容