使用网络依赖注入的装饰器模式

问题

假设您的代码库中有一个(全局使用的)接口(来自您自己的或来自第三方的),并且您想要引入需要应用于所有实例的通用行为。假设您想要添加额外的日志记录、构建弹性或添加一些缓存。您将如何解决这个问题?

实现此目的的一种方法是创建一个包含新行为的新(抽象)类,然后在所有派生类中从该类继承。
这似乎是最直接的事情,但您现在或将来会遇到一些问题:

  • 您必须修改派生类中的代码以调用父类的行为
  • 当实现在未来发生变化时,您可能需要再次更新派生类
  • 派生类可能变得更难测试
  • 当您需要添加其他行为时,您会怎么做?

虽然这个实现乍一看很简单,但仍有几个问题需要考虑。
如果您想将行为添加到接口的所有继承者,它甚至变得无法维护。

(手动) 解决方案

相反,最好求助于“四人帮”一书中的 装饰器模式
在这篇博文中,我们将介绍如何利用依赖注入 (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 来自动化此配置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,让我们了解一下装饰器模式的概念。装饰器模式是一种结构型设计模式,它允许你通过将对象放入包装器中来动态地修改其行为。这种模式对于需要在运行时增加或更改对象功能的情况非常有用。 下面是一个示例,展示如何使用装饰器模式实现依赖注入: 假设我们有一个接口 `Person`,其中包含一个 `speak()` 方法: ```java public interface Person { void speak(); } ``` 现在我们有一个具体的实现类 `ChinesePerson`: ```java public class ChinesePerson implements Person { @Override public void speak() { System.out.println("我是中国人,我说中文"); } } ``` 我们想要为 `ChinesePerson` 类添加一些新功能,比如记录日志。为此,我们创建一个装饰器类 `LoggerDecorator`: ```java public class LoggerDecorator implements Person { private final Person person; public LoggerDecorator(Person person) { this.person = person; } @Override public void speak() { System.out.println("记录日志开始..."); person.speak(); System.out.println("记录日志结束..."); } } ``` 在 `LoggerDecorator` 类中,我们将 `Person` 对象作为参数传递给构造函数,并在 `speak()` 方法中添加记录日志的功能。 现在,我们可以使用依赖注入来获取 `Person` 对象。假设我们有一个 `PersonFactory` 工厂类,它返回一个 `Person` 对象: ```java public class PersonFactory { public static Person createPerson() { return new ChinesePerson(); } } ``` 现在,我们使用装饰器模式来获取装饰后的 `Person` 对象: ```java public class Main { public static void main(String[] args) { Person person = PersonFactory.createPerson(); person = new LoggerDecorator(person); person.speak(); } } ``` 在上面的代码中,我们首先调用 `PersonFactory.createPerson()` 方法获取 `Person` 对象,然后将其传递给 `LoggerDecorator` 构造函数以装饰该对象。最后,我们调用 `speak()` 方法来输出日志和原始 `ChinesePerson` 对象的方法调用结果。 这就是装饰器模式的实现方式。通过这种方式,我们可以在运行时动态地增加或修改对象的功能,这对于需要灵活的依赖注入非常有用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值