行为篇-访问者模式


前言

访问者模式(Visitor)主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不“污染”数据本身,访问者模式会将多种算法独立归类,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并且确保算法的自由扩展。


提示:以下是本篇文章正文内容,下面案例可供参考

一、多样化的商品

访问者模式也许是最复杂的一种设计模式,这让很多人望而却步。为了更轻松、深刻地理解其核心思想,我们从最简单的超市购物实例开始,由浅入深、逐层突破。超市货架上摆放着琳琅满目的商品,有水果、糖果及各种酒水饮料等,这些商品有些按斤卖,有些按袋卖,而有些则按瓶卖,并且优惠力度也各不相同,所以它们应该对应不同的商品计价方法。

无论商品的计价方法多么复杂,我们都不必太操心,因为最终结账时由收银员统一集中处理,毕竟在商品类里加入多变的计价方法是不合理的设计。首先我们来看如何定义商品对应的POJO类,假设货架上的商品有糖果类、酒类和水果类,除各自的特征之外,它们应该拥有一些类似的属性与方法。为了简化代码,我们将这些通用的数据封装,抽象到商品父类中去。

1.商品抽象类

public abstract class Product {
    private String name;// 商品名
    private LocalDate produceDate;//商品日期
    private float price;//单品价格

    public Product(String name, LocalDate produceDate, float price) {
        this.name = name;
        this.produceDate = produceDate;
        this.price = price;
    }

    public String getName() {
        return name;
    }

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

    public LocalDate getProduceDate() {
        return produceDate;
    }

    public void setProduceDate(LocalDate produceDate) {
        this.produceDate = produceDate;
    }

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }
}

2.糖果类、酒类、水果类

//糖果
public class Candy extends Product{
    public Candy(String name, LocalDate produceDate, float price) {
        super(name, produceDate, price);
    }
}

//酒
public class Wine extends Product{
    public Wine(String name, LocalDate produceDate, float price) {
        super(name, produceDate, price);
    }
}

//水果
public class Fruit extends Product{
    private float weight;

    public Fruit(String name, LocalDate produceDate, float price, float weight) {
        super(name, produceDate, price);
        this.weight = weight;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }
}

说明:

  1. 糖果类Candy与酒类Wine都是成品,不管是按瓶出售还是按袋出售都可以继承父类的单品价格,一个对象代表一件商品。而水果类Fruit则有些特殊,因为它是散装出售并且按斤计价的,所以单品对象的价格不固定,我们为其增加了一个重量属性weight。

二、多变的计价方法

商品数据类定义好后,顾客便可以挑选商品并加入购物车了,最后一定少不了去收银台结账的步骤,这时收银员会对商品上的条码进行扫描以确定单品价格。这就像“访问”了顾客的商品信息,并将其显示在屏幕上,最终将商品价格累加完成计价,所以收银员角色非常类似于商品的“访问者”。

基于此,我们来思考一下如何设计访问者。我们先做出对商品类别的判断,能否用instanceof运算符判断商品类别呢?不能,否则代码里就会充斥着大量以“if”“else”组织的逻辑,显然太混乱。有些读者可能想到了使用多个同名方法的方式,以不同的商品类别作为入参来分别处理。没错,这种情况用重载方法再合适不过了。

1. 访问者接口

public interface Visitor {
    public void visit(Candy candy);// 糖果重载方法

    public void visit(Wine wine);// 酒类重载方法

    public void visit(Fruit fruit);// 水果重载方法
}

2.折扣计价访问者

public class DiscountVisitor implements Visitor {

    private LocalDate billDate;

    public DiscountVisitor(LocalDate billDate) {
        this.billDate = billDate;
        System.out.println("结算日期" + billDate);
    }

    @Override
    public void visit(Candy candy) {
        System.out.println("===糖果【" + candy.getName() + "】打折后价格===");
        float rate = 0;
        long days = billDate.toEpochDay() - candy.getProduceDate().toEpochDay();
        if (days > 180) {
            System.out.println("超过半年的糖果, 请勿食用!");
        } else {
            rate = 0.9f;
        }
        float discountPrice = candy.getPrice() * rate;
        System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));
    }

    @Override
    public void visit(Wine wine) {
        System.out.println("===酒【" + wine.getName() + "】无折扣价格===");
        System.out.println(NumberFormat.getCurrencyInstance().format(wine.getPrice()));
    }

    @Override
    public void visit(Fruit fruit) {
        System.out.println("===水果【" + fruit.getName() + "】打折后价格===");
        float rate = 0;
        long days = billDate.toEpochDay() - fruit.getProduceDate().toEpochDay();
        if (days > 7) {
            System.out.println("¥0.00元(超过7天的水果,请勿食用!)");
        } else if (days > 3) {
            rate = 0.5f;
        } else {
            rate = 1;
        }
        float discountPrice = fruit.getPrice() * fruit.getWeight() * rate;
        System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));
    }
}

说明:

  1. 虽然计价方法略显复杂,但读者不必过度关注此处的方法实现,我们只需要清楚一点:折扣计价访问者的3个重载方法分别实现了3类商品的计价方法,展现出访问方法visit()的多态性。

3.客户端类

public class Client {
    public static void main(String[] args) {
        Candy candy = new Candy("小白兔糖", LocalDate.of(2019, 10, 1), 20.00f);
        Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2000, 1, 1));
        discountVisitor.visit(candy);
    }
}
输出结果:
结算日期2000-01-01
===糖果【小白兔糖】打折后价格===18.00

说明:

  1. 顾客买了一包奶糖并交给收银员进行计价结算,最终于第8行输出最终价格。输出结果显示糖果价格成功按九折计价,显然访问者能够顺利识别传入的参数是糖果类商品,并成功派发了相应的糖果计价方法visit (Candy candy)。当然,重载方法责有所归,其他商品类也同样适用于这种自动派发机制。

4.泛型购物车

至此,我们已经利用访问者的重载方法实现了计价方法的自动派发机制,难道这就是访问者模式吗?其实并非如此简单。通常顾客去超市购物不会只购买一件商品,尤其是当超市举办更大力度的商品优惠活动时,顾客们会将打折的商品一并加入购物车,结账时一起计价。

针对这种特殊时期的计价方法也不难,只需要另外实现一个“优惠活动计价访问者类”就可以了。值得深思的是,访问者的重载方法只能对单个“具体”商品类进行计价,当顾客推着装有多件商品的购物车来结账时,“含糊不清”的“泛型”商品可能会引起重载方法的派发问题。实践出真知,我们用之前的访问者来做一个清空购物车的实验,请参看代码清单。

public class Client {
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
                new Candy("小白糖", LocalDate.of(2018, 10, 1), 20.00f),
                new Wine("老猫白兔", LocalDate.of(2017, 1, 1), 1000.00f),
                new Fruit("草莓", LocalDate.of(2018, 12, 26), 10.00f,2.5f)
        );
        Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2018, 1, 1));
        //迭代购物车中的商品
        for (Product product:products){
            discountVisitor.visit(product); //此处会报错
        }
    }
}

说明:

  1. 重载方法自动派发不能再正常工作了,这是由于编译器对泛型化的商品类Product茫然无措,分不清到底是糖果还是酒,所以也就无法确定应该调用哪个重载方法了。

三、访问和承接

超市购物例程在接近尾声时却出了编译问题,我们来重新整理一下思路。当前这种状况类似于交警(访问者)对车辆(商品)进行的违法排查工作。例如有些司机的驾照可能过期了,有些司机存在持C类驾照开大车等情况。由于交警并不清楚每个司机驾照的具体状况(泛型),因此这时就需要司机主动接受排查并出示自己的驾照,这样交警便能针对每种驾照状况做出相应的处理了。基于这种“主动亮明身份”的理念,我们对系统进行重构,之前定义的商品模块就需要作为“接待者”主动告知“访问者”自己的身份,所以它们要一定拥有“接待排查”的能力。

1. 接待者接口

我们定义一个接待者接口来统一这个行为标准,请参看代码。

public interface Acceptable {
    // 主动接待访问者
    public void accept(Visitor visitor);
}

2.重构糖果类

public class Candy extends Product implements Acceptable{
    public Candy(String name, LocalDate produceDate, float price) {
        super(name, produceDate, price);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this); //把自己交给访问者
    }
}

说明:

  1. 糖果类Candy实现接待者接口Acceptable,顺理成章地成为了“接待者”,并把自己(this)交给了访问者以亮明身份。

3.客户端类

public class Client {
    public static void main(String[] args) {
        List<Acceptable> products = Arrays.asList(
                new Candy("小白糖", LocalDate.of(2018, 10, 1), 20.00f),
                new Wine("老猫白兔", LocalDate.of(2017, 1, 1), 1000.00f),
                new Fruit("草莓", LocalDate.of(2018, 12, 26), 10.00f, 2.5f)
        );
        Visitor discountVisitor = new DiscountVisitor(LocalDate.of(2019, 1, 1));
        //迭代购物车中的商品
        for (Acceptable product : products) {
            product.accept(discountVisitor);//此处会报错
        }
    }
}
输出结果:
结算日期2019-01-01
===糖果【小白糖】打折后价格===18.00
===酒【老猫白兔】无折扣价格===1,000.00
===水果【草莓】打折后价格===12.50

说明:

  1. 简单来讲,因为重载方法不允许将泛型对象作为入参,所以我们先让接待者将访问者“派发”到自己的接待方法中,要访问先接待,然后再将自己(此时this已经是确切的对象类型了)“派发”回给访问者,告知自己的身份。这时访问者也明确知道应该调用哪个重载方法了,2次派发成功地化解了重载方法与泛型间的矛盾。

总结

提示:这里对文章进行总结:

  1. 访问者模式成功地将数据资源(需实现接待者接口)与数据算法(需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,而数据则保持相对固定,最终形成一个算法类对应一套数据。此外,利用双派发确保了访问者对泛型数据元素的识别与算法匹配,使数据集合的迭代与数据元素的自动分拣成为可能。

  2. 访问者模式的各角色定义如下。

  • Element(元素接口):被访问的数据元素接口,定义一个可以接待访问者的行为标准,且所有数据封装类需实现此接口,通常作为泛型并被包含在对象容器中。对应本章例程中的接待者接口Acceptable。
  • ConcreteElement(元素实现):具体数据元素实现类,可以有多个实现,并且相对固定。其accept实现方法中调用访问者并将自己“this”传回。对应本章例程中的糖果类Candy、酒类Wine和水果类Fruit。
  • ObjectContainer(对象容器):包含所有可被访问的数据对象的容器,可以提供数据对象的迭代功能,可以是任意类型的数据结构。对应本章例程中定义为List< Acceptable>类型的购物车。
  • Visitor(访问者接口):可以是接口或者抽象类,定义了一系列访问操作方法以处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定哪个重载方法被调用。
  • ConcreteVisitor(访问者实现):访问者接口的实现类,可以有多个实现,每个访问者类都需实现所有数据元素类型的访问重载方法,对应本章例程中的各种打折方法计价类,如折扣计价访问者DiscountVisitor。
  • ConcreteVisitor(访问者实现):访问者接口的实现类,可以有多个实现,每个访问者类都需实现所有数据元素类型的访问重载方法,对应本章例程中的各种打折方法计价类,如折扣计价访问者DiscountVisitor。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhixuChen200

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值