c#设计模式—访问者模式
更实际的例子来解释它(访问者模式)的威力。
访问者模式的真正价值
当前代码的问题分析
代码展示了机制,但没有展示为什么需要这种机制。让我用一个财务报表的例子来演示:
using System;
using System.Collections.Generic;// 财务数据元素接口
public interface IFinancialElement
{void Accept(IFinancialVisitor visitor); // 接受财务访问者decimal GetAmount(); // 获取金额
}// 收入项
public class IncomeItem : IFinancialElement
{public string Description { get; set; } // 描述public decimal Amount { get; set; } // 金额public IncomeItem(string description, decimal amount){Description = description;Amount = amount;}public void Accept(IFinancialVisitor visitor){visitor.VisitIncome(this); // 接受访问者访问收入项}public decimal GetAmount() => Amount;
}// 支出项
public class ExpenseItem : IFinancialElement
{public string Description { get; set; } // 描述public decimal Amount { get; set; } // 金额public string Category { get; set; } // 类别public ExpenseItem(string description, decimal amount, string category){Description = description;Amount = amount;Category = category;}public void Accept(IFinancialVisitor visitor){visitor.VisitExpense(this); // 接受访问者访问支出项}public decimal GetAmount() => Amount;
}// 资产项
public class AssetItem : IFinancialElement
{public string Name { get; set; } // 资产名称public decimal Value { get; set; } // 价值public decimal Depreciation { get; set; } // 折旧public AssetItem(string name, decimal value, decimal depreciation){Name = name;Value = value;Depreciation = depreciation;}public void Accept(IFinancialVisitor visitor){visitor.VisitAsset(this); // 接受访问者访问资产项}public decimal GetAmount() => Value;
}// 财务访问者接口
public interface IFinancialVisitor
{void VisitIncome(IncomeItem income); // 访问收入void VisitExpense(ExpenseItem expense); // 访问支出void VisitAsset(AssetItem asset); // 访问资产
}// 具体访问者1:财务报表生成器
public class ReportGeneratorVisitor : IFinancialVisitor
{private decimal _totalIncome = 0; // 总收入private decimal _totalExpense = 0; // 总支出private decimal _totalAssets = 0; // 总资产public void VisitIncome(IncomeItem income){_totalIncome += income.Amount; // 累加收入Console.WriteLine($"收入: {income.Description} - ¥{income.Amount}"); // 输出收入详情}public void VisitExpense(ExpenseItem expense){_totalExpense += expense.Amount; // 累加支出Console.WriteLine($"支出: {expense.Description} ({expense.Category}) - ¥{expense.Amount}"); // 输出支出详情}public void VisitAsset(AssetItem asset){_totalAssets += asset.Value; // 累加资产Console.WriteLine($"资产: {asset.Name} - 价值: ¥{asset.Value}, 折旧: ¥{asset.Depreciation}"); // 输出资产详情}public void PrintReport() // 打印报表{Console.WriteLine("\n=== 财务报表 ===");Console.WriteLine($"总收入: ¥{_totalIncome}"); // 输出总收入Console.WriteLine($"总支出: ¥{_totalExpense}"); // 输出总支出Console.WriteLine($"总资产: ¥{_totalAssets}"); // 输出总资产Console.WriteLine($"净利润: ¥{(_totalIncome - _totalExpense)}"); // 输出净利润Console.WriteLine($"净资产: ¥{(_totalIncome - _totalExpense + _totalAssets)}"); // 输出净资产}
}// 具体访问者2:税务计算器
public class TaxCalculatorVisitor : IFinancialVisitor
{private decimal _taxableIncome = 0; // 应纳税收入private decimal _deductibleExpenses = 0; // 可抵扣支出private decimal _assetDepreciation = 0; // 资产折旧public void VisitIncome(IncomeItem income){_taxableIncome += income.Amount; // 所有收入都应纳税Console.WriteLine($"应税收入: {income.Description} - ¥{income.Amount}"); // 输出应税收入}public void VisitExpense(ExpenseItem expense){if (expense.Category == "业务" || expense.Category == "办公"){_deductibleExpenses += expense.Amount; // 只有业务相关支出可抵扣Console.WriteLine($"可抵扣支出: {expense.Description} - ¥{expense.Amount}"); // 输出可抵扣支出}}public void VisitAsset(AssetItem asset){_assetDepreciation += asset.Depreciation; // 资产折旧可抵扣Console.WriteLine($"资产折旧抵扣: {asset.Name} - ¥{asset.Depreciation}"); // 输出折旧抵扣}public void PrintTaxReport() // 打印税务报告{decimal netTaxableIncome = _taxableIncome - _deductibleExpenses - _assetDepreciation; // 计算净应纳税收入decimal taxAmount = netTaxableIncome * 0.25m; // 假设税率25%Console.WriteLine("\n=== 税务报告 ===");Console.WriteLine($"应纳税收入: ¥{_taxableIncome}"); // 输出应纳税收入Console.WriteLine($"可抵扣支出: ¥{_deductibleExpenses}"); // 输出可抵扣支出Console.WriteLine($"资产折旧: ¥{_assetDepreciation}"); // 输出资产折旧Console.WriteLine($"净应纳税收入: ¥{netTaxableIncome}"); // 输出净应纳税收入Console.WriteLine($"应缴税款: ¥{taxAmount}"); // 输出应缴税款}
}// 具体访问者3:数据导出器
public class DataExporterVisitor : IFinancialVisitor
{public void VisitIncome(IncomeItem income){// 导出为JSON格式Console.WriteLine($"{{\"type\": \"income\", \"description\": \"{income.Description}\", \"amount\": {income.Amount}}}"); // JSON格式输出收入}public void VisitExpense(ExpenseItem expense){// 导出为JSON格式Console.WriteLine($"{{\"type\": \"expense\", \"description\": \"{expense.Description}\", \"category\": \"{expense.Category}\", \"amount\": {expense.Amount}}}"); // JSON格式输出支出}public void VisitAsset(AssetItem asset){// 导出为JSON格式Console.WriteLine($"{{\"type\": \"asset\", \"name\": \"{asset.Name}\", \"value\": {asset.Value}, \"depreciation\": {asset.Depreciation}}}"); // JSON格式输出资产}
}class Program
{static void Main(){// 创建财务数据var financialData = new List<IFinancialElement>{new IncomeItem("产品销售", 50000),new IncomeItem("服务收入", 30000),new ExpenseItem("员工工资", 20000, "人事"),new ExpenseItem("办公室租金", 8000, "办公"),new ExpenseItem("业务招待", 3000, "业务"),new ExpenseItem("个人消费", 5000, "个人"),new AssetItem("办公设备", 15000, 3000),new AssetItem("公司车辆", 80000, 16000)};Console.WriteLine("=== 财务报表生成 ===");var reportGenerator = new ReportGeneratorVisitor();ProcessFinancialData(financialData, reportGenerator); // 处理财务数据生成报表reportGenerator.PrintReport(); // 打印报表Console.WriteLine("\n=== 税务计算 ===");var taxCalculator = new TaxCalculatorVisitor();ProcessFinancialData(financialData, taxCalculator); // 处理财务数据计算税务taxCalculator.PrintTaxReport(); // 打印税务报告Console.WriteLine("\n=== 数据导出 ===");var dataExporter = new DataExporterVisitor();ProcessFinancialData(financialData, dataExporter); // 处理财务数据导出}// 统一的处理方法static void ProcessFinancialData(List<IFinancialElement> data, IFinancialVisitor visitor){foreach (var element in data){element.Accept(visitor); // 每个元素接受访问者访问}}
}/* 输出结果:
=== 财务报表生成 ===
收入: 产品销售 - ¥50000
收入: 服务收入 - ¥30000
支出: 员工工资 (人事) - ¥20000
支出: 办公室租金 (办公) - ¥8000
支出: 业务招待 (业务) - ¥3000
支出: 个人消费 (个人) - ¥5000
资产: 办公设备 - 价值: ¥15000, 折旧: ¥3000
资产: 公司车辆 - 价值: ¥80000, 折旧: ¥16000=== 财务报表 ===
总收入: ¥80000
总支出: ¥36000
总资产: ¥95000
净利润: ¥44000
净资产: ¥139000=== 税务计算 ===
应税收入: 产品销售 - ¥50000
应税收入: 服务收入 - ¥30000
可抵扣支出: 办公室租金 - ¥8000
可抵扣支出: 业务招待 - ¥3000
资产折旧抵扣: 办公设备 - ¥3000
资产折旧抵扣: 公司车辆 - ¥16000=== 税务报告 ===
应纳税收入: ¥80000
可抵扣支出: ¥11000
资产折旧: ¥19000
净应纳税收入: ¥50000
应缴税款: ¥12500=== 数据导出 ===
{"type": "income", "description": "产品销售", "amount": 50000}
{"type": "income", "description": "服务收入", "amount": 30000}
{"type": "expense", "description": "员工工资", "category": "人事", "amount": 20000}
{"type": "expense", "description": "办公室租金", "category": "办公", "amount": 8000}
{"type": "expense", "description": "业务招待", "category": "业务", "amount": 3000}
{"type": "expense", "description": "个人消费", "category": "个人", "amount": 5000}
{"type": "asset", "name": "办公设备", "value": 15000, "depreciation": 3000}
{"type": "asset", "name": "公司车辆", "value": 80000, "depreciation": 16000}
*/
访问者模式的核心价值
1. 分离关注点
- 数据类(IncomeItem, ExpenseItem, AssetItem):只负责存储数据
- 算法类(各种Visitor):只负责处理逻辑
2. 开闭原则
- 对扩展开放:要加新功能(如数据导出),只需新增Visitor,不用修改数据类
- 对修改关闭:数据类一旦稳定,就不需要再改动
3. 实际应用场景
// 编译器中的访问者模式应用
public interface IASTNode
{void Accept(IASTVisitor visitor);
}// 语法树节点
public class VariableDeclarationNode : IASTNode
{public string Type { get; set; }public string Name { get; set; }public void Accept(IASTVisitor visitor) => visitor.VisitVariableDeclaration(this);
}public class MethodCallNode : IASTNode
{public string MethodName { get; set; }public List<IASTNode> Arguments { get; set; }public void Accept(IASTVisitor visitor) => visitor.VisitMethodCall(this);
}// 不同的编译器阶段
public interface IASTVisitor
{void VisitVariableDeclaration(VariableDeclarationNode node);void VisitMethodCall(MethodCallNode node);
}public class TypeCheckerVisitor : IASTVisitor { /* 类型检查 */ }
public class CodeGeneratorVisitor : IASTVisitor { /* 代码生成 */ }
public class OptimizerVisitor : IASTVisitor { /* 代码优化 */ }
4. 什么时候使用访问者模式?
✅ 适用场景:
- 对象结构稳定,但需要频繁添加新操作
- 需要对对象结构中的元素进行多种不相关的操作
- 希望避免"污染"对象类的接口
❌ 不适用场景:
- 对象结构经常变化
- 元素类很少增加新操作
- 元素类接口需要经常修改
总结
访问者模式的真正威力在于:让你能在不修改现有类的情况下,为它们添加新的操作。
就像你有一个工具箱(数据类),访问者模式让你可以:
- 用锤子Visitor(报表生成)来敲打
- 用螺丝刀Visitor(税务计算)来拧螺丝
- 用尺子Visitor(数据导出)来测量
所有工具都作用于同一个工具箱,但互不干扰,而且可以随时添加新工具!