文章目录
前言
访问者模式(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;
}
}
说明:
- 糖果类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));
}
}
说明:
- 虽然计价方法略显复杂,但读者不必过度关注此处的方法实现,我们只需要清楚一点:折扣计价访问者的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
说明:
- 顾客买了一包奶糖并交给收银员进行计价结算,最终于第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); //此处会报错
}
}
}
说明:
- 重载方法自动派发不能再正常工作了,这是由于编译器对泛型化的商品类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); //把自己交给访问者
}
}
说明:
- 糖果类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
说明:
- 简单来讲,因为重载方法不允许将泛型对象作为入参,所以我们先让接待者将访问者“派发”到自己的接待方法中,要访问先接待,然后再将自己(此时this已经是确切的对象类型了)“派发”回给访问者,告知自己的身份。这时访问者也明确知道应该调用哪个重载方法了,2次派发成功地化解了重载方法与泛型间的矛盾。
总结
提示:这里对文章进行总结:
-
访问者模式成功地将数据资源(需实现接待者接口)与数据算法(需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,而数据则保持相对固定,最终形成一个算法类对应一套数据。此外,利用双派发确保了访问者对泛型数据元素的识别与算法匹配,使数据集合的迭代与数据元素的自动分拣成为可能。
-
访问者模式的各角色定义如下。
- Element(元素接口):被访问的数据元素接口,定义一个可以接待访问者的行为标准,且所有数据封装类需实现此接口,通常作为泛型并被包含在对象容器中。对应本章例程中的接待者接口Acceptable。
- ConcreteElement(元素实现):具体数据元素实现类,可以有多个实现,并且相对固定。其accept实现方法中调用访问者并将自己“this”传回。对应本章例程中的糖果类Candy、酒类Wine和水果类Fruit。
- ObjectContainer(对象容器):包含所有可被访问的数据对象的容器,可以提供数据对象的迭代功能,可以是任意类型的数据结构。对应本章例程中定义为List< Acceptable>类型的购物车。
- Visitor(访问者接口):可以是接口或者抽象类,定义了一系列访问操作方法以处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定哪个重载方法被调用。
- ConcreteVisitor(访问者实现):访问者接口的实现类,可以有多个实现,每个访问者类都需实现所有数据元素类型的访问重载方法,对应本章例程中的各种打折方法计价类,如折扣计价访问者DiscountVisitor。
- ConcreteVisitor(访问者实现):访问者接口的实现类,可以有多个实现,每个访问者类都需实现所有数据元素类型的访问重载方法,对应本章例程中的各种打折方法计价类,如折扣计价访问者DiscountVisitor。