访问者模式

引子

访问者模式在23种设计模式中应该算是最复杂也是最难以理解的一种模式了,因此在解释的时候我不打算从定义说起,以实际的例子带入可能会比较好吧。

例子

我们的例子是交通局需要对外公布所有交通工具的票价,假设现在交通局只负责管理公交车和火车的事儿,实际上也许他们不止管理这点东西

已有系统代码

代码枯燥但是也是必要的,交通工具抽象成接口如下

public interface Vehicle {
    int getBasePrice();
}

两个子类

public class Bus implements Vehicle {
    @Override
    public int getBasePrice() {
        return 50;
    }
}

public class Train implements Vehicle {
    @Override
    public int getBasePrice() {
        return 100;
    }
}

系统的正事不要忘记,交通局要公布票价的

public class Manager {
    
    List<Vehicle> vehicles = new ArrayList<Vehicle>();
    
    public void add(Vehicle vehicle) {
        vehicles.add(vehicle);
    }
    
    public void remove(Vehicle vehicle) {
        vehicles.remove(vehicle);
    }
    
    public void printAllPrice() {
        for (Vehicle vehicle : vehicles) {
            System.out.println(vehicle.getBasePrice());
        }
    }
    
    public static void main(String[] args) {
        // 构造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 公布票价
        manager.printAllPrice();
    }
}

新需求

新年到了,票价变了,交通局要公布新年票价了,每个交通工具涨价幅度不一样,对原系统要做修改了。
首先是交通工具接口,新增一个方法

public interface Vehicle {

    int getBasePrice();
    
    int getNewYearPrice();
}

然后子类要修改,去实现这个方法

public class Bus implements Vehicle {
    
    @Override
    public int getBasePrice() {
        return 50;
    }

    @Override
    public int getNewYearPrice() {
        return getBasePrice() * 2;
    }

}

public class Train implements Vehicle {

    @Override
    public int getBasePrice() {
        return 100;
    }

    @Override
    public int getNewYearPrice() {
        return getBasePrice() * 3;
    }
}

好像涨价涨的有点凶,暂时不去吐槽这个问题,来看看交通局怎么办

public class Manager {
    
    List<Vehicle> vehicles = new ArrayList<Vehicle>();
    
    public void add(Vehicle vehicle) {
        vehicles.add(vehicle);
    }
    
    public void remove(Vehicle vehicle) {
        vehicles.remove(vehicle);
    }
    
    public void printNewYearPrice() {
        for (Vehicle vehicle : vehicles) {
            System.out.println(vehicle.getNewYearPrice());
        }
    }
    
    public static void main(String[] args) {
        // 构造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 公布新年票价
        manager.printNewYearPrice();
    }

}

大功告成

新需求+1+2+N

春节到了,交通局要公布春节票价了,这回可能要涨的更多了,毕竟春运嘛。。。儿童节到了,要公布儿童专属票价……

public interface Vehicle {

    int getBasePrice();
    
    int getNewYearPrice();

    int getXxxPrice();

    int getYyyPrice();

    int getZzzPrice

    // ......
}

我们发现,这种修改方法会导致接口内的方法需要被修改,然后每一个子类也都需要做相应改动,如果子类不止Bus和Train的话,这种体力劳动我觉得应该是能让人喝一壶的,这时候我们就可以考虑使用访问者来重构这个系统了。

访问者

现在我依然不去解释什么是访问者模式,但是我要说的是为了使用访问者模式,我们需要在交通工具接口里新增一个方法accept,该方法接受访问者作为参数,子类实现时则通过这个访问者参数访问自身

这个过程的子类通用的实现就是Visitor.visit(this)

访问者重构系统

还是通过代码来看吧,下面是接口,可以看到增加了一个以Visitor作为参数的accept方法

public interface Vehicle {

    void accept(IVisitor visitor);

    int getBasePrice();
}

两个子类实现

public class Bus implements Vehicle {
    
    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    @Override
    public int getBasePrice() {
        return 50;
    }
}

public class Train implements Vehicle {

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

    @Override
    public int getBasePrice() {
        return 100;
    }
}

那这个

visitor.visit(this)

到底是什么东西呢,来看看访问者的的实现就明白了
访问者有一个接口,它定义了在访问不同的交通工具时可以有的不同方法,我们有两个交通工具,因此这个接口里有两个方法

public interface IVisitor {
    
    void visit(Bus bus);
    
    void visit(Train train);
}

管理局要打印票价,那么作为交通工具的访问者,可以满足这个需求,我们实现一个访问者类

public class BasePricePrinter implements IVisitor {

    @Override
    public void visit(Bus bus) {
        System.out.println(bus.getBasePrice());
    }

    @Override
    public void visit(Train train) {
        System.out.println(train.getBasePrice());
    }
}

这个访问者类在访问不同的交通工具时就是简单的打印票价而已,那交通局要做的事情就简单啦

public class Manager {
    
    List<Vehicle> vehicles = new ArrayList<Vehicle>();
    
    public void add(Vehicle vehicle) {
        vehicles.add(vehicle);
    }
    
    public void remove(Vehicle vehicle) {
        vehicles.remove(vehicle);
    }
    
    public void printPrice(IVisitor visitor) {
        for (Vehicle vehicle : vehicles) {
            vehicle.accept(visitor);
        }
    }
    
    public static void main(String[] args) {
        // 构造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 构造一个基础票价的打印器访问者
        IVisitor basePricePrinter = new BasePricePrinter();
        
        // 公布票价
        manager.printPrice(basePricePrinter);
    }
}

交通局通过一个小弟基础票价访问者搞定了自己要做的所有事情,看看这个过程都在printPrice方法中,遍历了所有交通工具,对每一个交通工具都使用基础票价访问者去访问他们,基础票价访问者负责处理交通局派给它的任务——公布基础票价

新需求

新年到了,交通局需要公布新年票价,我们要做的仅仅是添加一个新年票价访问者的类就可以了

public class NewYearPricePrinter implements IVisitor {

    @Override
    public void visit(Bus bus) {
        System.out.println(bus.getBasePrice() * 2);
    }

    @Override
    public void visit(Train train) {
        System.out.println(train.getBasePrice() * 3);
    }

}

交通局的大爷不需要任何代码改动,派出新年票价访问者小弟解决所有事情

public static void main(String[] args) {
        // 构造交通局
        Manager manager = new Manager();
        
        // 初始化,即添加交通局要管理的所有交通工具
        manager.add(new Bus());
        manager.add(new Train());
        
        // 构造一个新年票价的打印器访问者
        IVisitor newYearPricePrinter = new NewYearPricePrinter();
        
        // 公布新年票价
        manager.printPrice(basePricePrinter);
}

可以看到,这种情况下我们的改动量很少,当需要公布春节票价,公布啥时候的票价都非常简单,只需要添加一个访问者就可以实现了。

定义

例子好长,终于讲完了,回头可以看看定义了,《设计模式》一书对于访问者模式给出的定义是

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

从定义可以看出结构对象是使用访问者模式必须条件,而且这个结构对象必须存在遍历自身各个对象的方法。
在我们的访问者模式案例中,对象结构就是交通局管理的一堆交通工具,我们在使用访问者模式的时候的确是在新增对这些交通工具的操作,而且这些操作丝毫没有改变这些交通工具的类。

组成结构

访问者模式中一共有五个角色(除去Client)

  • 抽象元素角色(Vehicle):定义一些Accept操作,它以一个访问者为参数,指定元素可以被哪些访问者访问。在我们的例子中就是Vehicle
  • 具体元素角色(Bus,Train):包含的方法分为两部分,一部分是Accept操作,这部分是实现抽线元素角色所必须的,指定自身可以被哪些访问者访问,其代码实现往往是简单的一句话(visitor.visit(this)),另一部分是包含自身的业务逻辑方法,具体元素不同,这部分也可以各不相同。在我们的例子中就是Bus和Train了
  • 对象结构角色(Manager):这是使用访问者模式必备的角色。它要具备以下特征:能枚举它的元素;可以提供一个高层的接口以允许该访问者访问它的元素;可以是一个复合(组合模式)或是一个集合,如一个列表或一个无序集合。我们可以把交通局当做一个对象结构角色。
  • 抽象访问者角色(IVisitor):为对象结构角色的每一类对象(Bus,Train)都声明一个访问操作结构,该操作接口的名字和参数标识了发送访问请求给具体访问者的具体元素角色。
  • 具体访问者角色(BasePricePrinter,NewYearPricePrinter):实现具体访问时要做的操作

类图

这个我就略过了,我想网络上应该有很多就不去画了。

优缺点

首先可以看到,使用访问者模式之后,对于原来的元素增加新的操作仅仅只需要实现一个新的访问者角色就行,而不比修改整个元素结构体(不需要去修改交通工具这个结构体所有实现类了),这样符合『开闭原则』的要求。而且由于每个访问者都对应于一个相关操作,所以如果这个操作变了,那么仅仅只需要修改这个具体访问者就行,比如例子里涨价幅度实在是太大了,群众闹变扭,就可以把价格调低一点。

访问者模式最适用的是元素结构变动不大的情况,即在可以预见的未来交通局还只能管理到汽车和火车的情况,如果哪天交通局权力变大了,要管UFO了,可以想象我们需要添加一个UFO类继承于Vehicle,这倒还好,关键是所有的访问者麻烦了,需要在抽象访问者中添加对UFO的处理,所有实现访问者也需要相应增加处理,感觉似乎回到了一开始我们面临的问题。除此之外,访问者模式需要让元素暴露自己的内部属性,也是他的缺点之一。

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

推荐阅读更多精彩内容