这个模式,韩老师讲得非常不好。所以本文整理自各种博文。
1.为什么使用?
当需要对一个对象结构类(这个结构由一些不相关的类组成)进行操作扩展,而又不想改变这个对象结构类时,可以考虑使用访问者模式。
举个案例,假设现在我们有一个描述商品的对象结构(想象成购物篮),当前装入了书和水果。那么现在这个结构看起来像这个样子:
现在超市由于促销活动,对大于500页的书,打8折活动;水果每买一斤,单位价格下降1%。于是很自然地想到在Book类和Fruit分别添加getPrice方法和buy3Get1()方法,然后实现具体的操作。如下:
这里我只分别对Book和Fruit做了一个操作扩展,考虑如果我们要对Book和Fruit做多个扩展呢?每个扩展我们都要打开实际的Book类或Fruit类来修改吗?这显然违反了ocp原则,不便于扩展和维护。
虽然Book和Fruit都是商品, 但是它们在打折行为上是相互独立的,这种情况我们该如何去扩展呢?下面就来介绍如何使用访问者模式解决这个问题。
2.介绍是什么?
第2部分如果没看懂,没关系,粗略过一下,看完实例再看一遍第2部分应该就懂了。
2.1 基本介绍
访问者设计模式是行为设计模式之一。 当我们必须对一组相似类型但的对象执行操作时,可以使用它。 借助访问者模式,我们可以将操作逻辑从对象移动到另一个类。
访问者模式由两部分组成:
- Visitor类:visitor类定义visit方法,这个方法将被所有对象结构中的元素调用。
- 被访问类,即对象结构中的元素,提供一个accpet方法,接受一个Visitor类。
2.2 类图
职责介绍:
- Client:Client类是访问者设计模式使用者。 它有权访问对象结构对象,并可以指示它们接受访问者者以执行适当的处理。
- Visitor:这是一个接口或抽象类,用于声明所有可访问类类型的访问操作。
- ConcreteVisitor:对于每种类型的访问者,必须实现在所有具体访问方法。 每个访问者将负责不同的操作。
- Visitable:这是一个声明接受操作的接口。 这是使访问者对象可以“访问”对象的入口点。
- ConcreteVisitable:这些类实现Visitable接口或类并定义接受操作。 使用accept操作将访问者对象传递给该对象。
OK,大概你已经有点晕了,来看实例吧。
3.如何使用?
我们先给出利用访问者模式解决“为什么要使用”中的不易扩展的问题的类图:
Book和Fruit都是商品,所以抽象出一个父类或接口,作为Visitable(被访问者),声明一个accept。
这里就以接口的方式作为讲解:
被访问者接口:
1 | // 作为被访问者的接口 |
访问者接口:
1 | public interface Visitor { |
也许你会问,Book和Fruit都是Goods,根据依赖倒转原则,不是应该依赖高层抽象吗。不然的话,每增加一个具体的商品,就会增加一个方法。下文会对这个做解释。
具体的被访问者,Book和Fruit
1 | // Book |
1 | public class Fruit implements Goods{ |
一定要注意Book在Fruit实现accept的操作,事实上所有的元素(这里就是每个具体的Goods)都应该做类似的实现,将操作这个元素的职责转移给Visitor。
ok,再看看具体的Visitor是如何实现的。这里对上文“Visitor的方法为什么不依赖Goods高层抽象呢“一问题就做了解释。
每种依赖的对象,内部结构可能是不同的,如Book有getPage方法,而Fruit有getPricePerWeight方法。访问者模式的使用要求之一是:对象结构内部元素固定(或尽可能固定),这样Visitor的方法设计一开始就已经制定好了。
1 | public class SaleCountVisitor implements Visitor { |
也许有人会书,那我就要依赖高层抽象,然后在内部做RTTI判断(也就是instanceof)后做类型强转,这其实也不是不可以,但是这样会违反单一职责原则,当操作过多时,你的方法会充斥着if else做RTTI判断。
Bukcet没什么好说的:
1 | public class Bucket { |
Client使用:
1 | public class Client { |
输出:
1 | Bucket{goods=[Book{price=80.0, page=600, name='设计模式'}, Fruit{weight=5.0, pricePerWeight=9.509900498999999, name='苹果'}]} |
访问者模式就是这样执行的,我个人看来其核心思想就是一种职责转移,将原本应该在具体商品类做的操作,委托到Visitor中执行。这样实现具体商品和逻辑操作的解耦,后续我们要添加任何操作,都和具体商品无关,我们只用定义操作类实现Visitor接口即可。
4.优缺点
4.1 优点
- 如果操作逻辑发生变化,那么我们只需要在访问者实现中进行更改,而不必在所有项目类中进行更改。(易维护性)
- 向系统添加新项目很容易,只需在访问者界面和实现中进行更改,现有项目类别将不会受到影响。(可扩展性)
4.2 缺点
- 在设计时,我们应该知道visit()方法的返回类型,否则我们将不得不更改接口及其所有实现。
- 如果访问者接口的实现过多,则很难扩展。
5.使用的注意事项
- 确认当前层次结构(称为元素层次结构)将相当稳定,并且这些类的公共接口足以满足Visitor类所需要的访问。 如果不满足这些条件,则访问者模式则不太适用。
- 为每个Element派生类型都在
Visior
类中创建带有visit(ElementXxx)
方法。(不要依赖高层抽象) - 在元素层次结构中添加一个“ accept(Visitor)”方法。 每个Element派生类中的实现都应该相同-
accept(Visitor v){v.visit(this); }
。 Element
层次结构仅耦合到Visitor基类,但是Visitor
层次结构耦合到每个Element
派生类。 如果元素层次结构的稳定性低,而访客层次结构的稳定性高; 则应该考虑交换两个层次结构的“角色”。- 为要在
Element
对象上执行的每个“操作”创建一个Visitor
派生类。 visit()实现将依赖于Element
的公共接口。. - 客户端创建访问者对象,并通过调用
accept()
将其传递给Element对象。
参考:
- Visitor pattern
- Visitor Design Pattern
- Visitor Design Pattern From Geek