.NET 8 新特性 Keyed Services 的正确打开方式

.NET 8 引入了一项新特性:键控服务(Keyed Services)。它允许我们使用不同的名称注册同一接口的多个实现,并在运行时根据名称选择合适的实现。

官方示例

官方文档给出了一个使用键控服务的示例代码:

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

上面的代码使用[FromKeyedServices]属性,按照名称来访问已注册的服务,比如在big接口使用BigCache提供的能力。

代码异味

然而,我认为这种用法并不是很合理:既然在设计阶段,我们就已经明确了需要使用哪个接口的实现,那么为什么还要依赖注入一个通用的接口呢?

在这种情况下,我建议直接定义两个继承自ICache的特定接口,IBigCacheISmallCache,并且在代码中使用这些继承的接口。

这样可以更清楚地表达程序的意图,而在通用的地方我们仍然可以使用ICache

builder.Services.AddSingleton<ICache, BigCache>();
builder.Services.AddSingleton<IBigCache, BigCache>();
builder.Services.AddSingleton<ISmallCache, SmallCache>();

app.MapGet("/common", (ICache cache) => cache.Get("date"));
app.MapGet("/big", (IBigCache bigCache) => bigCache.Get("date"));
app.MapGet("/small", (ISmallCache smallCache) => smallCache.Get("date"));

那么,键控服务毫无用武之地吗?

不!

键控服务的优势在于,它可以让我们在运行时动态地选择合适的服务,而不是在编译时就固定好。

这样,我们可以根据不同的场景或条件,使用不同的服务实现。

策略模式

策略模式是一种行为型设计模式,它定义了一系列算法,并将它们封装成一个个独立的类。然后,根据不同的情况,选择合适的算法来执行。

例如,假设我们在开发一个电商网站,需要实现将订单信息发送给客户的功能。而具体发送方式是由客户决定的,存储在用户表的SendType字段(Email 和 Sms)。

如果不使用键控服务,我们可能会写出这样的代码:

# 依赖注入
builder.Services.AddTransient<IEmailSender, EmailSender>();
builder.Services.AddTransient<ISmsSender, SmsSender>();
 
# 使用
if(user.SendType=="Email")
{
    emailSender.Send(user, order);
}
else if(user.SendType=="Sms")
{
    smsSender.Send(user, order);
}

上面的代码存在一些问题:如果增加了发送方式,比如Weixin,那么我们必须同时修改依赖注入和使用代码。而且,这样的代码也不够灵活和可扩展。

如果使用键控服务,我们可以使用如下代码实现相同功能:

builder.Services
  .AddKeyedTransient<ISender, EmailSender>("Email");
builder.Services
  .AddKeyedTransient<ISender, SmsSender>("Sms");

[ApiController]
[Route("[controller]")]
public class MyIOController : ControllerBase
{
    private readonly IServiceProvider _serviceProvider;
    public MyIOController(IServiceProvider serviceProvider)
    {
        this._serviceProvider = serviceProvider;
    }

    [HttpGet]
    public async Task Send(int userId, int orderId)
    {
        var user = GetUser(userId);
        var order = GetOrder(orderId);
        var service = _serviceProvider
            .GetRequiredKeyedService<ISender>(user.SendType);
        service.Send(user, order);
    }
}

上面的代码,使用了user.SendType作为键。这样,我们就可以根据用户表中存储的值,在运行时选择合适的发送方式。

当增加了发送方式,比如Weixin,我们只需再注册一个键控服务即可:

builder.Services
  .AddKeyedTransient<ISender, WeixinSender>("Weixin");

总结

键控服务是.NET 8 的一个新特性,它可以让我们为同一个接口注册多个实现类,并在运行时根据名称选择合适的服务。

键控服务适合用于实现策略模式。如果你有其他使用键控服务的场景或想法,欢迎留言分享!