1. 引子
- 搬砖N年,好不容易凑齐几十万,开了个卤肉店(原谅作者是个吃货😂)
- 为了吸引顾客,推出了会员制:非会员原价,白银会员8.8折,铂金会员8折
- 店里祖传秘方,就做卤土鸭、卤鸡爪、卤猪头肉
- 搬砖出生,不自己写个收银程序都对不起自己
2. 原始版的收银程序
2.1 会员等级
-
根据需求,定义枚举类
MembershipLevel
来记录会员等级public enum MembershipLevel { NORMAL, SILVER, PLATINUM }
2.2 店里的各种卤肉
-
店里有三种卤肉,每种卤肉两个属性:名称和单价,两个操作:获取名称、根据会员等级返回折扣后的单价
-
抽象出Meat接口:
public interface Meat { double getPrice(MembershipLevel level); String getName(); }
-
实现三种卤肉类:卤土鸭、卤鸡爪、卤猪头
public class CookedDark implements Meat { private final double price; public CookedDark(double price) { this.price = price; } @Override public double getPrice(MembershipLevel level) { if (MembershipLevel.NORMAL == level) { return price; } else if (MembershipLevel.SILVER == level) { return price * 0.88; } else { return price * 0.8; } } @Override public String getName() { return "卤土鸭"; } } public class CookedChickenFeet implements Meat { private final double price; public CookedChickenFeet(double price) { this.price = price; } @Override public double getPrice(MembershipLevel level) { if (MembershipLevel.NORMAL == level) { return price; } else if (MembershipLevel.SILVER == level) { return price * 0.88; } else { return price * 0.8; } } @Override public String getName() { return "卤凤爪"; } } public class CookedPigHead implements Meat { private final double price; public CookedPigHead(double price) { this.price = price; } @Override public double getPrice(MembershipLevel level) { if (MembershipLevel.NORMAL == level) { return price; } else if (MembershipLevel.SILVER == level) { return price * 0.88; } else { return price * 0.8; } } @Override public String getName() { return "卤猪头"; } }
2.2 卤肉店
-
卤肉店中有各种各样的卤肉,根据顾客的购买情况和会员等级计算价格
public class CookedFoodShop { private Map<String, Meat> meatMap; public CookedFoodShop() { meatMap = new HashMap<>(); } public CookedFoodShop addMeat(Meat meat) { meatMap.put(meat.getName(), meat); return this; } public void calculatePrice(Map<String, Double> weight, MembershipLevel level) { double total = 0; System.out.println("Membership level: " + level); System.out.println("Meat Price Weight Cost"); for (Map.Entry<String, Double> item : weight.entrySet()) { Meat meat = meatMap.get(item.getKey()); double cost = item.getValue() * meat.getPrice(level); total += cost; System.out.printf("%s %.2f %.2f %.2f\n", meat.getName(), meat.getPrice(level), item.getValue(), cost); } System.out.printf("Total cost: %.2f\n", total, total * 0.88); } }
2.3 卤肉店开始营业
-
创建自己的卤肉店,将不同卤肉的价格定好后,就可以开门迎客,使用自己写的程序计算价格
public class CalculatePrice { public static void main(String[] args) { // 创建自己的卤肉店 CookedFoodShop shop = new CookedFoodShop() .addMeat(new CookedDark(38)) .addMeat(new CookedChickenFeet(42)) .addMeat(new CookedPigHead(35)); // 输入各种卤肉的重量 Map<String, Double> weight = new HashMap<>(); weight.put("卤凤爪", 2.4); weight.put("卤猪头", 1.5); // 计算价格 shop.calculatePrice(weight, MembershipLevel.SILVER); } }
-
最终,一名白银会员消费情况如下:
3. 会员等级丰富起来了
3.1 尴尬局面
- 因为祖传手艺,吸引了很多的顾客,这时会员等级越来越丰富了
- 公司食堂那种大客户,直接75折;加盟商,直接7折
- 这下又得哼哧哼哧改代码了
- 首先,需要需要丰富
MembershipLevel
中的会员等级 - 其次,修改各卤肉,丰富
getPrice()
方法中对各种会员等级的支持:要么丰富if - else
分支,要么使用switch
语句
- 首先,需要需要丰富
- 这样的设计显然不合理:新会员等级的加入,需要修改所有卤肉的
getPrice()
方法,违反了著名的开闭原则(对扩展开放,对修改关闭) 。
3.2 引入visitor模式
-
认真观察,我们不难发现:整个需求中,卤肉的种类不变,变化的是会员等级,以及会员价格计算逻辑
getPrice()
方法 -
如果能将会员价格计算逻辑与卤肉分离开,则会员等级发生变化时,已有的卤肉可以不用修改
-
首先,创建一个
Visitor
接口,里面包含针对不同的卤肉类型,获取卤肉原始价格并计算会员价格的visit()
方法public interface Visitor { double visit(CookedDark dark); double visit(CookedChickenFeet feet); double visit(CookedPigHead pigHead); // 获取会员等级,帮助打印小票 MembershipLevel getLevel(); }
-
接着,更新Meat接口中的
getPrice()
方法。同时,移除各卤肉类中会员价格的计算逻辑,直接返回原价// 将Meat接口中的getPrice()方法修改如下: double getPrice(); // 在各卤肉类中,实现getPrice()方法,直接返回原始价格 @Override public double getPrice() { return this.price; }
-
然后,基于Visitor接口,实现不同会员等级对应的Visitor类
public class NormalVisitor implements Visitor { private static final MembershipLevel level = MembershipLevel.NORMAL; @Override public double visit(CookedDark dark) { return dark.getPrice(); } @Override public double visit(CookedChickenFeet feet) { return feet.getPrice(); } @Override public double visit(CookedPigHead pigHead) { return pigHead.getPrice(); } @Override public MembershipLevel getLevel() { return level; } } public class SilverVisitor implements Visitor { private static final MembershipLevel level = MembershipLevel.SILVER; @Override public double visit(CookedDark dark) { return dark.getPrice() * 0.88; } @Override public double visit(CookedChickenFeet feet) { return feet.getPrice() * 0.88; } @Override public double visit(CookedPigHead pigHead) { return pigHead.getPrice() * 0.88; } @Override public MembershipLevel getLevel() { return level; } } public class PlatinumVisitor implements Visitor { public static final MembershipLevel level = MembershipLevel.PLATINUM; @Override public double visit(CookedDark dark) { return dark.getPrice() * 0.8; } @Override public double visit(CookedChickenFeet feet) { return feet.getPrice() * 0.8; } @Override public double visit(CookedPigHead pigHead) { return pigHead.getPrice() * 0.8; } @Override public MembershipLevel getLevel() { return level; } }
-
现在,整个visitor模式的关键点来了:如何将卤肉类和Visitor类关联起来?
- 为Meat接口增加一个
double accept(Visitor visitor)
方法 - 卤肉类实现
accept()
方法时,将自身传递给visitor的visit()方法public class CookedDark implements Meat { ... // 其他代码省略 @Override public double accept(Visitor visitor) { return visitor.visit(this); } } public class CookedChickenFeet implements Meat { ... // 其他代码省略 @Override public double accept(Visitor visitor) { return visitor.visit(this); } } public class CookedPigHead implements Meat { ... // 其他代码省略 @Override public double accept(Visitor visitor) { return visitor.visit(this); } }
- 此时,以卤鸭 + 白银会员为例,形成了这样的一个关系链:
- 为Meat接口增加一个
-
修改
CookedFoodShop.calculatePrice()
方法public void calculatePrice(Map<String, Double> weight, Visitor visitor) { double total = 0; System.out.println("Membership level: " + visitor.getLevel()); System.out.println("Meat Price Weight Cost"); for (Map.Entry<String, Double> item : weight.entrySet()) { // meatMap记录了卤肉名与卤肉对象的映射,如:卤鸡爪 --> cookedChickenFeet Meat meat = meatMap.get(item.getKey()); double cost = item.getValue() * meat.accept(visitor); total += cost; System.out.printf("%s %.2f %.2f %.2f\n", meat.getName(), meat.accept(visitor), item.getValue(), cost); } System.out.printf("Total cost: %.2f\n", total, total * 0.88); }
-
计算铂金会员所需支付的费用
public class CalculatePrice { public static void main(String[] args) { // 创建自己的卤肉店 CookedFoodShop shop = new CookedFoodShop() .addMeat(new CookedDark(38)) .addMeat(new CookedChickenFeet(42)) .addMeat(new CookedPigHead(35)); // 输入各种卤肉的重量 Map<String, Double> weight = new HashMap<>(); weight.put("卤凤爪", 2.4); weight.put("卤猪头", 1.5); // 计算价格 shop.calculatePrice(weight, new PlatinumVisitor()); } }
-
小票内容如下:
4. visitor模式
4.1 概念
- 在代码开发时,我们经常遇到这样的场景:
- 一个复合对象,包含一组相似且固定的Element
- 如果需要为这组Element引入新的操作,则需要修改所有的Element(更新/新建方法)以及复合对象(引用Element的新方法)
- 这的代码设计,明显违背了开闭原则
- 这时,可以考虑使用访问者(visitor)模式,达到定义一个新的操作而不修改现有对象结构的目的
- visitor模式属于行为设计模式,有两个重要方法:
- Element中的
accept(visitor)
方法:可以接收不同类型的访问者(visitor),并在方法中执行visitor.visit(this)
- Visitor中的
visit(concreteElement)
方法:访问具体的Element,实现对Element的操作逻辑;当需要为Element引入的新操作时,只需要新增具体的Visitor类 - 在
accept()
方法中执行visitor.visit(this)
,巧妙地将Visitor与Element关联起来,实现了double dispatch
- Element中的
- 从上面的描述不难发现:visitor模式满足开闭原则,实现了操作逻辑与被操作对象的分离
- 自己对
double dispatch
:- dispatch 1:通过element.accept(visitor),将visitor dispatch给element
- dispatch 2:通visitor.visit(this),将element dispatch给visitor。至此,visitor便可以根据对element执行特定操作
- 相比直接调用element.operation(),这样的调用逻辑可以称为
double dispatch
4.2 UML图
-
visitor模式的UML图如下:
-
Client:client是visitor模式中类的消费者,它负责将visitor对象注入到复合对象(Object Structure)中
-
Object Structure: 遍历自身包含的Element对象,通过调用Element对象的
accept()
方法,将visitor对象作用于每个Element- 自己实现的卤肉店收银程序代码中,并未明显体现对Element对象的遍历
- 关于visitor模式的示例代码中,这点非常明显
public void accept(Visitor v) { for (Element e : this.elements) { e.accept(v); } }
-
Visitor:接口或抽象类,为复合对象中的每个Element定义了对应的
visit(element)
方法 -
ConcreteVisitor : 对于不同类型的visitor,必须实现Visitor中的
visit()
方法,在visit()
方法中定义对Element对象的操作逻辑 -
Element:接口或抽象类,内含一个接受visitor的
accept(visitor)
方法,是visitor访问Element对象的入口 -
ConcreteElement:具体的Element,必须实现Element中的
accept()
方法,通过this关键字(visitor.visit(this)
)将自身传递给visitor的visit()方法,从而实现double dispatch
4.3 优缺点
4.3.1 优点
- 将操作逻辑与被操作对象(Element)分离,使得新增操作无需修改已有的Element,符合开闭原则
- 对Element引入新操作时,只需要新增对应的Visitor类,具有很好的扩展性
4.3.2 缺点
- 当新增Element时,代码维护变得困难,还违背了开闭原则
- 修改已有的Visitor类,以添加处理新Element的visit()方法
- 如果存在很多Visitor类,这样将是一项十分繁重的工作
- Visitor直接访问Element,使得Element的相关业务逻辑暴露给了所有的visitor,违背了迪米特原则
- 为了实现对不同Element的区别对待,直接访问具体类,而非接口或抽象类,违反了依赖倒置原则
5. 后记
- visitor模式一个有名的应用场景就是antlr,后续将介绍如何使用visitor模式实现一个简单的整数计算器
- 参考链接: