decorator模式在c#中通过接口+组合+委托实现,核心是不修改原类而动态添加行为;需定义统一接口(如ilogger),装饰器类组合该接口实例并转发调用,增强逻辑置于转发前后,支持链式组装与灵活扩展。

Decorator模式在C#里不是靠语法糖,而是靠组合和接口
C#没有像Python那样的@decorator语法,所谓“装饰器模式”是经典GoF设计模式的落地实现,核心是用对象组合替代继承来动态添加行为。它不改变原对象,而是在其外部包一层新对象,该对象实现相同接口并持有原对象引用。
常见错误是试图用partial class或attribute模拟装饰——这两者完全不满足Decorator模式的运行时可叠加、可替换、接口一致这三条关键约束。
- 必须定义统一接口(如
IComponent),所有具体组件和装饰器都实现它 - 装饰器类(如
LoggingDecorator)构造函数接收IComponent,内部保存为私有字段 - 装饰器的每个方法都先执行自身逻辑(如写日志),再调用被装饰对象的对应方法
- 可以多层嵌套:
new CompressionDecorator(new LoggingDecorator(new ConcreteComponent()))
避免把Decorator写成继承链或条件分支
有人会把功能增强写成子类继承(class LoggedFileWriter : FileWriter),或者用if (logEnabled) Log(...)硬编码进原类——这两种都破坏了开闭原则,也失去了运行时灵活装配的能力。
典型反例:在FileWriter.Write()里加if (_shouldLog)判断。这导致职责混杂,且无法单独启用/禁用压缩、加密等其他装饰行为。
- 装饰器必须是独立类,不能修改被装饰类源码
- 所有增强逻辑(日志、缓存、重试、超时)都应拆成各自实现
IDecorator的类 - 如果多个装饰器需共享上下文(如请求ID),可通过构造参数传入
IServiceProvider或HttpContext,但不要让装饰器互相依赖
用.NET 6+的依赖注入容器简化装饰器组装
手写new CompressionDecorator(new LoggingDecorator(...))容易出错且难以维护。.NET内置支持装饰器注册,但仅限于服务类型(IServiceCollection),且要求装饰器构造函数第一个参数是被装饰的服务类型。
例如注册ILogger<myservice></myservice>的装饰器时,必须写成:
services.Decorate<ilogger>, LoggingDecorator>();</ilogger>
注意:Decorate是第三方扩展(如Scrutor),.NET原生IServiceCollection不提供此API。官方只支持AddTransient<tservice timplementation>()</tservice>这种绑定,装饰需手动封装工厂。
- 若用
Scrutor,安装ScrutorNuGet包后调用services.Decorate<irepository auditingdecorator>()</irepository> - 装饰器类必须公开构造函数,且首个参数类型严格匹配被装饰接口(不能是基类或派生类)
- 生命周期必须兼容:若被装饰服务是
Scoped,装饰器也必须注册为Scoped,否则DI容器会报错
装饰器链中异常处理和异步支持要显式设计
同步装饰器直接try/catch没问题,但异步方法(如Task<string> GetDataAsync()</string>)的装饰器必须自己实现async/await,不能简单包装同步逻辑。否则会阻塞线程或丢失上下文。
常见坑是装饰器里用了result.GetAwaiter().GetResult()——这在ASP.NET Core请求上下文中可能引发死锁,尤其当调用方使用.Result时。
- 异步装饰器必须声明
async Task<t> MethodAsync()</t>,并在内部await inner.MethodAsync() - 异常处理应放在装饰器内,但不要吞掉原始异常:用
catch记录日志后throw或throw ex(保留堆栈) - 若需在异常后执行清理(如释放资源),用
using或try/finally,别依赖Dispose自动触发——装饰器链的Dispose顺序不可控
HashSet<icomponent></icomponent>或作为字典键,而没重写Equals和GetHashCode,那同一逻辑的两个装饰器实例会被视为不同对象。这在缓存、去重场景下会悄悄出问题。











