问题
假设您的代码库中有一个(全局使用的)接口(来自您自己的或来自第三方的),并且您想要引入需要应用于所有实例的通用行为。假设您想要添加额外的日志记录、构建弹性或添加一些缓存。您将如何解决这个问题?
实现此目的的一种方法是创建一个包含新行为的新(抽象)类,然后在所有派生类中从该类继承。
这似乎是最直接的事情,但您现在或将来会遇到一些问题:
- 您必须修改派生类中的代码以调用父类的行为
- 当实现在未来发生变化时,您可能需要再次更新派生类
- 派生类可能变得更难测试
- 当您需要添加其他行为时,您会怎么做?
虽然这个实现乍一看很简单,但仍有几个问题需要考虑。
如果您想将行为添加到接口的所有继承者,它甚至变得无法维护。
(手动) 解决方案
相反,最好求助于“四人帮”一书中的 装饰器模式。
在这篇博文中,我们将介绍如何利用依赖注入 (DI) 容器来实现装饰器模式。
装饰器模式是一种结构设计模式,它允许您向现有类引入一些新行为,而不会影响原始类。它通过将类封装在一个(或多个)装饰器类中来实现这一点,这些装饰器类将方法调用委托给链接的类。
您可以将装饰器模式视为服装分层系统。
作为一名跑步者,这引起了我的共鸣,尤其是在寒冷的月份。
当气温下降时,我仍然穿着与“正常”天气相同的衣服,但我会在上面加一件额外的衣服来保暖。
下雨时,我会穿上防水夹克以保持干燥。
在寒冷的月份,底层不会改变,但会根据天气情况添加额外的层。
没有坏天气,只有坏衣服 - 挪威谚语
同样的理念也适用于装饰器模式。
将额外的层添加到现有实现中以为其添加额外的功能。
让我们看一个例子。
在下面的例子中,我们定义了一个装饰器,它在调用存储库(IRepository
)的Get
方法之前和之后记录一条消息。
装饰器RepositoryLoggerDecorator
接收“内部”存储库IRepository<T>
,它是真实实例(或装饰实例),并用自己的实现包装它。在其实现中,记录消息,并且装饰版本调用内部方法。
public class RepositoryLoggerDecorator<T>(
IRepository<T> repository,
ILogger<IRepository<T>> logger)
: IRepository<T> where T : class
{
public async Task<T> Get()
{
logger.LogInformation("Before calling Get() on {Repository}", typeof(T).Name);
var result = await repository.Get();
logger.LogInformation("After calling Get() on {Repository}", typeof(T).Name);
return result;
}
}
正如您在上面的代码片段中看到的,存储库实现没有改变。
如果是这种情况,则需要更新多个存储库。
或者,如果我们使用继承,那么所有派生类都必须注入一个 ILogger
实例以将其传递给父类。
要使用 RepositoryLoggerDecorator
装饰存储库,请配置 DI 容器以新建一个装饰实例并向其传递所需的依赖项。
builder.Services.AddScoped<Repository<Customer>>();
builder.Services.AddScoped<IRepository<Customer>>(provider =>
{
var repository = provider.GetRequiredService<Repository<Customer>>();
var logger = provider.GetRequiredService<ILogger<IRepository<Customer>>>();
return new RepositoryLoggerDecorator<Customer>(repository, logger);
});
现在,每次请求 IRepository<Customer>
实例时,它都会解析为装饰版本。
瞧,我们通过 .NET 的依赖注入实现了装饰器模式。
使用 Scrutor 的(自动)解决方案
上述实现有一个缺点。
我们必须求助于工厂函数来手动实例化装饰器。
这会泄露一些实现细节,每次构造函数参数发生变化时,我们都必须更新工厂。
通过引入 Scrutor,我们可以让这个过程更简单、更可靠。
虽然 Scrutor 以其程序集扫描功能而闻名,但它还包括一个可以轻松装饰实例的扩展。
要使用 Scrutor,首先使用以下命令(或通过 UI)安装 NuGet 包。
dotnet add package Scrutor
接下来,更新 DI 配置以使用 Scrutor 的“Decorate”方法,而不是使用工厂方法手动执行此操作。
在下一个代码片段中,“Decorate”将使用“RepositoryLoggerDecorator”类装饰“IRepository”。
builder.Services.AddScoped<IRepository<Customer>, CustomerRepository>();
builder.Services.Decorate<IRepository<Customer>, RepositoryLoggerDecorator<Customer>>();
:::warning
使用此模式时,请确保首先将派生类注册到 DI 容器。
否则,这将导致无法找到所需服务(IRepository<Customer>
)的错误。
:::
我们可以通过使用以下Decorate
重载使此过程更加简单。
这将使用RepositoryLoggerDecorator
装饰IRepository<T>
的所有通用实例。
例如,在下一个代码片段中,IRepository<Customer>
和IRepository<Order>
都将被装饰。
builder.Services.AddScoped<IRepository<Customer>, CustomerRepository>();
builder.Services.AddScoped<IRepository<Order>, OrderRepository>();
builder.Services.Decorate(typeof(IRepository<>), typeof(RepositoryLoggerDecorator<>));
最后,为了自动注册所有·IRepository·实例,我们可以利用 Scrutor 的扫描功能。
builder.Services.Scan(scan => scan
.FromAssembliesOf(typeof(IRepository<>))
.AddClasses(classes => classes.AssignableTo(typeof(IRepository<>)))
.AsImplementedInterfaces()
.WithScopedLifetime());
builder.Services.Decorate(typeof(IRepository<>), typeof(RepositoryLoggerDecorator<>));
:::info
要了解 Scrutor 如何实现这一点,我推荐阅读 Andrew Lock 的文章 使用 Scrutor 将装饰类添加到 ASP.NET Core DI 容器。
:::
结论
使用装饰器模式可以在现有实现的基础上添加额外功能,而无需更改原始代码。
在这篇博文中,我们已经看到 .NET 的 DI 容器允许我们使用装饰器配置特定实例。
为了简化这一过程,我们可以引入 Scrutor 来自动化此配置。