在构造函数中组合依赖项和值

本文关键字:依赖 组合 构造函数 | 更新日期: 2023-09-27 18:15:25

我经常遇到这样的问题:一个类既需要依赖项又需要初始化一些字符串值。例如,考虑以下代码:

public class CustomerService
{
   public CustomerService(ITcpConnection connection, string realm)
   {
   }
}

它的问题是,由于额外的字符串参数,它不容易从DI容器中取出。解决这个问题的最好方法是什么?以下是我能想到的方法:

  1. Initialize方法(它不够好,因为它提供了Mark Seeman的定义的时间耦合,我同意这不是一个很好的方式)
  2. 抽象工厂(我真的不需要额外的间接层,因为参数是一个配置,我知道它在我做配置的时候)
  3. 在示例中使用,并在DI容器
  4. 中注册类的实例

在构造函数中组合依赖项和值

最好避免在同一个构造函数中混合基本配置值和(服务)依赖项。它们使DI配置复杂化,并且经常导致不可读和脆弱的组合根。

改为:

选项1:抽象配置值

当将相同的配置值注入多个类时,通常会丢失一个抽象。一个很好的例子是将string connectionString值注入到所有存储库中。在这种情况下,应用程序可能缺乏IConnectionFactory抽象(或类似的东西)。不是将连接字符串注入到许多类中,而是将连接字符串注入到IConnectionFactory实现中(可能只将该连接字符串作为其构造函数参数),并让其他服务依赖于IConnectionFactory而不是string。这样,这些服务就不必处理连接的创建;连接工厂可以这样做。

选项2:将配置值封装到配置类型

基本类型在解析类型时会导致歧义。您注入的string是文件路径还是连接字符串?你注入的int,是重试次数,还是顾客购买物品的最低年龄?

为了防止歧义,最好将类的所有配置值包装到它自己的配置类型中,即使该类只需要一个值。以包装TimeSpanSqlUnitOfWorkSettings为例。

public sealed class SqlUnitOfWorkSettings
{
    public readonly TimeSpan ConnectionTimeout;
    public SqlUnitOfWorkSettings(TimeSpan connectionTimeout)
    {
        this.ConnectionTimeout = connectionTimeout;
    }
}

不再依赖TimeSpan, SqlUnitOfWork现在可以依赖SqlUnitOfWorkSettings了:

public sealed class SqlUnitOfWork : IUnitOfWork
{
    private readonly SqlUnitOfWorkSettings settings;
    public SqlUnitOfWork(SqlUnitOfWorkSettings settings)
    {
        this.settings = settings;
    }
}

新的配置类的使用简化了在DI容器中的注册,因为没有歧义:

container.RegisterInstance(
    new SqlUnitOfWorkSettings(connectionTimeout: TimeSpan.FromSeconds(30));
container.Register<IUnitOfWork, SqlUnitOfWork>(Lifestyle.Scoped);

选项3:创建子类型

如果不能使用配置类(例如,因为你不能更改源代码),你可以派生一个放在你的组合根中的子类型,并定义适当的构造函数,然后注册该子类型:

public class MyService : SomeExternalService
{
    public MyService(ISomeDependency dep) : base(dep, "My Config Value") { }
}
// Registration
container.Register<IService, MyService>();