1.访问者模式的概念
访问者模式(Visitor)表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
一些情况下,我们会把 算法 和 数据结构抽出来。在数据结构较为稳定,算法易于变化的情况下,使用访问者模式,可以使算法模块的增删改更加容易。
2.代码例子
一个来自于《大话》中的例子。
2.1 if…else的堆叠
给个题目:人类在性别上分男人和女人,对于一些时候、一些状态,男人和女人会表现出不同的反应。
↑根据这个编程。
由于我们知道男人和女人都是抽象于人,所以我们用面向对象的思想可以写出这样的类:
// Person.java 人的类,是抽象类,定义了人的行为
public abstract class Person{
public String action; //定义状态
public abstract void doAction(String action);
... //关于action的getter 和 setter
}
//男人类,继承自人,对不同的状态有不同的反应
public class Man extends Person{
... //构造方法
@Override
public void doAction(){
if(action.equals("Success") {
... //成功的时候男人做了什么
}else if(action.equals("Fail")) {
... //失败的时候男人做了什么
}
}
}
//女人类,继承自人,对不同的状态有不同的反应
public class Woman extends Person{
... //构造方法
@Override
public void doAction(){
if(action.equals("Success") {
... //成功的时候女人做了什么
}else if(action.equals("Fail")) {
... //失败的时候女人做了什么
}
}
}
// 客户端类
public class VisitorMain{
public static void main(String[] args){
List<Person> hunman = new ArrayList<>();
//添加成功时男女的反应
String action = "Success";
Person man1 = new Man(action);
Person woman1 = new Woman(action);
human.add(man1);
human.add(woman1);
//添加失败时男女的反应
String action = "Fail";
Person man2 = new Man(action);
Person woman2 = new Woman(action);
human.add(man2);
human.add(woman2);
// 分别打印出来
for(Person p : human){
p.doAction();
}
}
}
这样写非常简单,而且易懂。
接下来的问题就是:假如我们需要加一个状态,除了“成功”和“失败”,我们还要加一个“恋爱中”的状态,我们的程序该怎么修改?
这并不难,我们在 Man类和 Woman类中在加个 else if()
就行了。这也是我们平时经常用的写法,而且这样的写法理论上,也没有什么毛病。
但是这符合 开放-封闭思想
吗?对于扩展状态,我们是直接对最初的数据结构(Man、Woman)进行更改的。很明显,这并不符合开放-封闭原则。
2.2 使用访问者模式分离算法和数据结构
我们先来看看访问模式下,该情形的UML图:
带着上面的UML图,我们写下面的程序:
// Action类,是所有状态的父类,它可以是抽象类,也可以是一个接口,定义了某状态下,每种数据结构要处理的算法,这里实现了多态
public abstract Action{
public abstract void visit(Man man); //男人的反应
public abstract void visit(Woman woman); //女人的反应
}
// Success类是一种状态类,它继承/实现Action类,实现了在Succes下男人/女人的反应(即算法)
public class Success extends Action{
@Override
public void visit(Man man) {
.... //男人在 Success状态下的具体算法实现
}
@Override
public void visit(Woman woman) {
.... //女人在 Success状态下的具体算法实现
}
}
... //这里省略 Fail状态,代码和Success差不多
// 重新定义Person类,它不会具体实现算法,它只会接收状态
public abstract class Person{
public abstract void accept(Action visitor);
}
// 男人类,只用于接收状态
public class Man extends Person{
@Override
public void accept(Action visitor){
visitor.visit(this); //接收到状态后,把自己传给Action,让Action来根据男人还是女人来做出相应的处理
}
}
// 女人类,同男人类
public class Woman extends Person{
@Override
public void accept(Action visitor){
visitor.visit(this); //接收到状态后,把自己传给Action,让Action来根据男人还是女人来做出相应的处理
}
}
先到这里,我们看看,我们把数据结构(Peron)和算法(Action)分离了开来。
Person类的子类做的事情:接收到状态后,给这个状态类传递自己的Person
Action类的子类就做的事情:根据传入的Person,做出对应的算法处理。
上面两个类形成了一个闭包,所以我们现在需要给Person派发Action,也是就是我们的访问者,所以我们需要设计一个类来进行状态的分发:
// 状态派发类, 在接收到状态之后派发该状态到每个Person去
public class PersonStruct{
List<Person> human = new ArrayList<>(); //维护数据结构的list
public void add(Person person){
human.add(person);
}
public void display(Action action) { //该方法用于派发action到每个Person去
for(Person p : human) {
p.accept(action);
}
}
}
上述类维护了一个Person的list。它的作用是派发Action。在display()
中,就把action派到了Man或者Woman类去了。
这里体现的是一个“双分派”的操作:Action传递给了Person后,这是第一个分派,然后Person调用了Action的方法,把this传给了Action,这是第二个分派。
接着我们事先客户端类,看看最后的实现:
public class VisitorMain {
public static void main(String[] args){
PersonStruct o = new PersonStruct();
o.add(new Man());
o.add(new Woman());
//派发 成功 状态
o.display(new Success());
//派发 失败 状态
o.display(new Fail());
}
}
如果这样我们就根据访问者模式写出了新的一套代码。
这个时候,面对“添加一个恋爱中的算法”的问题,我们需要做的就是两件事情:
- 编写一个 “恋爱中”的状态,继承自Action类,并根据传入的Woman和Man实现 visit算法
- 在 main中添加
o.display("恋爱中")
就完成了,这样相比于2.1节中的if…else堆叠,它的好处只有一点:不会再对Man、Woman类再做修改了
这个好处正好体现出了设计模式中的 开放-封闭原则
思想。
3. UML图
来看下访问者模式的UML图解析:
模板类就不写了~可以照着上面抄。
4.总结
访问者模式有这么几个特点:
- 分离了数据结构与算法,非常利于算法的增删改,体现了 对扩展开放,对修改关闭的 开放-封闭原则
- 适用场景为 数据结构不易变,而算法易变的情况
- 如果数据结构经常变的,则势必会修改访问者接口,如果频繁修改,则说明并不适用于访问者模式。
访问者模式是23个设计模式中最难的模式,它可读性不高,而且用到的地方很少。但是并不代表这个设计模式没用、可以不学,在一些特定的情况下,使用访问者模式,可以让程序变得很灵活。