访问者模式
1.访问者模式是什么
1.百度百科
访问者模式(Visitor Pattern)表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
2.维基百科
In object-oriented programming and software engineering, the visitor design pattern is a way of separating an algorithm from an object structure on which it operates. A practical result of this separation is the ability to add new operations to existent object structures without modifying the structures. It is one way to follow the open/closed principle.
3.lz理解
在父类中添加一个抽象方法 那么所欲的子类会产生改变。那么在不更改该父类和子类的结构情况下添加一个作用于子类元素的方法。
在被访问者中创建一个方法的引用。引用调用的是访问者的访问方法。当被访问者调用该方法时,被访问者对象会将本身传递给访问者访问。从而达到访问对象的目的。这种访问方式是动态的。
关键点:
1.对象结构不改变,比如继承结构、组合结构等。
2.需要给对象成员变量添加一个新的操作成员变量的方法。
3.用于统一操作所有元素。类似父类操作成员变量的抽象方法。
4.核心角色
抽象访问者角色(Visitor): 它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变(不能改变的意思是说,如果元素类的个数经常改变,则说明不适合使用访问者模式)。也就是说该接口的实现类的数量需要固定。
具体访问者角色(ConcreteVisitor):它需要给出对每一个元素类访问时所产生的具体行为。也就是实现vistitor接口.
抽象元素角色、抽象被访问角色(Element):元素接口,它定义了一个接受访问者(accept)的方法,其意义是指,每一个元素都要可以被访问者访问。也就是需要将访问者的的访问方式“挂入”被访问角色的对象中。
具体元素角色、具体被访问角色(ConcreteElement):具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。也就是将被访问者自己和访问者“挂上”。
结构对象角色(ObjectStructure):这个便是定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。
也就是将一系列被访问者放入容器中,方便统一管理。
2.访问者模式解决了什么问题
项目的抽象结构不改变的情况下是需要对其实现对象中的元素增加一个新的方法。
访问者模式的优点
符合单一职责原则:具体元素角色也就是Employee抽象类的两个子类负责数据加载,而Visitor类负责报表的呈现,两个不同的职责非常明确的分离开来,各自演绎变化;
优秀的扩展性:由于职责分开,继续增加对数据的操作是非常快捷的,例如,想要生成一份不同格式的报表,直接在Visitor中增加一个方法;
灵活性非常高:当数据汇总时,如果要统计所有员工的工资,而且不同类型的员工工资系数不同,则可以通过访问者模式实现。
3.访问者模式用法
以公司员工举例子,员工有名称绩效年龄功能工资等属性。每个员工各个属性不一样。而公司各个部门关注的员工属性也不一样但是都是基于者三个属性的组合
员工抽象类。抽象元素角色、抽象被访问角色。该角色是被访问的。有一个访问者的钩子。调用访问者的方法将该对象传递给访问者访问。
public interface Employee {
//访问
void accept(DepartmentManager departmentManager);
//获取姓名
String getName();
//获取年龄
int getAge();
//获取工资
double getSalary();
//获取绩效
int getPerformance();
}
具体的员工、具体元素角色、具体被访问角色
为了简便这里只有一种最普通的员工。
public class Emp implements Employee {
private String name;
private int age;
private double salary;
private int performance;
public Emp(String name, int age, double salary, int performance) {
super();
this.name = name;
this.age = age;
this.salary = salary;
this.performance = performance;
}
@Override
public String toString() {
return "Emp [name=" + name + ", age=" + age + ", salary=" + salary + ", performance=" + performance + "]";
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setSalary(double salary) {
this.salary = salary;
}
public void setPerformance(int performance) {
this.performance = performance;
}
@Override
public void accept(DepartmentManager departmentManager) {
departmentManager.view(this);
}
@Override
public String getName() {
return name;
}
@Override
public int getAge() {
return age;
}
@Override
public double getSalary() {
return salary;
}
@Override
public int getPerformance() {
return performance;
}
}
部门经理、抽象访问者角色。定义被访问者回调那个方法将自己传送给访问者访问。
public interface DepartmentManager {
public void view(Emp emp);
}
HR、PM 具体访问者角色 含有部分拓展业务。
//hr部门 统计平均工资 统计平均年龄 统计员工数 员工工资
public class HR implements DepartmentManager {
private double totalAge;
private double totalSalary;
private double num = 0.0;
@Override
public void view(Emp emp) {
totalAge += emp.getAge();
totalSalary += emp.getSalary();
num++;
}
public double avgAge() {
return totalAge / num;
}
public double avgSalary() {
return totalSalary / num;
}
public double getTotalAge() {
return totalAge;
}
public double getTotalSalary() {
return totalSalary;
}
public double getNum() {
return num;
}
}
public class PM implements DepartmentManager {
private double totalPerformance;
private double totalSalary;
private double num = 0;
@Override
public void view(Emp emp) {
totalPerformance += emp.getPerformance();
totalSalary += emp.getSalary();
num++;
}
public double avgPerformance() {
return totalPerformance/num;
}
public double PerformanceForSalary() {
return totalSalary/totalPerformance;
}
public double getTotalPerformance() {
return totalPerformance;
}
public double getTotalSalary() {
return totalSalary;
}
public double getNum() {
return num;
}
}
员工列表角色、结构对象角色
用于批量处理大量被访问角色的聚合任务、这个角色并不是必须的。
public class EmpList {
private List<Employee> empList = new ArrayList<Employee>();
public void addEmp(Employee emp) {
empList.add(emp);
}
public void show (DepartmentManager departmentManager) {
for(Employee emp:empList) {
emp.accept(departmentManager);
}
}
}
这里我们用测试下该类的执行效果
public class Report {
public static void main(String[] args) {
//员工大会开始
EmpList empList = new EmpList();
//员工开始入场
Emp emp1 = new Emp("罡大爷",36,18000.0,86);
empList.addEmp(emp1);
Emp emp2 = new Emp("张大爷",29,28000.0,90);
empList.addEmp(emp2);
Emp emp3 = new Emp("封大爷",30,25000.0,82);
empList.addEmp(emp3);
Emp emp4 = new Emp("任大爷",24,15000.0,79);
empList.addEmp(emp4);
Emp emp5 = new Emp("王二爷",22,12000.0,70);
empList.addEmp(emp5);
//员工入场完毕
//下面有请人力资源部经理作报告
HR hr = new HR();
empList.show(hr);
System.out.println("目前我司员工总计:"+hr.getNum()+"人,平均年龄:"+hr.avgAge()+" 平均工资:"+hr.avgSalary()+" 实发工资总计:"+hr.getTotalSalary());
//下面有请项目经理做报告
PM pm = new PM();
empList.show(pm);
System.out.println("我司全体员工平均绩效为:"+pm.avgPerformance()+" 每点绩效付出的工资为:"+pm.PerformanceForSalary());
//员工大会结束
}
}
结果
目前我司员工总计:5.0人,平均年龄:28.2 平均工资:19600.0 实发工资总计:98000.0
我司全体员工平均绩效为:81.4 每点绩效付出的工资为:240.7862407862408
这里对数字的类型选择并没有考究希望读者见谅。
4.访问者模式的问题
被访问者细节公开 访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是访问者关注了其他类的内部细节,这是迪米特法则所不建议的;
具体被访问者数量变动困难 具体元素角色的增加、删除、修改都是比较困难的,如上例中增加一个年龄字段,则Visitor就需要修改,如果有多个Visitor那么会改动很大;
违背了依赖倒置的原则 访问者依赖的是具体元素,而不是抽象元素,这违背了依赖倒置原则,特别是在面向对象的编程中,抛弃对接口的依赖,而直接依赖实现类,扩展比较困难。上例中的抽象部门经理角色依赖的就是具体的被访问者角色。相当于抽象依赖具体,这是违反依赖倒置原则的。
5.访问者模式总结
使用场景:
一个对象结构包含很多类对象,他们有不同的接口,想对这些对象实施一些依赖于其具体类的操作,也就是用迭代器模式已经不能胜任的场景。
需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。
碎碎念:
访问者模式确实是比较难以理解的设计模式。其难点在于访问者和被访问者互相挂钩这块的逻辑。当调用被访问者的访问方法时,被访问者会调用访问者的方法并将自己的实例作为参数传入访问者的方法中,让访问者调用被访问的传来的实例以便于完成其他操作或者业务的添加。动态的添加更多的访问者可以在完全不破坏被访问者的前提下增加被访问者的业务逻辑。其拓展性也是比较优秀的。但是好处必然伴随着风险,就是当被访问者添加新的成员变量时,当旧访问者想读取新成员变量就会产生巨大的灾难,所以说这种拓展只适合有固定接口固定实现数量的被访问对象的方法拓展。并不适合实现数量不固定或者其成员变量不固定的对象进行方法拓展。从这个模式中我真的感觉到了自己基础知识的不扎实。对JDK动态选择方法的原理一无所知。我建议同学们要学习访问者模式必须了解下动态分派和静态分派。这样理解访问者模式会轻松很多。