访问者模式

访问者模式

问题引入1.0

我有很多某系列的类,比如猫类、狗类、鸟类等,这些类是已经发布上线的类。

现在有一个新需求:需要给每一个动物制作一张身份证,也即按照某种格式导出其相关的信息。
解决方案很简单:在每个类中都加入一个export方法即可。甚至可以用多态调用这些方法,不错

好,又来了个新需求:需要给某些动物做一件衣服。好,你又分别往这些对应的类中添加了做衣服的方法。仍然可以用多态,但已经不是所有的动物都需要这个方法了。

然后又来了个新需求:要给某些动物做量身定制的饭,然后要做量身定制的菜…

需求是无休止的,显然你不能总是往这些类里面塞东西。这些功能或许都不应该是类关心的功能,违反了单一职责原则;同时它们是已经上线的,对此大肆修改可能会引入潜在的风险。即使不考虑这些,创造出一个臃肿不堪的类也是让人十分难受的。

因此:可以把所有的这些访问类的方法,统统包装到一个访问者类中。,同时将要访问的对象作为参数传递给这些方法(组合),这样方法就可以获取对象的所有信息了。即 visitCat(cat), visitDog(dog)

通过外包的访问者来调用这些方法,一个访问者,多个方法

问题引入2.0

那么继续,让我们再回到第一个需求上:为每一个动物办身份证。createIDCard(Animal x);
按理说,只需 for (Animal x : animals) x.doSth()即可。可是,如何让猫找到它对应的方法doCatThings?或许可以用ifelse来判断,instanceof等等。但是如此多的if-else,显然不够优雅,也不好维护。

怎么解决这个问题呢?其实,要找猫调用哪个方法,这个问题应该去问猫。因为它自己肯定知道自己适合哪个方法。
所以:我们在每一个类中,都定义一个accept(visitor)方法。在这个方法中,猫调用猫的方法,狗调用狗的方法。而我们只需要利用多态机制,调用这个accept方法就可以了。

代码框架

这就是访问者模式。可能有点绕,但是看代码就明白了
首先是访问者,里面“注册”了所有已知的访问方法:

public abstract class AbstractVisitor{
	public void visitAnimal(Animal animal){animal.accept(visitor:this);}

	public void visitCat(Cat c);
	public void visitDog(Dog d);
	public void visitBird(Bird b);
	...

然后是我们具体想实现的功能:

public class CreateIDCard extends AbstractVisitor{
	@overide
	public void visitCat(Cat c){sout << c.id;}
	@overide
	public void visitDog(Dog d){sout << d.id;}
	...
	// 功能的入口
	public void create(List<Animal> animals){
		for(Animal a : animals){
			visitAnimal(a);
		}
	}

到这里,我们整合了这些访问方法,下面是对动物类的改动。是的,终究还是会改动一些原来的类的,但是改动很小。

public abstract class Animal{
	...
	public void accept(AbstractVisitor visitor);
}

然后是每一个具体的类

public class Cat extends Animal{
	@overide
	public void accept(AbstractVisitor visitor){
		// 由猫类自己来决定调用哪一个访问函数。是Cat,所以调用visitCat
		visitor.visitCat(this);
	}
}

public class Dog extends Animal{
	@overide
	public void accept(AbstractVisitor visitor){
		visitor.visitDog(this);
	}
}

流程分析

现在让我们分析下整个的执行流程:

public static void main(String[] args){
	CreateIDCard creator = new CreateIDCard();
	List<Animal> animals = Arrays.ofList(catA, catB, dogC);
	creator.create(animals);
}

首先我们创建了一个creator,其实就是继承了抽象访问者类的一个visitor,然后调用其中的入口函数,指定动物的id就被输出出来了。
怎么做到的呢?

首先执行的是 visitAnimal(catA),这个方法我们没有重载,因此会去调用父类AbstractVisitor中的visitAnimal方法,该方法又会调用抽象类Animal中的accept函数,accpet的参数是this,也就是我们定义的creator。而是谁调用的这个accept呢,是参数“animal”,也即我们传入的catA,所以,根据多态机制,这里又会去调用Cat的accept方法,传入的是creator。好的,还有一重多态,因为creator是AbstractVisitor的子类,其中重载了visitCat方法,因此实际上调用的是子类CreateIDCard中的方法,也即输出出id。

所以最终的效果就是,我们给了一个参数catA,执行visitAnimal(catA)方法,经过这一圈的兜转,最终实现了一个执行 creator.visitCat(catA){sout<<catA.id;} 的效果。

实例使用

在编译器领域,访问者模式使用的非常多。

请看背景:我有一棵解析树,或者说语法树(AST)。它上面有很多节点,比如叶节点LeafNode、分支节点IfStmtNode、循环节点ForStmtNode、赋值节点AssignStmtNode、函数调用节点FunctionCallNode等等等等。它们都是语法树节点ASTRootNode的子类。
对节点的操作是多样的,但是我们不应该把每一个操作都内置到节点类中。考虑访问者模式:

定义一个ASTAbstractVisitor,里面注册了对所有节点的访问方法visitLeafNode(LeafNode leaf);等,并提供了一个接口visitNode(ASTRootNode node){node.visit(this)}
然后根节点ASTRootNode中定义了一个方法visit(ASTAbstractVisitor visitor),其实就是前面说的accept方法啦。有时候写成visit,你也不要见外。它接受一个语法树抽象访问者作为参数。每一个派生于此节点的节点,都会重载此方法,重载为visit对应的节点。比如leafNode就会重载为visit(ASTAbstractVisitor visitor){visitor.visitLeafNode(this);}

然后,对于想遍历树的操作,我们就可以从抽象访问者派生一个方法,比如要打印树ASTPrinter()。然后你需要做的工作有:重载所有你想打印的节点的访问方法,然后编写接口print(node),去访问树根或列表或节点。访问的时候会先找到抽象访问者的访问接口,从这里进入抽象节点的内部,即被accept,然后再根据多态分发给对应的节点,比如叶节点。叶节点accept这个外来的访问者,让它visitSelf()。这时访问者的身份是抽象访问者,所以实际上是去抽象访问者类中找这个visit方法,然后再使用多态机制,分发给我们刚重载的那个方法。

总结

优点

  • 拓展操作方便:新增操作只需要从抽象访问者派生即可
  • 集中相关操作,分离无关操作:只需要在派生类中重载实现功能需要的方法,别的方法不用管
  • 累积状态:访问者在遍历元素的时候,可以累计状态,比如计数。如果没有访问者,就需要传递额外参数,或者定义为全局变量。
  • 访问者可以做到访问无关(不是同一个父类)的元素,只要它们都实现了accept接口

缺点

  • 拓展元素不方便:新增元素,需要在所有的具体访问者中重载相应的访问方法。不过你可以通过在抽象元素类中定义默认操作来解决这一问题,但这只是一个技巧,并不是从根本上解决问题。
  • 破坏封装:很多时候仅靠抽象元素接口提供的操作是不够实现功能的,你很可能需要去访问每个元素内部的具体实现,即其公有方法才可以。

双分派

访问者模式还涉及到一个性质:双分派。
C++是单分派语言。什么意思呢?你的请求对应的操作,是由请求名和接收者决定的。比如我想访问类A(接收者)中的foo(请求名)方法,那么你得到的操作就是A::foo(x),不管foo访问的元素是x,是y,还是猫狗,都是执行的这个操作,比如说,都是对元素进行输出。

而在双分派中,执行的具体是什么操作,还跟访问的元素有关,也即它由三个因素决定:请求名、接收者、访问者
accept方法就是双分派的。观察下accept方法的实现,elem.accept(visitor),请求名就是accept函数了。接收者是elem,那么,如果是猫,接受的操作就是访问猫,狗的就是访问狗。所以元素有决定性作用,但不是唯一决定的。当访问者visitor是创建身份证时,“访问猫”执行的操作就是输出ID,当访问者是做饭的时候,访问猫执行的操作就是做猫粮。也就是说具体的操作是由访问者和接收者共同决定的!

当然,双分派也只是多分派的一种情况。那么可以理解,其实在实现了双分派功能的语言中,访问者模式就不那么必要了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值