乐趣区

关于设计:一文弄懂访问者模式

对于设计模式,咱们得联合生存中的案例来学习;最近我在网上也看了不少文章,明天想跟大家分享一下对于访问者模式的一些常识,先来看一个简略的案例吧。

置信大家都去过医院,看完病,医生都会给咱们开一个处方单,很多医院都存在如下解决流程:划价人员拿到处方单之后依据药品名称和数量计算总价,药房工作人员依据药品名称和数量筹备药品。

咱们能够将处方单看成一个药品信息的汇合,外面蕴含了一种或多种不同类型的药品信息,不同类型的工作人员(如划价人员和药房工作人员)在操作同一个药品信息汇合时将提供不同的解决形式,而且可能还会减少新类型的工作人员来操作处方单。

在软件开发中,有时候咱们也须要解决像处方单这样的汇合对象构造,在该对象构造中存储了多个不同类型的对象信息,而且对同一对象构造中的元素的操作形式并不惟一,可能须要提供多种不同的解决形式,还有可能减少新的解决形式。在设计模式中,有一种模式能够满足上述要求,其模式动机就是以不同的形式操作简单对象构造,该模式就是咱们本章将要介绍的访问者模式。

模式概述

访问者模式(Visitor Pattern):提供一个作用于某对象构造中的各元素的操作示意,它使咱们能够在不扭转各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

访问者模式是一种较为简单的行为型设计模式,它蕴含访问者和被拜访元素两个次要组成部分,这些被拜访的元素通常具备不同的类型,且不同的访问者能够对它们进行不同的拜访操作。例如处方单中的各种药品信息就是被拜访的元素,而划价人员和药房工作人员就是访问者。访问者模式使得用户能够在不批改现有零碎的状况下扩大零碎的性能,为这些不同类型的元素减少新的操作。

在应用访问者模式时,被拜访元素通常不是独自存在的,它们存储在一个汇合中,这个汇合被称为“对象构造”,访问者通过遍历对象构造实现对其中存储的元素的一一操作。

其构造如下图所示:

  • Vistor(形象访问者):形象访问者为对象构造中每一个具体元素类 ConcreteElement 申明一个拜访操作,从这个操作的名称或参数类型能够分明晓得须要拜访的具体元素的类型,具体访问者须要实现这些操作方法,定义对这些元素的拜访操作。
  • ConcreteVisitor(具体访问者):具体访问者实现了每个由形象访问者申明的操作,每一个操作用于拜访对象构造中一种类型的元素。
  • Element(形象元素):形象元素个别是抽象类或者接口,它定义一个 accept()办法,该办法通常以一个形象访问者作为参数。
  • ConcreteElement(具体元素):具体元素实现了 accept()办法,在 accept()办法中调用访问者的拜访办法以便实现对一个元素的操作。
  • ObjectStructure(对象构造):对象构造是一个元素的汇合,它用于寄存元素对象,并且提供了遍历其外部元素的办法。它能够联合组合模式来实现,也能够是一个简略的汇合对象,如一个 List 对象或一个 Set 对象。

实用场景

(1)一个对象构造蕴含多个类型的对象,心愿对这些对象施行一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个拜访操作,不同类型的对象能够有不同的拜访操作。

(2)须要对一个对象构造中的对象进行很多不同的并且不相干的操作,而须要防止让这些操作“净化”这些对象的类,也不心愿在减少新操作时批改这些类。访问者模式使得咱们能够将相干的拜访操作集中起来定义在访问者类中,对象构造能够被多个不同的访问者类所应用,将对象自身与对象的拜访操作拆散。

(3)对象构造中对象对应的类很少扭转,但常常须要在此对象构造上定义新的操作。

案例场景一

某银行开发了一套 OA 零碎,在该 OA 零碎中蕴含一个员工信息管理子系统,该银行员工包含正式员工和临时工,每周人力资源部和财务部等部门须要对员工数据进行汇总,汇总数据包含员工工作工夫、员工工资等。该公司根本制度如下:

(1)正式员工 (Full time Employee) 每周工作工夫为 40 小时,不同级别、不同部门的员工每周基本工资不同;如果超过 40 小时,超出局部依照 100 元 / 小时作为加班费;如果少于 40 小时,所缺工夫依照销假解决,销假所扣工资以 80 元 / 小时计算,直到基本工资扣除到零为止。除了记录理论工作工夫外,人力资源部需记录加班时长或销假时长,作为员工平时体现的一项根据。

(2)临时工 (Part time Employee) 每周工作工夫不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需记录理论工作工夫。

人力资源部和财务部工作人员能够依据各自的须要对员工数据进行汇总解决,人力资源部负责汇总每周员工工作工夫,而财务部负责计算每周员工工资。

一坨坨代码实现

class EmployeeList {private ArrayList<Employee> list = new ArrayList<Employee>(); // 员工汇合  

    // 解决员工数据  
    public void handle(String departmentName) {
        // 财务部解决员工数据  
        if (departmentName.equalsIgnoreCase("财务部")) {for (Object obj : list) {if (obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee")) {System.out.println("财务部解决全职员工数据!");
                } else {System.out.println("财务部解决兼职员工数据!");
                }
            }
            // 人力资源部解决员工数据  
        } else if (departmentName.equalsIgnoreCase("人力资源部")) {for (Object obj : list) {if (obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee")) {System.out.println("人力资源部解决全职员工数据!");
                } else {System.out.println("人力资源部解决兼职员工数据!");
                }
            }
        }
    }
}  

在 handle()办法中,通过对部门名称和员工类型进行判断,不同部门对不同类型的员工进行了不同的解决,满足了员工数据汇总的要求。然而该解决方案存在如下几个问题:

(1)EmployeeList 类承当了过多的职责,既不不便代码的复用,也不利于零碎的扩大,违反了“繁多职责准则”。

(2)在代码中蕴含大量的“if…else…”条件判断语句,既须要对不同部门进行判断,又须要对不同类型的员工进行判断,还将呈现嵌套的条件判断语句,导致测试和保护难度增大。

(3)如果要减少一个新的部门来操作员工汇合,不得不批改 EmployeeList 类的源代码,在 handle()办法中减少一个新的条件判断语句和一些业务解决代码来实现新部门的拜访操作。这违反了“开闭准则”,零碎的灵活性和可扩展性有待进步。

(4)如果要减少一种新类型的员工,同样须要批改 EmployeeList 类的源代码,在不同部门的解决代码中减少对新类型员工的解决逻辑,这也违反了“开闭准则”。

重构代码

定义形象元素

// 员工类:形象元素类
public interface Employee {void accept(Department handler); // 承受一个形象访问者拜访
} 

具体元素类

// 全职员工类:具体元素类
public class FullTimeEmployee implements Employee {

    private String name;
    private double weeklyWage;
    private int workTime;

    public FullTimeEmployee(String name, double weeklyWage, int workTime) {
        this.name = name;
        this.weeklyWage = weeklyWage;
        this.workTime = workTime;
    }

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

    public void setWeeklyWage(double weeklyWage) {this.weeklyWage = weeklyWage;}

    public void setWorkTime(int workTime) {this.workTime = workTime;}

    public String getName() {return (this.name);
    }

    public double getWeeklyWage() {return (this.weeklyWage);
    }

    public int getWorkTime() {return (this.workTime);
    }

    public void accept(Department handler) {handler.visit(this); // 调用访问者的拜访办法  
    }
}
// 兼职员工类:具体元素类
public class PartTimeEmployee implements Employee {

    private String name;
    private double hourWage;
    private int workTime;

    public PartTimeEmployee(String name, double hourWage, int workTime) {
        this.name = name;
        this.hourWage = hourWage;
        this.workTime = workTime;
    }

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

    public void setHourWage(double hourWage) {this.hourWage = hourWage;}

    public void setWorkTime(int workTime) {this.workTime = workTime;}

    public String getName() {return (this.name);
    }

    public double getHourWage() {return (this.hourWage);
    }

    public int getWorkTime() {return (this.workTime);
    }

    public void accept(Department handler) {handler.visit(this); // 调用访问者的拜访办法  
    }
} 

定义形象访问者类

// 部门类:形象访问者类
public abstract class Department {
    // 申明一组重载的拜访办法,用于拜访不同类型的具体元素  
    public abstract void visit(FullTimeEmployee employee);
    public abstract void visit(PartTimeEmployee employee);
}

具体访问者类

// 财务部类:具体访问者类
public class FADepartment extends Department {

    // 实现财务部对全职员工的拜访  
    public void visit(FullTimeEmployee employee) {int workTime = employee.getWorkTime();
        double weekWage = employee.getWeeklyWage();
        if (workTime > 40) {weekWage = weekWage + (workTime - 40) * 100;
        } else if (workTime < 40) {weekWage = weekWage - (40 - workTime) * 80;
            if (weekWage < 0) {weekWage = 0;}
        }
        System.out.println("正式员工" + employee.getName() + "实际工资为:" + weekWage + "元。");
    }

    // 实现财务部对兼职员工的拜访  
    public void visit(PartTimeEmployee employee) {int workTime = employee.getWorkTime();
        double hourWage = employee.getHourWage();
        System.out.println("临时工" + employee.getName() + "实际工资为:" + workTime * hourWage + "元。");
    }
} 
// 人力资源部类:具体访问者类
public class HRDepartment extends Department {

    // 实现人力资源部对全职员工的拜访  
    public void visit(FullTimeEmployee employee) {int workTime = employee.getWorkTime();
        System.out.println("正式员工" + employee.getName() + "理论工作工夫为:" + workTime + "小时。");
        if (workTime > 40) {System.out.println("正式员工" + employee.getName() + "加班工夫为:" + (workTime - 40) + "小时。");
        } else if (workTime < 40) {System.out.println("正式员工" + employee.getName() + "销假工夫为:" + (40 - workTime) + "小时。");
        }
    }

    // 实现人力资源部对兼职员工的拜访  
    public void visit(PartTimeEmployee employee) {int workTime = employee.getWorkTime();
        System.out.println("临时工" + employee.getName() + "理论工作工夫为:" + workTime + "小时。");
    }
}

定义数据结构以及测试

// 员工列表类:对象构造
class EmployeeList {

    // 定义一个汇合用于存储员工对象  
    private ArrayList<Employee> list = new ArrayList<>();

    public EmployeeList(){list.add(new FullTimeEmployee("张三",3200.00,45));
        list.add(new FullTimeEmployee("李四",2500.00,40));
        list.add(new PartTimeEmployee("王二",80.00,20));
        list.add(new PartTimeEmployee("李强",100.00,30));
    }

    // 遍历拜访员工汇合中的每一个员工对象  
    public void accept(Department handler) {for (Object obj : list) {((Employee) obj).accept(handler);
        }
    }

    public static void main(String[] args) {EmployeeList employeeList = new EmployeeList();
        System.out.println("\r\n 财务部:");
        employeeList.accept(new FADepartment());
        System.out.println("\r\n 人力资源部:");
        employeeList.accept(new HRDepartment());
    }
} 

测试后果如下:

财务部:正式员工张三实际工资为:3700.0 元。正式员工李四实际工资为:2500.0 元。临时工王二实际工资为:1600.0 元。临时工李强实际工资为:3000.0 元。人力资源部:正式员工张三理论工作工夫为:45 小时。正式员工张三加班工夫为:5 小时。正式员工李四理论工作工夫为:40 小时。临时工王二理论工作工夫为:20 小时。临时工李强理论工作工夫为:30 小时。

从以上的业务场景中能够看到,在嵌⼊访问者模式后,能够让整个⼯程构造变得容易增加和批改。

如果要在零碎中减少一种新的访问者,毋庸批改源代码,只有减少一个新的具体访问者类即可,在该具体访问者中封装了新的操作元素对象的办法。从减少新的访问者的角度来看,访问者模式合乎“开闭准则”。

如果要在零碎中减少一种新的具体元素,例如减少一种新的员工类型为“退休人员”,因为原有零碎并未提供相应的拜访接口(在形象访问者中没有申明任何拜访“退休人员”的办法),因而必须对原有零碎进行批改,在原有的形象访问者类和具体访问者类中减少相应的拜访办法。从减少新的元素的角度来看,访问者模式违反了“开闭准则”。

综上所述,访问者模式与形象工厂模式相似,对“开闭准则”的反对具备歪斜性,能够很不便地增加新的访问者,然而增加新的元素较为麻烦。

案例场景二

校园中有学生和老师两种身份的用户,那么对于家长和校长关怀的角度来看,他们的视角是不同的。家长更关怀孩子的问题和老师的能力,校长更关怀老师所在班级学生的人数和升学率。

那么这样学生和老师就是一个固定信息的内容,而想让不同视角的用户获取关怀的信息,就比拟适宜应用访问者模式来实现,从而让实体与业务解耦,加强扩展性。

代码实现

定义用户抽象类

// 根底用户信息
public abstract class User {

    public String name;      // 姓名
    public String identity;  // 身份;重点班、普通班 | 特级教师、一般老师、实习老师
    public String clazz;     // 班级

    public User(String name, String identity, String clazz) {
        this.name = name;
        this.identity = identity;
        this.clazz = clazz;
    }

    // 外围拜访办法
    public abstract void accept(Visitor visitor);

}   

实现用户信息(老师和学生)

老师类

public class Teacher extends User {public Teacher(String name, String identity, String clazz) {super(name, identity, clazz);
    }

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

    // 升本率
    public double entranceRatio() {return BigDecimal.valueOf(Math.random() * 100).setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();}

}

学生类

public class Student extends User {public Student(String name, String identity, String clazz) {super(name, identity, clazz);
    }

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

    // 排名
    public int ranking() {return (int) (Math.random() * 100);
    }
}  

定义拜访数据接口

public interface Visitor {

    // 拜访学生信息
    void visit(Student student);

    // 拜访老师信息
    void visit(Teacher teacher);
}   

实现拜访类型(校长和家长)

访问者:校长

public class Principal implements Visitor {private Logger logger = LoggerFactory.getLogger(Principal.class);

    public void visit(Student student) {logger.info("学生信息 姓名:{} 班级:{}", student.name, student.clazz);
    }

    public void visit(Teacher teacher) {logger.info("学生信息 姓名:{} 班级:{} 升学率:{}", teacher.name, teacher.clazz, teacher.entranceRatio());
    }

}  

访问者:家长

public class Parent implements Visitor {private Logger logger = LoggerFactory.getLogger(Parent.class);

    public void visit(Student student) {logger.info("学生信息 姓名:{} 班级:{} 排名:{}", student.name, student.clazz, student.ranking());
    }

    public void visit(Teacher teacher) {logger.info("老师信息 姓名:{} 班级:{} 级别:{}", teacher.name, teacher.clazz, teacher.identity);
    }

}   

数据结构

public class DataView {List<User> userList = new ArrayList<User>();

    public DataView() {userList.add(new Student("小马", "重点班", "一年一班"));
        userList.add(new Student("小张", "重点班", "一年一班"));
        userList.add(new Student("小刘", "普通班", "二年三班"));
        userList.add(new Student("小董", "普通班", "三年四班"));
        userList.add(new Teacher("Tom", "特级教师", "一年一班"));
        userList.add(new Teacher("Jack", "特级教师", "一年一班"));
        userList.add(new Teacher("Rose", "一般老师", "二年三班"));
        userList.add(new Teacher("Bob", "实习老师", "三年四班"));
    }

    // 展现
    public void show(Visitor visitor) {for (User user : userList) {user.accept(visitor);
        }
    }
}

编写测试类

@Test
public void test(){DataView dataView = new DataView();      

    logger.info("\r\n 家长视角拜访:");
    dataView.show(new Parent());     // 家长

    logger.info("\r\n 校长视角拜访:");
    dataView.show(new Principal());  // 校长
}

测试后果

家长视角拜访:12:33:21.336 [main] INFO  o.i.demo.design.visitor.impl.Parent - 学生信息 姓名:小马 班级:一年一班 排名:58
12:33:21.336 [main] INFO  o.i.demo.design.visitor.impl.Parent - 学生信息 姓名:小张 班级:一年一班 排名:17
12:33:21.336 [main] INFO  o.i.demo.design.visitor.impl.Parent - 学生信息 姓名:小刘 班级:二年三班 排名:29
12:33:21.336 [main] INFO  o.i.demo.design.visitor.impl.Parent - 学生信息 姓名:小董 班级:三年四班 排名:67
12:33:21.336 [main] INFO  o.i.demo.design.visitor.impl.Parent - 老师信息 姓名:Tom 班级:一年一班 级别:特级教师
12:33:21.336 [main] INFO  o.i.demo.design.visitor.impl.Parent - 老师信息 姓名:Jack 班级:一年一班 级别:特级教师
12:33:21.337 [main] INFO  o.i.demo.design.visitor.impl.Parent - 老师信息 姓名:Rose 班级:二年三班 级别:一般老师
12:33:21.337 [main] INFO  o.i.demo.design.visitor.impl.Parent - 老师信息 姓名:Bob 班级:三年四班 级别:实习老师

校长视角拜访:12:33:21.338 [main] INFO  o.i.d.design.visitor.impl.Principal - 学生信息 姓名:小马 班级:一年一班
12:33:21.339 [main] INFO  o.i.d.design.visitor.impl.Principal - 学生信息 姓名:小张 班级:一年一班
12:33:21.339 [main] INFO  o.i.d.design.visitor.impl.Principal - 学生信息 姓名:小刘 班级:二年三班
12:33:21.339 [main] INFO  o.i.d.design.visitor.impl.Principal - 学生信息 姓名:小董 班级:三年四班
12:33:21.342 [main] INFO  o.i.d.design.visitor.impl.Principal - 老师信息 姓名:Tom 班级:一年一班 升学率:61.33
12:33:21.343 [main] INFO  o.i.d.design.visitor.impl.Principal - 老师信息 姓名:Jack 班级:一年一班 升学率:83.64
12:33:21.343 [main] INFO  o.i.d.design.visitor.impl.Principal - 老师信息 姓名:Rose 班级:二年三班 升学率:87.57
12:33:21.343 [main] INFO  o.i.d.design.visitor.impl.Principal - 老师信息 姓名:Bob 班级:三年四班 升学率:30.34
  • 通过测试后果能够看到,家长和校长的拜访视角同步,数据也是差异化的。
  • 通过这样的测试后果,能够看到访问者模式的初心和后果,在适宜的场景使用适合的模式,十分有利于程序开发。

总结

因为访问者模式的应用条件较为刻薄,自身构造也较为简单,因而在理论利用中应用频率不是特地高。当零碎中存在一个较为简单的对象构造,且不同访问者对其所采取的操作也不雷同时,能够思考应用访问者模式进行设计。在 XML 文档解析、编译器的设计、简单汇合对象的解决等畛域访问者模式失去了肯定的利用。

好的学习⽅式才好更容易接受常识,学习编程的更须要的不单单是看,⽽是操作。⼆⼗多种设计模式每⼀种都有⾃⼰的设计技巧,也能够说是奇妙之处,这些奇妙的地⽅往往是解决简单难题的最佳视⻆。亲力亲为,能力随心所欲,为了⾃⼰的欲望而努⼒!

退出移动版