【设计模式笔记】(十四)- 访问者模式

一、简述

访问者模式是一种将数据操作和数据结构分离的设计模式,是23种设计模式中非常复杂的一种,而且使用频率并不高。

定义:封装一些作用于某种数据结构中的各元素的操作(访问),可以在不改变这个数据的前提下定义作用于这些元素的新操作。

顾名思义,某些不能改变的数据,对于不同的访问者有不同的访问(或者操作),为不同的访问者提供相对应的操作。例如:公司CEO就能看到公司所有的真实财报数据,而作为一个员工可能就只能知道同比去年的增长比例。

访问者模式
  • Visitor:访问者抽象类(或者接口),它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素;理论上,它的方法个数与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,不能频繁的添加、移除元素。如果出现频繁修改Visitor接口的情况,说明可能并不适合使用访问者模式。
  • ConcreteVisitor:具体的访问者,需要实现每一个元素类访问时所产生的具体行为。
  • Element:元素接口(或抽象类),它定义了一个接收访问者的方法(accept()方法),意义在于每一个元素都要刻意被访问者访问。
  • ElementA、ElementB:具体的元素类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素共访问者访问。

访问者模式的最大优点就是增加访问者非常容易,新创建一个实现了Visitor接口的类,然后实现两个visit()方法对不同的元素进行不同的操作,从而达到数据与数据操作分离的目的。如果不实用访问者模式,必定需要使用if-else和类型转换,这便是代码的维护难度升级了。由此可以看出访问者模式的作用。

PS:访问者模式违反了迪米特原则(对访问者公布元素细节)以及依赖倒置原则(依赖了具体类,没有依赖抽象),由此可见,此模式需要应用在特定的情况中。

二、案例实现

这里就以公司为例,公司员工暂且分为开发人员和运营人员,而公司的CEO和CTO对于不同员工的KPI关注点不同,因此我们需要做出不同的处理,接着看看代码实现

员工基类

很简单,名字初始化和一个抽象的accept()方法

public abstract class Staff {
    public String name;

    public int kpi;

    public Staff(String name){
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    //接受Visitor访问
    public abstract void accept(Visitor visitor);
}

具体员工类

具体的员工,根据各自不同的职责添加了不同的方法,开发人员的KPI和代码产量相关,于是添加了获取代码行数的方法,而运营人员的KPI和新增用户量相关,于是添加了获取新增用户数的方法。

/**
 * 开发人员
 */
public class Developer extends Staff {
    public Developer(String name) {
        super(name);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    //代码量
    public int getCodeLines(){
        return new Random().nextInt(10 * 1000);
    }
}

/**
 * 运营人员
 */
public class Operator extends Staff {
    public Operator(String name) {
        super(name);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    //新增用户数
    public int getNewUserNum(){
        return new Random().nextInt(10 * 10000);
    }
}

访问者

接下来看看访问者类的定义

public interface Visitor {
    //访问开发人员
    public void visit(Developer developer);
    //访问运营人员
    public void visit(Operator operator);
}

这里可以看到,直接从方法上就区分DeveloperOperator,这里主要考虑到的是,如果使用基类Staff作为参数的话代码就会是这个样子

public void visit(Staff staff){
        if(staff instanceof Developer){
            Developer developer = (Developer)staff;
            System.out.print("开发" + developer.name
                    + ",KPI:" + developer.kpi + ",代码" + developer.getCodeLines() + "行");
        }else if(staff instanceof Operator){
            Operator operator = (Operator) staff;
            System.out.print("运营" + operator.name + ",KPI:" + operator.kpi);
        }
    }

可以看到,在visit()方法中,我们就需要判断参数的类型以及类型强制转换,这样的代码难以扩展和维护。

这是访问者模式的一个优点,也是一个缺点,优点在于代码清晰,某种程度上代码的维护和扩张更好;而缺点也是一样,如果需要添加一类Staff,所有的Visitor都需要在实现一个新的visit()方法。

接下来是具体的访问者代码,这里设定CTO更加关注开发人员,CEO更加关注运营人员。

public class CTOVisitor implements Visitor {
    @Override
    public void visit(Developer developer) {
        System.out.print("开发" + developer.name
                + ",KPI:" + developer.kpi + ",代码" + developer.getCodeLines() + "行");
    }

    @Override
    public void visit(Operator operator) {
        System.out.print("运营" + operator.name + ",KPI:" + operator.kpi);
    }
}

public class CEOVisitor implements Visitor {
    @Override
    public void visit(Developer developer) {
        System.out.print("开发" + developer.name + ",KPI:" + developer.kpi);
    }

    @Override
    public void visit(Operator operator) {
        System.out.print("运营" + operator.name
                + ",KPI:" + operator.kpi + "新增用户:" + operator.getNewUserNum());
    }
}

对象结构

这里的对象结构,直接就设定成了公司,集合就是员工们

public class Company {
    private List<Staff> staffList = new ArrayList<>();

    public void action(Visitor visitor){
        for(Staff staff:staffList){
            staff.accept(visitor);
        }
    }

    /**
     *
     * @param staff
     */
    public void addStaff(Staff staff){
        staffList.add(staff);
    }
}

客户端代码

public class Client {
    public static void main(String[] agrs){
        Company company = new Company();
        company.addStaff(new Developer("Bruce Wayne"));
        company.addStaff(new Developer("ClarkKent"));
        company.addStaff(new Developer("Barry Allen"));

        company.addStaff(new Operator("Diana Prince"));
        company.addStaff(new Operator("Oliver Queen"));
        company.addStaff(new Operator("Dinah Lance"));

        CEOVisitor ceo = new CEOVisitor();
        company.action(ceo);

        CTOVisitor cto = new CTOVisitor();
        company.action(cto);
    }
}

具体输出如下:

CEO所看到的======
开发Bruce Wayne,KPI:6
开发ClarkKent,KPI:2
开发Barry Allen,KPI:8
运营Diana Prince,KPI:4,新增用户:46642
运营Oliver Queen,KPI:1,新增用户:7687
运营Dinah Lance,KPI:3,新增用户:67382
 
CTO所看到的======
开发Bruce Wayne,KPI:6,代码8285行
开发ClarkKent,KPI:2,代码8351行
开发Barry Allen,KPI:8,代码658行
运营Diana Prince,KPI:4
运营Oliver Queen,KPI:1
运营Dinah Lance,KPI:3

三、分派

变量被声明时的类型叫做变量的静态类型(Static Type),静态变量类型又可以叫做明显类型(Apparent Type);而变量所引用的对象的正式类型叫做变量的实际类型(Actual Type)。

List list = new ArrayList();

在Java代码中很常见的一种写法,声明父类对象创建子类对象;声明是List类型(也就是静态类型即明显类型),创建的是ArrayList的对象(实际类型)。

这里就需要提到一个词,分派(Dispatch)。当使用上述形式声明并创建对象,根据对象的类型对方法进行选择,这就是分派,而分派有可以分为静态分派(Static Dispatch)和动态分派(Dynamic Dispatch)。

  • 静态分派,对应的就是编译时,根据静态类型信息发生的分派。方法重载就属于静态分派
  • 动态分派,对应的就是运行时,动态地置换掉某个方法。方法重写就属于动态分派

静态分派

简化三个类之间的关系

public class Staff {
     
}

public class Developer extends Staff {

}

public class Operator extends Staff {

}

执行类,execute()方法有三个重载方法,方法的参数分别上面对应的三个类型StaffDeveloperOperator的对象。

public class Execute {

    public void execute(Staff staff){
        System.out.println("员工");
    }

    public void execute(Developer developer){
        System.out.println("开发人员");
    }

    public void execute(Operator operator){
        System.out.println("运营人员");
    }
}

测试代码以及测试结果

public class Client {
    public static void main(String[] agrs){
        System.out.println("运行结果:");
       
        Staff staff = new Staff();
        Staff staff1 = new Developer();
        Staff staff2 = new Operator();
        
        Execute execute = new Execute();
        execute.execute(staff);
        execute.execute(staff1);
        execute.execute(staff2);
    }
}

运行结果:
员工
员工
员工

可以推断出,传入三个对象,最后执行的方法都是参数类型是Staff的方法,即使三个对象有不同的真实类型

方法重载中实际起作用的是它们静态类型,也就是在编译时期就完成了分派,即静态分派

动态分派

三个类自带execute()方法,DeveloperOperator继承Staff,并重写了execute()方法

public class Staff {
   public void execute(){
       System.out.println("员工");
   }
}

public class Developer extends Staff {
    @Override
    public void execute() {
        System.out.println("开发人员");
    }
}

public class Operator extends Staff {
    @Override
    public void execute() {
        System.out.println("运营人员");
    }
}

测试代码以及结果

public class Client {
    public static void main(String[] agrs) {
        System.out.println("运行结果:");
        Staff staff = new Staff();
        staff.execute();
        
        Staff staff1 = new Developer();
        staff1.execute();
        
        Staff staff2 = new Operator();
        staff2.execute();
    }
}

运行结果:
员工
开发人员
运营人员

测试时的情况相同,三个对象,其静态类型都是Staff,而实际类型分别是StaffDeveloperOperator。可以看到重写execute()方法都生效了,各自输出了对应的内容。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。

单分派与多分派

首先需要了解一个叫宗量的概念。一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。而根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言多分派语言

单分派语言根据一个宗量的类型(真实类型)进行对方法的选择
多分派语言根据多个的宗量的类型对方法进行选择

Java属于什么类型呢?

我们可以分析一下,Java中静态分派时决定方法的选择的宗量包括方法的接收者和方法参数的静态类型,所以是多分派;而在动态分派时,方法的选择只会考虑方法的接收者的实际类型,所以是单分派。其实Java语言是支持静态多分派和动态单分派的语言。

双(重)分派

那双重分派又是什么呢?分派和访问者模式又有什么关系呢?接下来就会解释这些问题

Java支持静态多分派和动态单分派,并不支持动态多分派;于是就有了两次单分派组成的双重分派来替代动态多分派。而访问者模式正好就用到了双重分派的技术。

双重分派技术就是在选择一个方法的时候,不仅仅要根据方法的接收者的运行时区别,还要根据参数的运行时区别(这样达到两次分派的效果)。

在访问者模式中,客户端将具体的对象传递给访问者,也就是staff.accept(visitor);方法的调用,完成第一次分派;然后具体的访问者作为参数传入到具体的对象的方法中,也就是这句代码visitor.visit(this);,将this作为参数传递进去完成第二次分派。双分派意味着得到的执行操作决定于请求的种类和接受者的类型。双重分派的核心就是this对象。

从访问者模式可以看出,双重分派就是在冲在方法委派的前面加上了继承的重写,使得从某种角度来说重载变成了动态。

Android源码中的访问者模式

相信注解应该不会陌生,现在很多出名框架的使用方式都是使用注解,例如:ButterKnifeDaggerRetrofit等等,都是以注解的方式使用,已达到简化代码或者降低耦合度的目的。而注解又可以分为运行时注解编译时注解,运行时注解由于性能问题也一直被人诟病,编译时注解的核心原理依赖APT(Annotation Processing Tools)实现,之前提到的框架也是基于APT实现的。

而对于注解的解析过程就是遵从访问者模式的,其元素就是包、类、方法、方法参数等(其实就是可以被添加注解那些元素),对于元素的访问者支持所有的元素访问,通过继承一个抽象的元素访问者实现针对不同类型进行不同的处理。

注解相关具体的内容我不是很了解,只是简单的说明一下

四、总结

访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。

优点

  • 扩展性好: 在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好: 通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
  • 分离无关行为: 通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

缺点

  • 对象结构变化很困难: 不适用于对象结构中的类经常变化的情况,因为对象结构发生了改变,访问者的接口和访问者的实现都要发生相应的改变,代价太高。
  • 破坏封装: 访问者模式通常需要对象结构开放内部数据给访问者和ObjectStructrue,这破坏了对象的封装性。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 目录 本文的结构如下: 引言 什么是访问者模式 模式的结构 典型代码 访问者模式中的伪动态双分派 代码示例 访问者...
    w1992wishes阅读 854评论 0 6
  • 在阎宏博士的《JAVA与模式》一书中开头是这样描述访问者(Visitor)模式的:访问者模式是对象的行为模式。访问...
    Ant_way阅读 618评论 0 0
  • 定义 访问者模式是对象的行为模式。访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改...
    步积阅读 1,220评论 0 3
  • 注:都是在百度搜索整理的答案,如有侵权和错误,希告知更改。 一、哪些情况下的对象会被垃圾回收机制处理掉  当对象对...
    Jenchar阅读 3,205评论 3 2
  • 变量没有类型,对象才有类型 一、基本数据类型 整型 int和long浮点型 float布尔型 bool 两个內建(...
    ustcmio阅读 426评论 0 1