.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
的特定接口,IBigCache
和ISmallCache
,并且在代码中使用这些继承的接口。
这样可以更清楚地表达程序的意图,而在通用的地方我们仍然可以使用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 的一个新特性,它可以让我们为同一个接口注册多个实现类,并在运行时根据名称选择合适的服务。
键控服务适合用于实现策略模式。如果你有其他使用键控服务的场景或想法,欢迎留言分享!