访问者模式
一、定义
访问者模式(Visitor Pattern) 是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元索的新的操作。属于行为型模式。
访问者模式被称为最复杂的设计模式,并且使用频率不高,设计模式的作者也评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。访问者模式的基本思想是, 针对系统中拥有固定类型数的对象结构(元素) , 在其内提供一个accept()方法用来接受访问者对象的访问。不同的访问者对同一元素的访问内容不同,使得相同的元素集合可以产生不同的数据结果。accept() 方法可以接收不同的访问者对象然后在内部将自己(元素) 转发到接收到的访问者对象的visit() 方法内。访问者内部对应类型的visit() 方法就会得到回调执行, 对元素进行操作。也就是通过两次动态分发(第一次是对访问者的分发accept() 方法,第二次是对元索的分发visit() 方法) , 才最终将一个具体的元素传递到一个具体的访问者。如此一来,就解耦了数据结构与操作,且数据操作不会改变元素状态。访问者模式的核心是,解耦数据结构与数据操作,使得对元索的操作具备优秀的扩展性。可以通过扩展不同的数据操作类型(访问者)实现对相同元索集的不同的操作。访问者模式主要包含五种角色:
- 抽象访问者(Visitor) :接口或抽象类, 该类提供了对每一个具体元素(Element) 的访问行为visit() 方法, 其参数就是具体的元素(Element) 对象。理论上来说, Visitor的方法个数与元索(Element) 个数是相等的。如果元素(Element) 个数经常变动, 会导致Visitor的方法也要进行变动,此时,该情形并不适用访问者模式;
- 具体访问者(Concrete Visitor) :实现对具体元素的操作;
- 抽象元素(Element) :接口或抽象类, 定义了一个接受访问者访问的方法accept() , 表示所有元索类型都支持被访问者访问;
- 具体元素(Concrete Element) :具体元素类型, 提供接受访问者的具体实现。通常的实现都为:visitor.visit(this) ;
- 结构对象(Object Struture) :该类内部维护了元素集合, 并提供方法接受访问者对该集合所有元素进行操作。
二、访问者模式的案例
访问者模式在生活场景中也是非常当多的, 例如每年年底的KPI考核, KPI考核标准是相对稳定的, 但是参与KPI考核的员工可能每年都会发生变化, 那么员工就是访问者。我们平时去食堂或者餐厅吃饭,餐厅的菜单和就餐方式是相对稳定的,但是去餐厅就餐的人员是每天都在发生变化的,因此就餐人员就是访问者。
当系统中存在类型数目稳定(固定)的一类数据结构时,可以通过访问者模式方便地实现对该类型所有数据结构的不同操作,而又不会数据产生任何副作用(脏数据)。简言之,就是对集合中的不同类型数据(类型数量稳定)进行多种操作,则使用访问者模式。下面总结一下访问者模式的适用场景:
- 数据结构稳定,作用于数据结构的操作经常变化的场景;
- 需要数据结构与数据操作分离的场景;
- 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。
1.标准写法
// 抽象元素 public interface IElement { void accept(IVisitor visitor); }
// 具体元素 public class ConcreteElementA implements IElement { public void accept(IVisitor visitor) { visitor.visit(this); } public String operationA() { return this.getClass().getSimpleName(); } }
// 具体元素 public class ConcreteElementB implements IElement { public void accept(IVisitor visitor) { visitor.visit(this); } public int operationB() { return new Random().nextInt(100); } }
// 抽象访问者 public interface IVisitor { void visit(ConcreteElementA element); void visit(ConcreteElementB element); }
// 具体访问者 public class ConcreteVisitorA implements IVisitor { public void visit(ConcreteElementA element) { String result = element.operationA(); System.out.println("result from " + element.getClass().getSimpleName() + ": " + result); } public void visit(ConcreteElementB element) { int result = element.operationB(); System.out.println("result from " + element.getClass().getSimpleName() + ": " + result); } }
// 具体访问者 public class ConcreteVisitorB implements IVisitor { public void visit(ConcreteElementA element) { String result = element.operationA(); System.out.println("result from " + element.getClass().getSimpleName() + ": " + result); } public void visit(ConcreteElementB element) { int result = element.operationB(); System.out.println("result from " + element.getClass().getSimpleName() + ": " + result); } }
// 结构对象 public class ObjectStructure { private List<IElement> list = new ArrayList<IElement>(); { this.list.add(new ConcreteElementA()); this.list.add(new ConcreteElementB()); } public void accept(IVisitor visitor) { for (IElement element : this.list) { element.accept(visitor); } } }
public class Test { public static void main(String[] args) { ObjectStructure collection = new ObjectStructure(); System.out.println("ConcreteVisitorA handle elements:"); IVisitor visitorA = new ConcreteVisitorA(); collection.accept(visitorA); System.out.println("------------------------------------"); System.out.println("ConcreteVisitorB handle elements:"); IVisitor visitorB = new ConcreteVisitorB(); collection.accept(visitorB); } }
2.利用访问者模式实现KPI考核的场景:
每到年底,管理层就要开始评定员工一年的工作绩效,员工分为工程师和经理;管理层有CEO和CTO。那么CTO关注工程师的代码量、经理的新产品数量; CEO关注的是工程师的KPI和经理的KPI以及新产品数量。由于CEO和CTO对于不同员工的关注点是不一样的, 这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。
Employee类定义了员工基本信息及一个accept() 方法, accept() 方法表示接受访问者的访问,由具体的子类来实现。访问者是个接口,传入不同的实现类,可访问不同的数据。
//元素抽象 public abstract class Employee { public String name; public int kpi; //员工KPI public Employee(String name) { this.name = name; kpi = new Random().nextInt(10); } //接收访问者的访问 public abstract void accept(IVisitor visitor); }
//// 具体元素:工程师 public class Engineer extends Employee { public Engineer(String name) { super(name); } public void accept(IVisitor visitor) { visitor.visit(this); } //考核指标是每年的代码量 public int getCodeLines(){ return new Random().nextInt(10* 10000); } }
//// 具体元素:项目经理 public class Manager extends Employee { public Manager(String name) { super(name); } public void accept(IVisitor visitor) { visitor.visit(this); } //考核的是每年新产品研发数量 public int getProducts(){ return new Random().nextInt(10); } }
// 抽象访问者 public interface IVisitor { void visit(Engineer engineer); void visit(Manager manager); }
//具体访问者:CEO public class CEOVistitor implements IVisitor { public void visit(Engineer engineer) { System.out.println("工程师" + engineer.name + ",KIP:" + engineer.kpi); } public void visit(Manager manager) { System.out.println("经理:" + manager.name + ",KPI:" + manager.kpi + ",产品数量:" + manager.getProducts()); } }
//具体访问者:CTO public class CTOVistitor implements IVisitor { public void visit(Engineer engineer) { System.out.println("工程师" + engineer.name + ",代码行数:" + engineer.getCodeLines()); } public void visit(Manager manager) { System.out.println("经理:" + manager.name + ",产品数量:" + manager.getProducts()); } }
//数据结构 public class BusinessReport { private List<Employee> employees = new LinkedList<Employee>(); public BusinessReport() { employees.add(new Manager("产品经理A")); employees.add(new Engineer("程序员A")); employees.add(new Engineer("程序员B")); employees.add(new Engineer("程序员C")); employees.add(new Manager("产品经理B")); employees.add(new Engineer("程序员D")); } public void showReport(IVisitor visitor){ for (Employee employee : employees) { employee.accept(visitor); } } }
public class Test { public static void main(String[] args) { BusinessReport report = new BusinessReport(); System.out.println("==========CEO看报表==============="); report.showReport(new CEOVistitor()); System.out.println("==========CTO看报表==============="); report.showReport(new CTOVistitor()); } }
访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以访问者,只要新实现一个访问者接口的类,从而达到数据对象与数据操作分离的效果,如果不使用访问者模式,而又不想对不同的元素进行不同的操作,那么必定使用 if-else 和类型转换和类型转换,这使得代码难以升级维护;说到访问者模式就有一个扩展的点可以说下,那就是分派;分派有静态分派和动态分派以及双分派;
1.静态分派
静态分派就是按照变量的静态类型进行分派,从而静态分派在编译时期就可以确定方法的版本。而静态分派最典型的应用就这段代码。
public class Main { public void test(String string){ System.out.println("string" + string); } public void test(Integer integer){ System.out.println("integer" + integer); } public static void main(String[] args) { String string = "1"; Integer integer = 1; Main main = new Main(); main.test(integer); main.test(string); } }
在静态分派判断的时候我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,那么这就是多分派的概念。因为我们有一个以上的考量标准。所以Java是静态多分派的语言。
2.动态分派
对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。动态分派最典型的应用就是多态的特性。举个例子,来看下面的这段代码。
public class Main { public static void main(String[] args) { Person man = new Man(); Person woman = new WoMan(); man.test(); woman.test(); } }
public interface Person { void test(); }
public class Man implements Person { public void test() { System.out.println("男人"); } }
public class WoMan implements Person { public void test() { System.out.println("女人"); } }
这段程序输出结果为依次打印男人和女人,然而这里的 test 方法版本,就无法根据Man和Woman的静态类型去判断了。他们的静态类型都是 Person接口,根本无从判断。
显然,产生的输出结果,就是因为 test 方法的版本是在运行时判断的,这就是动态分派。动态分派判断的方法是在运行时获取到Man和 Woman的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念。这时我们的考量标准只有一个,即变量的实际引用类型。相应的,这说明Java是动态单分派的语言。
3.访问者模式中的伪动态双分派
通过前面分析, 我们知道Java是静态多分派、动态单分派的语言。Java底层不支持动态的双分派。但是通过使用设计模式, 也可以在Java语言里实现伪动态双分派。在访问者模式中使用的就是伪动态双分派。所谓动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。回到前面的KPI考核业务场景当中;
public void showReport(IVisitor visitor){ for (Employee employee : employees) { employee.accept(visitor); } }
这里就是依据Employee和I Visitor两个实际类型决定了showReport() 方法的执行结果,从而决定了accept() 方法的动作。分析accept() 方法的调用过程:当调用accept() 方法时, 根据Employee的实际类型决定是调用Engineer还是Manager 的accept() 方法。这时accept() 方法的版本已经确定, 假如是Engineer, 它的accept() 方法是调用这行代码。visitor.visit(this) ;此时的this是Engineer类型, 所以对应的是I Visitor接口的visit(Engineer engineer) 方法, 此时需要再根据访问者的实际类型确定visit() 方法的版本, 如此一来, 就完成了动态双分派的过程。以上的过程就是通过两次动态双分派, 第一次对accept() 方法进行动态分派, 第二次对访问者的visit 方法进行动态分派, 从而达到了根据两个实际类型确定一个方法的行为的效果。
三、总结
优点:
- 解耦了数据结构与数据操作,使得操作集合可以独立变化;
- 扩展性好:可以通过扩展访问者角色,实现对数据集的不同操作;
- 元素具体类型并非单一,访问者均可操作;
- 各角色职责分离,符合单一职责原则。
缺点:
- 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,连背了开闭原则;
git源码:https://gitee.com/TongHuaShuShuoWoDeJieJu/design_pattern.git