目录
访问者模式(Visitor Pattern) 的原始定义是:允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。
这个定义会比较抽象,但是我们依然能看出两个关键点:
访问者模式主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不污染数据本身,访问者会将多种算法独立归档,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并确保算法的自由扩展。
访问者模式在实际开发中使用的非常少,因为它比较难以实现并且应用该模式肯能会导致代码的可读性变差,可维护性变差,在没有特别必要的情况下,不建议使用访问者模式。
访问者模式包含以下主要角色:
我们以超市购物为例,假设超市中的三类商品: 水果,糖果,酒水进行售卖。我们可以忽略每种商品的计价方法,因为最终结账时由收银员统一集中处理,在商品类中添加计价方法是不合理的设计。我们先来定义糖果类和酒类、水果类。
package main.java.cn.test.visitor.V1;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:52:37
* @description 抽象商品父类
*/
public abstract class Product {
//商品名
private String name;
// 生产日期
private LocalDate producedDate;
//单品价格
private double price;
public Product(String name, LocalDate producedDate,
double 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 double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
}
package main.java.cn.test.visitor.V1;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:53:58
* @description 糖果类
*/
public class Candy extends Product {
public Candy(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
}
package main.java.cn.test.visitor.V1;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:54:37
* @description 酒水类
*/
public class Wine extends Product {
public Wine(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
}
package main.java.cn.test.visitor.V1;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:55:05
* @description 水果类
*/
public class Fruit extends Product {
//重量
private float weight;
public Fruit(String name, LocalDate producedDate, double price, float weight) {
super(name, producedDate, price);
this.weight = weight;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
}
package main.java.cn.test.visitor.V1;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:58:57
* @description 访问者接口-根据入参不同调用对应的重载方法
*/
public interface Visitor {
//糖果重载方法
public void visit(Candy candy);
//酒类重载方法
public void visit(Wine wine);
//水果重载方法
public void visit(Fruit fruit);
}
package main.java.cn.test.visitor.V1;
import java.text.NumberFormat;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:59:38
* @description 折扣计价访问者类
*/
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());
System.out.println("生产日期:" + candy.getProducedDate());
//获取产品生产天数
long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
if (days > 180) {
System.out.println("超过半年的糖果,请勿食用!");
} else {
double rate = 0.9;
double 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) {
String message = null;
System.out.println("水果: " + fruit.getName());
System.out.println("生产日期:" + fruit.getProducedDate());
//获取产品生产天数
long days = billDate.toEpochDay() -
fruit.getProducedDate().toEpochDay();
double rate = 0;
if (days > 7) {
message = "超过七天的水果,请勿食用!";
} else if (days > 3) {
rate = 0.5;
message = "水果超打折后的价格";
} else {
rate = 1;
message = "水果价格: ";
}
double discountPrice = fruit.getPrice() * fruit.getWeight() * rate;
System.out.println(message + NumberFormat.getCurrencyInstance().format(discountPrice));
}
}
package main.java.cn.test.visitor.V1;
import java.text.NumberFormat;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 16:02:09
* @description 测试类
*/
public class Test {
public static void main(String[] args) {
//德芙巧克力,生产日期2023-10-1 ,原价 10元
Candy candy = new Candy("德芙巧克力", LocalDate.of(2023, 10, 1), 10.0);
Visitor visitor = new DiscountVisitor(LocalDate.of(2024, 1, 11));
visitor.visit(candy);
System.out.println("====================");
// 徐福记,生产日期2022年-10-1,原价10元
Candy candy1 = new Candy("徐福记", LocalDate.of(2022, 10, 1), 10.0);
Visitor visitor1 = new DiscountVisitor(LocalDate.of(2024, 1, 11));
visitor1.visit(candy1);
System.out.println("====================");
// 茅台酒,生产日期2022年-10-1,原价5000元
Wine wine = new Wine("茅台原浆酒",LocalDate.of(2022,10,1),5000);
Visitor visitor2 = new DiscountVisitor(LocalDate.of(2024, 1, 11));
visitor2.visit(wine);
System.out.println("====================");
// 橘子,生产日期2024年-1-10,原价 8元,买3斤
Fruit fruit = new Fruit("广西小沙糖桔",LocalDate.of(2024,1,10),8.00,3);
Visitor visitor3 = new DiscountVisitor(LocalDate.of(2024, 1, 15));
visitor3.visit(fruit);
}
}
上面的代码虽然可以完成当前的需求,但是设想一下这样一个场景: 由于访问者的重载方法只能对当个的具体商品进行计价,如果顾客选择了多件商品来结账时,就可能会引起重载方法的派发问题(到底该由谁来计算的问题)。
首先我们定义一个接待访问者的类 Acceptable,其中定义了一个accept(Visitorvisitor)方法,只要是visitor的子类都可以接收。
package main.java.cn.test.visitor.V2;
/**
* @author ningzhaosheng
* @date 2024/1/15 16:33:22
* @description 接待者接口(抽象元素角色)
*/
public interface Acceptable {
//接收所有的Visitor访问者的子类实现类
public void accept(Visitor visitor);
}
package main.java.cn.test.visitor.V2;
import main.java.cn.test.visitor.V1.Product;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 16:35:22
* @description 糖果类
*/
public class Candy extends Product implements Acceptable {
public Candy(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
@Override
public void accept(Visitor visitor) {
//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型
visitor.visit(this);
}
}
package main.java.cn.test.visitor.V2;
import main.java.cn.test.visitor.V1.Product;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:54:37
* @description 酒水类
*/
public class Wine extends Product implements Acceptable{
public Wine(String name, LocalDate producedDate, double price) {
super(name, producedDate, price);
}
@Override
public void accept(Visitor visitor) {
//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型
visitor.visit(this);
}
}
package main.java.cn.test.visitor.V2;
import main.java.cn.test.visitor.V1.Product;
import java.time.LocalDate;
/**
* @author ningzhaosheng
* @date 2024/1/15 15:55:05
* @description 水果类
*/
public class Fruit extends Product implements Acceptable {
//重量
private float weight;
public Fruit(String name, LocalDate producedDate, double price, float weight) {
super(name, producedDate, price);
this.weight = weight;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
@Override
public void accept(Visitor visitor) {
//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型
visitor.visit(this);
}
}
package main.java.cn.test.visitor.V2;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
/**
* @author ningzhaosheng
* @date 2024/1/15 16:43:32
* @description 测试类
*/
public class Test {
public static void main(String[] args) {
//模拟添加多个商品的操作
List<Acceptable> products = Arrays.asList(
new Candy("德芙巧克力", LocalDate.of(2023, 10, 1), 10.0),
new Candy("徐福记", LocalDate.of(2022, 10, 1), 10.0),
new Wine("茅台原浆酒", LocalDate.of(2022, 10, 1), 5000),
new Fruit("广西小沙糖桔", LocalDate.of(2024, 1, 10), 8.00, 3)
);
Visitor visitor = new DiscountVisitor(LocalDate.of(2024, 1, 15));
for (Acceptable product : products) {
product.accept(visitor);
}
}
}
代码编写到此处,就可以应对计价方式或者业务逻辑的变化了,访问者模式成功地将数据资源(需实现接待者接口)与数据算法 (需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,数据则保持相对固定,最终形成?个算法类对应?套数据。
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
访问者模式依赖了具体类,而没有依赖抽象类。
比如,上面例子中路由器本身的内部构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等。
比如,扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作。
比如,对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。
好了,本次分享就到这里,欢迎大家继续阅读《设计模式》专栏其他设计模式内容,如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!