【问题标题】:Dependency Injection and initialization methods依赖注入和初始化方法
【发布时间】:2013-07-27 13:27:52
【问题描述】:

我阅读了Miško Hevery's Guide: Writing Testable Code,如果“构造函数完成后对象未完全初始化(注意初始化方法)”,它会发出警告信号。

假设我编写了一个 Redis 包装类,它有一个接受主机名和端口的 init 方法。根据 Miško 的说法,这是一个警告信号,因为我需要调用它的 init 方法。

我正在考虑的解决方案如下: 对于每个需要这种初始化的类,创建一个工厂类,该类具有创建该类的 Create 方法,并调用它的 init 方法。

现在在代码中:而不是使用类似的东西:

class Foo
{
    private IRedisWrapper _redis;
    public Foo(IRedisWrapper redis)
    {
       _redis = redis;
    }
}
....
IRedisWrapper redis = new RedisWrapper();
redis.init("localhost", 1234);
Foo foo = new Foo(redis);

我会使用类似的东西:

class Foo
{
    private IRedisWrapper _redis;
    public Foo(IRedisWrapper redis)
    {
       _redis = redis;
    }
}
....
RedisWrapperFactory redisFactory = new RedisWrapperFactory();
IRedisWrapper redisWrapper = redisFactory.Create();
Foo foo = new Foo(redisWrapper);

我使用Simple Injector 作为 IOC 框架,这使得上述解决方案成为问题 - 在这种情况下,我会使用类似的东西:

class Foo
{
    private RedisWrapper _redis;
    public Foo(IRedisWrapperFactory redisFactory)
    {
       _redis = redisFactory.Create();
    }
}

我真的很想听听您对上述解决方案的意见。

谢谢

【问题讨论】:

    标签: c# unit-testing design-patterns dependency-injection simple-injector


    【解决方案1】:

    也许我误解了你的问题,但我不认为 Simple Injector 是这里的限制因素。由于构造函数应该做的越少越好,你不应该在你的构造函数中调用Create 方法。这样做甚至是一件奇怪的事情,因为工厂旨在延迟类型的创建,但由于您在构造函数中调用Create,因此不会延迟创建。

    您的Foo 构造函数应该只依赖于IRedisWrapper,并且您应该将redisFactory.Create() 调用提取到您的DI 配置中,如下所示:

    var redisFactory = new RedisWrapperFactory();
    
    container.Register<IRedisWrapper>(() => redisFactory.Create());
    

    但是由于工厂的唯一目的是防止在整个应用程序中重复初始化逻辑,它现在失去了它的目的,因为工厂唯一使用的地方是在 DI 配置中。所以你可以把工厂扔出去,写下如下注册:

    container.Register<IRedisWrapper>(() =>
    {
        IRedisWrapper redis = new RedisWrapper();
        redis.init("localhost", 1234);
        return redis;
    });
    

    您现在将Create 方法的主体放置在匿名委托中。您的 RedisWrapper 类当前有一个默认构造函数,所以这种方法很好。但是如果RedisWrapper 开始获得自己的依赖项,最好让容器创建该实例。这可以按如下方式完成:

    container.Register<IRedisWrapper>(() =>
    {
        var redis = container.GetInstance<RedisWrapper>();
        redis.init("localhost", 1234);
        return redis;
    });
    

    当您需要在创建后初始化您的类时,正如RedisWrapper 显然需要的那样,建议的方法是使用RegisterInitializer 方法。最后的代码sn -p可以这样写:

    container.Register<IRedisWrapper, RedisWrapper>();
    
    container.RegisterInitializer<RedisWrapper>(redis =>
    {
        redis.init("localhost", 1234);
    });
    

    这将注册RedisWrapper 以在请求IRedisWrapper 并使用注册的初始化程序初始化RedisWrapper 时返回。此注册可防止对容器的隐藏调用。这提高了性能并提高了容器到diagnose your configuration的能力。

    【讨论】:

    • 太棒了,这正是我正在寻找的答案 - 谢谢!
    • 我想澄清的最后一件事 - 当 Misko 声明“构造函数完成后对象未完全初始化(注意初始化方法)”时 - 他到底是什么意思?
    • 关于temporal coupling。那是一种设计的味道。
    • 所以,实际上,当前的解决方案也是一种设计味道,对吧?删除init方法并将主机名和端口号作为参数添加到构造函数是否更好? (并使用类似的东西:simpleinjector.codeplex.com/discussions/406271) - 我是对的吗?对于这种情况,您有更好的解决方案吗?
    • 抱歉耽搁了。如果您可以防止时间耦合,那就太好了。如果您可以将基元移动到 ctor,那是个好主意。
    【解决方案2】:

    RedisWrapperFactory 作为依赖项似乎不太正确,因为它并不是您真正想要的工厂。当然,除非您需要将特定参数传递给Create()

    我不知道Simple Injector,但我建议如果它不允许您自定义创建对象以使用您的工厂,您可能需要查看其他一些 DI 框架。我使用StructureMap,但还有其他可供选择。

    编辑:话虽如此,如果IRedisWrapper的契约是在构造函数被调用后必须以某种特定的方式初始化,如果你在Foo 无需调用 init()。特别是对于熟悉IRedisWrapper(很多人),而不是熟悉该特定应用程序的 IOC 设置的人(不是很多人)。当然,如果您要使用工厂,正如 Arghya C 所说,也可以通过接口使用它,否则您实际上并没有实现任何目标,因为您无法选择要注入的 IRedisWrapper

    【讨论】:

      【解决方案3】:

      如果您的 RedisWrapperFactory 定义在其他层(它从 DB/文件/某些服务获取数据),则代码将终止依赖注入的目的。您的图层直接依赖于另一个图层。此外,这不再可测试,因为您无法创建用于测试的模拟/伪造对象。显然您不想在测试中进行真正的数据库操作或 I/O 读写或服务调用。

      你可能想做一些类似...

      class Foo
      {
          private IRedisWrapper _redis;
      
          public Foo(IRedisWrapperFactory redisFactory)
          {
              _redis = redisFactory.Create();
          }
      }
      

      在另一层

      public class RedisWrapperFactory : IRedisWrapperFactory
      {
          public IRedisWrapper Create()
          {
              var r = RedisWrapper();
              r.Init("localhost", 1234); //values coming from elsewhere
              return r;
          }                                   
      }
      

      在您的 Bootstrapper() 或 application_Start() 方法中,注入工厂,类似于

      container.Register<IRedisWrapperFactory, RedisWrapperFactory>();
      

      【讨论】:

      • 这正是我的意思 - 你在哪里看到差异?
      • 唯一的区别是 Foo 构造函数中使用的接口而不是具体类。因此,为了测试,您可以模拟/伪造接口的实际实现,并使用它来测试 Foo 的行为,而不是 RedisWrapperFactory!
      • 这是 Ninject 的一个很好的例子,即依赖注入。你能举一个国际奥委会的例子吗?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-05-15
      • 1970-01-01
      • 1970-01-01
      • 2018-01-15
      • 2012-08-28
      • 2011-04-19
      • 2013-07-08
      相关资源
      最近更新 更多