依赖注入和初始化方法
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 的一个很好的例子,即依赖注入。你能举一个国际奥委会的例子吗?以上是关于依赖注入和初始化方法的主要内容,如果未能解决你的问题,请参考以下文章