依赖注入和初始化方法

Posted

技术标签:

【中文标题】依赖注入和初始化方法【英文标题】: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();
    

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

谢谢

【问题讨论】:

【参考方案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 的一个很好的例子,即依赖注入。你能举一个国际奥委会的例子吗?

以上是关于依赖注入和初始化方法的主要内容,如果未能解决你的问题,请参考以下文章

依赖注入

Android依赖注入Dagger的使用和源码解析(上篇)

spring依赖注入使用的啥设计模式?

[IoC容器Unity]第三回:依赖注入

Spring 依赖注入

iOS之深入解析依赖注入的原理与应用