23种设计模式之访问者模式
参考资料
- Java设计模式:23种设计模式全面解析(超级详细)
- 韩顺平老师的Java设计模式(图解+框架源码剖析)
- 秦小波老师的《设计模式之禅》
下文如有错漏之处,敬请指正
一、简介
定义
在不改变集合元素的前提下,为集合中的每个元素提供多种访问操作,即可以扩展操作元素的功能。
特点
- 访问者模式是一种行为型模式
- 访问者模式将数据结构与数据操作分离,解决数据结构和操作耦合性的问题。
通用类图
访问者模式的主要角色:
-
Visitor
抽象访问者角色
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。
-
ConcreteVisitor
具体访问者角色
实现抽象访问者角色中声明的所有访问操作,确定访问者访问一个元素时该做什么,即对元素执行什么操作
-
Element
抽象元素角色
接口或者抽象类,声明一个包含 accept() 的接口,accept()方法的参数由具体访问者角色进行确定。
-
ConcreteElement
具体元素角色
实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
-
ObjectStructure
对象结构角色是一个包含元素角色的容器,通常由 List、Set、Map 等类实现。
优点
-
扩展性好,能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
-
复用性好,可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
-
灵活性好,访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
-
符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
缺点
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
应用场景
- 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖于其具体类的操作,也就说是用迭代器模式已经不能胜任的情景。
- 需要对一个对象结构中的对象进行很多不同且不相关的操作,而你想避免让这些操作“污染”这些对象的类。
- 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作,即针对访问的对象不同,执行不同的操作。
二、访问者模式
需求:
超市现在有三种产品,按斤称的水果,按瓶卖的饮料,按袋数卖的薯片,现在超市要搞促销活动(分为春季和冬季促销活动),三种产品都有各自的折扣程度,使用访问者模式解决超市打折活动。
抽象访问者:
package visitor;
import java.math.BigDecimal;
import java.math.RoundingMode;
public interface Visitor {
void visit(Fruit fruit);
void visit(Drinks drinks);
void visit(Crisps crisps);
// 统计总价
void getTotalPay();
// 将数值进行四舍五入
default float halfAdjust(float price) {
BigDecimal bigDecimal = new BigDecimal(price);
return bigDecimal.setScale(2, RoundingMode.HALF_UP).floatValue();
}
}
具体访问者:
package visitor;
public class DiscountBySpringVisitor implements Visitor {
// 统计所有商品打折前的总价
private float preDiscountTotalPay = 0.0f;
// 统计所有商品打折后的总价
private float afterDiscountTotalPay = 0.0f;
@Override
public void visit(Fruit fruit) {
// 水果按每斤30%折扣
float discount = fruit.getWeight() * fruit.getPrice() * 0.7f;
// 四舍五入
discount = halfAdjust(discount);
System.out.println(fruit.getName() + "春季打折前的价格是:" + fruit.getWeight() * fruit.getPrice() + "----->" + "春季打折后的价格是:" + discount);
// 统计总价
preDiscountTotalPay += halfAdjust(fruit.getWeight() * fruit.getPrice());
afterDiscountTotalPay += discount;
}
@Override
public void visit(Drinks drinks) {
// 饮料每瓶降价0.5元
float discount = drinks.getPrice() - 0.5f;
// 四舍五入
discount = halfAdjust(discount);
System.out.println(drinks.getName() + "春季打折前的价格是:" + drinks.getPrice() + "----->" + "春季打折后的价格是:" + discount);
// 统计总价
preDiscountTotalPay += halfAdjust(drinks.getPrice());
afterDiscountTotalPay += discount;
}
@Override
public void visit(Crisps crisps) {
// 薯片每包打对折
float discount = crisps.getPrice() * 0.5f;
// 四舍五入
discount = halfAdjust(discount);
System.out.println(crisps.getName() + "春季打折前的价格是:" + crisps.getPrice() + "----->" + "春季打折后的价格是:" + discount);
// 统计总价
preDiscountTotalPay += halfAdjust(crisps.getPrice());
afterDiscountTotalPay += discount;
}
@Override
public void getTotalPay() {
System.out.println("所有商品打折前的总价是:" + halfAdjust(preDiscountTotalPay) + "----->" + "所有商品打折后的总价是:" + halfAdjust(afterDiscountTotalPay));
}
}
package visitor;
public class DiscountByWinterVisitor implements Visitor {
// 统计所有商品打折前的总价
private float preDiscountTotalPay = 0.0f;
// 统计所有商品打折后的总价
private float afterDiscountTotalPay = 0.0f;
@Override
public void visit(Fruit fruit) {
// 水果按每斤10%折扣
float discount = fruit.getWeight() * fruit.getPrice() * 0.9f;
// 四舍五入
discount = halfAdjust(discount);
System.out.println(fruit.getName() + "冬季打折前的价格是:" + fruit.getWeight() * fruit.getPrice() + "----->" + "冬季打折后的价格是:" + discount);
// 统计总价
preDiscountTotalPay += halfAdjust(fruit.getWeight() * fruit.getPrice());
afterDiscountTotalPay += discount;
}
@Override
public void visit(Drinks drinks) {
// 饮料每瓶降价1.0元
float discount = drinks.getPrice() - 1.0f;
// 四舍五入
discount = halfAdjust(discount);
System.out.println(drinks.getName() + "冬季打折前的价格是:" + drinks.getPrice() + "----->" + "冬季打折后的价格是:" + discount);
// 统计总价
preDiscountTotalPay += halfAdjust(drinks.getPrice());
afterDiscountTotalPay += discount;
}
@Override
public void visit(Crisps crisps) {
// 薯片每包打8折
float discount = crisps.getPrice() * 0.8f;
// 四舍五入
discount = halfAdjust(discount);
System.out.println(crisps.getName() + "冬季打折前的价格是:" + crisps.getPrice() + "----->" + "冬季打折后的价格是:" + discount);
// 统计总价
preDiscountTotalPay += halfAdjust(crisps.getPrice());
afterDiscountTotalPay += discount;
}
@Override
public void getTotalPay() {
System.out.println("所有商品打折前的总价是:" + halfAdjust(preDiscountTotalPay) + "----->" + "所有商品打折后的总价是:" + halfAdjust(afterDiscountTotalPay));
}
}
抽象元素:
package visitor;
public interface Element {
public void accept(Visitor visitor);
}
具体元素:
package visitor;
public abstract class Product {
private String name; //商品名字
private float price; //商品价格
public Product(String name, float price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
}
package visitor;
public class Crisps extends Product implements Element {
public Crisps(String name, float price) {
super(name, price);
}
// 该方法为双派发,执行接收参数的方法,并将当前对象作为该方法的参数(即动态绑定)
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
package visitor;
public class Drinks extends Product implements Element {
public Drinks(String name, float price) {
super(name, price);
}
// 该方法为双派发,执行接收参数的方法,并将当前对象作为该方法的参数(即动态绑定)
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
package visitor;
public class Fruit extends Product implements Element {
private float weight;
public Fruit(String name, float price, float weight) {
super(name, price);
this.weight = weight;
}
public float getWeight() {
return weight;
}
public void setWeight(float weight) {
this.weight = weight;
}
// 该方法为双派发,执行接收参数的方法,并将当前对象作为该方法的参数(即动态绑定)
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
对象结构:
package visitor;
import java.util.ArrayList;
import java.util.List;
public class ObjectStructure {
// 定义购物车,购物车里面保存产品,即元素
private List<Element> carts = new ArrayList<>();
// 添加产品,即添加元素
public void addProduct(Element element) {
carts.add(element);
}
// 打印购物结算单
public void checkBill(Visitor visitor) {
for (Element product : carts) {
// 使用打折访问者进行计算
product.accept(visitor);
}
// 显示总价
visitor.getTotalPay();
}
}
Client:
package visitor;
public class Client {
public static void main(String[] args) {
// 模拟用户买了1瓶可乐,1斤苹果,1斤芒果,1包乐事薯片
Drinks cola=new Drinks("百事可乐",2.8f);
Fruit apple=new Fruit("苹果",10.2f,1);
Fruit mango=new Fruit("芒果",15.2f,2);
Crisps lays=new Crisps("乐事薯片",8.9f);
ObjectStructure carts=new ObjectStructure();
carts.addProduct(cola);
carts.addProduct(apple);
carts.addProduct(mango);
carts.addProduct(lays);
// 春季购物
System.out.println("==========春季购物==========");
carts.checkBill(new DiscountBySpringVisitor());
// 冬季购物
System.out.println("==========冬季购物==========");
carts.checkBill(new DiscountByWinterVisitor());
/**
* 输出结果:
*==========春季购物==========
* 百事可乐春季打折前的价格是:2.8----->春季打折后的价格是:2.3
* 苹果春季打折前的价格是:10.2----->春季打折后的价格是:7.14
* 芒果春季打折前的价格是:30.4----->春季打折后的价格是:21.28
* 乐事薯片春季打折前的价格是:8.9----->春季打折后的价格是:4.45
* 所有商品打折前的总价是:52.3----->所有商品打折后的总价是:35.17
* ==========冬季购物==========
* 百事可乐冬季打折前的价格是:2.8----->冬季打折后的价格是:1.8
* 苹果冬季打折前的价格是:10.2----->冬季打折后的价格是:9.18
* 芒果冬季打折前的价格是:30.4----->冬季打折后的价格是:27.36
* 乐事薯片冬季打折前的价格是:8.9----->冬季打折后的价格是:7.12
* 所有商品打折前的总价是:52.3----->所有商品打折后的总价是:45.46
*/
}
}
-
此处的具体访问者的功能存在算法冗余,可以结合模板方法模式,封装步骤相同的部分,将特定(不同)步骤的部分交由子类完成,以此达到代码复用。
-
当有新的打折活动后,只需要扩展一个具体访问者类,然后在对象结构中增加相应的操作方法即可,对原有的代码没有影响。
-
如果要新增一个元素,如家电产品,会比较麻烦,要在抽象访问者中增加一个visit重载方法,每个具体访问者都要重写该visit()方法,不符合开闭原则。
三、总结
访问者模式是一种集中管理模式,特别适用于大规模重构的项目,通过访问者模式可以很容易把功能集中化管理,如一个统一的报表运算、UI展现等。