访问者模式将数据结构与操作分离,允许在不改动已有类的情况下增添新操作,在电商平台案例中,商品类(如手机、电脑)可通过接受访问者对象来实现多种操作(如打折、加入购物车),避免了类臃肿,降低了耦合度,这种模式提升了代码的可扩展性与维护性,使添加新操作更为灵活。
定义
访问者模式是一种行为设计模式,它允许你在不修改已有类的情况下增加新的操作,访问者模式通过将数据结构与数据操作分离,来达到在不改变各类的前提下增加新的操作。
在访问者模式中,我们通常有两个层次的结构:元素(Element)结构和访问者(Visitor)结构,元素结构包括各个被访问的具体元素类,它们通常拥有一个接受访问者的方法,访问者结构则包括各个访问者类,它们实现了访问元素所需要的方法。
举一个现实业务中的例子:假设有一个电商平台,平台上有多种商品,如手机、电脑、衣服等,现在需要对这些商品进行多种操作,比如打折、加入购物车、评价等,如果我们直接在每个商品类中添加这些方法,会导致类变得臃肿且不易维护,这时,可以使用访问者模式来解决这个问题,首先定义一个商品接口,包含接受访问者的方法,然后为每个具体商品(手机、电脑、衣服)创建类,实现商品接口,接着定义一个访问者接口,包含对商品的各种操作(打折、加入购物车、评价),最后为每个具体操作创建访问者类,实现访问者接口。
当用户需要对某个商品进行操作时(比如打折),我们只需创建一个对应的访问者对象(打折访问者),然后调用商品的接受访问者方法,将访问者对象传递给商品,商品在接受到访问者对象后,会调用访问者对象的相应方法(打折方法)来完成操作。
这样,我们就实现了在不修改商品类的情况下,为商品增加了新的操作,同时访问者模式还使商品类与操作类之间的耦合度降低,提高了代码的可扩展性和可维护性。
代码案例
反例
下面是一个反例代码,模拟一个简单的电商系统中的商品类以及对其进行操作的方式,首先有一个Product
基类和一些具体的商品类(如Phone
、Computer
、Clothes
),每个商品类中都直接包含了可能对其进行的操作(如discount
打折、addToCart
加入购物车),如下代码:
// 商品基类
class Product {
String name;
double price;
public Product(String name, double price) {
= name;
this.price = price;
}
// 显示商品信息
public void display() {
System.out.println(name + ": " + price);
}
// 默认打折方法(假设所有商品打折方式一样,实际上可能不同)
public void discount() {
this.price *= 0.9; // 打9折
System.out.println(name + " discounted. New price: " + price);
}
// 默认加入购物车方法
public void addToCart() {
System.out.println(name + " added to cart.");
}
}
// 具体的商品类 - 手机
class Phone extends Product {
public Phone(String name, double price) {
super(name, price);
}
// 手机可能有特殊的打折方式,但在这个反例中我们仍然使用默认方式
}
// 具体的商品类 - 电脑
class Computer extends Product {
public Computer(String name, double price) {
super(name, price);
}
// 电脑可能有特殊的打折方式,但在这个反例中我们仍然使用默认方式
}
// 客户端代码
public class Client {
public static void main(String[] args) {
// 创建商品对象
Phone phone = new Phone("iPhone 14", 8999.0);
Computer computer = new Computer("MacBook Pro", 15999.0);
// 显示商品信息
phone.display();
computer.display();
// 对商品进行操作
phone.discount(); // 打折
phone.addToCart(); // 加入购物车
computer.discount(); // 打折
// 假设我们不想将电脑加入购物车
// computer.addToCart(); // 这行代码被注释掉了
}
}
输出结果:
iPhone 14: 8999.0
MacBook Pro: 15999.0
iPhone 14 discounted. New price: 8099.1
iPhone 14 added to cart.
MacBook Pro discounted. New price: 14399.1
在这个反例中,暴露出了几个问题:
- 每个商品类中都包含了操作方法(打折、加入购物车),这导致了数据和操作的紧密耦合。
- 如果想要为不同的商品类添加新的操作或者修改现有操作的行为,可能需要修改每个商品类的代码,这违反了开闭原则。
- 即使某些商品类不需要某些操作(比如在这个例子中,我们假设
Computer
类不应该被加入购物车),这些操作仍然存在于类中,可能会导致误用。
通过访问者模式,可以将操作逻辑从商品类中分离出来,放到单独的访问者类中,从而解决上述问题,如下正例。
正例
下面是一个使用模板方法模式的正例代码,模拟一个简单的咖啡和茶的制作过程,如下代码:
// 抽象类,表示饮品,定义模板方法
abstract class Beverage {
// 模板方法,定义制作饮品的算法骨架
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (wantsCondiments()) {
addCondiments();
}
}
// 以下是算法的步骤,由子类根据需要选择覆盖
void boilWater() {
System.out.println("Boiling water");
}
// 声明为抽象方法,因为具体制作饮品的方式依赖于子类
abstract void brew();
void pourInCup() {
System.out.println("Pouring into Cup");
}
// 钩子方法,子类可以覆盖它以改变行为
boolean wantsCondiments() {
return true;
}
// 这个方法也可能由子类覆盖
void addCondiments() {
System.out.println("Adding Condiments");
}
}
// 具体类,表示咖啡
class Coffee extends Beverage {
// 实现brew方法,提供制作咖啡的具体步骤
@Override
void brew() {
System.out.println("Dripping Coffee through filter");
}
// 覆盖钩子方法,表示咖啡不需要额外添加调料
@Override
boolean wantsCondiments() {
return false;
}
}
// 具体类,表示茶
class Tea extends Beverage {
// 实现brew方法,提供制作茶的具体步骤
@Override
void brew() {
System.out.println("Steeping the tea");
}
// 可以选择覆盖addCondiments方法来添加特定的调料
// 这里我们保持默认行为
}
// 客户端代码
public class Client {
public static void main(String[] args) {
// 创建咖啡对象
Beverage coffee = new Coffee();
System.out.println("Making coffee...");
coffee.prepareRecipe(); // 调用模板方法制作咖啡
// 创建茶对象
Beverage tea = new Tea();
System.out.println("\nMaking tea...");
tea.prepareRecipe(); // 调用模板方法制作茶
}
}
输出结果:
Making coffee...
Boiling water
Dripping Coffee through filter
Pouring into Cup
Making tea...
Boiling water
Steeping the tea
Pouring into Cup
Adding Condiments
在这个例子中,Beverage
是一个抽象类,定义了一个制作饮品的模板方法 prepareRecipe
,这个方法调用了其他几个方法,其中 brew
是一个抽象方法,需要子类具体实现,Coffee
和 Tea
是 Beverage
的具体子类,它们分别实现了 brew
方法来提供制作咖啡和茶的具体步骤,此外,Beverage
类还提供了一个钩子方法 wantsCondiments
,子类可以覆盖它来改变添加调料的行为,client代码创建了咖啡和茶的对象,并调用了它们的模板方法来制作饮品。
核心总结
访问者模式在日常开发中非常常见,常被用于将数据结构与数据操作分离情况,增加程序的灵活性和可扩展性,它能够在不修改已有类的情况下增加新的操作,符合开闭原则,同时,访问者模式可以将复杂的、贯穿多个类的行为集中到一个访问者类中,方便代码的管理和维护。
它的缺点也很明显,1、增加了类的数量,以及类之间的耦合度,当数据结构变化时,可能需要修改所有访问者类的接口,成本较高,2、如果频繁进行数据结构和操作的变化,访问者模式可能不是最佳选择。
个人思考
访问者模式和状态模式虽然都属于行为型设计模式,但它们在解决的问题和应用场景上有显著的不同。
访问者模式
想象一下有一个由多种不同类型的元素组成的集合,想对这些元素执行多种不同的操作,但又不希望修改这些元素的类,这时候可以用访问者模式。
访问者模式允许定义一个新的操作,只需添加一个实现访问者接口的类,而无需修改已有的元素类,这就像有一个客人(访问者)来到的家(对象结构),他可以根据自己的需要(操作)来与的家具(元素)互动,而不需要改变家具本身。
状态模式
想象一下有一个对象结构,它的行为会根据其内部状态的变化而变化,比如一个电灯开关,按下时灯会打开,再次按下时灯会关闭,这里,“打开”和“关闭”就是电灯的不同状态,而按下开关这个动作会导致电灯状态的转换以及相应的行为变化。
状态模式允许在不修改对象类的情况下,为其添加新的状态或修改现有状态的行为,可以定义一个状态接口和一系列实现该接口的具体状态类,对象会根据其当前状态委托给相应的状态对象来处理请求,从而实现行为的动态变化。
它们的区别:
- 关注点不同:访问者模式关注的是对复杂对象结构中的元素执行多种操作,而状态模式关注的是对象在不同状态下的行为变化。
- 结构不同:访问者模式中,访问者类和元素类是分开的,访问者类包含对元素类的操作;而在状态模式中,状态类通常被包含在上下文类中,上下文类根据当前状态委托给相应的状态类来处理请求。
- 扩展性:访问者模式在添加新的操作时比较容易,只需添加新的访问者类;而状态模式在添加新的状态时比较容易,只需添加新的状态类。但两者在添加新的元素类或状态类时都可能需要修改已有的代码。