C#为 ServiceCollection 实现装饰器模式

Intro

在二十四种设计模式中,有一个模式叫做装饰器模式

一般用来动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活,有更好的扩展性,我们也可以借助 Decorator 来实现一个切面逻辑,实现 AOP 的一些功能。.

使用场景

装饰模式是为已有功能动态地添加更多功能的一种方式

当系统需要新功能的时候,是向旧的类中添加新的代码,这些新加的代码通常装饰了原有类的核心职责或主要行为,但是往往会在主类中加入新的字段/方法/逻辑,从而增加了主类的复杂度, 而这些新加入的东西仅仅是为了满足一些只在某种特定情况下才会执行的特殊行为的需要

装饰模式提供了一个很好的方案,它把每个要装饰的功能放在单独的类中,并让这个类包装它要装饰的对象, 当需要执行特殊行为时,就可以在运行时根据需要有选择地、按顺序地使用装饰功能包装对象了。

装饰模式的优点是把类中的装饰功能从类中搬移去除,这样可以简化原有的类,这样做就有效地把类的核心职责和装饰功能区分开了,而且可以去除相关类中重复的装饰逻辑。

Sample

直接来看示例效果吧:

首先我们定义了一个抽象接口 IJob,接口里有一个 Name 属性和一个 Execute 方法

然后定义了一个实现 Sleepy 实现了我们的 IJob 接口,

最后添加了一个装饰器 JobDecorator 也实现了我们的 IJob 接口

private interface IJob
{
    string Name { get; }
    void Execute();
}

private sealed class Sleepy : IJob
{
    public string Name => nameof(Sleepy);

    public void Execute()
    {
        Console.WriteLine("Sleeping...");
    }
}

private sealed class JobDecorator : IJob
{
    private readonly IJob _job;
    public JobDecorator(IJob job)
    {
        _job = job;
    }

    public string Name => $"??? {_job.Name}";

    public void Execute()
    {
        Console.WriteLine("Before execute");

        _job.Execute();

        Console.WriteLine("After execute");
    }
}

接着我们看使用的示例吧:

var services = new ServiceCollection();
services.AddSingleton<IJob, Sleepy>();
services.Decorate<IJob, JobDecorator>();
using var sp = services.BuildServiceProvider();

var job = sp.GetRequiredService<IJob>();
Console.WriteLine(job.Name);
job.Execute();

输出结果如下:

C#为 ServiceCollection 实现装饰器模式

output

可以看到我们在原有实现的基础上加入了我们装饰器的逻辑,我们的 Before 和 After 也都执行了

Implement

Decorate 方法实现如下:

    /// <summary>
    /// Register decorator for TService
    /// </summary>
    /// <typeparam name="TService">service type</typeparam>
    /// <typeparam name="TDecorator">decorator type</typeparam>
    /// <param name="services">services</param>
    /// <returns>services</returns>
    public static IServiceCollection Decorate<TService, TDecorator>(this IServiceCollection services)
         where TService : class
         where TDecorator : class, TService
    {
        return services.Decorate(typeof(TService), typeof(TDecorator));
    }

    /// <summary>
    /// Register service decorator
    /// </summary>
    /// <param name="services">services</param>
    /// <param name="serviceType">serviceType</param>
    /// <param name="decoratorType">decoratorType</param>
    /// <returns>services</returns>
    /// <exception cref="InvalidOperationException">throw exception when serviceType not registered</exception>
    public static IServiceCollection Decorate(this IServiceCollection services, Type serviceType, Type decoratorType)
    {
        var service = services.FirstOrDefault(x => x.ServiceType == serviceType);
        if (service == null)
        {
            throw new InvalidOperationException("The service is not registed, service need to be registered before decorating");
        }

        var objectFactory = ActivatorUtilities.CreateFactory(decoratorType, new[] { serviceType });
        var decoratorService = new ServiceDescriptor(serviceType, sp => objectFactory(sp, new object?[]
        {
            sp.CreateInstance(service)
        }), service.Lifetime);

        services.Replace(decoratorService);
        return services;
    }

实现逻辑主要是将原来这个 service 的注册从原来的服务替换成 Decorator,Decorator 也实现了服务类型,是可以替换掉服务接口的注册的,实现比较简单,有些情况可能会有问题,主要是实现思路的分享

More

Github 上有一个关于 Decorator 的 issue,感兴趣的看一下:https://github.com/dotnet/runtime/issues/36021

另外有一个开源项目 https://github.com/khellang/Scrutor 已经实现了基于 ServiceCollection 的 Decorator 支持,我们可以借助它来实现 Decorator 模式(原本以为只有服务注册的扩展,偶然间发现还有 decorator 的实现)

使用示例:

var collection = new ServiceCollection();

// First, add our service to the collection.
collection.AddSingleton<IDecoratedService, Decorated>();

// Then, decorate Decorated with the Decorator type.
collection.Decorate<IDecoratedService, Decorator>();

// Finally, decorate Decorator with the OtherDecorator type.
// As you can see, OtherDecorator requires a separate service, IService. We can get that from the provider argument.
collection.Decorate<IDecoratedService>((inner, provider) => new OtherDecorator(inner, provider.GetRequiredService<IService>()));

var serviceProvider = collection.BuildServiceProvider();

// When we resolve the IDecoratedService service, we'll get the following structure:
// OtherDecorator -> Decorator -> Decorated
var instance = serviceProvider.GetRequiredService<IDecoratedService>();

感兴趣的可以看一下实现,大体上和上面的思路是一样的