25.3  模式讲解

25.3.1  认识访问者模式

(1)访问者的功能

访问者模式能给一系列对象,透明的添加新功能。从而避免在维护期间,对这一系列对象进行修改,而且还能变相实现复用访问者所具有的功能。

由于是针对一系列对象的操作,这也导致,如果只想给一系列对象中的部分对象添加功能,就会有些麻烦;而且要始终能保证把这一系列对象都要调用到,不管是循环也好,还是递归也好,总之要让每个对象都要被访问到。

(2)调用通路

访问者之所以能实现“为一系列对象透明的添加新功能”,注意是透明的,也就是这一系列对象是不知道被添加功能的。

重要的就是依靠通用方法,访问者这边说要去访问,就提供一个访问的方法,如visit方法;而对象那边说,好的,我接受你的访问,提供一个接受访问的方法,如accept方法。这两个方法并不代表任何具体的功能,只是构成一个调用的通路,那么真正的功能实现在哪里呢?又如何调用到呢?

很简单,就在accept方法里面,回调visit的方法,从而回调到访问者的具体实现上,而这个访问者的具体实现的方法才是要添加的新的功能。

(3)两次分发技术

访问者模式能够实现在不改变对象结构的情况下,就能给对象结构中的类增加功能,实现这个效果所使用的核心技术就是两次分发的技术。

在访问者模式中,当客户端调用ObjectStructure的时候,会遍历ObjectStructure中所有的元素,调用这些元素的accept方法,让这些元素来接受访问,这是请求的第一次分发;在具体的元素对象中实现accept方法的时候,会回调访问者的visit方法,等于请求被第二次分发了,请求被分发给访问者来进行处理,真正实现功能的正是访问者的visit方法。

两次分发技术具体的调用过程示意如图25.5所示:

图25.5  两次分发

两次分发技术使得客户端的请求不再被静态的绑定在元素对象上,这个时候真正执行什么样的功能同时取决于访问者类型和元素类型,就算是同一种元素类型,只要访问者类型不一样,最终执行的功能也不会一样,这样一来,就可以在元素对象不变的情况下,通过改变访问者的类型,来改变真正执行的功能。

两次分发技术还有一个优点,就是可以在程序运行期间进行动态的功能组装和切换,只需要在客户端调用时,组合使用不同的访问者对象实例即可。

从另一个层面思考,Java回调技术也有点类似于两次分发技术,客户端调用某方法,这个方法就类似于accept方法,传入一个接口的实现对象,这个接口的实现对象就有点像是访问者,在方法内部,会回调这个接口的方法,就类似于调用访问者的visit方法,最终执行的还是接口的具体实现里面实现的功能。

(4)为何不在Component中实现回调visit方法

在看上面的示例的时候,细心的朋友会发现,在企业客户对象和个人客户对象中实现的accept方法从表面上看是相似的,都需要回调访问者的方法,可能就会有朋友想,为什么不把回调访问者方法的调用语句放到父类中去,那样不就可以复用了吗?

请注意,这是不可以的,虽然看起来是相似的语句,但其实是不同的,主要的玄机就在传入的this身上。this是代表当前的对象实例的,在企业客户对象中传递的就是企业客户对象的实例,在个人客户对象中传递的就是个人客户对象的实例,这样在访问者的实现中,就可以通过这不同的对象实例来访问不同的实例对象的数据了。

如果把这句话放到父类中,那么传递的就是父类对象的实例,是没有子对象的数据的,因此这句话不能放到父类中去。

(5)访问者模式的调用顺序示意图

访问者模式的调用顺序如图25.6所示:

图25.6  访问者模式调用顺序示意图

(6)空的访问方法

并不是所有的访问方法都需要实现,由于访问者模式默认的是访问对象结构中的所有元素,因此在实现某些功能的时候,如果不需要涉及到某些元素的访问方法,这些方法可以实现成为空的,比如:这个访问者只想要处理组合对象 ,那么访问叶子对象的方法就可以为空,虽然还是需要访问所有的元素对象。

还有一种就是有条件接受访问,在自己的accept方法里面进行判断,满足要求的接受,不满足要求的,就相当于空的访问方法,什么都不用做。

25.3.2  操作组合对象结构

访问者模式一个很常见的应用,就是和组合模式结合使用,通过访问者模式来给由组合模式构建的对象结构增加功能。

对于使用组合模式构建的组合对象结构,对外有一个统一的外观,要想添加新的功能也不是很困难,只要在组件的接口上定义新的功能就可以了,麻烦的是这样一来,需要修改所有的子类。而且,每次添加一个新功能,都需要这么痛苦一回,修改组件接口,然后修改所有的子类,这是相当糟糕的。

为了让组合对象结构更灵活、更容易维护和更好的扩展性,接下来把它改造成访问者模式和组合模式组合来实现。这样在今后再进行功能改造的时候,就不需要再改动这个组合对象结构了。

访问者模式和组合模式组合使用的思路:首先把组合对象结构中的功能方法分离出来,虽然维护组合对象结构的方法也可以分离出来,但是为了维持组合对象结构本身,这些方法还是放在组合对象结构里面;然后把这些功能方法分别实现成为访问者对象,通过访问者模式添加到组合的对象结构中去。

下面通过访问者模式和组合模式组合来实现如下功能:输出对象的名称,在组合对象的名称前面添加“节点:”,在叶子对象的名称前面添加“叶子:”。

(1)先来定义访问者接口

访问者接口非常简单,只需要定义访问对象结构中不同对象的方法,示例代码如下:/**

* 访问组合对象结构的访问者接口

*/

public interface Visitor {

/**

* 访问组合对象,相当于给组合对象添加访问者的功能

* @param composite 组合对象

*/

public void visitComposite(Composite composite);

/**

* 访问叶子对象,相当于给叶子对象添加访问者的功能

* @param leaf 叶子对象

*/

public void visitLeaf(Leaf leaf);

}

(2)改造组合对象的定义

然后来对已有的组合对象进行改造,添加通用的功能方法,当然在参数上需要传入访问者。先在组件定义上添加这个方法,然后到具体的实现类里面去实现。除了新加这个方法外,组件定义没有其它改变,示例代码如下:/**

* 抽象的组件对象,相当于访问者模式中的元素对象

*/

public abstract class Component {

/**

* 接受访问者的访问

* @param visitor 访问者对象

*/

publicabstract 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(

"对象不支持这个功能");

}

}

(3)实现组合对象和叶子对象

改变了组件定义,那么需要在组合类和叶子类上分别实现这个方法,组合类中实现的时候,通常会循环让所有的子元素都接受访问,这样才能为所有的对象都添加上新的功能,示例代码如下:/**

* 组合对象,可以包含其它组合对象或者叶子对象,

* 相当于访问者模式的具体Element实现对象

*/

public class Composite extends Component{

publicvoid accept(Visitor visitor) {

//回调访问者对象的相应方法

visitor.visitComposite(this);

//循环子元素,让子元素也接受访问

for(Component c : childComponents){

//调用子对象接受访问,变相实现递归

c.accept(visitor);

}

}

/**

* 用来存储组合对象中包含的子组件对象

*/

private List childComponents =

new ArrayList();

/**

* 组合对象的名字

*/

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{

publicvoid accept(Visitor visitor) {

//回调访问者对象的相应方法

visitor.visitLeaf(this);

}

/**

* 叶子对象的名字

*/

private String name = "";

/**

* 构造方法,传入叶子对象的名字

* @param name 叶子对象的名字

*/

public Leaf(String name){

this.name = name;

}

public String getName() {

return name;

}

}

(4)实现一个访问者

组合对象结构已经改造好了,现在需要提供一个访问者的实现,它会实现真正的功能,也就是要添加到对象结构中的功能。示例代码如下:/**

* 具体的访问者,实现:输出对象的名称,在组合对象的名称前面添加"节点:",

* 在叶子对象的名称前面添加"叶子:"

*/

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());

}

}

(5)访问所有元素对象的对象——ObjectStructure

访问者是给一系列对象添加功能的,因此一个访问者需要访问所有的对象,为了方便遍历整个对象结构,通常会定义一个专门的类出来,在这个类里面进行元素迭代访问,同时这个类提供客户端访问元素的接口。

对于这个示例,由于在组合对象结构里面,已经实现了对象结构的遍历,本来是可以不需要这个ObjectStructure的,但是为了更清晰的展示访问者模式的结构,也为了今后的扩展或实现方便,还是定义一个ObjectStructure。示例代码如下:/**

* 对象结构,通常在这里对元素对象进行遍历,让访问者能访问到所有的元素

*/

public class ObjectStructure {

/**

* 表示对象结构,可以是一个组合结构

*/

private Component root = null;

/**

* 提供给客户端操作的高层接口

* @param visitor 客户端需要使用的访问者

*/

publicvoid handleRequest(Visitor visitor){

//让组合对象结构中的根元素,接受访问

//在组合对象结构中已经实现了元素的遍历

if(root!=null){

root.accept(visitor);

}

}

/**

* 传入组合对象结构

* @param ele 组合对象结构

*/

public void setRoot(Component ele){

this.root = ele;

}

}

(6)写个客户端,来看看如何通过访问者去为对象结构添加新的功能,示例代码如下: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);

}

}

输出的效果如下:节点:服装

节点:男装

叶子:衬衣

叶子:夹克

节点:女装

叶子:裙子

叶子:套装

看看结果,是不是期望的那样呢?

好好体会一下,想想访问者模式是如何实现动态的给组件添加功能的?尤其是要想想,实现的机制是什么?真正实现新功能的地方在哪里?

(7)现在的程序结构

前面是分步的示范,大家已经体会了一番,接下来小结一下。

如同前面的示例,访问者的方法就相当于作用于组合对象结构中各个元素的操作,是一种通用的表达,同样的访问者接口和同样的方法,只要提供不同的访问者具体实现,就表示不同的功能。

同时在组合对象中,接受访问的方法,也是一个通用的表达,不管你是什么样的功能,统统接受就好了,然后回调回去执行真正的功能。这样一来,各元素的类就不用再修改了,只要提供不同的访问者实现,然后通过这个通用表达,就结合到组合对象中来了,就相当于给所有的对象提供了新的功能。

示例的整体结构,如图25.7所示:

图25.7  访问者模式结合组合模式的示例的结构示意图

25.3.3  谁负责遍历所有元素对象

在访问者模式中,访问者必须要能够访问到对象结构中的每个对象,因为访问者要为每个对象添加功能,为此特别在模式中定义出一个ObjectStructure来,然后由ObjectStructure来负责遍历访问一系列对象中的每个对象。

(1)在ObjectStructure迭代所有的元素时,又分成两种情况。一种是元素的对象结构是通过集合来组织的,那么直接在ObjectStructure中对集合进行迭代,对每一个元素调用accept就好了。如同前面25.2.4的示例所采用的方式。

另一种情况是元素的对象结构是通过组合模式来组织的,通常可以构成对象树,这种情况一般就不需要在ObjectStructure中迭代了,而通常的做法是在组合对象的accept方法里面,递归遍历它的子元素,然后调用子元素的accept方法,如同前面25.3.2的示例中Composite的实现,在accept方法里面进行递归调用子对象的操作。

(2)不需要ObjectStructure的时候

在实际开发中,有一种典型的情况可以不需要ObjectStructure对象,那就是只有一个被访问对象的时候。只有一个被访问对象,当然就不需要使用ObjectStructure来组合和迭代了,只要调用这个对象就好了。

事实上还有一种情况也可以不使用ObjectStructure,比如上面访问的组合对象结构,从客户端的角度看,他访问的其实就是一个对象,因此可以把ObjectStructure去掉,然后直接从客户端调用元素的accept方法。

还是通过示例来说明,先把ObjectStructure类去掉,由于没有了ObjectStructure,那么客户端调用的时候就直接调用组合对象结构的根元素的accept方法,示例代码如下:public class Client {

public static void main(String[] args) {

//定义组件数据,组装对象树,跟刚才的测试一样,这里就省略了

Visitor psVisitor = new PrintNameVisitor();

root.accept(psVisitor);

}

}

(3)有些时候,遍历元素的方法也可以放到访问者当中去,当然也是需要递归遍历它的子元素的。出现这种情况的主要原因是:想在访问者中实现特别复杂的遍历,访问者的实现依赖于对象结构的操作结果。

比如25.3.2的示例,使用访问者模式和组合模式组合来实现了输出名称的功能,如果现在要实现把组合的对象结构按照树的形式输出,就是按照在组合模式中示例的那样,输出如下的树形结构:+服装

+男装

-衬衣

-夹克

+女装

-裙子

-套装

要实现这个功能,在组合对象结构中去遍历子对象的方式就比较难于实现,因为要输出这个树形结构,需要控制每个对象在输出的时候,向后的退格数量,这个需要在对象结构的循环中来控制,这种功能可以选择在访问者当中去遍历对象结构。

来改造上面的示例,看看通过访问者来遍历元素如何实现这样的功能。

首先在Composite的accept实现中去除掉递归调用子对象的代码,同时添加一个让访问者访问到其所包含的子对象的方法,示例代码如下:public class Composite extends Component{

//其它相同部分就省略了,只看变化的方法

public void accept(Visitor visitor) {

//回调访问者对象的相应方法

visitor.visitComposite(this);

for(Component c : childComponents){

//调用子对象接受访问,变相实现递归

c.accept(visitor);

}

}

public List 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);

}

}

25.3.4  访问者模式优缺点

l          好的扩展性    能够在不修改对象结构中的元素的情况下,给对象结构中的元素添加新的功能

l          好的复用性    可以通过访问者来定义整个对象结构通用的功能,从而提高复用程度

l          分离无关行为    可以通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

l          对象结构变化很困难    不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高

l          破坏封装    访问者模式通常需要对象结构开放内部数据给访问者和ObjectStructrue,这破坏了对象的封装性

25.3.5  思考访问者模式

1:访问者模式的本质

访问者模式的本质:预留通路,回调实现。

仔细思考访问者模式,它的实现主要就是通过预先定义好调用的通路,在被访问的对象上定义accept方法,在访问者的对象上定义visit方法;然后在调用真正发生的时候,通过两次分发的技术,利用预先定义好的通路,回调到访问者具体的实现上。

明白了访问者模式的本质,就可以在定义一些通用功能,或者设计工具类的时候让访问者模式派上大用场了。你可以把已经实现好的一些功能,把它们作为已有的对象结构,因为在今后可能会根据实际需要给它们增加新的功能,甚至你希望开放接口来让其它开发人员扩展这些功能,那么你就可以用访问者模式来设计,在这个对象结构上预留好通用的调用通路,在以后添加功能,或者是其它开发人员来扩展的时候,只需要提供新的访问者实现,就能够很好的加入到系统中来了。

2:何时选用访问者模式

建议在如下情况中,选用访问者模式:如果想对一个对象结构,实施一些依赖于对象结构中的具体类的操作,可以使用访问者模式

如果想对一个对象结构中的各个元素,进行很多不同的而且不相关的操作,为了避免这些操作使得类变得杂乱,可以使用访问者模式,把这些操作分散到不同的访问者对象中去,每个访问者对象实现同一类功能

如果对象结构很少变动,但是需要经常给对象结构中的元素对象定义新的操作,可以使用访问者模式

25.3.6  相关模式

l          访问者模式和组合模式    这两个模式可以组合使用。    如同前面示例的那样,通过访问者模式给组合对象预留下扩展功能的接口,使得给组合模式的对象结构添加功能非常容易。

l          访问者模式和装饰模式    这两个模式从表面看功能有些相似,都能够实现在不修改原对象结构的情况下修改原对象的功能。但是装饰模式更多的是实现对已有功能加强、或者修改、或者完全全新实现;而访问者模式更多的是实现给对象结构添加新的功能。

l          访问者模式和解释器模式    这两个模式可以组合使用。    解释器模式在构建抽象语法树的时候,是使用组合模式来构建的,也就是说解释器模式解释并执行的抽象语法树是一个组合对象结构,这个组合对象结构是很少变动的,但是可能经常需要为解释器增加新的功能,实现对同一对象结构的不同解释和执行的功能,这正好是访问者模式的优势所在,因此这在使用解释器模式的时候通常会组合访问者模式来使用。

更多内容搜索“Java私塾”即可

java私塾设计模式_Java私塾:研磨设计模式 之 访问者模式(Visitor)相关推荐

  1. 设计模式:访问者模式(Visitor Pattern)

    访问者模式(Visitor Pattern): 封装一些作用于某种数据结构的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作. 主要将数据结构与数据操作分离,解决数据结构和操作 ...

  2. 【每天一个java设计模式(二十三)】 - 访问者模式

    在访问者模式中,我们使用了一个访问者类,它改变了元素类的执行算法.通过这种方式,元素的执行算法可以随着访问者改变而改变.这种类型的设计模式属于行为型模式.根据模式,元素对象已接受访问者对象,这样访问者 ...

  3. 设计模式 -- 访问者模式(Visitor)

    写在前面的话:读书破万卷,编码如有神 -------------------------------------------------------------------- 主要内容包括: 初识访问 ...

  4. 设计模式笔记二十五:访问者模式

    原文:http://www.runoob.com/design-pattern/ 少许个人理解,如有错误请指出.欢迎一起讨论. (本文多摘自原文,对于访问者模式的作用还是有些不是很明白,这篇文章貌似比 ...

  5. 设计模式之访问者模式(Visitor)摘录

    23种GOF设计模式一般分为三大类:创建型模式.结构型模式.行为模式. 创建型模式抽象了实例化过程,它们帮助一个系统独立于如何创建.组合和表示它的那些对象.一个类创建型模式使用继承改变被实例化的类,而 ...

  6. 设计模式(17) 访问者模式(VISITOR) C++实现

    意图: 表示一个作用于某对象结构的各元素的操作.它使你可以再不改变各元素的类的前提下定义作用于这些元素的新操作. 动机: 之前在学校的最后一个小项目就是做一个编译器,当时使用的就是访问者模式. 在静态 ...

  7. [设计模式] 23 访问者模式 visitor Pattern

    在GOF的<设计模式:可复用面向对象软件的基础>一书中对访问者模式是这样说的:表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作.访问 ...

  8. 【设计模式】—— 访问者模式Visitor

    对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同.此时,就是访问者模式的典型应用了. 应用场景 1 不同的子类,依赖于不同的其他对象 2 需要对一组对象,进行许多不相关的操作,又 ...

  9. Java的单分派与双分派以及访问者模式的关系

    引言 在学习访问者模式的过程中了解到了单双分派这一词,本文将重点为大家解释Java中的单分派与双分派到底是什么,以及为什么会与访问者模式扯上关系 首先,我们给出一个定义:"Java是一种支持 ...

  10. java的visitor模式_访问者模式Visitor

    一 概述 场景:通常来说,用于封装数据所用到的pojo类,其只包含get.set,对应的业务逻辑是在Service上完成的:但如果出现多个pojo类都共用一套逻辑时,则应该考虑将逻辑进行抽象,不同类型 ...

最新文章

  1. linux下不同程序如何协同,Linux程序员怎么和Exchange协同工作
  2. 基础SQL面试题(1)
  3. MyBatis-13MyBatis动态SQL之【where、set、trim】
  4. Emacs常用快捷键
  5. 2020 前端开源领域技术展望
  6. 《精通J2EE网络编程》中讲的JNDI 6.3总结
  7. 如何实现移动端轮播图的左滑右滑效果
  8. sqlmap绕过d盾_Waf功能、分类与绕过
  9. react 交互_如何在React应用程序中跟踪用户交互
  10. Python基础(五)--函数
  11. 【LeetCode笔记】剑指Offer 59. I 滑动窗口的最大值(Java、单调队列)
  12. Centos7.x 安装 CDH 6.x
  13. 科学宿命论-我们到底有没有自由意志
  14. 在Struts2中实现登陆后跳转到登录前页面
  15. springsecurity 登录失败_145-Spring Security
  16. 在线php网站扫描,一个简单的php在线端口扫描器
  17. 解决xShell4某些情况下按删除键会输出^H的问题
  18. 下载bilibili视频
  19. localbridge.exe 参数错误
  20. vchart 坐标轴标题_ECharts xAxis配置 x坐标轴名称的文本样式

热门文章

  1. Redis设计与实现-监视器
  2. 雷军:《硅谷之火》给了我一个世界级的梦想
  3. 玩转 Flowable 流程实例
  4. 金蝶财务软件有哪些缺点
  5. c 语言dll源码查看,易语言DLL函数查看器源码
  6. 教你如何在 Pycharm 中制作自己的爬虫代码模板
  7. android modbus 串口,手机Modbus 安卓Modbus调试软件
  8. php执行js加密解密
  9. 基于粒子群的ieee30节点优化、配电网有功-无功优化
  10. 游戏设计类毕业论文文献(推荐10篇)