模板方法模式
模板方法模式
简介
模板方法(Template Method)在基类中定义算法的骨架,将某些步骤推迟到子类实现。理解模板方法模式,有助于在不改变算法整体结构的前提下,灵活定制各个步骤。
模板方法模式是面向对象编程中最基础的设计模式之一,也是继承机制的典型应用场景。它的核心思想是:在一个方法中定义一个算法的骨架(即模板),将某些步骤的具体实现延迟到子类。这样,子类可以在不改变算法整体结构的情况下,重新定义算法中的某些特定步骤。
在 .NET 框架中,模板方法模式无处不在:ASP.NET Core 的中间件管道(虽然更接近责任链,但整体生命周期是模板化的)、Component 类的 OnInitialized/OnParametersSet 等生命周期方法、DbContext 的 SaveChanges 流程(可重写 SaveChangesAsync 的各个阶段)等。
特点
结构分析
UML 类图
+---------------------------+
| AbstractClass |
+---------------------------+
| +TemplateMethod() [sealed] |
| +Step1() [abstract] |
| +Step2() [abstract] |
| +Step3() [virtual] |
| +Hook() [virtual] |
+---------------------------+
^
|
+---------+---------+
| |
+--------+--------+ +------+--------+
| ConcreteClassA | | ConcreteClassB|
+------------------+ +---------------+
| +Step1() | | +Step1() |
| +Step2() | | +Step2() |
| +Step3() override| | +Hook() |
+------------------+ +---------------+模板方法执行流程
TemplateMethod()
|
+---> Step1() [abstract - 子类必须实现]
|
+---> Step2() [abstract - 子类必须实现]
|
+---> Hook() [virtual - 子类可选覆盖]
|
+---> Step3() [virtual - 有默认实现]
|
+---> PostProcess() [sealed - 基类固定逻辑]实现
数据处理管道模板
// 抽象模板 — 数据处理器
public abstract class DataProcessor<TInput, TOutput>
{
// 模板方法 — 定义算法骨架(sealed 防止重写)
public sealed async Task<TOutput> ProcessAsync(TInput input, CancellationToken ct)
{
// 1. 验证
Validate(input);
// 2. 前置处理
var preprocessed = PreProcess(input);
// 3. 核心处理(子类实现)
var result = await TransformAsync(preprocessed, ct);
// 4. 后置处理
var postprocessed = PostProcess(result);
// 5. 日志/指标
OnProcessed(postprocessed);
return postprocessed;
}
// 抽象方法 — 子类必须实现
protected abstract Task<TOutput> TransformAsync(TInput input, CancellationToken ct);
// 虚方法 — 子类可选覆盖
protected virtual void Validate(TInput input) { }
protected virtual TInput PreProcess(TInput input) => input;
protected virtual TOutput PostProcess(TOutput output) => output;
protected virtual void OnProcessed(TOutput output) { }
}
// 具体实现 — CSV 导入处理器
public class CsvImportProcessor : DataProcessor<string, ImportResult>
{
private int _lineCount;
protected override void Validate(string input)
{
if (string.IsNullOrWhiteSpace(input)) throw new ArgumentException("CSV 内容为空");
}
protected override string PreProcess(string input) => input.Trim();
protected override async Task<ImportResult> TransformAsync(string csv, CancellationToken ct)
{
var lines = csv.Split('\n', StringSplitOptions.RemoveEmptyEntries);
_lineCount = lines.Length;
await Task.Delay(100, ct); // 模拟数据库写入
return new ImportResult { TotalRows = _lineCount, SuccessRows = _lineCount };
}
protected override void OnProcessed(ImportResult result) =>
Console.WriteLine($"导入完成: {result.SuccessRows}/{result.TotalRows} 行");
}
public record ImportResult { public int TotalRows { get; init; } public int SuccessRows { get; init; } }报表生成模板
public abstract class ReportGenerator
{
public string GenerateReport(DateTime startDate, DateTime endDate)
{
var sb = new StringBuilder();
sb.Append(GenerateHeader(startDate, endDate));
sb.Append(FetchData(startDate, endDate));
sb.Append(CalculateMetrics());
sb.Append(GenerateCharts());
sb.Append(GenerateFooter());
return sb.ToString();
}
protected abstract string GenerateHeader(DateTime start, DateTime end);
protected abstract string FetchData(DateTime start, DateTime end);
protected abstract string CalculateMetrics();
protected abstract string GenerateCharts();
// 钩子方法 — 子类可选覆盖
protected virtual string GenerateFooter() => $"\n报告生成时间: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss}\n";
}
public class SalesReportGenerator : ReportGenerator
{
protected override string GenerateHeader(DateTime start, DateTime end)
=> $"=== 销售报表 ({start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}) ===\n";
protected override string FetchData(DateTime start, DateTime end)
=> "数据: 获取销售记录 1,250 条\n";
protected override string CalculateMetrics()
=> "指标: 总销售额 Y2,500,000, 环比增长 15%\n";
protected override string GenerateCharts()
=> "图表: [柱状图-月度销售趋势]\n";
}
public class InventoryReportGenerator : ReportGenerator
{
protected override string GenerateHeader(DateTime start, DateTime end)
=> $"=== 库存报表 ({start:yyyy-MM-dd}) ===\n";
protected override string FetchData(DateTime start, DateTime end)
=> "数据: 获取库存记录 8,500 条\n";
protected override string CalculateMetrics()
=> "指标: 周转率 4.2, 呆滞库存 12%\n";
protected override string GenerateCharts()
=> "图表: [饼图-库存分类分布]\n";
protected override string GenerateFooter() => $"\n库存盘点报告 - 机密\n";
}测试框架模板
public abstract class IntegrationTestBase : IDisposable
{
protected HttpClient Client { get; private set; } = null!;
protected WebApplicationFactory<Program> Factory { get; private set; } = null!;
// 模板方法 — 测试执行流程
public async Task RunTestAsync(string testName, Func<HttpClient, Task> testAction)
{
Console.WriteLine($"[Setup] {testName}");
Factory = CreateFactory();
Client = Factory.CreateClient();
await SeedDataAsync();
try
{
await testAction(Client);
Console.WriteLine($"[Pass] {testName}");
}
catch (Exception ex)
{
Console.WriteLine($"[Fail] {testName}: {ex.Message}");
throw;
}
}
// 子类定制
protected virtual WebApplicationFactory<Program> CreateFactory() => new();
protected virtual Task SeedDataAsync() => Task.CompletedTask;
public virtual void Dispose() => Factory?.Dispose();
}实战:支付处理模板
在支付系统中,不同支付渠道(支付宝、微信、银行卡)的支付流程整体相同,但某些步骤(签名、发送请求、验签)的实现不同。
public enum PaymentStatus { Success, Failed, Pending }
public class PaymentResult
{
public PaymentStatus Status { get; init; }
public string TransactionId { get; init; } = "";
public string Message { get; init; } = "";
}
public abstract class PaymentProcessor
{
// 模板方法 — 支付流程骨架
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
// 1. 验证请求参数
ValidateRequest(request);
Console.WriteLine($" [验证] 请求参数有效");
// 2. 构建支付数据(子类实现)
var paymentData = BuildPaymentData(request);
Console.WriteLine($" [构建] 支付数据已准备");
// 3. 签名(子类实现)
var signedData = SignData(paymentData);
Console.WriteLine($" [签名] 数据已签名");
// 4. 发送请求(子类实现)
var response = await SendRequestAsync(signedData);
Console.WriteLine($" [请求] 已发送, 响应: {response}");
// 5. 验签(子类实现)
VerifyResponse(response);
Console.WriteLine($" [验签] 响应有效");
// 6. 记录日志(基类通用逻辑)
LogPayment(request, response);
return response;
}
// 抽象方法 — 子类必须实现
protected abstract Dictionary<string, string> BuildPaymentData(PaymentRequest request);
protected abstract Dictionary<string, string> SignData(Dictionary<string, string> data);
protected abstract Task<PaymentResult> SendRequestAsync(Dictionary<string, string> data);
protected abstract void VerifyResponse(PaymentResult response);
// 基类通用逻辑
protected virtual void ValidateRequest(PaymentRequest request)
{
if (request.Amount <= 0) throw new ArgumentException("金额必须大于 0");
if (string.IsNullOrEmpty(request.OrderId)) throw new ArgumentException("订单号不能为空");
}
protected virtual void LogPayment(PaymentRequest request, PaymentResult result)
{
Console.WriteLine($" [日志] 订单 {request.OrderId}, 状态: {result.Status}");
}
}
public record PaymentRequest(string OrderId, decimal Amount, string Currency = "CNY");
// 支付宝实现
public class AlipayProcessor : PaymentProcessor
{
protected override Dictionary<string, string> BuildPaymentData(PaymentRequest request)
=> new() { ["out_trade_no"] = request.OrderId, ["total_amount"] = request.Amount.ToString("F2") };
protected override Dictionary<string, string> SignData(Dictionary<string, string> data)
{
data["sign"] = "alipay_sign_" + string.Join("", data.Values.OrderBy(_ => _));
return data;
}
protected override Task<PaymentResult> SendRequestAsync(Dictionary<string, string> data)
=> Task.FromResult(new PaymentResult { Status = PaymentStatus.Success, TransactionId = "ALI_" + Guid.NewGuid().ToString("N")[..8] });
protected override void VerifyResponse(PaymentResult response)
{
if (!response.TransactionId.StartsWith("ALI_"))
throw new InvalidOperationException("无效的支付宝响应");
}
}
// 微信支付实现
public class WechatPayProcessor : PaymentProcessor
{
protected override Dictionary<string, string> BuildPaymentData(PaymentRequest request)
=> new() { ["out_trade_no"] = request.OrderId, ["total_fee"] = ((int)(request.Amount * 100)).ToString() };
protected override Dictionary<string, string> SignData(Dictionary<string, string> data)
{
data["sign"] = "wechat_sign_" + string.Join("", data.Values.OrderBy(_ => _));
return data;
}
protected override Task<PaymentResult> SendRequestAsync(Dictionary<string, string> data)
=> Task.FromResult(new PaymentResult { Status = PaymentStatus.Success, TransactionId = "WX_" + Guid.NewGuid().ToString("N")[..8] });
protected override void VerifyResponse(PaymentResult response)
{
if (!response.TransactionId.StartsWith("WX_"))
throw new InvalidOperationException("无效的微信支付响应");
}
}实战:文件解析模板
在数据导入场景中,不同格式的文件(CSV、JSON、XML)解析流程相同,但具体解析步骤不同。
// 文件解析模板
public abstract class FileParser<TRecord>
{
// 模板方法 — 文件解析的完整流程
public sealed async Task<List<TRecord>> ParseFileAsync(string filePath, CancellationToken ct)
{
// 1. 验证文件
ValidateFile(filePath);
// 2. 读取文件内容
var content = await ReadFileAsync(filePath, ct);
// 3. 解析内容(子类实现)
var records = ParseContent(content);
// 4. 数据校验(钩子)
var validRecords = ValidateRecords(records);
// 5. 后处理(钩子)
var processedRecords = PostProcess(validRecords);
// 6. 日志
OnParseCompleted(filePath, processedRecords.Count);
return processedRecords;
}
protected virtual void ValidateFile(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException($"文件不存在: {filePath}");
var ext = Path.GetExtension(filePath).ToLowerInvariant();
if (!GetSupportedExtensions().Contains(ext))
throw new NotSupportedException($"不支持的文件格式: {ext}");
}
protected virtual Task<string> ReadFileAsync(string path, CancellationToken ct)
=> File.ReadAllTextAsync(path, ct);
protected abstract List<TRecord> ParseContent(string content);
protected abstract string[] GetSupportedExtensions();
protected virtual List<TRecord> ValidateRecords(List<TRecord> records) => records;
protected virtual List<TRecord> PostProcess(List<TRecord> records) => records;
protected virtual void OnParseCompleted(string filePath, int count)
=> Console.WriteLine($"解析完成: {filePath}, {count} 条记录");
}
// CSV 解析器
public class CsvOrderParser : FileParser<OrderRecord>
{
protected override List<OrderRecord> ParseContent(string content)
{
var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var records = new List<OrderRecord>();
foreach (var line in lines.Skip(1)) // 跳过表头
{
var fields = line.Split(',');
if (fields.Length >= 4)
{
records.Add(new OrderRecord
{
OrderId = fields[0].Trim(),
Product = fields[1].Trim(),
Quantity = int.Parse(fields[2].Trim()),
Price = decimal.Parse(fields[3].Trim())
});
}
}
return records;
}
protected override string[] GetSupportedExtensions() => new[] { ".csv" };
protected override List<OrderRecord> ValidateRecords(List<OrderRecord> records)
{
return records.Where(r => r.Quantity > 0 && r.Price > 0).ToList();
}
}
// JSON 解析器
public class JsonOrderParser : FileParser<OrderRecord>
{
protected override List<OrderRecord> ParseContent(string content)
{
return System.Text.Json.JsonSerializer.Deserialize<List<OrderRecord>>(content) ?? new();
}
protected override string[] GetSupportedExtensions() => new[] { ".json" };
}
public class OrderRecord
{
public string OrderId { get; set; } = "";
public string Product { get; set; } = "";
public int Quantity { get; set; }
public decimal Price { get; set; }
}实战:用委托替代继承的模板方法
C# 中可以用委托(Func/Action)参数替代继承,避免类膨胀,在只需要少量定制步骤时更灵活。
// 基于委托的模板方法 — 无需继承
public class DataPipeline<TInput, TOutput>
{
private Action<TInput>? _validate;
private Func<TInput, TInput> _preProcess = x => x;
private Func<TInput, Task<TOutput>> _transform;
private Func<TOutput, TOutput> _postProcess = x => x;
private Action<TOutput>? _onCompleted;
public DataPipeline<TInput, TOutput> Validate(Action<TInput> validate)
{ _validate = validate; return this; }
public DataPipeline<TInput, TOutput> PreProcess(Func<TInput, TInput> preProcess)
{ _preProcess = preProcess; return this; }
public DataPipeline<TInput, TOutput> Transform(Func<TInput, Task<TOutput>> transform)
{ _transform = transform; return this; }
public DataPipeline<TInput, TOutput> PostProcess(Func<TOutput, TOutput> postProcess)
{ _postProcess = postProcess; return this; }
public DataPipeline<TInput, TOutput> OnCompleted(Action<TOutput> onCompleted)
{ _onCompleted = onCompleted; return this; }
public async Task<TOutput> ExecuteAsync(TInput input, CancellationToken ct = default)
{
_validate?.Invoke(input);
var preprocessed = _preProcess(input);
var transformed = await _transform(preprocessed);
var postprocessed = _postProcess(transformed);
_onCompleted?.Invoke(postprocessed);
return postprocessed;
}
}
// 使用 — 无需创建子类
var pipeline = new DataPipeline<string, ImportResult>()
.Validate(input =>
{
if (string.IsNullOrWhiteSpace(input)) throw new ArgumentException("输入为空");
})
.PreProcess(input => input.Trim())
.Transform(async input =>
{
var lines = input.Split('\n');
await Task.Delay(10); // 模拟 IO
return new ImportResult { TotalRows = lines.Length, SuccessRows = lines.Length };
})
.PostProcess(result => result)
.OnCompleted(result => Console.WriteLine($"完成: {result.SuccessRows} 行"));
var result = await pipeline.ExecuteAsync(" line1\nline2\nline3 ");模板方法与 Hook 方法的配合
钩子方法(Hook Method)是模板方法的重要扩展点,用于提供可选的定制能力。
// 钩子方法的三种使用策略
// 策略 1:条件钩子 — 控制模板方法中的某个步骤是否执行
public abstract class ReportProcessor
{
public void Process(Report report)
{
Validate(report);
if (ShouldCompressData()) // 钩子:是否压缩数据
CompressData(report);
Export(report);
if (ShouldSendNotification()) // 钩子:是否发送通知
SendNotification(report);
}
protected virtual bool ShouldCompressData() => false; // 默认不压缩
protected virtual bool ShouldSendNotification() => true; // 默认发送通知
protected abstract void Validate(Report report);
protected abstract void Export(Report report);
protected virtual void CompressData(Report report) { /* 默认压缩实现 */ }
protected virtual void SendNotification(Report report) { /* 默认通知实现 */ }
}
// 策略 2:前后钩子 — 在关键步骤前后插入逻辑
public abstract class DataMigration
{
public async Task MigrateAsync()
{
await OnBeforeMigrationAsync(); // 前置钩子
await ReadSourceAsync();
await TransformAsync();
await WriteTargetAsync();
await OnAfterMigrationAsync(); // 后置钩子
}
protected virtual Task OnBeforeMigrationAsync() => Task.CompletedTask;
protected virtual Task OnAfterMigrationAsync() => Task.CompletedTask;
protected abstract Task ReadSourceAsync();
protected abstract Task TransformAsync();
protected abstract Task WriteTargetAsync();
}
// 策略 3:替换钩子 — 子类可以完全替换某个步骤的实现
public abstract class MessageSender
{
public async Task SendAsync(string message)
{
var formatted = FormatMessage(message); // 可替换的格式化
await DeliverAsync(formatted); // 必须实现的投递
LogDelivery(message); // 可选的日志
}
protected virtual string FormatMessage(string message) => message; // 默认不格式化
protected abstract Task DeliverAsync(string message); // 必须实现
protected virtual void LogDelivery(string message) { } // 默认不记录
}模板方法 vs 策略模式
模板方法模式 策略模式
+--------+ +--------+
| 继承 | | 组合 |
| 实现 | | 委托 |
+--------+ +--------+
| 编译时 | | 运行时 |
| 决定 | | 切换 |
+--------+ +--------+
| 骨架 | | 算法 |
| 固定 | | 可换 |
+--------+ +--------+
选择标准:
- 算法步骤固定,部分步骤可定制 -> 模板方法
- 整个算法可以替换 -> 策略模式
- 可以结合使用:模板方法中的某个步骤用策略模式实现最佳实践
- 模板方法标记为 sealed:防止子类重写算法骨架,确保流程的一致性。
- 最小化抽象方法:只有真正需要子类定制的步骤才标记为 abstract。
- 使用钩子方法提供扩展点:对于可选的定制步骤,使用 virtual 方法提供默认实现。
- 避免过深的继承层次:模板方法通常只需要一层继承,避免继承过深。
- 考虑使用函数参数代替继承:C# 中可以用委托参数代替继承,更灵活。
优点
缺点
总结
模板方法在基类中用 sealed 方法定义算法骨架,用 abstract 方法要求子类实现关键步骤,用 virtual 方法提供可选的钩子扩展点。适合数据处理管道、报表生成、测试框架等流程固定但步骤可变的场景。C# 中可配合泛型增强类型安全。建议在多个类有相似执行流程但个别步骤不同时使用模板方法模式。
模板方法模式的本质价值在于:将多个类中相同的执行流程提取到基类中形成"模板",只将变化的部分留给子类实现。这是一种"先找共同点、再找差异点"的设计思路。
关键知识点
- 模式不是目标,降低耦合和控制变化才是目标。
- 先找变化点、稳定点和协作边界,再决定是否引入模式。
- 同一个模式在不同规模下的收益和代价差异很大。
项目落地视角
- 优先画出参与对象、依赖方向和调用链,再落到代码。
- 把模式放到一个真实场景里,比如支付、规则引擎、工作流或插件扩展。
- 配合单元测试或契约测试,保证重构后的行为没有漂移。
常见误区
- 为了看起来"高级"而套模式。
- 把简单问题拆成过多抽象层,导致阅读和排障都变难。
- 只会背 UML,不会解释为什么这里需要这个模式。
进阶路线
- 继续关注模式之间的组合用法,而不是孤立记忆。
- 从业务建模、演进策略和团队协作角度看模式的适用性。
- 把模式结论沉淀为项目模板、基类或约束文档。
适用场景
- 当你准备把《模板方法模式》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在业务规则频繁变化、分支增多或对象协作复杂时引入。
- 当你希望提高扩展性,但又不想把系统拆得过度抽象时,这类主题很有参考价值。
落地建议
- 先识别变化点,再决定是否引入模式,而不是反过来套模板。
- 优先为模式的边界、依赖和调用路径画出简单结构图。
- 把模式落到一个明确场景,例如支付、规则计算、插件扩展或工作流。
排错清单
- 检查抽象层是否过多,导致调用路径和责任不清晰。
- 确认引入模式后是否真的减少了条件分支和重复代码。
- 警惕"为了模式而模式",尤其是在简单业务里。
复盘问题
- 如果把《模板方法模式》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《模板方法模式》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《模板方法模式》最大的收益和代价分别是什么?
