访问者模式(Visitor Pattern)核心解析
1. 模式定义
访问者模式允许一个或者多个操作应用到一组对象上,设计意图是解耦操作和对象本身,保持类职责单一、满足开闭原则以及应对代码的复杂性。
对于访问者模式,学习的主要难点在代码实现。而代码实现比较复杂的主要原因是,函数重载在大部分面向对象编程语言中是静态绑定的。也就是说,调用类的哪个重载函数,是在编译期间,由参数的声明类型决定的,而非运行时,根据参数的实际类型决定的。
正是因为代码实现难理解,所以,在项目中应用这种模式,会导致代码的可读性比较差。如果你的同事不了解这种设计模式,可能就会读不懂、维护不了你写的代码。所以,除非不得已,不要使用这种模式。
2. 核心思想
双重分发(Double Dispatch)
通过两次方法调用动态绑定具体操作:第一次分发:对象通过
Accept
方法将自身类型信息传递给访问者。第二次分发:访问者根据对象类型调用对应的
VisitXxx
方法。
逆向控制
对象结构(如商品类)仅需实现Accept
方法,而将具体操作逻辑委托给外部访问者。
3. 核心组件
4. 工作流程
sequenceDiagram
participant Client
participant ConcreteElementA
participant ConcreteVisitor
Client->>ConcreteElementA: Accept(Visitor)
ConcreteElementA->>ConcreteVisitor: VisitConcreteElementA(this)
ConcreteVisitor-->>ConcreteElementA: 执行操作
ConcreteVisitor-->>Client: 返回结果
5. 核心优势
扩展性强:新增操作只需添加访问者类,无需修改元素类。
职责分离:将易变的操作逻辑从稳定的数据结构中剥离。
集中管理:相关操作集中在同一访问者中,避免代码分散。
6. 适用场景
一般来说,访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中。
需要对复杂对象结构(如抽象语法树、UI组件树)执行多种独立操作。
对象结构稳定,但需频繁新增或修改操作逻辑。
需要运行时动态切换算法(如不同折扣策略、数据导出格式)。
7. 潜在缺点
破坏封装:访问者可能需要访问元素内部状态。
增加复杂度:元素类型变化会导致所有访问者修改。
性能开销:双重分发机制可能引入额外调用开销。
8. 与其他模式的关系
组合模式:访问者常用来遍历组合结构。
策略模式:均封装算法,但访问者处理对象结构,策略处理单一对象。
装饰器模式:访问者从外部扩展功能,装饰器通过包装对象扩展。
9. 设计哲学
好莱坞原则:“不要调用我们,我们会调用你”——对象结构通过
Accept
反向控制操作流程。开闭原则:对扩展开放(新增访问者),对修改封闭(不修改元素类)。
10. 经典应用
编译器:语法树检查、代码生成、优化等不同阶段。
文档处理:格式转换、字数统计、语法检查等操作。
游戏引擎:渲染、碰撞检测、AI计算等子系统。
访问者模式通过双重分发机制和逆向控制,实现了数据结构与操作逻辑的优雅解耦,是处理复杂对象结构扩展需求的利器。
代码示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DesignPatternsPractice.Src.Behavioral.VisitorDesignPattern
{
/// <summary>
/// 一个商品打折逻辑的案例
/// </summary>
public class VisitorDesign2
{
static void Main()
{
List<IProduct> products = new List<IProduct>()
{
new Book{ Price =100},
new Electronics{ Price = 2000},
new Food{ Price = 50}
};
//节假日 折扣
var HolidayDiscountVisitor = new HolidayDiscountVisitor();
foreach (IProduct item in products)
{
// 这里是反向调用 把商品类的具体类型传给 折扣逻辑然后折扣逻辑在执行
item.Accept(HolidayDiscountVisitor);
}
// 应用 清仓活动
var clearanceVisitor = new ClearanceVisitor();
foreach (var product in products)
{
product.Accept(clearanceVisitor);
}
}
}
public interface IProduct
{
/// <summary>
/// 价格
/// </summary>
double Price { get; }
void Accept(IVisitor visitor);
}
/// <summary>
/// 具体商品类 书
/// </summary>
public class Book : IProduct
{
public double Price { set; get; }
public void Accept(IVisitor visitor)
{
visitor.VisitBook(this);
}
}
public class Electronics : IProduct
{
public double Price { set; get; }
public void Accept(IVisitor visitor)
{
visitor.VisitElectronics(this);
}
}
public class Food : IProduct
{
public double Price { set; get; }
public void Accept(IVisitor visitor)
{
visitor.VisitFood(this);
}
}
/// <summary>
/// 访问者接口
/// </summary>
public interface IVisitor
{
void VisitBook(Book book);
void VisitElectronics(Electronics electronics);
void VisitFood(Food food);
}
/// <summary>
/// 具体访问者
///
/// 节假日折扣访问者
/// </summary>
//
public class HolidayDiscountVisitor : IVisitor
{
public void VisitBook(Book book)
{
book.Price *= 0.8; // 图书8折
Console.WriteLine($"节假日图书折扣价: {book.Price}");
}
public void VisitElectronics(Electronics electronics)
{
electronics.Price *= 0.9; // 电子产品9折
Console.WriteLine($"节假日电子产品折扣价: {electronics.Price}");
}
public void VisitFood(Food food)
{
Console.WriteLine($"食品不参与节假日折扣: {food.Price}");
}
}
// 清仓活动访问者
public class ClearanceVisitor : IVisitor
{
public void VisitBook(Book book)
{
book.Price *= 0.5; // 图书5折
Console.WriteLine($"清仓图书折扣价: {book.Price}");
}
public void VisitElectronics(Electronics electronics)
{
electronics.Price *= 0.6; // 电子产品6折
Console.WriteLine($"清仓电子产品折扣价: {electronics.Price}");
}
public void VisitFood(Food food)
{
food.Price *= 0.8; // 食品8折
Console.WriteLine($"清仓食品折扣价: {food.Price}");
}
}
}