一 概述
访问者模式可以说是 GOF23 种设计模式中最复杂的一个,但日常开发中使用频率却不高,所以说上帝喜欢简洁!增删改查虽然简单,却是大部分程序员日常主要工作,是可以混饭吃的家伙式。你技术再牛逼,企业用不到,那对于企业来说也没屌用,所以说合适的才是最好的。但不常用不等于没有用,这一点的认识到。
访问者模式试图解决如下问题:
一个类农场里面包含各种元素,例如有大雁,狗子,鸭子。而每个元素的操作却经常变换,一会让大雁排成一字,一会让大雁排成人字。当大雁排成一字的时候狗子要排成S形状,鸭子要排成B形状,当大雁排成人字时候狗子要叫两声,鸭子要跳起来…。
但对农场这类有要求,第一:可以迭代这些元素,第二:里面的元素不能频繁变动,你不能一会把鸭子杀了吃了,一会又买回一匹马…,如果是这样的话就不适合使用 Visitor 模式
如果我们不采用设计模式,那么就要频繁的修改这些元素类,违背了开闭原则,降低代码的可维护和扩展性。
1.1 定义
封装一些作用于某种数据结构中各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
- 访问者模式属于行为型模式
- 访问者模式是一种将数据结构和数据操作分离的设计模式
- 访问者模式比较复杂,而且实际使用的地方并不多
- 访问者模式适用于数据结构稳定的元素操作上,一旦数据结构易变,则不适用
1.2 使用场景
当你有个类,里面的包含各种类型的元素,这个类结构比较稳定,不会经常增删不同类型的元素。而需要经常给这些元素添加新的操作的时候,考虑使用此设计模式。
1.3 UML 类图
角色说明:
- Visitor(抽象访问者):接口或者抽象类,为每一个元素(Element)声明一个访问的方法,一般是有几个元素就相应的有几个 visite 方法
- ConcreteVisitor(具体访问者):visitor 的实现类,即对每一个元素都有其具体的访问行为
- Element(抽象元素):接口或者抽象类,定义一个 accept(Visiotr) 方法,能够接受访问者(Visitor)的访问
- ConcreteElementA、ConcreteElementB(具体元素):实现抽象元素中的 accept 方法,通常是调用访问者提供的访问该元素的方法
- Client(客户端类):即要使用访问者模式的地方
二 实现
王二狗刚参加工作那会,由于社会经验不足误入了一个大忽悠公司,公司老板不舍得花钱,只给公司招了3个人,一个 HR,一个程序员,一个测试,但关键是老板总想追风口,啥都想做,一会社交,一会短视频。二狗多次提出说人太少,申请加几个人,至少加个保洁阿姨啊,每天都自己打扫卫生,累屁了。每到此时老板就画大饼:你现在刚毕业正是要奋斗的时候,此时不奋斗什么时候奋斗?过两年公司上市了,你作为元老就财富自由拉。。。balabala
这个场景就很适合使用访问者模式:
- 大忽悠公司结构很稳定,老板舍不得花钱招人,总共就那么3个人,还是3种角色,即只有3个元素
- 大忽悠公司老板想法多,这就要求这3个人承担各种新技能,即不断的给元素增加新的算法
2.1 构建 Element
毕竟改变的是元素的算法,所以这里我们先构建元素。
元素类只有一个 accept 方法,它需要一个访问者接口类型的参数。
public interface CorporateSlave {
void accept(CorporateSlaveVisitor visitor);
}
构建3个元素的实现类:
1、程序员
public class Programmer implements CorporateSlave {
private String name;
public Programmer(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
}
2、测试
public class Tester implements CorporateSlave {
private String name;
public Tester(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
}
3、人力
同上,省略
...
注意在 element 类里面将自己传递给 visitor 的 visit() 方法
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
2.2 构建 ObjectStructure
BigHuYouCompany 类里面需要包含相对稳定的元素(大忽悠老板就招这3个人,再也不肯招人),而且要求可以对这些元素迭代访问。此处我们以集合存储3位员工。
public class BigHuYouCompany {
private List<CorporateSlave> employee= new ArrayList<>();
public BigHuYouCompany() {
employee.add(new Programmer("王二狗"));
employee.add(new HumanSource("上官无需"));
employee.add(new Tester("牛翠花"));
}
public void startProject(CorporateSlaveVisitor visitor){
for (CorporateSlave slave : employee) {
slave.accept(visitor);
}
}
}
2.3 构建 Visitor
Visitor 接口里面一般会存在与各元素对应的 visit 方法,例如此例我们有3个角色,所以这里就有3个方法。
public interface CorporateSlaveVisitor {
void visit(Programmer programmer);
void visit(HumanSource humanSource);
void visit(Tester tester);
}
2.4 Visitor 实现类
因为老板觉得社交是人类永恒的需求,所以开始想做社交 App,他觉得他能成为微信第二。
这就相当于要为每一个元素定义一套新的算法,让程序员仿照微信开发设计 app,让测试完成即时通信的测试,让人力发软文。
public class SocialApp implements CorporateSlaveVisitor {
@Override
public void visit(Programmer programmer) {
System.out.println(String.format(
"%s: 给你一个月,先仿照微信搞个类似的APP出来,要能语音能发红包,将来公司上市了少不了你的,好好干...",
programmer.getName()));
}
@Override
public void visit(HumanSource humanSource) {
System.out.println(String.format(
"%s: 咱现在缺人,你暂时就充当了陪聊吧,在程序员开发APP期间,你去发发软文,积攒点粉丝...",
humanSource.getName()));
}
@Override
public void visit(Tester tester) {
System.out.println(String.format(
"%s: 这是咱创业的第一炮,一定要打响,测试不能掉链子啊,不能让APP带伤上战场,以后给你多招点人,你就是领导了...",
tester.getName()));
}
}
过了一段时间,老板又觉的短视频很火,又要做短视频,这就要求给每一员工增加一套新的算法。
public class LiveApp implements CorporateSlaveVisitor {
@Override
public void visit(Programmer programmer) {
System.out.println(String.format(
"%s: 最近小视频很火啊,咱能不能抄袭下抖音,搞他一炮,将来公司上市了,你的身价至少也是几千万,甚至上亿...",
programmer.getName()));
}
@Override
public void visit(HumanSource humanSource) {
System.out.println(String.format(
"%s: 咱公司就数你长得靓,哪天化化妆,把你的事业线适当露一露,要是火了你在北京买房都不是梦...",
humanSource.getName()));
}
@Override
public void visit(Tester tester) {
System.out.println(String.format(
"%s: 你也开个账户,边测试边直播,两不耽误...",tester.getName()));
}
}
再过段时间老板可能要开 KTV,程序员王二狗可能要下海当鸭子,其他两位也需要解锁新技能。。。
2.5 客户端测试
public class VisitorClient {
public void startProject(){
BigHuYouCompany bigHuYou= new BigHuYouCompany();
//可以很轻松的更换Visitor,但是要求BigHuYouCompany的结构稳定
System.out.println("-----------------启动社交APP项目--------------------");
bigHuYou.startProject(new SocialApp());
System.out.println("-----------------启动短视频APP项目--------------------");
bigHuYou.startProject(new LiveApp());
}
}
输出结果:
-----------------启动社交APP项目--------------------
王二狗: 给你一个月,先仿照微信搞个类似的APP出来,要能语音能发红包,将来公司上市了少不了你的,好好干...
上官无需: 咱现在缺人,你暂时就充当了陪聊吧,在程序员开发APP期间,你去发发软文,积攒点粉丝...
牛翠花: 这是咱创业的第一炮,一定要打响,测试不能掉链子啊,不能让APP带伤上战场,以后给你多招点人,你就是领导了...
-----------------启动短视频APP项目--------------------
王二狗: 最近小视频很火啊,咱能不能抄袭下抖音,搞他一炮,将来公司上市了,你的身价至少也是几千万,甚至上亿...
上官无需: 咱公司就数你长得靓,哪天化化妆,把你的事业线适当露一露,要是火了你在北京买房都不是梦...
牛翠花: 你也开个账户,边测试边直播,两不耽误...
你看虽然大忽悠老板的需求变化这么快,但至始至终我们只是在增加新的 Visitor 实现类,而没有去修改任何一个 Element 类,这就很好的符合了开闭原则。
三 总结
3.1 特点
- 准确识别出 Visitor 使用的场景,如果一个对象结构不稳定决不可使用,不然在增删元素时改动将非常巨大
- 对象结构中的元素要可以迭代访问
- Visitor 里一般存在与元素个数相同的 visit 方法
- 元素通过 accept 方法通过 this 将自己传递给了 Visitor
3.2 双分派(dispatch)
访问者模式存在一个叫"伪动态双分派”的技术,这个还是比较难懂的,访问者模式之所以是最复杂的设计模式与其有很大的关系。
什么叫分派?根据对象的类型而对方法进行的选择,就是分派(Dispatch)。
发生在编译时的分派叫静态分派,例如重载(overload),发生在运行时的分派叫动态分派,例如重写(overwrite)。
单分派与多分派
-
单分派
依据单个宗量进行方法的选择就叫单分派,Java 动态分派只根据方法的接收者一个宗量进行分配,所以其是单分派 -
多分派
依据多个宗量进行方法的选择就叫多分派,Java 静态分派要根据方法的接收者与参数这两个宗量进行分配,所以其是多分派
好了,理论的只是罗列出来了,那具体到访问者模式是个什么情况呢?
先看在 BigHuYouCompany 类里的分派代码:slave.accept(visitor); 中 accept 方法的分派是由 slave 的运行时类型决定的。若 slave 是 Programer 就执行 Programer 的 accept 方法。若 slave 是 Tester 那么就执行 Tester 的 accept 方法。
public void startProject(CorporateSlaveVisitor visitor){
for (CorporateSlave slave : employee) {
slave.accept(visitor);
}
}
通过此步骤就完成了一次动态单分派。
再看一下具体的 Element 里的分派代码:visitor.visit(this); 中 visit 方法的分派是由参数 this 的运行时类型决定的。若 this 是 Programer 就执行 Visitor 中的 visit(Programer) 方法。若 slave 是 Tester 那么就执行 Visitor 的 visit(Tester) 方法。
@Override
public void accept(CorporateSlaveVisitor visitor) {
visitor.visit(this);
}
通过这一步又完成了一次动态单分派。
两次动态单分派结合起来就完成了一次伪动态双分派,为什么叫伪动态双分派呢?因为在 Java 中动态分派是单分派的,而此处是通过两次动态单分派达到了双分派的效果,所以说是伪的!
3.3 优点
- 各种角色各司其职,符合单一职责原则
- 原有的类上新增操作只需实现一个具体访问者就可以,不必修改整个类层次,符合开闭原则
- 数据操作和数据结构的解耦
- 使得给结构稳定的对象增加新算法变得容易,提搞了代码的可维护性,可扩展性
3.4 缺点
- 具体元素对访问者公布了实现细节,破坏了类的封装性,违反了迪米特原则
- 违反了依赖倒置原则,为了达到区别对待依赖了具体而不是抽象
- 具体元素修改的成本太大
- 新增具体元素困难,需要在抽象访问者角色中增加一个新的抽象操作,违反了开闭原则
3.5 其他
访问者模式实际使用中比较少,但是真正需要用到时,还是很有用的。