论坛首页 Java企业应用论坛

访问者模式的进阶

浏览 16358 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (4)
作者 正文
   发表时间:2009-03-08   最后修改:2009-03-08
     在好久之前看过一篇文章,关于如何改造访问者模式,有点感触。在论坛上比较少关于访问者模式的帖子,我把自己的想法写下来,一来与大家分享一下,二来抛砖引玉,希望大家能提出关于改造访问者模式的更好的意见:)
原文在http://rjx2008.iteye.com/blog/340083写在blog没什么人看,没办法和更多的人一起讨论。

这里非结合反射(Reflection)去解决问题,而是采用另外一种的思维方式去改造访问者模式,相对于反射,我更偏向于这种。用反射改造访问者模式将在http://rjx2008.iteye.com/blog/345272http://rjx2008.iteye.com/blog/345369中有提及过。

这里引用《大话设计模式》中男人与女人原例子。
既定访问者模式的代码:
具体元素的接口与实现类
public interface Person {
      void accept(Visitor visitor);
}


public class Woman implements Person{

	public void accept(Visitor visitor) {
          visitor.visit(this);
	}
}


public class Man implements Person{

	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
}


访问者接口与实现类,分别代表男人与女人在不同的状态下的表现
public interface Visitor {
      public void visit(Man man);
      public void visit(Woman girl);
}

//成功时Man与Woman的不同表现
public class Success implements Visitor{

	public void visit(Man man) {
		System.out.println("当男人成功时,背后多半有一个伟大的女人");
	}


	public void visit(Woman woman) {
		System.out.println("当女人成功时,背后大多有一个不成功的男人");
	}
}

//恋爱时Man与Woman的不同表现
public class Love implements Visitor{

	public void visit(Man man) {
		System.out.println("当男人恋爱时,凡事不懂也装懂");
	}


	public void visit(Woman girl) {
		System.out.println("当女人恋爱时,遇事懂也装不懂");
	}
}


ObjectStructure与客户端测试代码
import java.util.*;

public class ObjectStructure {
    private List<Person> elements = new ArrayList<Person>();

    public void attach(Person element){
    	elements.add(element);
    }
    
    public void detach(Person element){
    	elements.remove(elements);
    }
    
    //遍历各种具体元素并执行他们的accept方法
    public void display(Visitor visitor){
    	for(Person p:elements){
    		p.accept(visitor);
    	}
    }
}


public class Client {
      public static void main(String[] args) {
		ObjectStructure o = new ObjectStructure();  //依赖于ObjectStructure
		//实例化具体元素
		o.attach(new Man());  
		o.attach(new Woman());
		
		//当成功时不同元素的不同反映
		Visitor success = new Success();           //依赖于抽象的Visitor接口
		o.display(success);
		
		//当恋爱时的不同反映
		Visitor amativeness = new Love();          //依赖于抽象的Visitor接口
		o.display(amativeness);
		
	}
}


需求的变化

假设现在需求要扩展数据结构,增加一种具体元素,男与女之外的一种不明物体,我们暂时把它称为“怪兽”,在既有访问者模式的架构下,应该怎样?首先增加一个Bruce类,实现Person接口。最麻烦的是要修改访问者接口及其所有具体访问者!

既定访问者模式的类图:




        因为Visit方法中没有包含访问Bruce对象的行为,因此我们被迫要去手工更改Visitor(包括抽象的,具体的),在其中添加有关Bruce对象的行为,这严重违反了“开放-封闭”原则。究其原因在于目前的结构下,被访问对象与访问对象互相依赖,自然不利于分离变化,必须去掉一层依赖关系。

我们尝试把Visitor对Person(元素)的依赖关系去掉,抽象出对应每个具体元素的ElementVisitor接口-->ManVisitor,WomanVisitor,然后把Visitor对Person的依赖关系转移到ManVisitor与WomanVisitor身上。
改造后访问者模式的类图:






现在Visitor接口已经没有任何抽象方法,只是一个空接口,每一个具体元素对应有一个ElementVisitor接口,每一个元素对应的ElementVisitor接口有访问该元素的visit(),相当把原来在Visitor接口中声明工作,交由各个具体ElementVisitor接口完成。


经过改造后的代码:
原Visitor接口
public interface Visitor {
      //退化到没有任何抽象方法
}


新增加ManVisitor,WomanVisitor接口
public interface ManVisitor {
      public void visit(Man man);
}

public interface WomanVisitor {
      public void visit(Woman w);
}



具体Visitor实现类现在同时实现3个接口
//由实现Visitor接口扩展成实现Visitor,WomanVisitor,ManVisitor三个接口
public class Success implements Visitor,WomanVisitor,ManVisitor{

	public void visit(Man man) {
		System.out.println("当男人成功时,背后多半有一个伟大的女人");
	}

	public void visit(Woman girl) {
		System.out.println("当女人成功时,背后大多有一个不成功的男人");
	}
}


//由实现Visitor接口扩展成实现Visitor,WomanVisitor,ManVisitor三个接口
public class Love implements Visitor,WomanVisitor,ManVisitor{

	public void visit(Man man) {
		System.out.println("当男人恋爱时,凡事不懂也装懂");
	}

	public void visit(Woman girl) {
		System.out.println("当女人恋爱时,遇事懂也装不懂");
	}
}


Person接口没有变化,依旧只依赖于Visitor接口
public interface Person {
      void accept(Visitor visitor);
}


改造后的具体元素类Man与Woman
public class Man implements Person {

	// 先对visitor进行类型转换,再执行visit方法,因为Visitor接口已经没有声明任何抽象方法了
	public void accept(Visitor visitor) {
		if (visitor instanceof ManVisitor) {
			ManVisitor mv = (ManVisitor) visitor;
			mv.visit(this);
		}
	}
}


public class Woman implements Person {

	// 先对visitor进行类型转换,再执行visit方法,因为Visitor接口已经没有声明任何抽象方法了
	public void accept(Visitor visitor) {
		if (visitor instanceof WomanVisitor) {
			WomanVisitor wv = (WomanVisitor) visitor;
			wv.visit(this);
		}
	}
}


ObjectStructure与客户端测试代码没有变化
import java.util.*;

public class ObjectStructure {
    private List<Person> elements = new ArrayList<Person>();

    public void attach(Person element){
    	elements.add(element);
    }
    
    public void detach(Person element){
    	elements.remove(elements);
    }
    
    //遍历各种具体元素并执行他们的accept方法
    public void display(Visitor visitor){
    	for(Person p:elements){
    		p.accept(visitor);
    	}
    }
}


public class Client {
      public static void main(String[] args) {
		ObjectStructure o = new ObjectStructure();  //依赖于ObjectStructure
		//实例化具体元素
		o.attach(new Man());  
		o.attach(new Woman());
		
		//当成功时不同元素的不同反映
		Visitor success = new Success();           //依赖于抽象的Visitor接口
		o.display(success);
		
		//当恋爱时的不同反映
		Visitor amativeness = new Love();          //依赖于抽象的Visitor接口
		o.display(amativeness);		
	}
}


至此改造完毕!我们执行客户端测试代码,结果显示:
当男人成功时,背后多半有一个伟大的女人
当女人成功时,背后大多有一个不成功的男人
当男人恋爱时,凡事不懂也装懂
当女人恋爱时,遇事懂也装不懂

此时,客户端仍然只依赖于Visitor空接口与ObjectStructure类。可能一开始大家会认为空接口没有什么用,现在就能体现出他的威力了,使客户端与具体Visitor的高度解耦!也正是这种思维的核心
在Java API中也有类似的应用,这种空接口被称为标识接口。比如java.io.Serializable与java.rmi.Remote等,标识接口里没有任何方法和属性,标识不对实现接口不对实现它的类有任何语义上的要求,它仅仅是表明实现它的类属于一种特定的类型。
上面具体访问者实现的多个接口被称为混合类型。这个概念《Java与模式》中有提及过:当一个具体类处于一个类的等级结构之中的时候,为这个具体类定义一个混合类型是可以保证基于这个类型的可插入性的关键。

=================================无敌分界线====================================

讲了这么长,现在我们测试下改造后的访问者模式
首先增加一种行为(状态),即原访问者模式的优点

增加一个具体访问者Fail,修改一下客户端测试代码
public class Fail implements Visitor,ManVisitor,WomanVisitor{

	public void visit(Man man) {
		System.out.println("当男人失败时,闷头喝酒,谁也不用劝");
	}

	public void visit(Woman woman) {
		System.out.println("当女人失败时,眼泪汪汪,谁也劝不了");
	}
}



public class Client {
      public static void main(String[] args) {
		ObjectStructure o = new ObjectStructure();  //依赖于ObjectStructure
		//实例化具体元素
		o.attach(new Man());  
		o.attach(new Woman());
		
		//当成功时不同元素的不同反映
		Visitor success = new Success();           //依赖于抽象的Visitor接口
		o.display(success);
		System.out.println();
		
		//当恋爱时的不同反映
		Visitor amativeness = new Love();          //依赖于抽象的Visitor接口
		o.display(amativeness);		
		System.out.println();
		
		//新增加失败时的不同反映
		Visitor fail = new Fail();
		o.display(fail);
	}
}


结果显示:
当男人成功时,背后多半有一个伟大的女人
当女人成功时,背后大多有一个不成功的男人

当男人恋爱时,凡事不懂也装懂
当女人恋爱时,遇事懂也装不懂

当男人失败时,闷头喝酒,谁也不用劝
当女人失败时,眼泪汪汪,谁也劝不了



增加新的行为(状态)与原来一样方便!只需要增加一个具体访问者即可!
现在我们来增加一个具体元素(正是写这篇文章的初衷)

首先增加一个具体元素Bruce
public class Bruce implements Person{
    
	public void accept(Visitor visitor) {		
		if(visitor instanceof BruceVisitor){
			BruceVisitor bv = (BruceVisitor) visitor;
			bv.visit(this);
		}
		//这个else可写可不写
		else{
			String s = visitor.getClass().getName();
			String state = s.substring(s.lastIndexOf(".")+1,s.length());
			System.out.println("噢..原来怪兽在"+state+"的时候是没有行为的!!");
		}		
	}
}


//按照新的思维方式增加一个对应的ElementVisitor接口
public interface BruceVisitor {
      public void visit(Bruce bruce);
}


我们让Success这个具体访问者多实现一个BruceVisitor访问者接口,和修改一下客户端代码进行测试
public class Success implements Visitor,WomanVisitor,ManVisitor,BruceVisitor{

	public void visit(Man man) {
		System.out.println("当男人成功时,背后多半有一个伟大的女人");
	}

	public void visit(Woman girl) {
		System.out.println("当女人成功时,背后大多有一个不成功的男人");
	}

	public void visit(Bruce bruce) {
		System.out.println("当怪兽成功时.........无语..........");
	}
}


public class Client {
      public static void main(String[] args) {
		ObjectStructure o = new ObjectStructure();  //依赖于ObjectStructure

		o.attach(new Man());  
		o.attach(new Woman());
		o.attach(new Bruce());      //新增一种具体元素Bruce
		
		Visitor success = new Success();           //依赖于抽象的Visitor接口
		o.display(success);
		System.out.println();
		
		Visitor amativeness = new Love();          //依赖于抽象的Visitor接口
		o.display(amativeness);		
		System.out.println();
		
		Visitor fail = new Fail();
		o.display(fail);
	}
}


显示结果:
当男人成功时,背后多半有一个伟大的女人
当女人成功时,背后大多有一个不成功的男人
当怪兽成功时.........无语..........

当男人恋爱时,凡事不懂也装懂
当女人恋爱时,遇事懂也装不懂
噢..原来怪兽在Love的时候是没有行为的!!

当男人失败时,闷头喝酒,谁也不用劝
当女人失败时,眼泪汪汪,谁也劝不了
噢..原来怪兽在Fail的时候是没有行为的!!


这个结果你满意吗?
虽然,这只是部分符合“开放-封闭”原则,我们不需要修改Visitor接口,但还是得去修改Success实现新的接口。但是修改具体类比修改接口的代价小得多,不需要重新编译所有访问接口和具体访问者。使我们面对新的变化也容易得多。而且这还有一个好处,就是可以让各种元素有选择地让别人访问,如上述例子,这样使访问者模式的运用起来更加灵活
   发表时间:2009-03-09  
讲得很清楚!
0 请登录后投票
   发表时间:2009-03-09  
为啥不让ManVisitor,WomanVisitor extends Visitor,却让Success实现3个接口?

0 请登录后投票
   发表时间:2009-03-09  
bloodrate 写道
为啥不让ManVisitor,WomanVisitor extends Visitor,却让Success实现3个接口?


这就是因为标识接口Visitor与ManVisitor,WomanVisitor并不是同一类的,或许说他们之间没有必然的继承关系。类似Java API,ArrayList实现了List接口与Serializable接口,按这种思维,为什么不直接让List来继承Serializable接口而要让ArrayList分别实现这两个接口?
假设需求需要扩展,系统要有一批接口extends ManVisitor,WomanVisitor,或者有一批类需要去实现这些元素Visitor,等等,但他们不能够是Visitor类型,这时候就会有问题了。
用在这个例子上,的确可以让ManVisitor,WomanVisitor extends Visitor,但如果只是为了方便其实完全可以废弃Visitor,然后客户端传入一个Object类型就可以。
这样Visitor就失去了他原有的意义了。
0 请登录后投票
   发表时间:2009-03-09  
我觉得你这个例子和ArrayList有本质区别,List和Serializable是没有联系,但是我觉得你的例子中

public void accept(Visitor visitor) {         
        if(visitor instanceof BruceVisitor){  
            BruceVisitor bv = (BruceVisitor) visitor;  
            bv.visit(this);  
        }  
}

你首先用Visitor类型的引用承接了一个对象,然后再判断它是否属于BruceVisitor,这体现出了一种继承关系,我首先知道他是访问者类,在这个前提下判断是哪一种访问者。
另外
class A implements Serializable{
  List l = new ArrayList();
}
这样写在例如IBM RSA这样的IDE里会报错,因为A类可序列化,但是A类的元素l呈现出不可序列化的特征,原因在于List和Serializable是并列的接口,用List去引用的对象体现不出Serializable的特征,如果改成ArrayList l = new ArrayList()问题就不存在了,倘若Serializable是List父类,那么没有这个问题。

我起初以为用一个类A接口引用这个类而使用B接口特性是不允许的,做了个实验证明还是允许的,但我总觉得这样不太好。
0 请登录后投票
   发表时间:2009-03-09   最后修改:2009-03-09
unsid 写道
我觉得你这个例子和ArrayList有本质区别,List和Serializable是没有联系,但是我觉得你的例子中

public void accept(Visitor visitor) {         
        if(visitor instanceof BruceVisitor){  
            BruceVisitor bv = (BruceVisitor) visitor;  
            bv.visit(this);  
        }  
}

你首先用Visitor类型的引用承接了一个对象,然后再判断它是否属于BruceVisitor,这体现出了一种继承关系,我首先知道他是访问者类,在这个前提下判断是哪一种访问者。
另外
class A implements Serializable{
  List l = new ArrayList();
}
这样写在例如IBM RSA这样的IDE里会报错,因为A类可序列化,但是A类的元素l呈现出不可序列化的特征,原因在于List和Serializable是并列的接口,用List去引用的对象体现不出Serializable的特征,如果改成ArrayList l = new ArrayList()问题就不存在了,倘若Serializable是List父类,那么没有这个问题。

我起初以为用一个类A接口引用这个类而使用B接口特性是不允许的,做了个实验证明还是允许的,但我总觉得这样不太好。

和Serializable是不一样的,我是想举例说下标识接口的作用,并不是生搬硬套进去。
之前也说过,可以理解为Visitor接口,与具体元素接口不是同一类的,或许说他们之间没有必然的继承关系。
新需求要有一批接口extends ManVisitor,WomanVisitor,或者有一些类需要去实现这些元素Visitor去完成某种功能,等等,但他们不能够是Visitor类型。又假如需求要变更,系统要求Visitor接口声明一到两个方法,所有Visitor类型都要实现这些方法达到某种规范。这样ElementVisitor接口去extends Visitor接口就会有矛盾。刚才说的那一批新加的类也要被迫同时实现Visitor的方法。。
当然从规范上说,当Visitor接口声明了一些方法之后,就不能再说是标识接口。如果单纯按上面的例子,其实你想怎么做,都不会有问题,也可以直接废弃那个Visitor接口。这个接口只是起到某种类型的规范作用。他和元素对应的ElementVisitor接口,有可能是同一类型,但不是必然是同一类型。
因此,让Success,Fail等同时实现Visitor与ElementVisitor接口,比ElementVisitor extends Visitor接口,更容易适应新需求的变化。。。
0 请登录后投票
   发表时间:2009-03-10   最后修改:2009-03-10
引用

和Serializable是不一样的,我是想举例说下标识接口的作用,并不是生搬硬套进去。
之前也说过,可以理解为Visitor接口,与具体元素接口不是同一类的,或许说他们之间没有必然的继承关系。
新需求要有一批接口extends ManVisitor,WomanVisitor,或者有一些类需要去实现这些元素Visitor去完成某种功能,等等,但他们不能够是Visitor类型。又假如需求要变更,系统要求Visitor接口声明一到两个方法,所有Visitor类型都要实现这些方法达到某种规范。这样ElementVisitor接口去extends Visitor接口就会有矛盾。刚才说的那一批新加的类也要被迫同时实现Visitor的方法。。
当然从规范上说,当Visitor接口声明了一些方法之后,就不能再说是标识接口。如果单纯按上面的例子,其实你想怎么做,都不会有问题,也可以直接废弃那个Visitor接口。这个接口只是起到某种类型的规范作用。他和元素对应的ElementVisitor接口,有可能是同一类型,但不是必然是同一类型。
因此,让Success,Fail等同时实现Visitor与ElementVisitor接口,比ElementVisitor extends Visitor接口,更容易适应新需求的变化。。。

 

我觉得这里,ManVisitor,WomanVisitor应该去实现Visitor接口,而不是把它作为一个标识接口,不论是子类ManVisitor等的命名,还是类层次性理解都会有点问题。

 

   你的代码中把ManVisitor,WomanVisitor和BruceVisitor三种实现类实现Visitor接口,却让他们依赖一个具体类Man,Woman和Brucem,为何不这么设计呢,

public interface Visitor{
    void accept(Person person);
}

这样Visitor不再依赖Man和Woman等具体类,Visitor只要是Person的实现类。

 

然后三个接口设计成三个抽象类,实现这个接口。

 

比如ManVisitor:

public abstract class ManVisitor implements Visitor
{
    public abstract void accept(Man man);

  
    public void accept(Person person){
             if(person instanceof Man)
             {
                    Man man=(Man)person;
                    accept(man);
             }
             else
                   throw new IllegalArgumentException("....");
    }

...
}

 

如果想要体现Visitor的标识性,何不把Visitor接口命名为Visitable呢?

 

0 请登录后投票
   发表时间:2009-03-10  
public void accept(Visitor visitor)
从这句话,我感觉到Visitor并不是典型的标识接口,比如什么时候见过一个方法public void foo(Serializable s),我认为标识接口是为了让一个类具有某种能力而不是多态特性,你这里显然用了多态特性...
典型的标识接口应该这么用:
public void foo(A a){
    doSomthing..           (1)

    if(a instanceof B){
       doAnotherThing....  (2)
    }

    continue doSomthing... (3)
}
其中B是A的标示接口,B接口赋予了A某种额外的能力,如果A类没有实现B接口,顶多是少一些功能,但是还是照常使用.比如Serializable,Comparable.不实现它的类依然可能正常使用只是不能序列化和比较,而你的接口,直接在方法内作为参数类型,不实现它程序编译不过去,并没有"赋予某种额外能力"的语义,属于功能性接口.
0 请登录后投票
   发表时间:2009-03-10  
当然,标示接口还有另外一种用法,就是Collections.sort()方法和Comparable接口,需要在主要类之外设计一个功能类,将部分功能从主类里独立出来.
0 请登录后投票
   发表时间:2009-03-10   最后修改:2009-03-10
当初提出标识接口只是提供对空接口Visitor一种理解方向。如果觉得不能这样理解或者理解错误不去这样理解就OK了,更不是要把Visitor与Serializable摆在一起,有点晕,或许我当初说得不好,感谢大家的意见,但不用再举类似例子了:)
觉得Visitor没有意义的朋友也可以选择不用,只是我认为他有存在的价值,至于说编译不过去有点怪。接受一个Object类型就可以了,不过我不太认同这种做法。
感谢mercyblitz的意见,但貌似行不通吧,一个访问者只能继承一种元素Visitor抽象类了。而且我认为必须有一处依赖于具体元素Man,Woman等,所以把原来Visitor对具体元素的依赖关系转移到对应的每个元素Visitor接口上。不需要依赖具体元素,这个可能就不是访问者模式了。
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics