首页 > 代码库 > [转]操作复杂对象结构——访问者模式

[转]操作复杂对象结构——访问者模式

原地址:http://blog.csdn.net/lovelion/article/details/7433523

想必大家都去过医院,虽然没有人喜欢去医院(爱岗敬业的医务工作人员除外,微笑)。在医生开具处方单(药单)后,很多医院都存在如下处理流程:划价人员拿到处方单之后根据药品名称和数量计算总价,药房工作人员根据药品名称和数量准备药品,如图26-1所示:

      在图26-1中,我们可以将处方单看成一个药品信息的集合,里面包含了一种或多种不同类型的药品信息,不同类型的工作人员(如划价人员和药房工作人员)在操作同一个药品信息集合时将提供不同的处理方式,而且可能还会增加新类型的工作人员来操作处方单。

      在 软件开发中,有时候我们也需要处理像处方单这样的集合对象结构,在该对象结构中存储了多个不同类型的对象信息,而且对同一对象结构中的元素的操作方式并不 唯一,可能需要提供多种不同的处理方式,还有可能增加新的处理方式。在设计模式中,有一种模式可以满足上述要求,其模式动机就是以不同的方式操作复杂对象 结构,该模式就是我们本章将要介绍的访问者模式。

 

26.1 OA系统中员工数据汇总

       Sunny软件公司欲为某银行开发一套OA系统,在该OA系统中包含一个员工信息管理子系统,该银行员工包括正式员工和临时工,每周人力资源部和财务部等部门需要对员工数据进行汇总,汇总数据包括员工工作时间、员工工资等。该公司基本制度如下:

       (1) 正式员工(Full time  Employee)每周工作时间为40小时,不同级别、不同部门的员工每周基本工资不同;如果超过40小时,超出部分按照100元/小时作为加班费;如果少于40小时,所缺时间按照请假处理,请假所扣工资以80元/小时计算,直到基本工资扣除到零为止。除了记录实际工作时间外,人力资源部需记录加班时长或请假时长,作为员工平时表现的一项依据。

       (2) 临时工(Part time  Employee)每周工作时间不固定,基本工资按小时计算,不同岗位的临时工小时工资不同。人力资源部只需记录实际工作时间。

       人力资源部和财务部工作人员可以根据各自的需要对员工数据进行汇总处理,人力资源部负责汇总每周员工工作时间,而财务部负责计算每周员工工资。

       Sunny软件公司开发人员针对上述需求,提出了一个初始解决方案,其核心代码如下所示:

[java] view plaincopy
  1. import java.util.*;  
  2.   
  3. class EmployeeList  
  4. {  
  5.     private ArrayList<Employee> list = new ArrayList<Employee>(); //员工集合  
  6.   
  7.     //增加员工  
  8.     public void addEmployee(Employee employee)   
  9.     {  
  10.         list.add(employee);  
  11.     }  
  12.       
  13.     //处理员工数据  
  14.     public void handle(String departmentName)  
  15.     {  
  16.         if(departmentName.equalsIgnoreCase("财务部")) //财务部处理员工数据  
  17.         {  
  18.             for(Object obj : list)  
  19.             {  
  20.                 if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee"))  
  21.                 {  
  22.                     System.out.println("财务部处理全职员工数据!");           
  23.                 }  
  24.                 else   
  25.                 {  
  26.                     System.out.println("财务部处理兼职员工数据!");  
  27.                 }  
  28.             }  
  29.         }  
  30.         else if(departmentName.equalsIgnoreCase("人力资源部")) //人力资源部处理员工数据  
  31.         {  
  32.             for(Object obj : list)  
  33.             {  
  34.                 if(obj.getClass().getName().equalsIgnoreCase("FulltimeEmployee"))  
  35.                 {  
  36.                     System.out.println("人力资源部处理全职员工数据!");                     
  37.                 }  
  38.                 else   
  39.                 {  
  40.                     System.out.println("人力资源部处理兼职员工数据!");  
  41.                 }  
  42.             }             
  43.         }  
  44.     }  
  45. }  

      在EmployeeList类的handle()方法中,通过对部门名称和员工类型进行判断,不同部门对不同类型的员工进行了不同的处理,满足了员工数据汇总的要求。但是该解决方案存在如下几个问题:

      (1) EmployeeList类非常庞大,它将各个部门处理各类员工数据的代码集中在一个类中,在具体实现时,代码将相当冗长,EmployeeList类承担了过多的职责,既不方便代码的复用,也不利于系统的扩展,违背了“单一职责原则”。

      (2)在代码中包含大量的“if…else…”条件判断语句,既需要对不同部门进行判断,又需要对不同类型的员工进行判断,还将出现嵌套的条件判断语句,导致测试和维护难度增大。

      (3)如果要增加一个新的部门来操作员工集合,不得不修改EmployeeList类的源代码,在handle()方法中增加一个新的条件判断语句和一些业务处理代码来实现新部门的访问操作。这违背了“开闭原则”,系统的灵活性和可扩展性有待提高。

      (4)如果要增加一种新类型的员工,同样需要修改EmployeeList类的源代码,在不同部门的处理代码中增加对新类型员工的处理逻辑,这也违背了“开闭原则”。

      如何解决上述问题?如何为同一集合对象中的元素提供多种不同的操作方式?访问者模式就是一个值得考虑的解决方案,它可以在一定程度上解决上述问题(解决大部分问题)。访问者模式可以为为不同类型的元素提供多种访问操作方式,而且可以在不修改原有系统的情况下增加新的操作方式。

 

 

 

 

26.2 访问者模式概述

 

      访问者模式是一种较为复杂的行为型设计模式,它包含访问者和被访问元素两个主要组成部分,这些被访问的元素通常具有不同的类型,且不同的访问者可以对它们 进行不同的访问操作。例如处方单中的各种药品信息就是被访问的元素,而划价人员和药房工作人员就是访问者。访问者模式使得用户可以在不修改现有系统的情况 下扩展系统的功能,为这些不同类型的元素增加新的操作。

 

      在使用访问者模式时,被访问元素通常不是单独存在的,它们存储在一个集合中,这个集合被称为“对象结构”,访问者通过遍历对象结构实现对其中存储的元素的逐个操作。

 

      访问者模式定义如下:

 

访问者模式(Visitor Pattern):提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

 

      访问者模式的结构较为复杂,其结构如图26-2所示:

 

 

      在访问者模式结构图中包含如下几个角色:

 

      ●Vistor(抽象访问者):抽象访问者为对象结构中每一个具体元素类ConcreteElement声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型,具体访问者需要实现这些操作方法,定义对这些元素的访问操作。

 

      ●ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每一个操作用于访问对象结构中一种类型的元素。

 

      ●Element(抽象元素):抽象元素一般是抽象类或者接口,它定义一个accept()方法,该方法通常以一个抽象访问者作为参数。【稍后将介绍为什么要这样设计。】

 

      ●ConcreteElement(具体元素):具体元素实现了accept()方法,在accept()方法中调用访问者的访问方法以便完成对一个元素的操作。

 

      ● ObjectStructure(对象结构):对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。它可以结合组合模式来实现,也可以是一个简单的集合对象,如一个List对象或一个Set对象。

 

      访问者模式中对象结构存储了不同类型的元素对象,以供不同访问者访问。访问者模式包括两个层次结构,一个是访问者层次结构,提供了抽象访问者和具体访问 者,一个是元素层次结构,提供了抽象元素和具体元素。相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同访问方式访问。在 访问者模式中,增加新的访问者无须修改原有系统,系统具有较好的可扩展性。

 

      在访问者模式中,抽象访问者定义了访问元素对象的方法,通常为每一种类型的元素对象都提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的命名一般有两种方式:一种是直接在方法名中标明待访问元素对象的具体类型,如visitElementA(ElementA elementA),还有一种是统一取名为visit(),通过参数类型的不同来定义一系列重载的visit()方法。当然,如果所有的访问者对某一类型的元素的访问操作都相同,则可以将操作代码移到抽象访问者类中,其典型代码如下所示:

 

[java] view plaincopy
  1. abstract class Visitor  
  2. {  
  3.     public abstract void visit(ConcreteElementA elementA);  
  4.     public abstract void visit(ConcreteElementB elementB);  
  5.     public void visit(ConcreteElementC elementC)  
  6.     {  
  7.         //元素ConcreteElementC操作代码  
  8.     }  
  9. }  

 

      在这里使用了重载visit()方法的方式来定义多个方法用于操作不同类型的元素对象。在抽象访问者Visitor类的子类ConcreteVisitor中实现了抽象的访问方法,用于定义对不同类型元素对象的操作,具体访问者类典型代码如下所示:

 

[java] view plaincopy
  1. class ConcreteVisitor extends Visitor  
  2. {  
  3.     public void visit(ConcreteElementA elementA)  
  4.     {  
  5.         //元素ConcreteElementA操作代码  
  6.     }  
  7.     public void visit(ConcreteElementB elementB)  
  8.     {  
  9.         //元素ConcreteElementB操作代码  
  10.     }  
  11. }  

 

      对于元素类而言,在其中一般都定义了一个accept()方法,用于接受访问者的访问,典型的抽象元素类代码如下所示:

 

[java] view plaincopy
  1. interface Element  
  2. {  
  3.     public void accept(Visitor visitor);  
  4. }  

 


      需要注意的是该方法传入了一个抽象访问者Visitor类型的参数,即针对抽象访问者进行编程,而不是具体访问者,在程序运行时再确定具体访问者的类型,并调用具体访问者对象的visit()方法实现对元素对象的操作。在抽象元素类Element的子类中实现了accept()方法,用于接受访问者的访问,在具体元素类中还可以定义不同类型的元素所特有的业务方法,其典型代码如下所示:

 

[java] view plaincopy
  1. class ConcreteElementA implements Element  
  2. {  
  3.     public void accept(Visitor visitor)  
  4.     {  
  5.         visitor.visit(this);  
  6.     }  
  7.       
  8.     public void operationA()  
  9.     {  
  10.         //业务方法  
  11.     }  
  12. }  

 


      在具体元素类ConcreteElementA的accept()方法中,通过调用Visitor类的visit()方法实现对元素的访问,并以当前对象作为visit()方法的参数。其具体执行过程如下:

 

      (1)调用具体元素类的accept(Visitor visitor)方法,并Visitor子类对象作为其参数

 

      (2)在具体元素类accept(Visitor visitor)方法内部调用传入的Visitor对象的visit()方法,如visit(ConcreteElementA elementA),将当前具体元素类对象(this)作为参数,如visitor.visit(this);

 

      (3)执行Visitor对象的visit()方法,在其中还可以调用具体元素对象的业务方法。

 

      这种调用机制也称为“双重分派”,正因为使用了双重分派机制,使得增加新的访问者无须修改现有类库代码,只需将新的访问者对象作为参数传入具体元素对象的accept()方法,程序运行时将回调在新增Visitor类中定义的visit()方法,从而增加新的元素访问方式。

 

 

思考

双重分派机制如何用代码实现?

 


      在访问者模式中,对象结构是一个集合,它用于存储元素对象并接受访问者的访问,其典型代码如下所示:

 

[java] view plaincopy
  1. class ObjectStructure  
  2. {  
  3.     private ArrayList<Element> list = new ArrayList<Element>(); //定义一个集合用于存储元素对象  
  4.   
  5.     public void accept(Visitor visitor)  
  6.     {  
  7.         Iterator i=list.iterator();  
  8.           
  9.         while(i.hasNext())  
  10.         {  
  11.             ((Element)i.next()).accept(visitor); //遍历访问集合中的每一个元素  
  12.         }  
  13.     }  
  14.   
  15.     public void addElement(Element element)  
  16.     {  
  17.         list.add(element);  
  18.     }  
  19.   
  20.     public void removeElement(Element element)  
  21.     {  
  22.         list.remove(element);  
  23.     }  
  24. }  

 


      在对象结构中可以使用迭代器对存储在集合中的元素对象进行遍历,并逐个调用每一个对象的accept()方法,实现对元素对象的访问操作。

 

 

 

思考

访问者模式是否符合“开闭原则”?【从增加新的访问者和增加新的元素两方面考虑。】