访问者模式(Visitor)主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不“污染”数据本身,访问者模式会将多种算法独立归类,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并且确保算法的自由扩展。
提示:以下是本篇文章正文内容,下面案例可供参考
访问者模式也许是最复杂的一种设计模式,这让很多人望而却步。为了更轻松、深刻地理解其核心思想,我们从最简单的超市购物实例开始,由浅入深、逐层突破。超市货架上摆放着琳琅满目的商品,有水果、糖果及各种酒水饮料等,这些商品有些按斤卖,有些按袋卖,而有些则按瓶卖,并且优惠力度也各不相同,所以它们应该对应不同的商品计价方法。
无论商品的计价方法多么复杂,我们都不必太操心,因为最终结账时由收银员统一集中处理,毕竟在商品类里加入多变的计价方法是不合理的设计。首先我们来看如何定义商品对应的POJO类,假设货架上的商品有糖果类、酒类和水果类,除各自的特征之外,它们应该拥有一些类似的属性与方法。为了简化代码,我们将这些通用的数据封装,抽象到商品父类中去。
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;}
}
//糖果
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;}
}
说明:
商品数据类定义好后,顾客便可以挑选商品并加入购物车了,最后一定少不了去收银台结账的步骤,这时收银员会对商品上的条码进行扫描以确定单品价格。这就像“访问”了顾客的商品信息,并将其显示在屏幕上,最终将商品价格累加完成计价,所以收银员角色非常类似于商品的“访问者”。
基于此,我们来思考一下如何设计访问者。我们先做出对商品类别的判断,能否用instanceof运算符判断商品类别呢?不能,否则代码里就会充斥着大量以“if”“else”组织的逻辑,显然太混乱。有些读者可能想到了使用多个同名方法的方式,以不同的商品类别作为入参来分别处理。没错,这种情况用重载方法再合适不过了。
public interface Visitor {public void visit(Candy candy);// 糖果重载方法public void visit(Wine wine);// 酒类重载方法public void visit(Fruit fruit);// 水果重载方法
}
public class DiscountVisitor implements Visitor {private LocalDate billDate;public DiscountVisitor(LocalDate billDate) {this.billDate = billDate;System.out.println("结算日期" + billDate);}@Overridepublic 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));}@Overridepublic void visit(Wine wine) {System.out.println("===酒【" + wine.getName() + "】无折扣价格===");System.out.println(NumberFormat.getCurrencyInstance().format(wine.getPrice()));}@Overridepublic 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));}
}
说明:
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
说明:
至此,我们已经利用访问者的重载方法实现了计价方法的自动派发机制,难道这就是访问者模式吗?其实并非如此简单。通常顾客去超市购物不会只购买一件商品,尤其是当超市举办更大力度的商品优惠活动时,顾客们会将打折的商品一并加入购物车,结账时一起计价。
针对这种特殊时期的计价方法也不难,只需要另外实现一个“优惠活动计价访问者类”就可以了。值得深思的是,访问者的重载方法只能对单个“具体”商品类进行计价,当顾客推着装有多件商品的购物车来结账时,“含糊不清”的“泛型”商品可能会引起重载方法的派发问题。实践出真知,我们用之前的访问者来做一个清空购物车的实验,请参看代码清单。
public class Client {public static void main(String[] args) {List 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); //此处会报错}}
}
说明:
超市购物例程在接近尾声时却出了编译问题,我们来重新整理一下思路。当前这种状况类似于交警(访问者)对车辆(商品)进行的违法排查工作。例如有些司机的驾照可能过期了,有些司机存在持C类驾照开大车等情况。由于交警并不清楚每个司机驾照的具体状况(泛型),因此这时就需要司机主动接受排查并出示自己的驾照,这样交警便能针对每种驾照状况做出相应的处理了。基于这种“主动亮明身份”的理念,我们对系统进行重构,之前定义的商品模块就需要作为“接待者”主动告知“访问者”自己的身份,所以它们要一定拥有“接待排查”的能力。
我们定义一个接待者接口来统一这个行为标准,请参看代码。
public interface Acceptable {// 主动接待访问者public void accept(Visitor visitor);
}
public class Candy extends Product implements Acceptable{public Candy(String name, LocalDate produceDate, float price) {super(name, produceDate, price);}@Overridepublic void accept(Visitor visitor) {visitor.visit(this); //把自己交给访问者}
}
说明:
public class Client {public static void main(String[] args) {List 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
说明:
提示:这里对文章进行总结:
访问者模式成功地将数据资源(需实现接待者接口)与数据算法(需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,而数据则保持相对固定,最终形成一个算法类对应一套数据。此外,利用双派发确保了访问者对泛型数据元素的识别与算法匹配,使数据集合的迭代与数据元素的自动分拣成为可能。
访问者模式的各角色定义如下。