定义
Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
访问者代表的是在集合元素上执行的某些操作。访问者可以让你不改变元素类的前提下,新增作用于这些元素的操作。
实列
想象一下,假如我们将博物馆比做一个集合,那它里面陈列的各种文物便是元素。
程序中我们要操作或访问集合中的元素一般是主动获取元素列表然后进行遍历操作,而生活中对于不熟悉博物馆布局的访客,他如何遍历文物呢?
显然,我们不能将文物搬出来,参观完后再返回去,而是进到博物馆里,然后由导游依次向我们展示各种文物。这就可以访问各种文物了,在程序中这种访问方式被称为访问者模式——它由集合对象向访问者开放入口,并向它依次展示其元素。
故事
上周,我安排了一同事将公司员工信息中的管理者和普通员工的信息导出成Excel。
员工信息的结构像一棵树但只有两层,第一层是管理者,第二成是普通员工,普通员工是管理者的下属节点。
导出的Excel中管理者和普通员工的字段信息有所不同,管理者的信息字段要求有一个部门字段,而普通员工的信息字段要求有一个入职时间字段,其它字段姓名、年龄都相同。
我还特意告诉他注意程序的扩展性,该信息结构以后会用于不同的场景,比如说下次可能要让他计算每个人的薪资。
但是,结果还是不符合我的期望。
伪代码如下:
public class Company {
protected List<Staff> staffs = new LinkedList<>();
protected void exportExcel(){
for (Staff staff : staffs) {
if(staff instanceof Manager){
appendToExcel(((Manager) staff).getDepartment(),staff.getAge()+ staff.getName());
}else if(staff instanceof Employee){
appendToExcel(((Employee) staff).getEntryDate(),staff.getAge()+ staff.getName());
}
}
}
public void add(Staff staff) {
staffs.add(staff);
}
public void remove(Staff element) {
staffs.remove(element);
}
}
问题
从上面的代码中我们可以看出,导出集合元素信息的方式是在对象结构Company中采用foreach来实现的,而且使用了instanceof来区分不同的元素。
但是,这样实现会存在几个问题。首先是耦合问题,Company直接耦合了具体的员工类型Manager和Employee以及导出操作。
其次是复杂度问题,如果该集合的结构有很多层级,那么势必增加功能实现时的复杂度。
最后是最重要的扩展性问题,如果我们要将其导出成PDF亦或是根据同样的结构计算员工的薪资,那么不但遍历的逻辑会重复,而且我们要修改集合对象或其元素的代码。
所以,有没有一种方式可以不修改集合对象的前提下,便可以新增功能?这便是访问者模式。
方案
访问者模式是一种行为型设计模式,它对操作集合元素的具体行为进行了抽象。
如果让该模式来重构上面的代码 那么它会这样实现:首先,它会将具体的行为如appendToExcel从集合对象中分离出去,只保留对元素的遍历结构;
然后,会将这些具体的行为抽象成访问者接口,用该接口中的方法替换遍历中的具体行为;最后,会将exportExcel重命名为accept(Visitor visitor)接受访问者访问该集合对象中的元素。
同样,如果元素本身也是集合对象的话,那么也是采用同样的方式解藕与具体行为的依赖。
这样,新增新的访问行为时,便不需要修改集合对象,只需实现访问者接口便可。
实现
接下来,我们使用访问者模式重新实现一下故事中的程序。
首先,我们将集合中操作元素的具体行为抽象成一个访问者接口。
/**访问者接口*/
public interface Visitor {
void visit(Manager manager);
void visit(Employee manager);
}
然后,让集合对象实现可访问者接口(Visitable),接受访问者访问其内部元素。
public class Company implements Visitable{
protected List<Staff> staffs = new LinkedList<>();
@Override
public void accept(Visitor visitor){
for (Staff staff : staffs) {
if(staff instanceof Manager){
visitor.visit((Manager)staff);
}else if(staff instanceof Employee){
visitor.visit((Employee)staff);
}
}
}
public void add(Staff staff) {
staffs.add(staff);
}
public void remove(Staff element) {
staffs.remove(element);
}
}
接着,我们新建一个类继承访问者接口,实现员工信息导出成Excel。当然,如果我们也要导出成PDF也是同样的方式。
public class ExcelVisitor implements Visitor{
@Override
public void visit(Manager staff) {
appendToExcel(((Manager) staff).getDepartment(),staff.getAge()+ staff.getName());
}
@Override
public void visit(Employee staff) {
appendToExcel(((Employee) staff).getEntryDate(),staff.getAge()+ staff.getName());
}
}
最后,我们在来看看客户端如何使用访问者模式将集合对象中的数据导出。
public class Client {
public static void main(String[] args) {
Company company = new Company();
company.add(new Manager());
company.add(new Employee());
//导出Excel
ExcelVisitor visitor = new ExcelVisitor();
company.accept(visitor);
//导出PDF
PDFVisitor pdfVisitor = new PDFVisitor();
company.accept(pdfVisitor);
}
}
结构
avatar对象结构(ObjectStructure):它可以是一个集合,也可以是由不同组成部分构成的复合体,主要负责遍历其结构中的元素并将它传递给访问者。
抽象访问者角色(Visitor):该抽象类或接口是对元素操作行为的抽象,声明了操作不同元素的方法。
具体访问者角色(ConcreteVisitor):它实现自抽象访问者,会对元素进行某些操作。
抽象可访问角色(Visitable):该接口声明了一个操作,该操作可以接受访问者的访问,实现该接口的对象会将其内部元素传递给访问者。
具体可访问角色(ConcreteVisitable):是一个集合对象或者集合中的元素,如果该元素也是一个集合的话。
总结
当一个复合对象中的元素,有很多不同的应用场景的时候,我们应该将遍历行为封装在对象中并将具体的行为分离出去用抽象访问者的方法将其替代。
这样,在我们扩展新功能时,便不需要修改该复合对象了。
网友评论