IoC,依赖注入和构造函数参数

本文关键字:构造函数 参数 注入 依赖 IoC | 更新日期: 2023-09-27 18:16:31

我有一个服务,我希望能够根据控制反转原理创建,所以我已经创建了一个接口和一个服务类。

public interface IMyService
{
    void DoSomeThing1();
    void DoSomeThing2();
    void DoSomeThing3();
    string GetSomething();
}
public class MyService : IMyService
{
    int _initialValue;
    //...
    public MyService(int initialValue)
    {
        _initialValue = initialValue;
    }
    public void DoSomeThing1()
    {
        //Do something with _initialValue
        //...
    }
    public void DoSomeThing2()
    {
        //Do something with _initialValue
        //...
    }
    public void DoSomeThing3()
    {
        //Do something with _initialValue
        //...
    }
    public string GetSomething()
    {
        //Get something with _initialValue
        //...
    }
}

以Unity为例,我可以设置我的IoC。

public static class MyServiceIoc
{
    public static readonly IUnityContainer Container;
    static ServiceIoc()
    {
        IUnityContainer container = new UnityContainer();
        container.RegisterType<IMyService, MyService>();
        Container = container;
    }
}

问题出在构造函数参数上。我可以使用ParameterOverride,比如

var service = MyServiceIoc.Container.Resolve<IMyService>(new ParameterOverrides
{                                                                                   
    {"initialValue", 42}
});

但是我不想使用松散类型的形参。如果有人更改了构造函数参数名或添加了一个参数怎么办?他不会在完成时得到警告,也许除了最终用户没有人会发现它。也许程序员为测试更改了IoC设置,但忘记了"发布"使用,那么即使是100%代码覆盖率的代码库也无法检测到运行时错误。

可以在接口和服务中添加init函数,但是服务的用户必须理解这一点,并记得每次获得服务的实例时调用init函数。该服务变得不那么容易解释,容易被错误使用。如果方法不依赖于调用的顺序,那是最好的。

使它更安全的一种方法是在Ioc上使用create -函数。
public static class MyServiceIoc
{
    //...
    public IMyService CreateService(int initialValue)
    {
        var service = Container.Resolve<IMyService>();
        service.Init(initialValue);
    }
}

但是如果你只看服务和它的接口,上面提到的问题仍然适用。

对于这个问题,谁有一个可靠的解决方案?在使用IoC的情况下,如何以安全的方式将初始值传递给服务?

IoC,依赖注入和构造函数参数

DI容器是基于反射的,并且基本上是弱类型的。这个问题比原始依赖要广泛得多——它无处不在。

一旦你做了下面的事情,你就已经失去了编译时的安全性:

IUnityContainer container = new UnityContainer();
container.RegisterType<IMyService, MyService>();
var service = container.Resolve<IMyService>(new ParameterOverrides
{                                                                                   
    {"initialValue", 42}
});
问题是你可以去掉第二个语句中的,并且代码仍然可以编译,但是现在它不再工作了:
IUnityContainer container = new UnityContainer();
var service = container.Resolve<IMyService>(new ParameterOverrides
{                                                                                   
    {"initialValue", 42}
});

请注意,缺乏编译时安全性与具体依赖无关,而是与DI容器相关的事实有关。

这也不是Unity的问题;它适用于所有DI容器。

的情况下,使用DI容器可能是有意义的,但在大多数情况下,纯DI是一种更简单、更安全的选择:
IMyService service = new MyService(42);

在这里,如果其他人在你看向别处时更改了API,你会得到一个编译器错误。这很好:编译器错误给你比运行时错误更直接的反馈。


作为题外话,当你传入一个原始依赖并无形地将其转化为具体依赖时,你会使客户端更难以理解发生了什么。

我建议这样设计:

public class MyService : IMyService
{
    AnotherClass _anotherObject;
    // ...
    public MyService(AnotherClass anotherObject)
    {
        _anotherObject = anotherObject;
    }
    // ...
}

使用Pure DI组合仍然是简单且类型安全的:

IMyService service = new MyService(new AnotherClass(42));

如何在使用IoC的情况下以安全的方式将初始值传递给我的服务?

你可以显式调用一个类型的构造函数,同时使用IUnityContainer在Unity中注册它。RegisterInstance方法:

container.RegisterInstance<IMyService>(new MyService(42));

这将为您提供您提到的编译时安全性,但代价是它将只实例化一次,并且将立即创建(而不是在第一次请求时创建)。

您可以通过使用一个方法重载来处理这个缺点,该方法接受一个LifetimeManager类。

这取决于您的用例,但在IoC容器世界中,它可能看起来像这样:

public class MyService : IMyService
{
    int _initialValue;
    // ...
    public MyService(IConfigurationService configurationService)
    {
        _initialValue = configurationService.GetInitialValueForMyService();
    }
    
    // ...
}

如果你的类的构造函数参数在你的代码之外(例如在第三方库中),你可以使用适配器。

public class AdaptedMyService : MyService
{
    public AdaptedMyService(IConfigurationService configurationService)
        : base(configurationService.GetInitialValueForMyService())
    {
    }
}

然后在IoC容器中注册适配类,像这样:

container.Register<IMyService, AdaptedMyService>();