设计模式笔记--访问者模式

常用设计模式有23中,分为:

创建型模式(主要用于创建对象)

1、单例模式    2、工厂方法模式    3、抽象工厂模式    4、建造者模式     5、原型模式 
行为型模式 (主要用于描述对象或类是怎样交互和怎样分配职责)

1、模板方法模式  2、中介者模式  3、命令模式    4、责任链模式   5、策略模式   6、迭代器模式  

7、观察者模式      8、备忘录模式   9、访问者模式   10、状态模式   11、解释器模式

结构型模式(主要用于处理类或对象的组合)

1、代理模式  2、装饰模式   3、适配器模式   4、组合模式   5、外观模式(门面模式)   6、享元模式    7、桥梁模式



访问者模式  

访问者模式(Visitor Pattern)是一个相对简单的模式,其定义如下:

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

设计模式笔记--访问者模式_第1张图片


● Visitor——抽象访问者
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。

● ConcreteVisitor——具体访问者
它影响访问者访问到一个类后该怎么干,要做什么事情。

● Element——抽象元素
接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。

● ConcreteElement——具体元素
实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。

● ObjectStruture——结构对象
元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。  


通用源码

先看抽象元素,如代码清单25-11所示。
代码清单25-11 抽象元素
public abstract class Element {
//定义业务逻辑
public abstract void doSomething();
//允许谁来访问
public abstract void accept(IVisitor visitor);
}
抽象元素有两类方法:一是本身的业务逻辑,也就是元素作为一个业务处理单元必须完成的职责;另外一个是允许哪一个访问者来访问。


具体元素
代码清单25-12 具体元素
public class ConcreteElement1 extends Element{
//完善业务逻辑
public void doSomething(){
//业务处理
}
//允许那个访问者访问
public void accept(IVisitor visitor){
visitor.visit(this);
}
}
public class ConcreteElement2 extends Element{
//完善业务逻辑
public void doSomething(){
//业务处理
}
//允许那个访问者访问
public void accept(IVisitor visitor){
visitor.visit(this);
}
}


我们再来看抽象访问者,一般是有几个具体元素就有几个访问 方法,如代码清单 25-13所示。
代码清单25-13 抽象访问者
public interface IVisitor {
//可以访问哪些对象
public void visit(ConcreteElement1 el1);
public void visit(ConcreteElement2 el2);
}


具体访问者如代码清单25-14所示。
代码清单25-14 具体访问者
public class Visitor implements IVisitor {
//访问el1元素
public void visit(ConcreteElement1 el1) {
el1.doSomething();
}
//访问el2元素
public void visit(ConcreteElement2 el2) {
el2.doSomething();
}
}


结构对象是产生出不同的元素对象,我们使用工厂方法模式来模拟,如代码清单25-15 所示。
代码清单25-15 结构对象
public class ObjectStruture {
//对象生成器,这里通过一个工厂方法模式模拟
public static Element createElement(){
Random rand = new Random();
if(rand.nextInt(100) > 50){
return new ConcreteElement1();
}else{
return new ConcreteElement2();
}
}
}


进入了访问者角色后,我们对所有的具体元素的访问就非常简单了,我们通过一个场景类模拟这种情况,如代码清单25-16所示。
代码清单25-16 场景类
public class Client {
public static void main(String[] args) {
for(int i=0;i<10;i++){
//获得元素对象
Element el = ObjectStruture.createElement();
//接受访问者访问
el.accept(new Visitor());
}
}
}


通过增加访问者,只要是具体元素就非常容易访问,对元素的遍历就更加容易了,甭管它是什么对象,只要它在一个容器中,都可以通过访问者来访问,任务集中化。
这就是访问者模式。  

优点 

●  符合单一职责原则
具体元素角色也就是Employee抽象类的两个子类负责数据的加载,而Visitor类则负责报表的展现,两个不同的职责非常明确地分离开来,各自演绎变化。

● 优秀的扩展性

● 灵活性非常高


缺点
● 具体元素对访问者公布细节
访问者要访问一个类就必然要求这个类公布一些方法和数据,也就是说访问者关注了其他类的内部细节,这是迪米特法则所不建议的。  

● 具体元素变更比较困难
具体元素角色的增加、删除、修改都是比较困难的  

● 违背了依赖倒置转原则 
访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。  


使用场景  

● 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。

● 需要对一个对象结构中的对象进行很多不同并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。  


业务规则要求遍历多个不同的对象时一定要考虑使用访问者模式


示例  查看公司员工
设计模式笔记--访问者模式_第2张图片


访问者接口IVisitor程序,如代码清单25-5所示。
代码清单25-5 访问者接口
public interface IVisitor {
//首先,定义我可以访问普通员工
public void visit(CommonEmployee commonEmployee);
//其次,定义我还可以访问部门经理
public void visit(Manager manager);
} 
该接口的意义是:该接口可以访问两个对象,一个是普通员工,一个是高层员工   


访问者实现
public class Visitor implements IVisitor {
//访问普通员工,打印出报表
public void visit(CommonEmployee commonEmployee) {
System.out.println(this.getCommonEmployee(commonEmployee));
}
//访问部门经理,打印出报表
public void visit(Manager manager) {
System.out.println(this.getManagerInfo(manager));
}
//组装出基本信息
private String getBasicInfo(Employee employee){
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女":"男info = info + "薪水:" + employee.getSalary() + "\t";
return info;
}
//组装出部门经理的信息
private String getManagerInfo(Manager manager){
String basicInfo = this.getBasicInfo(manager);
String otherInfo = "业绩:"+manager.getPerformance() + "\t";
return basicInfo + otherInfo;
}
//组装出普通员工信息
private String getCommonEmployee(CommonEmployee commonEmployee){
String basicInfo = this.getBasicInfo(commonEmployee);
String otherInfo = "工作:"+commonEmployee.getJob()+"\t";
return basicInfo + otherInfo;
}
}
在具体的实现类中,定义了两个私有方法,作用就是产生需要打印的数据和格式,然后 在访问者访问相关的对象时产生这个报表。


抽象员工Employee稍有修改,如代码清单25-7所 示。
代码清单25-7 抽象员工类
public abstract class Employee {
public final static int MALE = 0; //0代表是男性
public final static int FEMALE = 1; //1代表是女性
//甭管是谁,都有工资
private String name;
//只要是员工那就有薪水
private int salary;
//性别很重要
private int sex;
//以下是简单的getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
//我允许一个访问者访问
public abstract void accept(IVisitor visitor);
}
增加了accept方法,接受访问者的访问。
 

我们继续来看员工实现类,普通员工代码清单25-8所示。
代码清单25-8 普通员工
public class CommonEmployee extends Employee {
//工作内容,这非常重要,以后的职业规划就是靠它了
private String job;
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
//我允许访问者访问
@Override
public void accept(IVisitor visitor){
visitor.visit(this);
}
}
上面是普通员工的实现类,该类的accept方法很简单,这个类就把自身传递过去,也就 是让访问者访问本身这个对象。


再看Manager类,如代码清单25-9所示。
代码清单25-9 管理层员工
public class Manager extends Employee {
//这类人物的职责非常明确:业绩
private String performance;
public String getPerformance() {
return performance;
}
public void setPerformance(String performance) {
this.performance = performance;
}
//部门经理允许访问者访问
@Override
public void accept(IVisitor visitor){
visitor.visit(this);
}
}


所有的业务定义都已经完成,我们来看看怎么模拟这个逻辑,如代码清单25-10所示。
public class Client {
public static void main(String[] args) {
for(Employee emp:mockEmployee()){
emp.accept(new Visitor());
}
}
//模拟出公司的人员情况,我们可以想象这个数据是通过持久层传递过来的
public static List mockEmployee(){
List empList = new ArrayList();
//产生张三这个员工
CommonEmployee zhangSan = new CommonEmployee();
zhangSan.setJob("编写Java程序,绝对的蓝领、苦工加搬运工");
zhangSan.setName("张三");
zhangSan.setSalary(1800);
zhangSan.setSex(Employee.MALE);
empList.add(zhangSan);
//产生李四这个员工
CommonEmployee liSi = new CommonEmployee();
liSi.setJob("页面美工,审美素质太不流行了!");
liSi.setName("李四");
liSi.setSalary(1900);
liSi.setSex(Employee.FEMALE);
empList.add(liSi);
//再产生一个经理
Manager wangWu = new Manager();
wangWu.setName("王五");
wangWu.setPerformance("基本上是负值,但是我会拍马屁呀");
wangWu.setSalary(18750);
wangWu.setSex(Employee.MALE);
empList.add(wangWu);
return empList;
}
} 

只要再产生一个IVisitor的实现类就可以产生一个新的报表格式,而其他的类都不用修改,
这就是访问者模式的优势所在  


访问者模式的扩展 
 
1)统计功能   
设计模式笔记--访问者模式_第3张图片


仔细看IVisitor接口,增加了一个getTotalSalary方法  


IVisitor实现类  
代码清单25-18 具体访问者
public class Visitor implements IVisitor {
//部门经理的工资系数是5
private final static int MANAGER_COEFFICIENT = 5;
//员工的工资系数是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通员工的工资总和
private int commonTotalSalary = 0;
//部门经理的工资总和
private int managerTotalSalary =0;
//计算部门经理的工资总和
private void calManagerSalary(int salary){
this.managerTotalSalary = this.managerTotalSalary + salary
*MANAGER_COEFFICIENT ;
}
//计算普通员工的工资总和
private void calCommonSlary(int salary){
this.commonTotalSalary = this.commonTotalSalary +
salary*COMMONEMPLOYEE_COEFFICIENT;
}
//获得所有员工的工资总和
public int getTotalSalary(){
return this.commonTotalSalary + this.managerTotalSalary;
}
}  
注意,我们在实现时已经考虑员工工资和经理工资的系数不同。  


Client类的模拟 
代码清单25-19 场景类
public class Client {
public static void main(String[] args) {
IVisitor visitor = new Visitor();
for(Employee emp:mockEmployee()){
emp.accept(visitor);
}
System.out.println("本公司的月工资总额是:"+visitor.getTotalSalary());
}
}

其中mockEmployee静态方法没有任何改动,  


2)多个访问者  

设计模式笔记--访问者模式_第4张图片

多了两个接口和两个实现类,分别负责展示表和汇总表的业务处理,IVisitor接口没有改变  


展示表的实现  
代码清单25-21 具体展示表
public class ShowVisitor implements IShowVisitor {
private String info = "";
//打印出报表
public void report() {
System.out.println(this.info);
}
//访问普通员工,组装信息
public void visit(CommonEmployee commonEmployee) {
this.info = this.info + this.getBasicInfo(commonEmployee)
+ "工作:"+commonEmployee.getJob()+"\t\n";
}
//访问经理,然后组装信息
public void visit(Manager manager) {
this.info = this.info + this.getBasicInfo(manager) + "业绩:
"+manager.getPerformance() + "\t\n";
}
//组装出基本信息
private String getBasicInfo(Employee employee){
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女":
"男") + "\t";
info = info + "薪水:" + employee.getSalary() + "\t";
return info;
}
}


汇总表实现数据汇总功能, 
代码清单25-23 具体汇总表
public class TotalVisitor implements ITotalVisitor {
//部门经理的工资系数是5
private final static int MANAGER_COEFFICIENT = 5;
//员工的工资系数是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通员工的工资总和
private int commonTotalSalary = 0;
//部门经理的工资总和
private int managerTotalSalary =0;
public void totalSalary() {
System.out.println("本公司的月工资总额是" + (this.commonTotalSalary +
this.managerTotalSalary));
}
//访问普通员工,计算工资总额
public void visit(CommonEmployee commonEmployee) {
this.commonTotalSalary = this.commonTotalSalary + commonEmployee.getSalary() }
//访问部门经理,计算工资总额
public void visit(Manager manager) {
this.managerTotalSalary = this.managerTotalSalary + manager.getSalary() }
}


场景类  
public class Client {
public static void main(String[] args) {
//展示报表访问者
IShowVisitor showVisitor = new ShowVisitor();
//汇总报表的访问者
ITotalVisitor totalVisitor = new TotalVisitor();
for(Employee emp:mockEmployee()){
emp.accept(showVisitor); //接受展示报表访问者
emp.accept(totalVisitor);//接受汇总表访问者
}
//展示报表
showVisitor.report();
//汇总报表
totalVisitor.totalSalary();
}
}


3)双分派   

说到访问者模式就不得不提一下双分派(double dispatch)问题,什么是双分派呢?

们先来解释一下什么是单分派(single dispatch)和多分派(multiple dispatch),

单分派语言处理一个操作是根据请求者的名称和接收到的参数决定的,在Java中有静态绑定和动态绑定之说,它的实现是依据重载(overload)和覆写(override)实现的,我们来说一个简单的例子。

例如,演员演电影角色,一个演员可以扮演多个角色,我们先定义一个影视中的两个角色:功夫主角和白痴配角,如代码清单25-25所示。

public interface Role {
//演员要扮演的角色
}
public class KungFuRole implements Role {
//武功天下第一的角色
}
public class IdiotRole implements Role {
//一个弱智角色
}


角色有了,我们再定义一个演员抽象类,如代码清单25-26所示。
代码清单25-26 抽象演员
public abstract class AbsActor {
//演员都能够演一个角色
public void act(Role role){
System.out.println("演员可以扮演任何角色");
}
//可以演功夫戏
public void act(KungFuRole role){
System.out.println("演员都可以演功夫角色");
}
}


这里使用了Java的重载,我们再来看青年演员和老年演员,采用覆写的方式来 细化抽象类的功能,如代码清单 25-27所示。
代码清单25-27 青年演员和老年演员
public class YoungActor extends AbsActor {
//年轻演员最喜欢演功夫戏
public void act(KungFuRole role){
System.out.println("最喜欢演功夫角色");
}
}
public class OldActor extends AbsActor {
//不演功夫角色
public void act(KungFuRole role){
System.out.println("年龄大了,不能演功夫角色");
}
}


覆写和重载都已经实现,我们编写一个场景,如代码清单25-28所示。
代码清单25-28 场景类
public class Client {
public static void main(String[] args) {
//定义一个演员
AbsActor actor = new OldActor();
//定义一个角色
Role role = new KungFuRole();
//开始演戏
actor.act(role);
actor.act(new KungFuRole());
}
}


运行结果如下所示。

演员可以扮演任何角色
年龄大了,不能演功夫角色


重载在编译器期就决定了要调用哪个方法,它是根据role的表面类型而决定调用act(Rolerole)方法,这是静态绑定;
Actor的执行方法act则是由其实际类型决定的,这是动态绑定。

一个演员可以扮演很多角色,我们的系统要适应这种变化,也就是根据演员、角色两个对象类型,完成不同的操作任务,该如何实现呢?

我们让访问者模式上场就可以解决该问题,只要把角色类稍稍修改即可,如代码清单25-29所示。
代码清单25-29 引入访问者模式
public interface Role {
//演员要扮演的角色
public void accept(AbsActor actor);
}p
ublic class KungFuRole implements Role {
//武功天下第一的角色
public void accept(AbsActor actor){
actor.act(this);
}
}p
ublic class IdiotRole implements Role {
//一个弱智角色,由谁来扮演
public void accept(AbsActor actor){
actor.act(this);
}
}


场景类稍有改动,如代码清单25-30所示。
代码清单25-30 场景类
public class Client {
public static void main(String[] args) {
//定义一个演员
AbsActor actor = new OldActor();
//定义一个角色
Role role = new KungFuRole();
//开始演戏
role.accept(actor);
}
}


运行结果如下所示。
年龄大了,不能演功夫角色  


看到没?不管演员类和角色类怎么变化,我们都能够找到期望的方法运行,这就是双反派。双分派意味着得到执行的操作决定于请求的种类和两个接收者的类型,它是多分派的一个特例。从这里也可以看到Java是一个支持双分派的单分派语言。  

访问者模式是一种集中规整模式,特别适用于大规模重构的项目,在这一个阶段需求已经非常清晰,原系统的功能点也已经明确,通过访问者模式可以很容易把一些功能进行梳理,达到最终目的——功能集中化  
 

你可能感兴趣的:(Android之设计模式)