在一个中型电商系统里,产品负责人突然提出:所有核心业务方法都要加上日志记录、性能监控和权限校验。
你打开代码编辑器,面对几十个 Service 类,每个类里十几个方法……手开始抖了。
如果硬着头皮去每个方法里加代码,不仅工作量巨大,更严重的是:业务逻辑和技术关注点彻底搅在一起。日后维护时,改一处日志格式,得翻遍整个项目。这类"横切关注点"(Cross-Cutting Concerns)问题,几乎是每个稍具规模项目的必经之痛。
有没有一种方式,能让你不动原有业务代码,就把这些能力"包裹"上去?
有。这就是装饰器模式(Decorator Pattern)要解决的核心问题。读完本文,你将掌握:
软件系统里有两类逻辑:
核心业务逻辑,比如下单、结算、库存扣减——这是系统存在的理由。
横切关注点,比如日志、缓存、权限验证、异常处理、性能监控——这些逻辑"横切"多个模块,哪里都需要,但哪里都不该是它的"家"。
问题在于,很多开发者会直接把它们写进业务方法里:
csharppublic 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;
}
这段代码里,真正的业务逻辑只有一行,其余全是"附加关注点"。这种写法带来三个真实的工程问题:
根据 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;
}
}
csharppublic 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;
}
}
csharppublic 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;
}
}
csharpnamespace 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);
}
}
}
运行输出:

踩坑预警:手写装饰器的最大问题是接口方法增加时,所有装饰器都要同步修改。如果接口有 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);
}
}
}

这种方式的优势在于:一个装饰器类,可以装饰任意接口,不需要为每个 Service 单独写日志装饰器。接口新增方法时,装饰器代码完全不需要改动。
适用场景:接口方法多(5个以上)、装饰器逻辑通用(如统一日志格式)、不依赖 DI 框架的项目。
在实际 .NET 项目中,依赖注入(DI)是标配。将装饰器与 DI 容器结合,是最具工程化价值的方式。
csharpusing 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)、HttpClient 的 DelegatingHandler 链、ASP.NET Core 的中间件管道,本质上都是装饰器思想的体现。
在你的项目里,有哪些横切关注点让你头疼过? 是日志、缓存、还是权限校验?你是怎么处理的——是直接写进方法里,还是用了某种抽象方式?欢迎在评论区分享你的实践经验,大家一起探讨更优雅的解法。
另外一个值得思考的问题:装饰器模式和 AOP(面向切面编程)在本质上有什么区别? 它们解决的是同一个问题,但实现路径不同,各有取舍——这个话题可以单独聊很深。
#C# #设计模式 #装饰器模式 #性能优化 #架构设计
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!