访问者模式Visitor —— 不出手则已,出手就所向披靡!
大多情况下,都不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。
一、什么出手?
既然一出手这么牛逼,那到底啥时候才会出手呢?
简单的说,就是当系统中存在一个结构比较稳定的对象,但是它的访问者会经常改变,且不同的访问者对其访问的操作也不同的时候,可以使用访问者模式。
举个例子:前一阵赘婿比较火哈,就好比选女婿,女婿分为金龟婿(有钱的)和经济适用男(人品好可靠的),丈母娘和女朋友选择的标准是不一样的,丈母娘现实一点也希望自己的女儿以后衣食无忧,就会更关注女婿是否买的起房买得起车是否有钱,那女朋友一般就相信爱情一些,更看重男朋友人品可靠度一些。
这种场景就满足访问者模式的使用场景了,这里要选的女婿就是被访问者,那丈母娘和女朋友就是访问者,女婿无外乎就是有钱、人品、颜值这些可供参考的指标,也就是说上面说的被访问者的对象结构比较稳定,但是它有多个访问者(丈母娘、女朋友、老板。。。),且丈母娘、女朋友、老板关注的重点都不一样,也就是上面说的不同的访问者并对其访问的操作也不同。
二、什么是访问者模式?
为什么说上面的场景访问者模式就可以出手了呢?
比如说上面的场景,现在丈母娘和女朋友标准不一样,每个心里都有一个评分表,这样的逻辑要实现若不用访问者模式会怎样?
那必然会有if、else这样的语句,因为你要去判断是哪个访问者,然后去返回这个访问者关注的标准,也就是通过if、else来对不同访问者进行判别来进行分开处理。但是这样问题也很明显:类的职责重,大量的if-else
代码一定不优雅,一旦访问者或者被访问对象需要修改/添加/移除等操作,这种方式基本就毫无扩展性而言。
那如果是访问者模式,它会去除if-else
将各个不同的操作封装到不同的访问者对象类中去。为了方便扩展,对元素和访问者都提供了抽象层,而元素层,我们还需要通过一个对象结构(可以就理解为是一个集合,来定义这些元素的结构的)来对元素的结构进行组织。结构如下:
- Visitor:接口或者抽象类,定义了对每个 Element 访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改 Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。
- ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。
- Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。
- ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。
三、代码实现
用访问者模式来实现上面选女婿的代码:
/**
* 抽象女婿类 —— 抽象元素
*/
public abstract class SonInLaw {
private String name;
// 有钱与否
private String money;
// 人品如何
private String personality;
public SonInLaw(String name) {
this.name = name;
}
public abstract void accept(Visitor visitor);
}
/**
* 具体女婿类,有钱金龟女婿 —— 具体元素
*/
public class RichSonInLaw extends SonInLaw{
public RichSonInLaw(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String getMoney(){
return "每年挣一亿¥";
}
}
/**
* 具体女婿类,人品好的女婿 —— 具体元素
*/
public class PersonalitySonInLaw extends SonInLaw {
public PersonalitySonInLaw(String name) {
super(name);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public String getPersonality(){
return "人品爆表";
}
}
/**
* 抽象访问者接口
*/
public interface Visitor {
void visit(PersonalitySonInLaw personalitySonInLaw);
void visit(RichSonInLaw richSonInLaw);
}
/**
* 评分表 —— ObjectStructure,这个结构应该相对稳定,不经常修改
*/
public class ScoreTable {
private List<SonInLaw> sonInLaws = new ArrayList<>();
public ScoreTable() {
sonInLaws.add(new RichSonInLaw("女婿一:金龟婿"));
sonInLaws.add(new RichSonInLaw("女婿二:金龟婿"));
sonInLaws.add(new PersonalitySonInLaw("女婿三:靠谱婿"));
sonInLaws.add(new PersonalitySonInLaw("女婿四:靠谱婿"));
}
public void showScoredTable(Visitor visitor) {
for (SonInLaw sonInLaw : sonInLaws) {
sonInLaw.accept(visitor);
}
}
}
/**
* 具体访问者 —— 丈母娘
*/
public class MotherInLaw implements Visitor{
@Override
public void visit(PersonalitySonInLaw personalitySonInLaw) {
Console.log("我是丈母娘,人品都是浮云,重要的是要有钱!");
}
@Override
public void visit(RichSonInLaw richSonInLaw) {
Console.log(richSonInLaw.getMoney() + "的金龟婿我喜欢!");
}
}
/**
* 具体访问者 —— 女朋友
*/
public class GirlFriend implements Visitor{
@Override
public void visit(PersonalitySonInLaw personalitySonInLaw) {
Console.log(personalitySonInLaw.getPersonality() + "我喜欢!");
}
@Override
public void visit(RichSonInLaw richSonInLaw) {
Console.log("爱情价更高!");
}
}
public class Client {
public static void main(String[] args) {
ScoreTable scoreTable = new ScoreTable();
scoreTable.showScoredTable(new MotherInLaw());
Console.log("====================");
scoreTable.showScoredTable(new GirlFriend());
}
}
重载的 visit 方法会对元素进行不同的操作,而通过注入不同的 Visitor 又可以替换掉访问者的具体实现,使得对元素的操作变得更灵活,可扩展性更高,同时也消除了类型转换、if-else 等“丑陋”的代码。
访问者模式最大的优点就是增加访问者非常容易,我们从代码中可以看到,如果要增加一个访问者,只要新实现一个 Visitor 接口的类,从而达到数据对象与数据操作相分离的效果。如果不实用访问者模式,而又不想对不同的元素进行不同的操作,那么必定需要使用 if-else 和类型转换,这使得代码难以升级维护。
四、适用场景
-
优点:
- 各角色职责分离,符合单一职责原则
通过UML类图和上面的示例可以看出来,Visitor、ConcreteVisitor、Element 、ObjectStructure,职责单一,各司其责。 - 具有优秀的扩展性
如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。 - 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
员工属性(数据结构)和CEO、CTO访问者(数据操作)的解耦。 - 灵活性
- 各角色职责分离,符合单一职责原则
-
缺点:
- 具体元素对访问者公布细节,违反了迪米特原则
CEO、CTO需要调用具体员工的方法。 - 具体元素变更时导致修改成本大
变更员工属性时,多个访问者都要修改。 - 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有以来抽象
访问者 visit 方法中,依赖了具体员工的具体方法。
- 具体元素对访问者公布细节,违反了迪米特原则
适用场景:
上面已经说过,当系统中存在一个结构比较稳定的对象,但是对它访问的者会经常改变,且不同的访问者并对其访问的操作也不同的时候,可以使用访问者模式。
具体的
- 对象结构比较稳定,但是经常需要在此对象结构上定义新的操作。
- 如果想对一个对象结构中的各个元素进行很多不同的而且不相关的操作,为了避免这些操作使类变得杂乱,可以使用访问者模式,把这些操作分散到不同的访问者中去,每个访问者实现一类功能。
访问者使用的条件较为苛刻,结构也很复杂,所以实际应用使用的频率不高。当你系统中存在一个比较复杂的对象结构,并且存在着不同的访问者并对其访问的操作也不同的时候,可以使用访问者模式。
现有的一些实际应用:XML文档解析,编译器设计等。