JAVA设计模式之访问者模式

一 概述

访问者模式可以说是 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 其他

访问者模式实际使用中比较少,但是真正需要用到时,还是很有用的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值