.NET依賴注入之一個介面多個實現

2023-02-26 18:00:33

前言

最近又在專案中碰到需要將原本單實現的介面改造成多個實現的場景,這裡記錄一下常見的幾種改法。

假設已經存在如下介面ICustomService和其實現CustomService,由於只有一種實現,注入和使用非常容易。

public interface ICustomService
{
    void MethodA();
    void MethodB();
}
public class CustomService: ICustomService
{
    public void MethodA()
    {
    }

    public void MethodB()
    {
    }
}

//注入
builder.Services.AddTransient<ICustomService, CustomService>();

//使用
private readonly ICustomService _customService;
public CustomController(ICustomService customService)
{
  _customService = customService;
}

現在我們需要增加一種實現。

使用多個介面實現

我們可以將原ICustomService內的方法移到到一個新的基介面,共用出來,需要多少個實現,就建立多少個空介面繼承該基介面。

//基介面
public interface ICustomBaseService
{
    void MethodA();
    void MethodB();
}

//多個空介面
public interface ICustomService : ICustomBaseService
{
}

public interface ICustomServiceV2 : ICustomBaseService
{
}

//第一種實現
public class CustomService: ICustomService
{
    public void MethodA()
    {
    }

    public void MethodB()
    {
    }
}

//第二種實現
public class CustomServiceV2: ICustomServiceV2
{
    public void MethodA()
    {
    }

    public void MethodB()
    {
    }
}

//注入
builder.Services.AddTransient<ICustomService, CustomService>();
builder.Services.AddTransient<ICustomServiceV2, CustomServiceV2>();


//使用
private readonly ICustomService _customService;
private readonly ICustomServiceV2 _customServiceV2;
public CustomController(ICustomService customService,ICustomServiceV2 customServiceV2)
{
  _customService = customService;
  _customServiceV2 = customServiceV2;
}

這種實現方式需要增加了一套空介面做隔離,看似可能比較「浪費」,但後期隨著專案的演進,ICustomServiceICustomServiceV2可能會慢慢分化,我們可以很方便的為它們擴充各種獨有方法。

使用單介面實現

如果我們確定不要要多個介面,也可以使用下面的單介面實現

public interface ICustomService
{
    void MethodA();
    void MethodB();
}

//第一種實現
public class CustomService: ICustomService
{
    public void MethodA()
    {
    }

    public void MethodB()
    {
    }
}

//第二種實現
public class CustomServiceV2: ICustomService
{
    public void MethodA()
    {
    }

    public void MethodB()
    {
    }
}

//注入
builder.Services.AddTransient<ICustomService, CustomService>();
builder.Services.AddTransient<ICustomService, CustomServiceV2>();


//使用
private readonly ICustomService _customService;
private readonly ICustomServiceV2 _customServiceV2;
public CustomController(IEnumerable<ICustomService> customServices)
{
 _customService = customServices.ElementAt(0);
 _customServiceV2 = customServices.ElementAt(1);
}

從上面程式碼可以看到,我們是為從介面ICustomService註冊兩個實現,並從IEnumerable<ICustomService>解析出了這兩個實現。這裡可能會有兩個疑問

  1. 為什麼第一個實現CustomService沒有被第二個實現CustomServiceV2替換掉?
  2. 為什麼可以從IEnumerable<ICustomService>解析到我們需要的服務?

答案在Microsoft.Extensions.DependencyInjection.ServiceDescriptorMicrosoft.Extensions.DependencyInjection.ServiceCollection 這兩個類裡,程序裡,依賴注入的服務,會被新增到ServiceCollection裡,ServiceCollection是一組ServiceDescriptor的集合,ServiceDescriptor通過服務型別、實現以及生命週期三個組合在一起構成的標識來確定服務。而ICustomService+CustomService+TransientICustomService+CustomServiceV2+Transient是兩個不同的ServiceDescriptor,因此不會被替換。同時服務型別的ServiceDescriptor會被聚合在一起,於是我們可以很方便的從IEnumerable物件中解析出所有的同型別的服務。

總結

本質上,兩種方法都是多型性(Polymorphism)的應用,沒有優劣之分,根據場景選擇合適的寫法。

連結

https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
https://github.com/dotnet/runtime