访问者模式
案例
1 2
| 年底,CEO和CTO开始评定员工一年的工作绩效,员工分为工程师和经理,CTO关注工程师的代码量、经理的新产品数量;CEO关注的是工程师的KPI和经理的KPI以及新产品数量。 由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。
|
传统方式
设计一个Man和Woman类,在类中判断是成功还是失败。
问题:
- 如果系统比较小,还是ok的,但是考虑系统增加越来越多新的功能时,对代码改动较大,违反了ocp原则
- 扩展性不好,比如增加了新的人员类型,或者管理方法,都不好做
基本介绍
- 访问者模式(
Visitor Pattern
),封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
- 主要将数据结构与数据操作分离,解决数据结构和操作耦合性问题
- 访问者模式的基本工作原理是:在被访问的类里面加一个对外提供接待访问者的接口
- 访问者模式主要应用场景是:需要对一个对象结构中的对象进行很多不同操作(这些操作彼此没有关联),同时需要避免让这些操作”污染”这些对象的类,可以选用访问者模式解决
角色
visitor
是抽象访问者。为该对象结构中的ConcreteElement的每一个类声明一个visit操作
Concretevisitor
:是一个具体的访问值实现每个有Visitor声明的操作,是每个操作实现的部分.
ObjectStructure
:能枚举它的元素,可以提供一个高层的接口,用来允许访问者访问元素
Element
:定义一个accept方法,接收二个访问者对象
ConcreteElement
为具体元素,实现了accet方法
解决问题
由于CEO和CTO对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。
1 2 3 4 5 6 7 8 9 10 11 12
| public abstract class Staff {
public String name; public int kpi;
public Staff(String name) { this.name = name; kpi = new Random().nextInt(10); } public abstract void accept(Visitor visitor); }
|
Staff 类定义了员工基本信息及一个 accept 方法,accept 方法表示接受访问者的访问,由子类具体实现。Visitor 是个接口,传入不同的实现类,可访问不同的数据。下面看看工程师和经理的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Engineer extends Staff {
public Engineer(String name) { super(name); }
@Override public void accept(Visitor visitor) { visitor.visit(this); } public int getCodeLines() { return new Random().nextInt(10 * 10000); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Manager extends Staff {
public Manager(String name) { super(name); }
@Override public void accept(Visitor visitor) { visitor.visit(this); } public int getProducts() { return new Random().nextInt(10); } }
|
工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。Staff、Engineer、Manager 3个类型就是对象结构,这些类型相对稳定,不会发生变化。
然后将这些员工添加到一个业务报表类中,公司高层可以通过该报表类的 showReport
方法查看所有员工的业绩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class BusinessReport {
private List<Staff> mStaffs = new LinkedList<>();
public BusinessReport() { mStaffs.add(new Manager("经理-A")); mStaffs.add(new Engineer("工程师-A")); mStaffs.add(new Engineer("工程师-B")); mStaffs.add(new Engineer("工程师-C")); mStaffs.add(new Manager("经理-B")); mStaffs.add(new Engineer("工程师-D")); }
public void showReport(Visitor visitor) { for (Staff staff : mStaffs) { staff.accept(visitor); } } }
|
Visitor 类型的定义, Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数
1 2 3 4 5 6 7 8
| public interface Visitor {
void visit(Engineer engineer);
void visit(Manager manager); }
|
首先定义了一个 Visitor 接口,该接口有两个 visit 函数,参数分别是 Engineer、Manager,也就是说对于 Engineer、Manager 的访问会调用两个不同的方法,以此达成区别对待、差异化处理。具体实现类为 CEOVisitor、CTOVisitor类
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class CEOVisitor implements Visitor { @Override public void visit(Engineer engineer) { System.out.println("工程师: " + engineer.name + ", KPI: " + engineer.kpi); }
@Override public void visit(Manager manager) { System.out.println("经理: " + manager.name + ", KPI: " + manager.kpi + ", 新产品数量: " + manager.getProducts()); } }
|
这里使用到了双分派,即首先在客户端程序中,将具体的状态作为参数传递给Staff,这是第一次分派然后Staff类调用了作为参数的具体方法中的方法visit,同时将自己的this作为参数传入,第二次分派
在CEO的访问者中,CEO关注工程师的 KPI,经理的 KPI 和新产品数量,通过两个 visitor 方法分别进行处理。如果不使用 Visitor 模式,只通过一个 visit 方法进行处理,那么就需要在这个 visit 方法中进行判断,然后分别处理。
1 2 3 4 5 6 7 8 9 10 11
| public class CTOVisitor implements Visitor { @Override public void visit(Engineer engineer) { System.out.println("工程师: " + engineer.name + ", 代码行数: " + engineer.getCodeLines()); }
@Override public void visit(Manager manager) { System.out.println("经理: " + manager.name + ", 产品数量: " + manager.getProducts()); } }
|
重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
1 2 3 4 5 6 7 8 9 10 11
| public class Client {
public static void main(String[] args) { BusinessReport report = new BusinessReport(); System.out.println("=========== CEO看报表 ==========="); report.showReport(new CEOVisitor()); System.out.println("=========== CTO看报表 ==========="); report.showReport(new CTOVisitor()); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| =========== CEO看报表 =========== 经理: 经理-A, KPI: 9, 新产品数量: 0 工程师: 工程师-A, KPI: 6 工程师: 工程师-B, KPI: 6 工程师: 工程师-C, KPI: 8 经理: 经理-B, KPI: 2, 新产品数量: 6 工程师: 工程师-D, KPI: 6 =========== CTO看报表 =========== 经理: 经理-A, 产品数量: 3 工程师: 工程师-A, 代码行数: 62558 工程师: 工程师-B, 代码行数: 92965 工程师: 工程师-C, 代码行数: 58839 经理: 经理-B, 产品数量: 6 工程师: 工程师-D, 代码行数: 53125
|
Staff 扮演了 Element 角色,而 Engineer 和 Manager 都是 ConcreteElement;CEOVisitor 和 CTOVisitor 都是具体的 Visitor 对象;而 BusinessReport 就是 ObjectStructure;Client就是客户端代码。
访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。
注意事项
- 优点
- 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
- 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
- 缺点
- 具体元素对访问者公布细节,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的,这样造成了具体元素变更比较困难
- 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素
- 因此,如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的.