说明:本文介绍行为型设计模式之一的访问者模式
定义
访问者模式(Visitor Pattern)是一种将数据结构与数据操作分离的设计模式,指封装一些作用于某种数据结构中的各元素的操作,可以在不改变数据结构的前提下定义作用于这些元素的新的操作,属于行为型设计模式。
(引自《设计模式就该这样学》P415)
购物打折
以超市购物打折为例,超市搞促销活动,购买不同类型的商品有不同的折扣,规则如下:
-
酒水类:没有折扣;
-
水果类:距生产日期3天内,不打折;超出3天,但不超过7天,打五折;超出7天,已过保质期,禁止出售;
-
糖果类:距生产日期180内,打九折;超出180,已过保质期,禁止出售;
首先,定义一个商品类,如下:
(商品类,Product)
import java.time.LocalDate;
/**
* 商品类
*/
public class Product {
/**
* 商品名称
*/
private String name;
/**
* 生产日期
*/
private LocalDate producedDate;
/**
* 价格
* 单位,元
*/
private Integer price;
/**
* 全参构造
*/
public Product(String name, LocalDate producedDate, Integer price) {
this.name = name;
this.producedDate = producedDate;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public LocalDate getProducedDate() {
return producedDate;
}
public void setProducedDate(LocalDate producedDate) {
this.producedDate = producedDate;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
不同类型的商品,继承商品类,如下:
(糖果类,Candy)
import java.time.LocalDate;
/**
* 糖果类
*/
public class Candy extends Product {
/**
* 全参构造
*/
public Candy(String name, LocalDate producedDate, Integer price) {
super(name, producedDate, price);
}
}
(水果类,Fruit,水果是按斤称计算价格的,需增加重量属性)
import java.time.LocalDate;
/**
* 水果类
*/
public class Fruit extends Product {
/**
* 重量
* 单位,斤
*/
private Integer weight;
/**
* 全参构造
*/
public Fruit(String name, LocalDate producedDate, Integer price, Integer weight) {
super(name, producedDate, price);
this.weight = weight;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
}
(酒水类,Wine)
import java.time.LocalDate;
/**
* 酒水类
*/
public class Wine extends Product {
/**
* 全参构造
*/
public Wine(String name, LocalDate producedDate, Integer price) {
super(name, producedDate, price);
}
}
(访问者接口,Visitor,定义不同商品的访问行为,也就是后面的计价行为)
/**
* 访问者接口
* 定义三种商品的访问行为
*/
public interface Visitor {
void visit(Fruit fruit);
void visit(Wine wine);
void visit(Candy candy);
}
(促销计算价格行为实现,DiscountVisitor,针对不同类型的商品写不同的实现代码)
import java.text.NumberFormat;
import java.time.LocalDate;
/**
* 促销计算价格访问者
*/
public class DiscountVisitor implements Visitor {
/**
* 计价日期
*/
private LocalDate discountDate;
public DiscountVisitor(LocalDate discountDate) {
this.discountDate = discountDate;
System.out.println("计价日期:" + discountDate);
}
/**
* 水果促销价格计算
* 折扣后价格 = 价格 * 重量 * 折扣率
* 折扣率计算:
* 在生产日期3天内,不打折;
* 超出生产日期3天,打5折;
* 超出7天,禁止出售
*/
@Override
public void visit(Fruit fruit) {
System.out.println("=================【水果类】" + fruit.getName() + "打折后价格=================");
double rete = 0;
// 统计商品距离当前已多少天
long days = discountDate.toEpochDay() - fruit.getProducedDate().toEpochDay();
// 折扣计算
if (days > 7) {
System.out.println("水果保质期7天,已超出保质期,禁止出售!");
} else if (days > 3) {
rete = 0.5;
} else {
rete = 1;
}
// 计算促销价格
double discountPrice = fruit.getPrice() * fruit.getWeight() * rete;
System.out.println(NumberFormat.getCurrencyInstance().format(discountPrice));
}
/**
* 糖果促销价格计算
* 折扣后价格 = 价格 * 折扣率
* 折扣率计算:
* 在生产日期180天内,打9折;
* 超出生产日期180天,禁止出售
*/
@Override
public void visit(Candy candy) {
System.out.println("=================【水果类】" + candy.getName() + "打折后价格=================");
double rete = 0;
// 统计商品距离当前已多少天
long days = discountDate.toEpochDay() - candy.getProducedDate().toEpochDay();
// 折扣计算
if (days > 180) {
System.out.println("糖果保质期180天,已超出保质期,请勿食用!");
} else {
rete = 0.9;
}
// 计算促销价格
double discountPrice = candy.getPrice() * rete;
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()));
}
}
(客户端使用,Client)
import java.time.LocalDate;
public class Client {
public static void main(String[] args) {
DiscountVisitor discountVisitor = new DiscountVisitor(LocalDate.of(2025, 5, 5));
// 椰子奶糖,30块
Candy candy = new Candy("椰子奶糖", LocalDate.of(2025, 1, 1), 30);
// 西瓜,2块钱一斤,15斤
Fruit watermelon = new Fruit("西瓜", LocalDate.of(2025, 5, 2), 2, 15);
// 酒水
Wine coke = new Wine("可乐", LocalDate.of(2025, 1, 3), 3);
// 计算促销价格
discountVisitor.visit(candy);
discountVisitor.visit(watermelon);
discountVisitor.visit(coke);
}
}
执行结果,糖果类打了九折,水果类生产日期未超出3天,未打折,酒水类没有折扣
看似很完美,但是不切实际,因为在实际的购物场景中,不太可能一次只买一样商品,会一次购买多种商品,如下:
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
DiscountVisitor discountVisitor = new DiscountVisitor(LocalDate.of(2025, 5, 5));
// 椰子奶糖,30块
Candy candy = new Candy("椰子奶糖", LocalDate.of(2025, 1, 1), 30);
// 西瓜,2块钱一斤,15斤
Fruit watermelon = new Fruit("西瓜", LocalDate.of(2025, 5, 2), 2, 15);
// 酒水
Wine coke = new Wine("可乐", LocalDate.of(2025, 1, 3), 3);
// 购物车
List<Product> productList = new ArrayList<>();
productList.add(candy);
productList.add(watermelon);
productList.add(coke);
// 计算促销价格
for (Product product : productList) {
discountVisitor.visit(product);
}
}
}
因为购物车集合,productList存的是商品的父类Product,而促销计算价格访问者的visit()方法,只能是Product的其中一个子类,所以计算促销价格这里,会编译报错。
访问者模式
针对上述困境,使用访问者模式进行改造,如下,定义一个接口,表示具有被访问的能力,并定义一个访问方法,传入一个访问对象
(可被访问的接口,Acceptable)
/**
* 可被访问的接口
*/
public interface Acceptable {
/**
* 接受访问者
*/
void accept(Visitor visitor);
}
每个商品的子类,都实现该接口,如下:
(糖果类,Candy)
/**
* 糖果类
*/
public class Candy extends Product implements Acceptable {
/**
* 全参构造
*/
public Candy(String name, LocalDate producedDate, Integer price) {
super(name, producedDate, price);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
(水果类,Fruit)
import java.time.LocalDate;
/**
* 水果类
*/
public class Fruit extends Product implements Acceptable {
/**
* 重量
* 单位,斤
*/
private Integer weight;
/**
* 全参构造
*/
public Fruit(String name, LocalDate producedDate, Integer price, Integer weight) {
super(name, producedDate, price);
this.weight = weight;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
(酒水类,Wine)
import java.time.LocalDate;
/**
* 酒水类
*/
public class Wine extends Product implements Acceptable {
/**
* 全参构造
*/
public Wine(String name, LocalDate producedDate, Integer price) {
super(name, producedDate, price);
}
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
客户端使用。购物车集合内,泛型声明为Acceptable,计算促销价格时,不再是计价访问者逐个去计算价格,而是商品调用自己的被访问方法,让计价访问者计算自己的价格。
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
public class Client {
public static void main(String[] args) {
DiscountVisitor discountVisitor = new DiscountVisitor(LocalDate.of(2025, 5, 5));
// 椰子奶糖,30块
Candy candy = new Candy("椰子奶糖", LocalDate.of(2025, 1, 1), 30);
// 西瓜,2块钱一斤,15斤
Fruit watermelon = new Fruit("西瓜", LocalDate.of(2025, 5, 2), 2, 15);
// 酒水
Wine coke = new Wine("可乐", LocalDate.of(2025, 1, 3), 3);
// 购物车
List<Acceptable> productList = new ArrayList<>();
productList.add(candy);
productList.add(watermelon);
productList.add(coke);
// 计算促销价格
for (Acceptable product : productList) {
product.accept(discountVisitor);
}
}
}
改造完成
使用场景
在《设计模式就该这样学》(P416)这本书中,提到访问者模式适用于以下场景:
(1)数据结构稳定,作用于数据结构的操作经常变化的场景。
(2)需要数据结构于数据操作分离的场景。
(3)需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。
可以对照上述购物打折的例子,支付场景(多种支付渠道)也可以考虑使用访问者模式。
总结
本文介绍了行为型设计模式中的访问者模式,参考《设计模式就该这样学》、《秒懂设计模式》两书,购物打折是《秒懂设计模式》中的举例。