【行为型模式十一】访问者模式
一、对不同的客户设置不同的价钱
现实生活中,不同类型的客户购买同一件商品时,价钱可能会不同。例如企业客户购买时通常会购买的比较多,买的越多则看到显示的单价也越便宜。而个人客户看到显示的单价则贵一些。且不仅购买的时候价格不同,修理、退货等各种场景时的价格可能都不同。像这种将数据操作和数据结构分离的需求,可使用访问者模式实现。
二、访问者模式
访问者模式是23中设计模式中最复杂的设计模式,并且使用频率不高,《设计模式》的作者评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。
访问者(Visitor)模式讲的是一个作用于某对象中各个元素的操作,在不改变元素的类前提下,定义作用于元素的新操作。是一种行为型模式。
适用于被访问的元素抽象类的子类数量稳定,不会增加或减少子类。但对这些子类的操作可能会经常需要增加或修改,这时候就需要使用访问者模式了。
2.1 访问者模式的角色
使用访问者模式实现对不同的客户设置不同价钱的类图:
- 抽象访问者(Visitor):定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。例如Action类,每一个访问者也可以看做是一种操作。
- 具体访问者(ConcreteVisitor):实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。例如RepairAction和BuyAction类。
- 抽象元素(Element):声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。例如Customer类,Customer的子类相对是稳定的,一般就分为企业客户和个人客户类。
- 具体元素(ConcreteElement):实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。例如PersonalCustomer和EnterpriseCustomer类。
- 对象结构(Object Structure):是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等聚合类实现。
三、访问者模式实现对不同客户设置不同价格
3.1 抽象元素类
public abstract class Customer {
protected String name;
protected double price;
public Customer(String name,double price) {
this.name = name;
this.price = price;
}
public abstract void accept(Action action);
}
3.2 具体元素类
- 个人客户类
public class PersonalCustomer extends Customer {
public PersonalCustomer(String name, double price) {
super(name, price);
}
@Override
public void accept(Action action) {
action.priceForPersonal(this);
}
public double getUnitPrice(int count) {
if (count < 2) {
return price;
}else {
return 0.98 * price;
}
}
public double getTotalPrice(int count) {
return getUnitPrice(count) * count;
}
public double unitRefund() {
return price * 0.9;
}
}
- 企业客户类
public class EnterpriseCustomer extends Customer {
public EnterpriseCustomer(String name, double price) {
super(name, price);
}
@Override
public void accept(Action action) {
action.priceForEnterprise(this);
}
public double getUnitPrice(int count) {
if (count < 100) {
return price;
} else if (count < 200) {
return 0.85 * price;
} else {
return 0.8 * price;
}
}
public double getTotalPrice(int count) {
return getUnitPrice(count) * count;
}
public double unitRepairPrice() {
return price * 0.1;
}
}
3.4 抽象访问者类
public abstract class Action {
public abstract void priceForEnterprise(EnterpriseCustomer enterpriseCustomer);
public abstract void priceForPersonal(PersonalCustomer personalCustomer);
}
3.5 具体访问者类
- 购买操作类
public class BuyAction extends Action {
private int count;
public BuyAction(int count) {
this.count = count;
}
@Override
public void priceForEnterprise(EnterpriseCustomer enterpriseCustomer) {
System.out.println(enterpriseCustomer.name + "公司购买了" + count + "个计算器");
System.out.println("单价为:"+ enterpriseCustomer.getUnitPrice(count) + "元");
System.out.println("总价为:"+ enterpriseCustomer.getTotalPrice(count) + "元");
}
@Override
public void priceForPersonal(PersonalCustomer personalCustomer) {
System.out.println(personalCustomer.name + "顾客购买了" + count + "个计算器");
System.out.println("单价为:"+ personalCustomer.getUnitPrice(count) + "元");
System.out.println("总价为:"+ personalCustomer.getTotalPrice(count) + "元");
}
}
- 修理操作类
public class RepairAction extends Action {
@Override
public void priceForEnterprise(EnterpriseCustomer enterpriseCustomer) {
System.out.println("为" + enterpriseCustomer.name + "公司修理每个计算器的价格为:" + enterpriseCustomer.unitRepairPrice() + "元");
}
@Override
public void priceForPersonal(PersonalCustomer personalCustomer) {
System.out.println(personalCustomer.name + "顾客送回坏计算器,每个退款为:"+personalCustomer.unitRefund() + "元");
}
}
3.6 对象结构类
public class ReportUtil {
private static Map<Customer,List<Action>> actionMap = new HashMap<>();
public static void addAction(Customer customer,Action action) {
List<Action> actionList;
if (actionMap.get(customer) == null) {
actionList = new ArrayList<>();
actionMap.put(customer,actionList);
}
actionList = actionMap.get(customer);
actionList.add(action);
}
public static void removeAction(Customer customer,Action action) {
if (actionMap.get(customer) == null) {
return;
}
List<Action> actionList = actionMap.get(customer);
actionList.remove(action);
if (actionList.size() == 0) {
actionMap.remove(customer);
}
}
public static void report(Customer customer) {
if (actionMap.get(customer) == null) {
return;
}
List<Action> actionList = actionMap.get(customer);
for (Action action:actionList) {
customer.accept(action);
}
}
}
3.7 客户端类
public class Client {
/**
* 使用访问者模式为企业客户和个人客户添加各种不同的行为
*/
public static void addActionForCustomer() {
EnterpriseCustomer enterpriseCustomer = new EnterpriseCustomer("阿里",10);
ReportUtil.addAction(enterpriseCustomer,new BuyAction(1000));
ReportUtil.addAction(enterpriseCustomer,new RepairAction());
ReportUtil.report(enterpriseCustomer);
PersonalCustomer personalCustomer = new PersonalCustomer("pxy",10);
ReportUtil.addAction(personalCustomer,new BuyAction(2));
ReportUtil.addAction(personalCustomer,new RepairAction());
ReportUtil.report(personalCustomer);
}
public static void main(String[] args) {
addActionForCustomer();
}
}
- 运行结果
阿里公司购买了1000个计算器
单价为:8.0元
总价为:8000.0元
为阿里公司修理每个计算器的价格为:1.0元
pxy顾客购买了2个计算器
单价为:9.8元
总价为:19.6元
pxy顾客送回坏计算器,每个退款为:9.0元
四、访问者模式总结
4.1 优点
- 各角色职责分离,符合单一职责原则。
- 具有优秀的扩展性
如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。
使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
员工属性(数据结构)和CEO、CTO访问者(数据操作)的解耦。
4.2 缺点
-
具体元素对访问者公布细节,违反了迪米特原则
-
具体元素变更时导致修改成本大
变更客户属性时,多个访问者都要修改。 -
违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象
访问者 Action中的方法,而依赖了具体客户的具体方法。
五、访问者模式的使用场景
-
被访问的对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
-
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。
我们要根据具体情况来评估是否适合使用访问者模式,例如,我们的对象结构是否足够稳定,是否需要经常定义新的操作,使用访问者模式是否能优化我们的代码,而不是使我们的代码变得更复杂。