Visitor Pattern Introduction
(wang hailong) 
Visitor Pattern可能是设计模式中最复杂的模式了。Visitor Pattern从Double Dispatch Pattern派生而来,由Double一词可见其复杂度。 
Visitor Pattern,顾名思义,有访问者和被访问者,既然,以访问者命名,那么,主要的工作都是访问者来做。 
本文不从程序设计入手,而从一个日常生产生活的例子入手,来解释Visitor Pattern。 
一个生产高能电池的厂家,下设一个客户服务部门,其主要任务之一就是收集用户的反馈意见,以便改进产品功能和服务质量。 
以前,客户服务部门只是采用发放调查问卷的方式。调查问卷的问题千篇一律,不能针对特殊用户的兴趣点,尤其一些集体用户,不会认真对待这些问卷,懒得把问卷发到具体的使用者手里,而且,很多用户不愿意花时间把调查问卷寄回。调查结果不全面,不真实,几乎没有任何效果。 
之后,个性化服务、CRM客户关系管理等概念兴起,客户服务部门引入了一套CRM客户(用户)关系管理系统,对客户信息进行管理。针对不同用户的特点,采用不同的调查方式。采用电话、E-Mail、传真、问卷、登门拜访等多种方式,对用户进行调查访问。针对一些集体用户,比如,企事业单位用户,客户服务人员首先访问联系该单位集体,安排好时间之后,客户服务人员具体访问每一个具体用户,这些用户接受客户服务人员的访问,给出亲身使用电池产品的第一手资料。 
还有的情况,集体单位下面还有子单位,比如,公司下面有子公司。那么,遵循同样的流程,分别访问下面的子公司,这些子公司接受客户服务人员的访问,安排好时间之后,客户服务人员具体访问每一个具体用户,这些用户接受客户服务人员的访问,给出亲身使用电池产品的第一手资料。 
我们可以看到,这是一个典型的Visitor Pattern。客户服务部门就是Visitor,不同类型的用户就是被访问者。客户服务部门(Visitor)几乎做了所有的工作,尽量不给用户增加负担。 
下面给出这个例子的示意代码。 
// 客户服务部门类, 
// 总结CRM客户关系管理系统的客户类型信息,定义以下的方法 
class ServiceDepartment{ 
  // Email访问方式 
  private Email sendEmail(…){ 
    … 
  } 
  
  // 电话访问方式 
  private Answer callPhone(…){ 
   … 
  } 
  
  // 访问用户。 
  // 这是ServiceDepartment类的入口点方法。 
// 注意,参照下面的User的定义代码,User是一个接口类型 
  public void visitUser(User aUser){ 
     user.accept(this); 
  } 
  
  // 访问习惯Email访问的用户 
  public void visitEmailUser (EmailUser anEmailUser){ 
     // send email to email user 
     Email reply = anEmailUser.replyEmail( this.sendEmail() ); 
     // put anwer to database 
  } 
  
  // 访问习惯电话访问的用户 
  public void visitPhoneUser (PhoneUser aPhoneUser){ 
     // call phone user 
      Answer answer = A.answerPhone( this. callPhone () ); 
      // put answer to database 
  } 
  
  // 访问公司用户 
  public void visitCompanyUser( CompanyUser companyUser){ 
    // visit company user 
    List userList = companyUser.getUserList(); 
  
    for each user in userList{  
      // 访问每个用户,每个用户都接受访问。 
      Visitor.visitUser(user); 
       
// 注意,这里User的类型有可能是CompanyUser类型。 
// 这时形成对子公司用户的递归调用。 
    } 
  } 
} 
  
// 以下列出每个用户类的代码, 
// 因为用户是被访问者,负担很少。所以,每个用户的方法都很简单。 
// 公共接口类,每个用户类都应该实现这个接口。 
public interface User{ 
  // 接受客户服务部门的访问 
  public void accept(ServiceDepartment visitor); 
}; 
  
// Email user 类 
public class EmailUser implements User { 
  // 用Email反馈 
  public Email replayEmail(Email question){ 
    … 
  } 
   
  // 接受客户服务部门的访问 
  public void accept(ServiceDepartment visitor){ 
    visitor.visitEmailUser(this); 
  } 
}; 
  
// Phone user 类 
public class PhoneUser implements User{ 
  // 接听电话,进行反馈 
  public Answer answerPhone(Phone question){ 
    … 
  } 
   
  // 接受客户服务部门的访问 
  public void accept(ServiceDepartment visitor){ 
    visitor.visitPhoneUser(this); 
  } 
}; 
  
// company user 类 
public class CompanyUser implements User { 
  // 接受客户服务部门的访问 
  public void accept(ServiceDepartment visitor){ 
    visitor.visitCompanyUser(this); 
  } 
}; 
  
下面给出一个使用上述模式的例子。 
public class TestMain{ 
public void main(String[] args){ 
  // create a visitor 
  ServiceDepartmant visitor = new ServiceDepartmant(); 
  
  // 从数据库中取得所有的用户信息 
  // 这些用户的类型,有可能是PhoneUser, EmailUser,还有可能是CompanyUser。 
  
  for each user created from database { 
    // 访问每一个用户,用户接受访问,给出反馈,存放到数据库中 
    visitor.visitUser(user); 
  } 
} 
} 
  
好了,所有的示意代码都在这里了。现在,我们考虑问题的变化和扩展。毕竟,设计模式的目的就是为了让变化的部分越小越好,越简单越好。 
客户服务部门引入CRM客户关系管理系统,就是为了更好地对应客户(用户)信息的变化。 
过了一段时间,CRM客户关系管理系统加入了一个新用户的信息,这个用户习惯使用传真回答调查问卷。这时,我们多了一个用户类,FaxUser。我们需要对上述的ServiceDepartment类(visitor类)进行扩展。 
第一种方法是,直接修改ServiceDepartment类,增加一个visitFaxUser(FaxUser)方法。这种方法比较直观,但是需要修改以前的代码。 
public class ServiceDepartment{ 
  … // 以前的代码 
  
  // 增加一个新的方法,发送传真 
  private Fax sendFax(…){ 
    … 
  } 
  
  // 增加一个新的方法,访问习惯传真的用户 
  void visitFaxUser(FaxUser aFaxUser){ 
    Fax fax = aFaxUser.replyFax(this.sendFax()); 
    // 把fax的信息存放到数据库 
  } 
}; 
  
这时,新增的FaxUser的代码如下: 
public class FaxUser implements User{ 
  // 用传真反馈 
  public Fax replyFax(Fax fax){ 
   … 
  } 
  
// 接受客户服务部门的访问 
  public void accept(ServiceDepartment visitor){ 
    visitor.visitFaxUser(this); 
  } 
} 
  
第二种方法是,继承ServiceDepartment类,定义一个新类ExtendedServiceDepartment,增加一个visitFaxUser(FaxUser)方法。这种方法的好处是不用修改以前的代码,但是,新增加的User类,需要知道新类ExtendedServiceDepartment的定义。 
下面给出示意代码,ExtendedServiceDepartment类。 
public class ExtendedServiceDepartment extends ServiceDepartment{ 
  // 增加一个新的方法,发送传真 
  private Fax sendFax(…){ 
    … 
  } 
  
  // 增加一个新的方法,访问习惯传真的用户 
  void visitFaxUser(FaxUser aFaxUser){ 
    Fax fax = aFaxUser.replyFax(this.sendFax()); 
    // 把fax的信息存放到数据库 
  } 
}; 
  
这时,新增的FaxUser的代码如下: 
public class FaxUser implements User{ 
  // 用传真反馈 
  public Fax replyFax(Fax fax){ 
   … 
  } 
  // 接受客户服务部门的访问 
  public void accept(ExtendedServiceDepartment visitor){ 
    visitor.visitFaxUser(this); 
  } 
} 
后记 为什么写这篇文章?
以前看到一本书,讲述TCP/IP编程。作者解释Socket的时候举了一个接听电话的例子,讲述的很明白。 
1.你要接听电话,首先你要有一个电话,所以,第一步,create a Socket. 这里,Socket就是你的电话。 
2.你还要有一个电话号码,对于你的Socket来说,你的IP地址和端口号就是电话号码,所以,第二步,bind to a port. 
3.你还要等在电话旁边,等电话铃响,listen to the port. 
4.别人要给你打电话,他(她)也要有一个电话,他需要create a Socket。 
5.他需要拨打你的电话号码,connect to your IP address and port. 
6.他的电话来了,你要接听他的电话,accept his connection。这时,还有来电显示,你知道他的电话号码(IP地址)。 
7.你同时能接听几个打来的电话,你和他们开始通话。read and write. 
8.通话结束,你说,“你先挂电话吧,我还要和别人讲话。”他把电话挂了,close his Socket。 
  
看了这段之后,我很受启发。后来写了一篇文章《Design Pattern Introduction》,提到Observer Pattern,举了邮件订阅,手机短信订阅的例子。 
Visitor Pattern,是一个比较复杂的设计模式。有很多关于Visitor Pattern的争论,有些人建议使用,有些人建议不使用。这里,我多费些笔墨,把Visitor Pattern作为一个日常生产生活的场景描述出来。 
  
   
 
  |