一起学习
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作为一个日常生产生活的场景描述出来。
下载本文示例代码
Visitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern IntroductionVisitor Pattern Introduction