C# 使用依赖项注入在运行时决定实现

C# 使用依赖项注入在运行时决定实现,c#,dependency-injection,C#,Dependency Injection,我有一个场景,我需要得到一个直到运行时才知道的实现。 到目前为止,我的方法是创建一个服务类(从使用它们的类中抽象逻辑)。 客户机和服务已向DI注册。调用类只请求服务 以下是两种不同的方法(简化): 以及: 公共类服务 { 私有只读服务器ViceProvider服务提供商; 公共服务(IServiceProvider服务提供商) { this.serviceProvider=serviceProvider; } 公共数据GetData(字符串客户端、字符串某物) { 如果(客户端==“客户端1”)

我有一个场景,我需要得到一个直到运行时才知道的实现。 到目前为止,我的方法是创建一个服务类(从使用它们的类中抽象逻辑)。 客户机和服务已向DI注册。调用类只请求服务

以下是两种不同的方法(简化):

以及:

公共类服务
{
私有只读服务器ViceProvider服务提供商;
公共服务(IServiceProvider服务提供商)
{
this.serviceProvider=serviceProvider;
}
公共数据GetData(字符串客户端、字符串某物)
{
如果(客户端==“客户端1”)
返回此.serviceProvider.GetRequiredService().GetData(某物);
返回此.serviceProvider.GetRequiredService().GetData(某物);
}
}
然后调用以下命令来使用:
service.GetData(“client1”,…)

这些备选方案中有哪一个是这样做的好方法?一个比另一个好吗?

两者都有缺点:

  • 版本1总是得到两个实例化的客户端。如果实例化是一个繁重的过程,这是不好的

  • 版本2隐藏了它的依赖关系。这是众所周知的反模式

完美的解决方案是注入
IClient1Factory
IClient2Factory
,并在需要时调用它们的工厂创建方法。这意味着您仍然只实例化一个,而不是两个,但您不会隐藏依赖项


正如总是没有完美的解决方案一样,您现在显然需要编写和维护这两个工厂。确保它是值得的。如果Client1/Client2类实例化只是一个简单的
new
,构造函数中没有发生任何事情,那么您可能希望选择更简单的版本1方法。如果它很简单并且有效,不要将它包装在太多的图案层中。只有在需要时才使用它们。

您可以做的是注入ClientXt一个界面,以获得更大的灵活性,如下面的代码所示:

public class Service<T> where T : IClient
{
    private readonly IServiceProvider serviceProvider;

    public Service(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public Data GetData<T>(string something)
    {
        return this.serviceProvider.GetRequiredService<T>().GetData(something);
    }
}
公共类服务,其中T:IClient
{
私有只读服务器ViceProvider服务提供商;
公共服务(IServiceProvider服务提供商)
{
this.serviceProvider=serviceProvider;
}
公共数据GetData(字符串)
{
返回此.serviceProvider.GetRequiredService().GetData(某物);
}
}

在所有情况下,选项2都是错误的

  • 服务定位器模式被广泛认为是一种反模式。它可能会解决手头的问题,但也会带来许多其他问题
  • 您让您的消费者决定使用哪个客户端,这实际上否定了让服务使用构造函数定义自己的依赖关系的想法
  • 魔术弦从来都不可取。如果您的消费者正在决定客户机,那么他们必须使用一些神奇的字符串来选择正确的客户机是没有意义的。让它们通过客户机本身的错误几率要小得多,但是
    服务
    不依赖客户机对象的DI框架,这可能会破坏设置的目的

如果每次调用
GetData()
时动态选择客户机,那么选项1是一种有效的方法

尽管我建议尽可能使用比“client1”和“client2”更具描述性的名称


如果客户机的选择是动态的,但在应用程序启动后保持不变,这意味着在同一运行时对
GetData()
的所有调用都将由同一客户机处理,那么最好在注册依赖项时选择此客户机:

// Startup.cs

if( /* selection criteria */)
{
    services.AddScoped<IClient, Client1>();
}
else
{
    services.AddScoped<IClient, Client2>();
}

// Service.cs

public class Service
{
    private readonly IClient client;

    public Service(IClient client)
    {
        this.client = client;
    }

    public Data GetData(string something)
    {
        return this.client.GetData(something);
    }
}
//Startup.cs
如果(/*选择标准*/)
{
services.addScope();
}
其他的
{
services.addScope();
}
//服务中心
公务舱服务
{
私有只读IClient客户端;
公共服务(IClient客户端)
{
this.client=client;
}
公共数据GetData(字符串)
{
返回这个.client.GetData(something);
}
}
尽管我建议尽可能使用比“client1”和“client2”更具描述性的名称

请注意,您的选择标准可以是您想要的任何标准,例如应用程序配置值、数据库值、环境值、编译类型(调试/发布)等等。。。世界就是你的牡蛎



还要评估是否最好实现一个额外的抽象,该抽象可以决定重定向到哪个客户端(例如
ClientFactory
ClientRouter
)。这并不总是必要的,但如果您的需求不那么琐碎,那么抽象可能有助于保持简单。

这是如何以及何时改变的?如果这可以在启动期间确定(这也是运行时),那么您只需向DI注册,而不必关心任何事情。或者这是在运行时不断变化的东西?在最后一种情况下,我可能会为它创建一个工厂,然后在任何可以使用不同实现的地方注入该工厂。第一种不应该工作(注入类而不是接口),第二种遵循服务定位器反模式,所以我不会使用它们。更好的方法取决于如何在调用中获得“客户机”,但我会选择类似工厂的方法为什么类而不是接口会停止DI?这取决于它是如何注册的。当你说“在运行时”,你的意思是决定是在应用程序启动时确定的,还是在应用程序启动后,对你处理的每个请求都会动态地做出选择?对于运行时,我的意思是它(可以)为每个请求动态地改变。如果客户端的实例化是一个繁重的过程,一个可能反复生成这些客户机的工厂(因为为什么还要使用一个绑定到特定客户机类型的工厂?)将是比inst更差的解决方案
public class Service<T> where T : IClient
{
    private readonly IServiceProvider serviceProvider;

    public Service(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
    }

    public Data GetData<T>(string something)
    {
        return this.serviceProvider.GetRequiredService<T>().GetData(something);
    }
}
// Startup.cs

if( /* selection criteria */)
{
    services.AddScoped<IClient, Client1>();
}
else
{
    services.AddScoped<IClient, Client2>();
}

// Service.cs

public class Service
{
    private readonly IClient client;

    public Service(IClient client)
    {
        this.client = client;
    }

    public Data GetData(string something)
    {
        return this.client.GetData(something);
    }
}