目录
一、访问者模式介绍
1.1 访问者模式定义
1.2 访问者模式原理
1.2.1 访问者模式类图
1.2.2 模式角色说明
二、访问者模式的应用
2.1 需求说明
2.2 需求实现
2.2.1 V1版本
2.2.1.1 抽象产品类
2.2.1.2 糖果类
2.2.1.3 酒水类
2.2.1.4 水果类
2.2.1.5 访问者接口
2.2.1.6 访问者实现类
2.2.1.7 测试类
2.2.2 V2版本
2.2.2.1 接待者接口
2.2.2.2 糖果类(优化)
2.2.2.3 酒水类(优化)
2.2.2.4 水果类(优化)
2.2.2.5 测试类
三、访问者模式总结
3.1 访问者模式的优点
3.2 访问者模式的缺点
3.3 访问者模式的使用场景
一、访问者模式介绍
1.1 访问者模式定义
访问者模式(Visitor Pattern) 的原始定义是:允许在运行时将一个或多个操作应用于一组对象,将操作与对象结构分离。
这个定义会比较抽象,但是我们依然能看出两个关键点:
- 一个是: 运行时使用一组对象的一个或多个操作,比如,对不同类型的文件(.pdf、.xml、.properties)进行扫描;
- 另一个是: 分离对象的操作和对象本身的结构,比如,扫描多个文件夹下的多个文件,对于文件来说,扫描是额外的业务操作,如果在每个文件对象上都加一个扫描操作,太过于冗余,而扫描操作具有统一性,非常适合访问者模式。
访问者模式主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不污染数据本身,访问者会将多种算法独立归档,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并确保算法的自由扩展。
访问者模式在实际开发中使用的非常少,因为它比较难以实现并且应用该模式肯能会导致代码的可读性变差,可维护性变差,在没有特别必要的情况下,不建议使用访问者模式。
1.2 访问者模式原理
1.2.1 访问者模式类图
1.2.2 模式角色说明
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:可以是接口或者抽象类,定义了一系列操作方法,用来处理所有数据元素,通常为同名的访问方法,并以数据元素类作为入参来确定那个重载方法被调用。
- 具体访问者(ConcreteVisitor)角色:访问者接口的实现类,可以有多个实现,每个访问者都需要实现所有数据元素类型的访问重载方法。
- 抽象元素(Element)角色:被访问的数据元素接口,定义了一个接受访问者的方法( accept ),其意义是指,每一个元素都要可以被访问者访问。
- 具体元素(ConcreteElement)角色: 具体数据元素实现类,提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法,其accept实现方法中调用访问者并将自己 "this" 传回。
- 对象结构(Object Structure)角色:包含所有可能被访问的数据对象的容器,可以提供数据对象的迭代功能,可以是任意类型的数据结构。
- 客户端 ( Client ) : 使用容器并初始化其中各类数据元素,并选择合适的访问者处理容器中的所有数据对象。
二、访问者模式的应用
2.1 需求说明
我们以超市购物为例,假设超市中的三类商品: 水果,糖果,酒水进行售卖。我们可以忽略每种商品的计价方法,因为最终结账时由收银员统一集中处理,在商品类中添加计价方法是不合理的设计。我们先来定义糖果类和酒类、水果类。
2.2 需求实现
2.2.1 V1版本
2.2.1.1 抽象产品类
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;}
}
2.2.1.2 糖果类
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);}
}
2.2.1.3 酒水类
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);}
}
2.2.1.4 水果类
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;}}
2.2.1.5 访问者接口
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);}
2.2.1.6 访问者实现类
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);}@Overridepublic 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));}}@Overridepublic void visit(Wine wine) {System.out.println("酒类: " + wine.getName() + ",无折扣价格!");System.out.println("原价: " + NumberFormat.getCurrencyInstance().format(wine.getPrice()));}@Overridepublic 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));}
}
2.2.1.7 测试类
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);}}
上面的代码虽然可以完成当前的需求,但是设想一下这样一个场景: 由于访问者的重载方法只能对当个的具体商品进行计价,如果顾客选择了多件商品来结账时,就可能会引起重载方法的派发问题(到底该由谁来计算的问题)。
2.2.2 V2版本
首先我们定义一个接待访问者的类 Acceptable,其中定义了一个accept(Visitorvisitor)方法,只要是visitor的子类都可以接收。
2.2.2.1 接待者接口
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);
}
2.2.2.2 糖果类(优化)
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);}@Overridepublic void accept(Visitor visitor) {//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型visitor.visit(this);}
}
2.2.2.3 酒水类(优化)
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);}@Overridepublic void accept(Visitor visitor) {//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型visitor.visit(this);}
}
2.2.2.4 水果类(优化)
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;}@Overridepublic void accept(Visitor visitor) {//accept实现方法中调用访问者并将自己 "this" 传回。this是一个明确的身份,不存在任何泛型visitor.visit(this);}
}
2.2.2.5 测试类
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);}}}
代码编写到此处,就可以应对计价方式或者业务逻辑的变化了,访问者模式成功地将数据资源(需实现接待者接口)与数据算法 (需实现访问者接口)分离开来。重载方法的使用让多样化的算法自成体系,多态化的访问者接口保证了系统算法的可扩展性,数据则保持相对固定,最终形成⼀个算法类对应⼀套数据。
三、访问者模式总结
3.1 访问者模式的优点
- 扩展性好
在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好
通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
- 分离无关行为
通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
3.2 访问者模式的缺点
- 对象结构变化很困难
在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 违反了依赖倒置原则
访问者模式依赖了具体类,而没有依赖抽象类。
3.3 访问者模式的使用场景
- 当对象的数据结构相对稳定,而操作却经常变化的时候。
比如,上面例子中路由器本身的内部构造(也就是数据结构)不会怎么变化,但是在不同操作系统下的操作可能会经常变化,比如,发送数据、接收数据等。
- 需要将数据结构与不常用的操作进行分离的时候。
比如,扫描文件内容这个动作通常不是文件常用的操作,但是对于文件夹和文件来说,和数据结构本身没有太大关系(树形结构的遍历操作),扫描是一个额外的动作,如果给每个文件都添加一个扫描操作会太过于重复,这时采用访问者模式是非常合适的,能够很好分离文件自身的遍历操作和外部的扫描操作。
- 需要在运行时动态决定使用哪些对象和方法的时候。
比如,对于监控系统来说,很多时候需要监控运行时的程序状态,但大多数时候又无法预知对象编译时的状态和参数,这时使用访问者模式就可以动态增加监控行为。
好了,本次分享就到这里,欢迎大家继续阅读《设计模式》专栏其他设计模式内容,如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!