定义

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





图纸

设计模式——2_A 访问者(Visitor)-LMLPHP




一个例子:如何给好奇宝宝提供他想知道的内容

某天,你发现的出生点居然是大洋彼岸的美利坚,正当你准备掐掐自己人中看看是不是还没醒的时候,肚子却提醒你该补充能量了。你坚信有一技傍身的人总是饿不死的,于是准备靠着祖传的川菜手艺在唐人街创出一片天地。摸爬滚打几年后,随着一串鞭炮被点燃,属于你的川菜馆终于开张,可是当你准备做一个电子菜单的时候却犯了愁

客人们恨不得了解自己将点的菜的全部信息,而你却不能公开自己赖以生存的秘方,这就是我们这次的例子(没错,前面那个浪迹美国的感人故事跟正文毫无关联)

准备好了吗?四人组圣经里的最后一个设计模式的例子也开始了:



菜单、菜品和配方

为了展示菜单,无论如何你需要一个和菜品相关的类簇,就像这样:

设计模式——2_A 访问者(Visitor)-LMLPHP

Menu(菜单) & Cuisine(菜品)
/**
 * 菜品
 */
public class Cuisine {

    /**
     * 菜品名
     */
    private String name;

    /**
     * 配料表
     */
    private List<Material> burdenSheet;

    public Cuisine(String name, List<Material> burdenSheet) {
        this.name = name;
        this.burdenSheet = burdenSheet;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setBurdenSheet(List<Material> burdenSheet) {
        this.burdenSheet = burdenSheet;
    }
}

/**
 * 菜单
 */
public class Menu {

    /**
     * 菜品列表
     */
    private List<Cuisine> cuisineList;

    public static Menu createMenu(){
        Menu menu = new Menu();
        //初始化cuisineList的动作
        return menu;
    }

    private Menu() {
    }
}
Material(物料、食材)
/**
 * 食材
 */
public class Material {

    /**
     * 食材名
     */
    private String name;
    /**
     * 辛辣度
     */
    private int spicyDegree;
    /**
     * 咸度
     */
    private int saltyDegree;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSpicyDegree() {
        return spicyDegree;
    }

    public void setSpicyDegree(int spicyDegree) {
        this.spicyDegree = spicyDegree;
    }

    public int getSaltyDegree() {
        return saltyDegree;
    }

    public void setSaltyDegree(int saltyDegree) {
        this.saltyDegree = saltyDegree;
    }
}

/**
 * 肉类
 */
public class Meat extends Material{

}

/**
 * 蔬菜
 */
public class Vegetable extends Material {

}

/**
 * 调料
 */
public class Flavour extends Material {

}


这个实现简单到不能称之为设计,只能说我们通过 Cuisine(菜品) 来表示一个菜品里面必须有的内容,比如配料表

配料表里面的食材我们通过 Material(食材) 类来表示,并根据类型给 Material 创建了三个子类,分别是 Meat(肉)Vegetable(蔬菜)Flavour(调料)。你可能会问,这仨子类有存在的必要吗?这不是仨空类吗?别着急,后面会用到他们

client 是通过 菜单 点菜的,为了让所有的 client 都可以在程序的任意位置都获取到正确的菜单。我们将川菜馆里面所有的菜品都集中到了 Menu(菜单) 中,并只允许 client 通过静态方法获取 Menu 对象


值得注意的是在 Cuisine 中,我只提供了 burdenSheet(配料表) 的 setter,因为将来调用这个模块的未必都是内部的系统,我不可能允许外部系统获取到我的配料表。别人学会了,我喝西北风去?



产地、有机蔬菜和卡路里

开张后第一个问题来了,客户们需要了解自己吃的牛肉是不是从大洋彼岸打飞的来的餐桌、送进嘴里的青椒是不是有机的 以及 咽下去的食物到底含有多少卡路里。也就是说,要求你在电子菜单上提供食材的 生产日期产地热量情况

上帝都发话了,那肯定要开搞,就像这样:

设计模式——2_A 访问者(Visitor)-LMLPHP

Cuisine & Material
/**
 * 菜品
 */
public class Cuisine {

    ……

    /**
     * 提供这道菜的热量
     */
    public int getCalorie() {
        //菜品的热量=食材的热量和
        int result = 0;

        for (Material material : burdenSheet) {
            //只有在食材是肉和蔬菜时才计算他的热量
            if (material instanceof Meat) {
                Meat meat = (Meat) material;
                result += meat.getCalorie();
            } else if (material instanceof Vegetable) {
                Vegetable meat = (Vegetable) material;
                result += meat.getCalorie();
            }
        }

        return result;
    }

    /**
     * 是否包含有机蔬菜
     */
    public boolean haveOrganicVegetable(){
        for (Material material : burdenSheet) {
            if(material instanceof Vegetable && ((Vegetable)material).isOrganic()){
                return true;
            }
        }

        return false;
    }
}

/**
 * 肉类
 */
public class Meat extends Material {

    /**
     * 卡路里
     */
    private int calorie;

    /**
     * 产地
     */
    private String productionPlace;

    public int getCalorie() {
        return calorie;
    }

    public void setCalorie(int calorie) {
        this.calorie = calorie;
    }

    public String getProductionPlace() {
        return productionPlace;
    }

    public void setProductionPlace(String productionPlace) {
        this.productionPlace = productionPlace;
    }
}

/**
 * 蔬菜
 */
public class Vegetable extends Material{

    /**
     * 卡路里
     */
    private int calorie;
    /**
     * 是否是有机蔬菜
     */
    private boolean isOrganic;

    public int getCalorie() {
        return calorie;
    }

    public void setCalorie(int calorie) {
        this.calorie = calorie;
    }

    public boolean isOrganic() {
        return isOrganic;
    }

    public void setOrganic(boolean organic) {
        isOrganic = organic;
    }
}


Flavour(调料) 的卡路里是忽略不计的,只有 Meat(肉) 是需要提供产地的,只有 Vegetable(蔬菜) 是区分有机和无机的

如果你将这些带有特殊性的属性全部都写到 Material 根类中,那么随着你对食材的描述越来越完善,这个根类也会复杂到让人害怕,而且有很多属性是没有任何意义的,所以你只能把他们分配到特定的子类中去

但是这种做法带来另一个问题,由于我不能直接公开菜品里的配料表,那就意味着客户的所有定制要求我都需要在 Cuisine 中实现对应的方法。如果只是简单的迭代获取信息倒是也无所谓,但是现在的状况是很多属性依赖的是具体子类的实现,而不是食材的根类,这就让我们必须对实例去做类型判断,才能决定执行什么逻辑


所以虽然上述实现可以完成需求,但是你已经预见到这将是一场噩梦

总有一天会有人希望你添加一个 是否包含香菜 这样的提示;又或者有位穆斯林大哥就要吃鱼香肉丝,你要怎么跟人家解释鱼香肉丝里没有鱼只有猪

至少,我们要找到一种实现可以把这些变化独立出来



访问者

如果你采用访问者改造上面的代码,那么就会得到这样的结果:

设计模式——2_A 访问者(Visitor)-LMLPHP

Visitor
/**
 * 访问者
 */
public interface Visitor<E> {

    /**
     * 菜品执行的内容
     */
    E doForCuisine(Cuisine cuisine);

    /**
     * 食材执行的内容
     */
    E doForMaterial(Material material);

    /**
     * 肉类执行的内容
     */
    E doForMeat(Meat meat);

    /**
     * 蔬菜执行的内容
     */
    E doForVegetable(Vegetable vegetable);

    /**
     * 调料执行的内容
     */
    E doForFlavour(Flavour flavour);
}

/**
 * 卡路里访问器
 */
public class CalorieVisitor implements Visitor<Integer>{

    @Override
    public Integer doForCuisine(Cuisine cuisine) {
        int result = 0;
        for (Material material : cuisine.getBurdenSheet()) {
            result += material.accept(this);
        }

        return result;
    }

    @Override
    public Integer doForMaterial(Material material) {
        return 0;
    }

    @Override
    public Integer doForMeat(Meat meat) {
        return meat.getCalorie();
    }

    @Override
    public Integer doForVegetable(Vegetable vegetable) {
        return vegetable.getCalorie();
    }

    @Override
    public Integer doForFlavour(Flavour flavour) {
        return 0;
    }
}

/**
 * 有机属性访问者
 */
public class OrganicVisitor implements Visitor<Boolean> {

    @Override
    public Boolean doForCuisine(Cuisine cuisine) {
        for (Material material : cuisine.getBurdenSheet()) {
            if(material.accept(this)){
                return true;
            }
        }

        return false;
    }

    @Override
    public Boolean doForMaterial(Material material) {
        return false;
    }

    @Override
    public Boolean doForMeat(Meat meat) {
        return false;
    }

    @Override
    public Boolean doForVegetable(Vegetable vegetable) {
        return vegetable.isOrganic();
    }

    @Override
    public Boolean doForFlavour(Flavour flavour) {
        return false;
    }
}
Cuisine & Material
/**
 * 菜品
 */
public class Cuisine {

    //……

    protected List<Material> getBurdenSheet() {
        return burdenSheet;
    }

    public <E> E accept(Visitor<E> v){
        return v.doForCuisine(this);
    }
}

/**
 * 食材
 */
public class Material {

    //……

    public <E> E accept(Visitor<E> v){
        return v.doForMaterial(this);
    }
}

/**
 * 肉类
 */
public class Meat extends Material {

    //……

    public <E> E accept(Visitor<E> v){
        return v.doForMeat(this);
    }
}

/**
 * 蔬菜
 */
public class Vegetable extends Material{

    // ……

    public <E> E accept(Visitor<E> v){
        return v.doForVegetable(this);
    }
}

/**
 * 调料
 */
public class Flavour extends Material{
    public <E> E accept(Visitor<E> v){
        return v.doForFlavour(this);
    }
}

我们创建了一个全新的Visitor(访问者)类簇,让Visitor去和菜品相关的所有类打交道,并获取其中的信息(这就是一开始说的被访问者必须向访问者公开自己的属性),为此我们还特地在Cuisine中添加了一个受保护的getBurdenSheet,以便访问者获取Cuisine内的信息

那访问者要怎么跟被访问者交互呢?还记得观察者模式吗,在观察者模式里我们给观察者和被观察者都做了修改。访问者是一样的,他不能也不应该直接访问被访问者内的信息,而是需要被访问者对他授权,也就是 accept 方法。但是和观察者模式不同的是,所有的被访问者子类都需要针对accept做出自己的特殊操作


这种实现方式堪称惊艳,就像变魔术一样,让所有的类型判断都消失了

其实仔细想想这些类型判断并不是消失了,而是 重写 帮我们代劳了。因为 MeatVegetableFlavour都是Material的子类,所以当我们在这三者中写入accept动作时,其实是在重写他们从Material中继承的方法。也就是说,如果到时候访问者的那个对象是属于下级子类的实例,那他就会优先调用被重写的accept方法

这写法可比if-else优雅多了,而且就算将来真的需要判断有没有香菜,或者有没有猪肉,只需要添加对应的Visitor子类就可以实现


而这正是一个标准的访问者实现




碎碎念

访问者和双分派

笔者读的书少,第一次看到访问者的实现时真的当场拍案叫绝,这种通过子类重写来避开类型判断的写法真的是太妙了

但是这种写法不是访问者的原创,他的行话叫 双分派(double-dispatch)。这是一种很著名的技术,有些编程语言甚至直接支持这种技术,但不包括Java

我们习惯了通过对象/类去点他里面的属性或者方法,就像这样:

a.b(c);

这时候a和b一定是确定的,只有c是动态变化的。这种模式就叫 单分派(single-dispatch)

而双分派实现的效果是可以让a都变得不确定,这是可能的,上例的accept就实现了这种效果

你有没有想过为什么上例的 dofor…accept 中都会出现调用 this,其实这就是在指定执行对象啊。我没有静态的指定谁调用谁,而是在程序执行到那里是才最终确定是谁调用了谁



访问者和代理

从实现上来看,访问者其实是一种变相的代理模式,说得更具体一点是 保护代理

就像上例我们使用访问者的契机其实是为了保护菜品里的配料表,访问者可以减少外部代码和被访问者之间的交互,特别是被访问者的结构错综复杂的时候,可以简化很多工作



写在最后的碎碎念

《庄子·养生主》中讲了一个叫庖丁的人给梁惠王表演杀牛。梁惠王惊讶于庖丁的杀牛技术,于是问他要怎么学才能像他一样。庖丁说:“因为我学习的是道,而不只是技巧。我刚开始杀牛的时候看到什么都是牛,都想用杀牛的方法去操作。三年后,我眼里就没有牛了,连牛在我眼里都不是牛了。因为我不觉得我是在杀牛,而是在解开他的经络,不是因为别人教我要怎么做,而是我的刀划到那里后自然而然应该这样去做,顺着刀势牛就已经被解了。”

这就是庖丁解牛的典故,我们也常用这个程序来形容某人的技术高超

在实战中使用设计模式和庖丁说的是一样的,23种基础设计模式只是”形“而已,他可能是某种情况下的最优解,但绝不是规则。实战中会遇到各种各样的情形,设计模式未必是正确答案,要不然也不会有反模式了

那你会说,用不上那我还学他干嘛?

你要学形而上的东西,你要学模式里的”道“。不是把模型生搬硬套到自己的实现中,而是去思考以前设计这些模式的人为什么要这样做,是什么思路让他做出这样的选择

直到将来的某一天,我相信一定有这样的某一天,道友你在不考虑设计模式的情况下,也会做出和设计模式一样的选择





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

04-25 02:16