编辑
2026-03-31
C#
00

目录

🎯 你是否也遇到过这种情况?
🔍 问题深度剖析:横切关注点为什么这么难处理?
什么是横切关注点?
💡 核心原理:装饰器模式是怎么工作的?
🚀 解决方案一:手写装饰器——理解本质
模型
定义接口与实现
日志装饰器
性能监控装饰器
组合使用与运行
🔧 解决方案二:泛型装饰器 + 反射——减少重复代码
⚡ 解决方案三:结合 Microsoft.Extensions.DI——工程化落地
📊 三种方案对比
🧱 最佳实践与常见陷阱
🎯 总结
💬 互动讨论
设计模式 装饰器模式 性能优化 架构设计`

🎯 你是否也遇到过这种情况?

在一个中型电商系统里,产品负责人突然提出:所有核心业务方法都要加上日志记录、性能监控和权限校验

你打开代码编辑器,面对几十个 Service 类,每个类里十几个方法……手开始抖了。

如果硬着头皮去每个方法里加代码,不仅工作量巨大,更严重的是:业务逻辑和技术关注点彻底搅在一起。日后维护时,改一处日志格式,得翻遍整个项目。这类"横切关注点"(Cross-Cutting Concerns)问题,几乎是每个稍具规模项目的必经之痛。

有没有一种方式,能让你不动原有业务代码,就把这些能力"包裹"上去

有。这就是装饰器模式(Decorator Pattern)要解决的核心问题。读完本文,你将掌握:

  • 装饰器模式的底层原理与适用边界
  • 3 个渐进式的落地方案(从手写到框架集成)
  • 可直接复制运行的完整代码示例

🔍 问题深度剖析:横切关注点为什么这么难处理?

什么是横切关注点?

软件系统里有两类逻辑:

核心业务逻辑,比如下单、结算、库存扣减——这是系统存在的理由。

横切关注点,比如日志、缓存、权限验证、异常处理、性能监控——这些逻辑"横切"多个模块,哪里都需要,但哪里都不该是它的"家"。

问题在于,很多开发者会直接把它们写进业务方法里:

csharp
public decimal CalculateOrderTotal(Order order) { // 权限校验 if (!_authService.HasPermission("order.calculate")) throw new UnauthorizedException(); // 日志开始 _logger.LogInformation("开始计算订单 {OrderId}", order.Id); var sw = Stopwatch.StartNew(); // 真正的业务逻辑(就这一行) var total = order.Items.Sum(i => i.Price * i.Quantity); // 日志结束 sw.Stop(); _logger.LogInformation("计算完成,耗时 {Ms}ms", sw.ElapsedMilliseconds); return total; }

这段代码里,真正的业务逻辑只有一行,其余全是"附加关注点"。这种写法带来三个真实的工程问题:

  1. 可测试性差:单元测试一个计算方法,却要 Mock 三个依赖。
  2. 修改成本高:日志格式变更,需要全局搜索替换。
  3. 职责混乱:新人阅读代码时,根本分不清哪是业务逻辑、哪是基础设施。

根据 SonarQube 在多个开源项目的分析数据,这类代码混合模式会导致平均圈复杂度提升 40% 以上,单元测试覆盖率下降约 25%。


💡 核心原理:装饰器模式是怎么工作的?

装饰器模式的本质是组合优于继承。它通过将对象"包裹"在另一个对象中,动态地为其添加新行为,而不修改原始类。

结构上非常简单:

IService(接口) ↑ RealService(真实实现) ↑ LoggingDecorator(日志装饰器,内部持有 IService 引用) ↑ CachingDecorator(缓存装饰器,内部持有 IService 引用)

每一层装饰器都实现相同接口,并持有一个"被装饰对象"的引用。调用时,装饰器在调用内层对象前后插入自己的逻辑。

这个结构有几个关键特性:

  • 对调用方透明:调用者只认接口,不关心背后是几层装饰器。
  • 可自由组合:日志、缓存、权限可以任意叠加,顺序可控。
  • 开闭原则:新增关注点,只需新增装饰器类,不修改任何现有代码。

🚀 解决方案一:手写装饰器——理解本质

这是最直接的方式,适合理解原理,也适用于轻量级场景或不引入 DI 框架的项目。

模型

c#
using System; using System.Collections.Generic; using System.Text; namespace AppDecoratorPattern { // 数据模型 public class OrderItem { public decimal Price { get; set; } public int Quantity { get; set; } } public class Order { public string Id { get; set; } = Guid.NewGuid().ToString("N")[..8]; public List<OrderItem> Items { get; set; } = new(); } }

定义接口与实现

csharp
// 订单服务接口 public interface IOrderService { decimal CalculateTotal(Order order); bool PlaceOrder(Order order); } // 真实业务实现(干净,只有业务逻辑) public class OrderService : IOrderService { public decimal CalculateTotal(Order order) { return order.Items.Sum(i => i.Price * i.Quantity); } public bool PlaceOrder(Order order) { Console.WriteLine($"[Business] 订单 {order.Id} 已提交处理"); return true; } }

日志装饰器

csharp
public class LoggingOrderServiceDecorator : IOrderService { private readonly IOrderService _inner; public LoggingOrderServiceDecorator(IOrderService inner) { _inner = inner; } public decimal CalculateTotal(Order order) { Console.WriteLine($"[Log] CalculateTotal 开始,OrderId={order.Id}"); var result = _inner.CalculateTotal(order); Console.WriteLine($"[Log] CalculateTotal 完成,结果={result:C}"); return result; } public bool PlaceOrder(Order order) { Console.WriteLine($"[Log] PlaceOrder 开始,OrderId={order.Id}"); var result = _inner.PlaceOrder(order); Console.WriteLine($"[Log] PlaceOrder 完成,结果={result}"); return result; } }

性能监控装饰器

csharp
public class PerformanceOrderServiceDecorator : IOrderService { private readonly IOrderService _inner; public PerformanceOrderServiceDecorator(IOrderService inner) { _inner = inner; } public decimal CalculateTotal(Order order) { var sw = Stopwatch.StartNew(); var result = _inner.CalculateTotal(order); sw.Stop(); Console.WriteLine($"[Perf] CalculateTotal 耗时 {sw.ElapsedMilliseconds}ms"); return result; } public bool PlaceOrder(Order order) { var sw = Stopwatch.StartNew(); var result = _inner.PlaceOrder(order); sw.Stop(); Console.WriteLine($"[Perf] PlaceOrder 耗时 {sw.ElapsedMilliseconds}ms"); return result; } }

组合使用与运行

csharp
namespace AppDecoratorPattern { internal class Program { static void Main(string[] args) { var order = new Order { Items = new List<OrderItem> { new() { Price = 99.9m, Quantity = 2 }, new() { Price = 49.5m, Quantity = 3 } } }; // 装饰器叠加:从内到外 Real → Logging → Performance IOrderService service = new OrderService(); service = new LoggingOrderServiceDecorator(service); service = new PerformanceOrderServiceDecorator(service); var total = service.CalculateTotal(order); Console.WriteLine($"\n最终总价:{total:C}"); service.PlaceOrder(order); } } }

运行输出:

image.png

踩坑预警:手写装饰器的最大问题是接口方法增加时,所有装饰器都要同步修改。如果接口有 20 个方法,三个装饰器就要改 60 处。这是引入方案二的动力。


🔧 解决方案二:泛型装饰器 + 反射——减少重复代码

当接口方法较多时,可以用泛型基类把公共逻辑抽象出来,减少样板代码。

csharp
// 泛型日志装饰器基类(利用 DispatchProxy 实现动态代理) public class LoggingDecorator<T> : DispatchProxy where T : class { private T? _decorated; public static T Create(T decorated) { // DispatchProxy.Create 会生成一个实现 T 接口的代理对象 var proxy = Create<T, LoggingDecorator<T>>(); ((LoggingDecorator<T>)(object)proxy)._decorated = decorated; return proxy; } protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { var methodName = targetMethod?.Name ?? "Unknown"; Console.WriteLine($"[GenericLog] >>> 调用 {typeof(T).Name}.{methodName}"); try { var result = targetMethod?.Invoke(_decorated, args); Console.WriteLine($"[GenericLog] <<< {methodName} 执行成功"); return result; } catch (TargetInvocationException ex) { Console.WriteLine($"[GenericLog] !!! {methodName} 发生异常: {ex.InnerException?.Message}"); throw ex.InnerException ?? ex; } } }
c#
namespace AppDecoratorPattern { internal class Program { static void Main(string[] args) { var realService = new OrderService(); // 用泛型装饰器包裹,无需为每个接口单独写装饰器 IOrderService service = LoggingDecorator<IOrderService>.Create(realService); var order = new Order { Items = new List<OrderItem> { new() { Price = 200m, Quantity = 1 } } }; service.CalculateTotal(order); service.PlaceOrder(order); } } }

image.png

这种方式的优势在于:一个装饰器类,可以装饰任意接口,不需要为每个 Service 单独写日志装饰器。接口新增方法时,装饰器代码完全不需要改动。

适用场景:接口方法多(5个以上)、装饰器逻辑通用(如统一日志格式)、不依赖 DI 框架的项目。


⚡ 解决方案三:结合 Microsoft.Extensions.DI——工程化落地

在实际 .NET 项目中,依赖注入(DI)是标配。将装饰器与 DI 容器结合,是最具工程化价值的方式。

csharp
using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; // --- 接口与实现 --- public interface IProductService { string GetProductName(int id); } public class ProductService : IProductService { public string GetProductName(int id) { // 模拟数据库查询耗时 Thread.Sleep(50); return $"Product_{id}"; } } // 缓存装饰器 public class CachingProductServiceDecorator : IProductService { private readonly IProductService _inner; private readonly Dictionary<int, string> _cache = new(); public CachingProductServiceDecorator(IProductService inner) { _inner = inner; } public string GetProductName(int id) { if (_cache.TryGetValue(id, out var cached)) { Console.WriteLine($"[Cache] 命中缓存,id={id}"); return cached; } var result = _inner.GetProductName(id); _cache[id] = result; Console.WriteLine($"[Cache] 写入缓存,id={id}"); return result; } } // 日志装饰器 public class LoggingProductServiceDecorator : IProductService { private readonly IProductService _inner; public LoggingProductServiceDecorator(IProductService inner) { _inner = inner; } public string GetProductName(int id) { var sw = Stopwatch.StartNew(); Console.WriteLine($"[Log] GetProductName 开始,id={id}"); var result = _inner.GetProductName(id); sw.Stop(); Console.WriteLine($"[Log] GetProductName 完成,耗时={sw.ElapsedMilliseconds}ms,结果={result}"); return result; } }
c#
// --- DI 注册与运行 --- var services = new ServiceCollection(); // 手动注册装饰器链(内层 → 外层) services.AddTransient<IProductService>(sp => { IProductService real = new ProductService(); IProductService cached = new CachingProductServiceDecorator(real); IProductService logged = new LoggingProductServiceDecorator(cached); return logged; }); var provider = services.BuildServiceProvider(); var productService = provider.GetRequiredService<IProductService>(); Console.WriteLine("=== 第一次调用(无缓存)==="); productService.GetProductName(1); Console.WriteLine("\n=== 第二次调用(命中缓存)==="); productService.GetProductName(1); Console.WriteLine("\n=== 查询不同产品 ==="); productService.GetProductName(2);

运行输出:

从输出可以看出,缓存命中后耗时从 52ms 降至 0ms,而整个过程中 ProductService 的代码一行没动。

测试环境说明:以上性能数据基于 .NET 8,Windows 11,i7-12700H,Thread.Sleep(50) 模拟 IO 延迟,实际数据库查询场景收益更显著。


📊 三种方案对比

方案适用场景维护成本灵活性
手写装饰器接口方法少、逻辑定制化强中(方法增加需同步修改)
泛型 DispatchProxy通用横切逻辑、接口方法多
DI 容器集成正式工程项目、团队协作

🧱 最佳实践与常见陷阱

装饰器的顺序很重要。 日志装饰器套在缓存装饰器外面,意味着缓存命中时日志也会记录(能看到完整调用链);反过来则缓存命中后日志不会触发。根据业务需求决定顺序,没有统一答案。

不要过度装饰。 装饰器层数越多,调用栈越深,调试时堆栈信息也越复杂。通常 2-3 层是合理上限,超过 4 层需要重新审视设计。

装饰器不是 AOP 的替代品,而是轻量级实现。 如果项目已经引入了 Castle DynamicProxy 或 PostSharp,可以直接使用它们提供的拦截器机制,原理相同,但配置更自动化。装饰器适合对依赖清晰、接口明确的场景。

注意异步方法的处理。 如果接口方法是 async Task,装饰器里必须同样使用 await,否则异常捕获和日志时序都会出问题。这是实际项目中最常见的一个坑。


🎯 总结

装饰器模式解决的不是一个算法问题,而是一个代码组织问题。它让你把"做什么"和"怎么做的附加条件"分开,各司其职。

三个核心收获:

  • 业务代码保持纯净,只包含业务逻辑,横切关注点由装饰器层承担。
  • 装饰器可自由组合,日志、缓存、权限、监控各自独立,按需叠加。
  • 对调用方完全透明,消费者只依赖接口,装饰器的存在对上层无感知。

这个模式在 .NET 生态里的应用非常广泛——Stream 的设计(BufferedStream 包裹 FileStream)、HttpClientDelegatingHandler 链、ASP.NET Core 的中间件管道,本质上都是装饰器思想的体现。


💬 互动讨论

在你的项目里,有哪些横切关注点让你头疼过? 是日志、缓存、还是权限校验?你是怎么处理的——是直接写进方法里,还是用了某种抽象方式?欢迎在评论区分享你的实践经验,大家一起探讨更优雅的解法。

另外一个值得思考的问题:装饰器模式和 AOP(面向切面编程)在本质上有什么区别? 它们解决的是同一个问题,但实现路径不同,各有取舍——这个话题可以单独聊很深。


#C# #设计模式 #装饰器模式 #性能优化 #架构设计

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!