#0 系列目录#设计模式
#1 场景问题# ##1.1 扩展客户管理的功能## 考虑这样一个应用:扩展客户管理的功能。工具
既然是扩展功能,那么确定是已经存在必定的功能了,先看看已有的功能:公司的客户分红两大类,一类是企业客户,一类是我的客户,现有的功能很是简单,就是能让客户提出服务申请
。目前的程序结构如图25.1所示:测试
现有的实现很简单,先看看Customer的实现,示例代码以下:ui
/** * 各类客户的父类 */ public abstract class Customer { /** * 客户编号 */ private String customerId; /** * 客户名称 */ private String name; /** * 客户提出服务请求的方法,示意一下 */ public abstract void serviceRequest(); }
接下来看看企业客户的实现示例代码以下:this
/** * 企业客户 */ public class EnterpriseCustomer extends Customer { /** * 联系人 */ private String linkman; /** * 联系电话 */ private String linkTelephone; /** * 企业注册地址 */ private String registerAddress; /** * 企业客户提出服务请求的方法,示意一下 */ public void serviceRequest(){ //企业客户提出的具体服务请求 System.out.println(this.getName()+"企业提出服务请求"); } }
再看看我的客户的实现示例代码以下:.net
/** * 我的客户 */ public class PersonalCustomer extends Customer{ /** * 联系电话 */ private String telephone; /** * 年龄 */ private int age; /** * 企业注册地址 */ private String registerAddress; /** * 我的客户提出服务请求的方法,示意一下 */ public void serviceRequest(){ //我的客户提出的具体服务请求 System.out.println("客户"+this.getName()+"提出服务请求"); } }
从上面的实现能够看出来,之前对客户的管理功能是不多的,如今随着业务的发展,须要增强对客户管理的功能,假设如今须要增长以下的功能:翻译
客户对公司产品的偏好分析,针对企业客户和我的客户有不一样的分析策略,主要是根据以往购买的历史、潜在购买意向等进行分析,对于企业客户还要添加上客户所在行业的发展趋势、客户的发展预期等的分析。设计
客户价值分析,针对企业客户和我的客户,有不一样的分析方式和策略。主要是根据购买的金额大小、购买的产品和服务的多少、购买的频率等进行分析。代理
其实除了这些功能,还有不少潜在的功能,只是如今尚未要求实现,好比:针对不一样的客户进行需求调查;针对不一样的客户进行满意度分析;客户消费预期分析等等。虽然如今没有要求实现,但不排除从此有可能会要求实现。code
##1.2 不用模式的解决方案## 要实现上面要求的功能,也不是很困难,一个很基本的想法就是:既然不一样类型的客户操做是不一样的,那么在不一样类型的客户里面分别实现这些功能,不就能够了。
因为这些功能的实现依附于不少其它功能的实现,或者是须要不少其它的业务数据,在示例里面不太好完整的体现其功能实现,都是示意一下,所以提早说明一下。
按照上述的想法,这个时候的程序结构如图25.2所示:
public abstract class Customer { private String customerId; private String name; public abstract void serviceRequest(); /** * 客户对公司产品的偏好分析,示意一下 */ public abstract void predilectionAnalyze(); /** * 客户价值分析,示意一下 */ public abstract void worthAnalyze(); }
public class EnterpriseCustomer extends Customer { private String linkman; private String linkTelephone; private String registerAddress; public void serviceRequest(){ //企业客户提出的具体服务请求 System.out.println(this.getName()+"企业提出服务请求"); } /** * 企业客户对公司产品的偏好分析,示意一下 */ public void predilectionAnalyze(){ //根据过往购买的历史、潜在购买意向 //以及客户所在行业的发展趋势、客户的发展预期等的分析 System.out.println("如今对企业客户"+this.getName()+"进行产品偏好分析"); } /** * 企业客户价值分析,示意一下 */ public void worthAnalyze(){ //根据购买的金额大小、购买的产品和服务的多少、购买的频率等进行分析 //企业客户的标准会比我的客户的高 System.out.println("如今对企业客户"+this.getName()+"进行价值分析"); } }
public class PersonalCustomer extends Customer{ private String telephone; private int age; public void serviceRequest(){ //我的客户提出的具体服务请求 System.out.println("客户"+this.getName()+"提出服务请求"); } /** * 我的客户对公司产品的偏好分析,示意一下 */ public void predilectionAnalyze(){ System.out.println("如今对我的客户"+this.getName()+"进行产品偏好分析"); } /** * 我的客户价值分析,示意一下 */ public void worthAnalyze(){ System.out.println("如今对我的客户"+this.getName()+"进行价值分析"); } }
public class Client { public static void main(String[] args) { //准备点测试数据 Collection<Customer> colCustomer = preparedTestData(); //循环对客户进行操做 for(Customer cm : colCustomer){ //进行偏好分析 cm.predilectionAnalyze(); //进行价值分析 cm.worthAnalyze(); } } private static Collection<Customer> preparedTestData(){ Collection<Customer> colCustomer = new ArrayList<Customer>(); //为了测试方便,准备点数据 Customer cm1 = new EnterpriseCustomer(); cm1.setName("ABC集团"); colCustomer.add(cm1); Customer cm2 = new EnterpriseCustomer(); cm2.setName("CDE公司"); colCustomer.add(cm2); Customer cm3 = new PersonalCustomer(); cm3.setName("张三"); colCustomer.add(cm3); return colCustomer; } }
运行结果以下:
如今对企业客户ABC集团进行产品偏好分析 如今对企业客户ABC集团进行价值分析 如今对企业客户CDE公司进行产品偏好分析 如今对企业客户CDE公司进行价值分析 如今对我的客户张三进行产品偏好分析 如今对我的客户张三进行价值分析
##1.3 有何问题## 以很简单的方式,实现了要求的功能,这种实现有没有什么问题呢?仔细分析上面的实现,发现有两个主要的问题:
在企业客户和我的客户的类里面,都分别实现了提出服务请求、进行产品偏好分析、进行客户价值分析等功能,也就是说,
这些功能的实现代码是混杂在同一个类里面的
;并且相同的功能分散到了不一样的类中去实现,这会致使整个系统难以理解、难以维护
。更为痛苦的是,采用这样的实现方式,若是要给客户扩展新的功能,好比前面提到的针对不一样的客户进行需求调查;针对不一样的客户进行满意度分析;客户消费预期分析等等。
每次扩展,都须要改动企业客户的类和我的客户的类
,固然也能够经过为它们扩展子类的方式,可是这样可能会形成过多的对象层次
。
那么有没有办法,可以在不改变客户这个对象结构中各元素类的前提下,为这些类定义新的功能?也就是要求不改变企业客户和我的客户类,就能为企业客户和我的客户类定义新的功能?
#2 解决方案# ##2.1 访问者模式来解决## 用来解决上述问题的一个合理的解决方案,就是使用访问者模式。那么什么是访问者模式呢?
仔细分析上面的示例,对于客户这个对象结构,不想改变类,又要添加新的功能,很明显就须要一种动态的方式,在运行期间把功能动态地添加到对象结构中去
。
有些朋友可能会想起装饰模式,装饰模式能够实现为一个对象透明的添加功能,但装饰模式基本上是在现有的功能的基础之上进行功能添加,其实是对现有功能的增强或者改造
。并非在现有功能不改动的状况下,为对象添加新的功能。
看来须要另外寻找新的解决方式了,能够应用访问者模式来解决这个问题,访问者模式实现的基本思路以下:
首先定义一个接口来表明要新加入的功能,为了通用,也就是定义一个通用的功能方法来表明新加入的功能;
而后在对象结构上添加一个方法,做为通用的功能方法,也就是能够表明被添加的功能,在这个方法中传入具体的实现新功能的对象;
而后在对象结构的具体实现对象里面实现这个方法,回调传入具体的实现新功能的对象,就至关于调用到新功能上了;
接下来的步骤就是提供实现新功能的对象;
最后再提供一个可以循环访问整个对象结构的类,让这个类来提供符合客户端业务需求的方法,来知足客户端调用的须要;
这样一来,只要提供实现新功能的对象给对象结构,就能够为这些对象添加新的功能
,因为在对象结构中定义的方法是通用的功能方法,因此什么新功能均可以加入。
##2.2 模式结构和说明## 访问者模式的结构如图25.3所示:
Visitor:访问者接口,为全部的访问者对象声明一个visit方法,
用来表明为对象结构添加的功能,理论上能够表明任意的功能
。ConcreteVisitor:具体的访问者实现对象,
实现要真正被添加到对象结构中的功能
。Element:抽象的元素对象,对象结构的顶层接口,定义接受访问的操做。
ConcreteElement:具体元素对象,对象结构中具体的对象,也是被访问的对象,一般会回调访问者的真实功能,同时开放自身的数据供访问者使用。
ObjectStructure:对象结构,一般包含多个被访问的对象,它能够遍历这多个被访问的对象,也可让访问者访问它的元素。能够是一个复合或是一个集合,如一个列表或无序集合。
可是请注意:这个ObjectStructure并非咱们在前面讲到的对象结构,前面一直讲的对象结构是指的一系列对象的定义结构,是概念上的东西
;而ObjectStructure能够当作是对象结构中的一系列对象的一个集合,是用来辅助客户端访问这一系列对象的
,因此为了避免形成你们的困惑,后面提到ObjectStructure的时候,就用英文名称来代替,不把它翻译成中文。
##2.3 访问者模式示例代码##
首先须要定义一个接口来表明要新加入的功能,把它称做访问者,访问谁呢?固然是访问对象结构中的对象了
。既然是访问,不能空手而去吧,这些访问者在进行访问的时候,就会携带新的功能,也就是说,访问者携带着须要添加的新的功能去访问对象结构中的对象,就至关于给对象结构中的对象添加了新的功能
。示例代码以下:/** * 访问者接口 */ public interface Visitor { /** * 访问元素A,至关于给元素A添加访问者的功能 * @param elementA 元素A的对象 */ public void visitConcreteElementA(ConcreteElementA elementA); /** * 访问元素B,至关于给元素B添加访问者的功能 * @param elementB 元素B的对象 */ public void visitConcreteElementB(ConcreteElementB elementB); }
/** * 被访问的元素的接口 */ public abstract class Element { /** * 接受访问者的访问 * @param visitor 访问者对象 */ public abstract void accept(Visitor visitor); }
/** * 具体元素的实现对象 */ public class ConcreteElementA extends Element { public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitConcreteElementA(this); } /** * 示例方法,表示元素已有的功能实现 */ public void opertionA(){ //已有的功能实现 } }
再看看元素B的实现,示例代码以下:
/** * 具体元素的实现对象 */ public class ConcreteElementB extends Element { public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitConcreteElementB(this); } /** * 示例方法,表示元素已有的功能实现 */ public void opertionB(){ //已有的功能实现 } }
/** * 具体的访问者实现 */ public class ConcreteVisitor1 implements Visitor { public void visitConcreteElementA(ConcreteElementA element) { //把去访问ConcreteElementA时,须要执行的功能实如今这里 //可能须要访问元素已有的功能,好比: element.opertionA(); } public void visitConcreteElementB(ConcreteElementB element) { //把去访问ConcreteElementB时,须要执行的功能实如今这里 //可能须要访问元素已有的功能,好比: element.opertionB(); } }
/** * 对象结构,一般在这里对元素对象进行遍历,让访问者能访问到全部的元素 */ public class ObjectStructure { /** * 示意,表示对象结构,能够是一个组合结构或是集合 */ private Collection<Element> col = new ArrayList<Element>(); /** * 示意方法,提供给客户端操做的高层接口 * @param visitor 客户端须要使用的访问者 */ public void handleRequest(Visitor visitor){ //循环对象结构中的元素,接受访问 for(Element ele : col){ ele.accept(visitor); } } /** * 示意方法,组建对象结构,向对象结构中添加元素。 * 不一样的对象结构有不一样的构建方式 * @param ele 加入到对象结构的元素 */ public void addElement(Element ele){ this.col.add(ele); } }
public class Client { public static void main(String[] args) { //建立ObjectStructure ObjectStructure os = new ObjectStructure(); //建立要加入对象结构的元素 Element eleA = new ConcreteElementA(); Element eleB = new ConcreteElementB(); //把元素加入对象结构 os.addElement(eleA); os.addElement(eleB); //建立访问者 Visitor visitor = new ConcreteVisitor1(); //调用业务处理的方法 os.handleRequest(visitor); } }
##2.4 使用访问者模式重写示例## 要使用访问者模式来重写示例,首先就要按照访问者模式的结构,分离出两个类层次来,一个是对应于元素的类层次,一个是对应于访问者的类层次
。
对于对应于元素的类层次,如今已经有了,就是客户的对象层次
。而对应于访问者的类层次,如今尚未,不过,按照访问者模式的结构
,应该是先定义一个访问者接口,而后把每种业务实现成为一个单独的访问者对象,也就是说应该使用一个访问者对象来实现对客户的偏好分析,而用另一个访问者对象来实现对客户的价值分析。
在分离好两个类层次事后,为了方便客户端的访问,定义一个ObjectStructure,其实就相似于前面示例中的客户管理的业务对象
。新的示例的结构如图25.4所示:
仔细查看图25.4所示的程序结构示意图,细心的朋友会发现,在图上没有出现对客户进行价值分析的功能了
。这是为了示范“使用访问者模式来实现示例功能事后,能够很容易的给对象结构增长新的功能”,因此先不作这个功能,等都实现好了,再来扩展这个功能
。接下来仍是看看代码实现,以更好的体会访问者模式。
新增一个接受访问者访问的方法;
把可以分离出去放到访问者中实现的方法,从Customer中删除掉,包括:客户提出服务请求的方法、对客户进行偏好分析的方法、对客户进行价值分析的方法等;
示例代码以下:
public abstract class Customer { private String customerId; private String name; /** * 接受访问者的访问 * @param visitor 访问者对象 */ public abstract void accept(Visitor visitor); }
public class EnterpriseCustomer extends Customer{ private String linkman; private String linkTelephone; private String registerAddress; public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitEnterpriseCustomer(this); } }
再看看我的客户的实现,示例代码以下:
public class PersonalCustomer extends Customer{ private String telephone; private int age; public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitPersonalCustomer(this); } }
/** * 访问者接口 */ public interface Visitor { /** * 访问企业客户,至关于给企业客户添加访问者的功能 * @param ec 企业客户的对象 */ public void visitEnterpriseCustomer(EnterpriseCustomer ec); /** * 访问我的客户,至关于给我的客户添加访问者的功能 * @param pc 我的客户的对象 */ public void visitPersonalCustomer(PersonalCustomer pc); }
/** * 具体的访问者,实现客户提出服务请求的功能 */ public class ServiceRequestVisitor implements Visitor { public void visitEnterpriseCustomer(EnterpriseCustomer ec){ //企业客户提出的具体服务请求 System.out.println(ec.getName()+"企业提出服务请求"); } public void visitPersonalCustomer(PersonalCustomer pc){ //我的客户提出的具体服务请求 System.out.println("客户"+pc.getName()+"提出服务请求"); } }
接下来看看实现对客户偏好分析功能的访问者,示例代码以下:
/** * 具体的访问者,实现对客户的偏好分析 */ public class PredilectionAnalyzeVisitor implements Visitor { public void visitEnterpriseCustomer(EnterpriseCustomer ec){ //根据过往购买的历史、潜在购买意向 //以及客户所在行业的发展趋势、客户的发展预期等的分析 System.out.println("如今对企业客户"+ec.getName()+"进行产品偏好分析"); } public void visitPersonalCustomer(PersonalCustomer pc){ System.out.println("如今对我的客户"+pc.getName()+"进行产品偏好分析"); } }
public class ObjectStructure { /** * 要操做的客户集合 */ private Collection<Customer> col = new ArrayList<Customer>(); /** * 提供给客户端操做的高层接口,具体的功能由客户端传入的访问者决定 * @param visitor 客户端须要使用的访问者 */ public void handleRequest(Visitor visitor){ //循环对象结构中的元素,接受访问 for(Customer cm : col){ cm.accept(visitor); } } /** * 组建对象结构,向对象结构中添加元素。 * 不一样的对象结构有不一样的构建方式 * @param ele 加入到对象结构的元素 */ public void addElement(Customer ele){ this.col.add(ele); } }
public class Client { public static void main(String[] args) { //建立ObjectStructure ObjectStructure os = new ObjectStructure(); //准备点测试数据,建立客户对象,并加入ObjectStructure Customer cm1 = new EnterpriseCustomer(); cm1.setName("ABC集团"); os.addElement(cm1); Customer cm2 = new EnterpriseCustomer(); cm2.setName("CDE公司"); os.addElement(cm2); Customer cm3 = new PersonalCustomer(); cm3.setName("张三"); os.addElement(cm3); //客户提出服务请求,传入服务请求的Visitor ServiceRequestVisitor srVisitor = new ServiceRequestVisitor(); os.handleRequest(srVisitor); //要对客户进行偏好分析,传入偏好分析的Visitor PredilectionAnalyzeVisitor paVisitor = new PredilectionAnalyzeVisitor(); os.handleRequest(paVisitor); } }
运行结果以下:
ABC集团企业提出服务请求 CDE公司企业提出服务请求 客户张三提出服务请求 如今对企业客户ABC集团进行产品偏好分析 如今对企业客户CDE公司进行产品偏好分析 如今对我的客户张三进行产品偏好分析
使得代码再也不杂乱,系统结构也更清晰,能方便的维护了
,算是解决了前面示例的一个问题。还有一个问题,就是看看能不能方便的增长新的功能
,前面在示例的时候,故意留下了一个对客户进行价值分析的功能没有实现,那么接下来就看看如何把这个功能增长到已有的系统中。在访问者模式中要给对象结构增长新的功能,只须要把新的功能实现成为访问者,而后在客户端调用的时候使用这个访问者对象来访问对象结构便可。
接下来看看实现对客户价值分析功能的访问者,示例代码以下:
/** * 具体的访问者,实现对客户价值分析 */ public class WorthAnalyzeVisitor implements Visitor { public void visitEnterpriseCustomer(EnterpriseCustomer ec){ //根据购买的金额大小、购买的产品和服务的多少、购买的频率等进行分析 //企业客户的标准会比我的客户的高 System.out.println("如今对企业客户"+ec.getName()+"进行价值分析"); } public void visitPersonalCustomer(PersonalCustomer pc){ System.out.println("如今对我的客户"+pc.getName()+"进行价值分析"); } }
使用这个功能,只要在客户端添加以下的代码便可,示例代码以下:
//要对客户进行价值分析,传入价值分析的Visitor WorthAnalyzeVisitor waVisitor = new WorthAnalyzeVisitor(); os.handleRequest(waVisitor);
#3 模式讲解# ##3.1 认识访问者模式##
访问者模式能给一系列对象,透明的添加新功能
。从而避免在维护期间,对这一系列对象进行修改,并且还能变相实现复用访问者所具备的功能
。
因为是针对一系列对象的操做,这也致使,若是只想给一系列对象中的部分对象添加功能,就会有些麻烦;并且要始终能保证把这一系列对象都要调用到,无论是循环也好,仍是递归也好,总之要让每一个对象都要被访问到。
访问者之因此能实现“为一系列对象透明的添加新功能”,注意是透明的,也就是这一系列对象是不知道被添加功能的
。
重要的就是依靠通用方法
,访问者这边说要去访问,就提供一个访问的方法,如visit方法;而对象那边说,好的,我接受你的访问,提供一个接受访问的方法,如accept方法。这两个方法并不表明任何具体的功能,只是构成一个调用的通路,那么真正的功能实如今哪里呢?又如何调用到呢?
很简单,就在accept方法里面,回调visit的方法,从而回调到访问者的具体实现上,而这个访问者的具体实现的方法才是要添加的新的功能
。
访问者模式可以实如今不改变对象结构的状况下,就能给对象结构中的类增长功能,实现这个效果所使用的核心技术就是两次分发的技术
。
在访问者模式中,当客户端调用ObjectStructure的时候,会遍历ObjectStructure中全部的元素,
调用这些元素的accept方法,让这些元素来接受访问,这是请求的第一次分发
;在具体的元素对象中实现accept方法的时候,
会回调访问者的visit方法,等于请求被第二次分发了
,请求被分发给访问者来进行处理,真正实现功能的正是访问者的visit方法;
两次分发技术具体的调用过程示意如图25.5所示:
两次分发技术使得客户端的请求再也不被静态的绑定在元素对象上
,这个时候真正执行什么样的功能同时取决于访问者类型和元素类型,就算是同一种元素类型,只要访问者类型不同,最终执行的功能也不会同样,这样一来,就能够在元素对象不变的状况下,经过改变访问者的类型,来改变真正执行的功能。
两次分发技术还有一个优势,就是能够在程序运行期间进行动态的功能组装和切换,只须要在客户端调用时,组合使用不一样的访问者对象实例便可。
从另外一个层面思考,Java回调技术也有点相似于两次分发技术,客户端调用某方法,这个方法就相似于accept方法,传入一个接口的实现对象,这个接口的实现对象就有点像是访问者,在方法内部,会回调这个接口的方法,就相似于调用访问者的visit方法,最终执行的仍是接口的具体实现里面实现的功能
。
在看上面的示例的时候,细心的朋友会发现,在企业客户对象和我的客户对象中实现的accept方法从表面上看是类似的,都须要回调访问者的方法,可能就会有朋友想,为何不把回调访问者方法的调用语句放到父类中去,那样不就能够复用了吗?
请注意,这是不能够的,虽然看起来是类似的语句,但实际上是不一样的,主要的玄机就在传入的this身上
。this是表明当前的对象实例的,在企业客户对象中传递的就是企业客户对象的实例,在我的客户对象中传递的就是我的客户对象的实例,这样在访问者的实现中,就能够经过这不一样的对象实例来访问不一样的实例对象的数据了。
若是把这句话放到父类中,那么传递的就是父类对象的实例,是没有子对象的数据的,所以这句话不能放到父类中去。
访问者模式的调用顺序如图25.6所示:
并非全部的访问方法都须要实现,因为访问者模式默认的是访问对象结构中的全部元素
,所以在实现某些功能的时候,若是不须要涉及到某些元素的访问方法,这些方法能够实现成为空的,好比:这个访问者只想要处理组合对象 ,那么访问叶子对象的方法就能够为空,虽然仍是须要访问全部的元素对象。
还有一种就是有条件接受访问
,在本身的accept方法里面进行判断,知足要求的接受,不知足要求的,就至关于空的访问方法,什么都不用作。
##3.2 操做组合对象结构## 访问者模式一个很常见的应用,就是和组合模式结合使用,经过访问者模式来给由组合模式构建的对象结构增长功能。
对于使用组合模式构建的组合对象结构,对外有一个统一的外观,要想添加新的功能也不是很困难,只要在组件的接口上定义新的功能就能够了,麻烦的是这样一来,须要修改全部的子类。并且,每次添加一个新功能,都须要这么痛苦一回,修改组件接口,而后修改全部的子类,这是至关糟糕的。
为了让组合对象结构更灵活、更容易维护和更好的扩展性,接下来把它改形成访问者模式和组合模式组合来实现
。这样在从此再进行功能改造的时候,就不须要再改动这个组合对象结构了。
访问者模式和组合模式组合使用的思路:
首先把组合对象结构中的功能方法分离出来,虽然维护组合对象结构的方法也能够分离出来,可是为了维持组合对象结构自己,这些方法仍是放在组合对象结构里面;而后把这些功能方法分别实现成为访问者对象,经过访问者模式添加到组合的对象结构中去。
下面经过访问者模式和组合模式组合来实现以下功能:输出对象的名称,在组合对象的名称前面添加“节点:”,在叶子对象的名称前面添加“叶子:”。
访问者接口很是简单,只须要定义访问对象结构中不一样对象的方法,示例代码以下:
/** * 访问组合对象结构的访问者接口 */ public interface Visitor { /** * 访问组合对象,至关于给组合对象添加访问者的功能 * @param composite 组合对象 */ public void visitComposite(Composite composite); /** * 访问叶子对象,至关于给叶子对象添加访问者的功能 * @param leaf 叶子对象 */ public void visitLeaf(Leaf leaf); }
而后来对已有的组合对象进行改造,添加通用的功能方法,固然在参数上须要传入访问者。先在组件定义上添加这个方法,而后到具体的实现类里面去实现。除了新加这个方法外,组件定义没有其它改变,示例代码以下:
/** * 抽象的组件对象,至关于访问者模式中的元素对象 */ public abstract class Component { /** * 接受访问者的访问 * @param visitor 访问者对象 */ public abstract void accept(Visitor visitor); /** * 向组合对象中加入组件对象 * @param child 被加入组合对象中的组件对象 */ public void addChild(Component child) { // 缺省实现,抛出例外,叶子对象没这个功能,或子组件没有实现这个功能 throw new UnsupportedOperationException("对象不支持这个功能"); } /** * 从组合对象中移出某个组件对象 * @param child 被移出的组件对象 */ public void removeChild(Component child) { // 缺省实现,抛出例外,叶子对象没这个功能,或子组件没有实现这个功能 throw new UnsupportedOperationException("对象不支持这个功能"); } /** * 返回某个索引对应的组件对象 * @param index 须要获取的组件对象的索引,索引从0开始 * @return 索引对应的组件对象 */ public Component getChildren(int index) { throw new UnsupportedOperationException("对象不支持这个功能"); } }
改变了组件定义,那么须要在组合类和叶子类上分别实现这个方法,组合类中实现的时候,一般会循环让全部的子元素都接受访问,这样才能为全部的对象都添加上新的功能
,示例代码以下:
/** * 组合对象,能够包含其它组合对象或者叶子对象, * 至关于访问者模式的具体Element实现对象 */ public class Composite extends Component{ public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitComposite(this); //循环子元素,让子元素也接受访问 for(Component c : childComponents){ //调用子对象接受访问,变相实现递归 c.accept(visitor); } } /** * 用来存储组合对象中包含的子组件对象 */ private List<Component> childComponents = new ArrayList<Component>(); /** * 组合对象的名字 */ private String name = ""; /** * 构造方法,传入组合对象的名字 * @param name 组合对象的名字 */ public Composite(String name){ this.name = name; } public void addChild(Component child) { childComponents.add(child); } public String getName() { return name; } }
叶子对象的基本实现,示例代码以下:
/** * 叶子对象,至关于访问者模式的具体Element实现对象 */ public class Leaf extends Component{ public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitLeaf(this); } /** * 叶子对象的名字 */ private String name = ""; /** * 构造方法,传入叶子对象的名字 * @param name 叶子对象的名字 */ public Leaf(String name){ this.name = name; } public String getName() { return name; } }
组合对象结构已经改造好了,如今须要提供一个访问者的实现,它会实现真正的功能,也就是要添加到对象结构中的功能。示例代码以下:
/** * 具体的访问者,实现:输出对象的名称,在组合对象的名称前面添加"节点:", * 在叶子对象的名称前面添加"叶子:" */ public class PrintNameVisitor implements Visitor { public void visitComposite(Composite composite) { //访问到组合对象的数据 System.out.println("节点:"+composite.getName()); } public void visitLeaf(Leaf leaf) { //访问到叶子对象的数据 System.out.println("叶子:"+leaf.getName()); } }
访问者是给一系列对象添加功能的,所以一个访问者须要访问全部的对象
,为了方便遍历整个对象结构,一般会定义一个专门的类出来,在这个类里面进行元素迭代访问,同时这个类提供客户端访问元素的接口。
对于这个示例,因为在组合对象结构里面,已经实现了对象结构的遍历,原本是能够不须要这个ObjectStructure的,可是为了更清晰的展现访问者模式的结构,也为了从此的扩展或实现方便,仍是定义一个ObjectStructure。示例代码以下:
/** * 对象结构,一般在这里对元素对象进行遍历,让访问者能访问到全部的元素 */ public class ObjectStructure { /** * 表示对象结构,能够是一个组合结构 */ private Component root = null; /** * 提供给客户端操做的高层接口 * @param visitor 客户端须要使用的访问者 */ public void handleRequest(Visitor visitor){ //让组合对象结构中的根元素,接受访问 //在组合对象结构中已经实现了元素的遍历 if(root!=null){ root.accept(visitor); } } /** * 传入组合对象结构 * @param ele 组合对象结构 */ public void setRoot(Component ele){ this.root = ele; } }
public class Client { public static void main(String[] args) { //定义全部的组合对象 Component root = new Composite("服装"); Component c1 = new Composite("男装"); Component c2 = new Composite("女装"); //定义全部的叶子对象 Component leaf1 = new Leaf("衬衣"); Component leaf2 = new Leaf("夹克"); Component leaf3 = new Leaf("裙子"); Component leaf4 = new Leaf("套装"); //按照树的结构来组合组合对象和叶子对象 root.addChild(c1); root.addChild(c2); c1.addChild(leaf1); c1.addChild(leaf2); c2.addChild(leaf3); c2.addChild(leaf4); //建立ObjectStructure ObjectStructure os = new ObjectStructure(); os.setRoot(root); //调用ObjectStructure来处理请求功能 Visitor psVisitor = new PrintNameVisitor(); os.handleRequest(psVisitor); } }
输出的效果以下:
节点:服装 节点:男装 叶子:衬衣 叶子:夹克 节点:女装 叶子:裙子 叶子:套装
看看结果,是否是指望的那样呢?
好好体会一下,想一想访问者模式是如何实现动态的给组件添加功能的?尤为是要想一想,实现的机制是什么?真正实现新功能的地方在哪里?
前面是分步的示范,你们已经体会了一番,接下来小结一下。
如同前面的示例,访问者的方法就至关于做用于组合对象结构中各个元素的操做,是一种通用的表达,一样的访问者接口和一样的方法,只要提供不一样的访问者具体实现,就表示不一样的功能
。
同时在组合对象中,接受访问的方法,也是一个通用的表达,无论你是什么样的功能,通通接受就行了,而后回调回去执行真正的功能。这样一来,各元素的类就不用再修改了,只要提供不一样的访问者实现,而后经过这个通用表达,就结合到组合对象中来了,就至关于给全部的对象提供了新的功能
。
示例的总体结构,如图25.7所示:
##3.3 谁负责遍历全部元素对象## 在访问者模式中,访问者必需要可以访问到对象结构中的每一个对象,由于访问者要为每一个对象添加功能
,为此特别在模式中定义出一个ObjectStructure来,而后由ObjectStructure来负责遍历访问一系列对象中的每一个对象。
一种是元素的对象结构是经过集合来组织的,那么直接在ObjectStructure中对集合进行迭代
,对每个元素调用accept就行了。
另外一种状况是元素的对象结构是经过组合模式来组织的,一般能够构成对象树,这种状况通常就不须要在ObjectStructure中迭代了
,而一般的作法是在组合对象的accept方法里面,递归遍历它的子元素,而后调用子元素的accept方法。
在实际开发中,有一种典型的状况能够不须要ObjectStructure对象,那就是只有一个被访问对象的时候
。只有一个被访问对象,固然就不须要使用ObjectStructure来组合和迭代了,只要调用这个对象就行了。
事实上还有一种状况也能够不使用ObjectStructure,好比上面访问的组合对象结构,从客户端的角度看,他访问的其实就是一个对象,所以能够把ObjectStructure去掉,而后直接从客户端调用元素的accept方法。
仍是经过示例来讲明,先把ObjectStructure类去掉,因为没有了ObjectStructure,那么客户端调用的时候就直接调用组合对象结构的根元素的accept方法
,示例代码以下:
public class Client { public static void main(String[] args) { //定义组件数据,组装对象树,跟刚才的测试同样,这里就省略了 Visitor psVisitor = new PrintNameVisitor(); root.accept(psVisitor); } }
遍历元素的方法也能够放到访问者当中去,固然也是须要递归遍历它的子元素的
。出现这种状况的主要缘由是:想在访问者中实现特别复杂的遍历,访问者的实现依赖于对象结构的操做结果。使用访问者模式和组合模式组合来实现了输出名称的功能,若是如今要实现把组合的对象结构按照树的形式输出,就是按照在组合模式中示例的那样,输出以下的树形结构:
+服装 +男装 -衬衣 -夹克 +女装 -裙子 -套装
要实现这个功能,在组合对象结构中去遍历子对象的方式就比较难于实现,由于要输出这个树形结构,须要控制每一个对象在输出的时候,向后的退格数量,这个须要在对象结构的循环中来控制,这种功能能够选择在访问者当中去遍历对象结构。
来改造上面的示例,看看经过访问者来遍历元素如何实现这样的功能。
首先在Composite的accept实现中去除掉递归调用子对象的代码,同时添加一个让访问者访问到其所包含的子对象的方法,示例代码以下:
public class Composite extends Component { //其它相同部分就省略了,只看变化的方法 public void accept(Visitor visitor) { //回调访问者对象的相应方法 visitor.visitComposite(this); // for(Component c : childComponents) { // // 调用子对象接受访问,变相实现递归 // c.accept(visitor); // } } public List<Component> getChildComponents() { return childComponents; } }
而后新实现一个访问者对象,在相应的visit实现里面,添加递归迭代全部子对象,示例代码以下:
/** * 具体的访问者,实现:输出组合对象自身的结构 */ public class PrintStructVisitor implements Visitor { /** * 用来累计记录对象须要向后退的格 */ private String preStr = ""; public void visitComposite(Composite composite) { //先把本身输出去 System.out.println(preStr+"+"+composite.getName()); //若是还包含有子组件,那么就输出这些子组件对象 if(composite.getChildComponents()!=null){ //而后添加一个空格,表示向后缩进一个空格 preStr+=" "; //输出当前对象的子对象了 for(Component c : composite.getChildComponents()){ //递归输出每一个子对象 c.accept(this); } //把循环子对象所多加入的一个退格给去掉 preStr = preStr.substring(0,preStr.length()-1); } } public void visitLeaf(Leaf leaf) { //访问到叶子对象的数据 System.out.println(preStr+"-"+leaf.getName()); } }
写个客户端来测试一下看看,是否能实现要求的功能。示例代码以下:
public class Client { public static void main(String[] args) { //定义全部的组合对象过程跟上一个client是同样的,这里省略了 //调用根元素的方法来接受请求功能 Visitor psVisitor = new PrintStructVisitor(); root.accept(psVisitor); } }
##3.4 访问者模式优缺点##
可以在不修改对象结构中的元素的状况下,给对象结构中的元素添加新的功能。
能够经过访问者来定义整个对象结构通用的功能,从而提升复用程度。
能够经过访问者来分离无关的行为,把相关的行为封装在一块儿,构成一个访问者,这样每个访问者的功能都比较单一。
不适用于对象结构中的类常常变化的状况,由于对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价过高。
访问者模式一般须要对象结构开放内部数据给访问者和ObjectStructrue,这破坏了对象的封装性。
##3.5 思考访问者模式##
访问者模式的本质:预留通路,回调实现。
仔细思考访问者模式,它的实现主要就是经过预先定义好调用的通路,在被访问的对象上定义accept方法,在访问者的对象上定义visit方法;而后在调用真正发生的时候,经过两次分发的技术,利用预先定义好的通路,回调到访问者具体的实现上
。
明白了访问者模式的本质,就能够在定义一些通用功能,或者设计工具类的时候让访问者模式派上大用场了
。你能够把已经实现好的一些功能,把它们做为已有的对象结构,由于在从此可能会根据实际须要给它们增长新的功能,甚至你但愿开放接口来让其它开发人员扩展这些功能,那么你就能够用访问者模式来设计,在这个对象结构上预留好通用的调用通路,在之后添加功能,或者是其它开发人员来扩展的时候,只须要提供新的访问者实现,就可以很好的加入到系统中来了。
建议在以下状况中,选用访问者模式:
若是想对一个对象结构,实施一些依赖于对象结构中的具体类的操做,可使用访问者模式。
若是想对一个对象结构中的各个元素,进行不少不一样的并且不相关的操做,为了不这些操做使得类变得杂乱,可使用访问者模式,把这些操做分散到不一样的访问者对象中去,每一个访问者对象实现同一类功能。
若是对象结构不多变更,可是须要常常给对象结构中的元素对象定义新的操做,可使用访问者模式。
##3.6 相关模式##
这两个模式能够组合使用。
如同前面示例的那样,经过访问者模式给组合对象预留下扩展功能的接口,使得给组合模式的对象结构添加功能很是容易。
这两个模式从表面看功能有些类似,都可以实如今不修改原对象结构的状况下修改原对象的功能
。可是装饰模式更多的是实现对已有功能增强、或者修改、或者彻底全新实现
;而访问者模式更多的是实现给对象结构添加新的功能
。
这两个模式能够组合使用。
解释器模式在构建抽象语法树的时候,是使用组合模式来构建的
,也就是说解释器模式解释并执行的抽象语法树是一个组合对象结构
,这个组合对象结构是不多变更的,可是可能常常须要为解释器增长新的功能
,实现对同一对象结构的不一样解释和执行的功能,这正好是访问者模式的优点所在,所以这在使用解释器模式的时候一般会组合访问者模式来使用。