如果我理解正确,依赖注入的典型机制是通过类构造函数或类的公共属性(成员)进行注入。
这会暴露被注入的依赖项,并违反了面向对象编程中的封装原则。
我对这种权衡做出的判断是否正确?如何处理这个问题?
请参阅下面我自己的答案。
如果我理解正确,依赖注入的典型机制是通过类构造函数或类的公共属性(成员)进行注入。
这会暴露被注入的依赖项,并违反了面向对象编程中的封装原则。
我对这种权衡做出的判断是否正确?如何处理这个问题?
请参阅下面我自己的答案。
这个问题可以有另一种有趣的理解方式。
当我们使用IoC/依赖注入时,我们并没有在使用面向对象编程(OOP)的概念。尽管我们使用的是面向对象语言作为“宿主”,但IoC背后的思想来自组件化软件工程,而不是面向对象。
组件化软件是关于管理依赖关系的 - 常见使用例子是.NET的程序集机制。每个程序集都发布它所引用的程序集列表,这使得汇集(和验证)运行应用程序所需的部分变得更加容易。
通过在我们的面向对象程序中应用类似的技术,如IoC,我们旨在使程序更容易配置和维护。发布依赖关系(例如构造函数参数等)是其中的一个关键部分。封装并不真正适用,因为在组件/服务导向的世界中,没有“实现类型”来泄漏细节。
不幸的是,我们的语言目前并没有将精细的面向对象概念与粗粒度的组件化概念区分开来,因此这是一个你必须仅靠脑海中理解的区别 :)
这是一个很好的问题 - 但是如果要满足对象的依赖关系,它最纯粹的封装形式在某些时候需要被违反。一些提供依赖项的提供者 必须 知道所讨论的对象需要 Foo
,并且提供者必须有一种方法来为对象提供 Foo
。
经典的情况是如你所说,通过构造函数参数或设置方法来处理后一种情况。但是,并非总是如此-例如,我知道 Java 中 Spring DI 框架的最新版本允许您使用私有字段进行注释 (例如,使用 @Autowired
),并且依赖项将通过反射设置而无需通过任何类的公共方法/构造函数来公开依赖项。这可能是您正在寻找的解决方案。
话虽如此,我认为构造函数注入也不是问题。我一直认为对象在构建之后应该是完全有效的,因此为了执行其角色(即处于有效状态),它需要的任何东西都应通过构造函数提供。如果您拥有一个需要协作才能工作的对象,则在新创建类的实例时公开此要求并确保其得到满足似乎对我是可以接受的。
理想情况下,在处理对象时,您始终通过接口与它们交互,并且您做得越多(并且通过 DI 进行依赖项连接),就越不必自己处理构造函数。在理想情况下,您的代码不会处理或甚至创建类的具体实例;因此,它只会通过 DI 得到一个 IFoo
,而不用担心 FooImpl
的构造函数需要执行其工作的事情,实际上甚至不知道 FooImpl
的存在。从这个角度来看,封装是完美的。
当然,这只是我的观点,但我认为DI并不一定违反封装性,事实上可以通过将所有必要的内部知识集中到一个位置来帮助封装。这本身就是一件好事,更好的是,这个位置在你自己的代码库之外,所以你编写的代码不需要了解类的依赖关系。
这会暴露出被注入的依赖项,违反了面向对象编程的封装原则。
说实话,其实任何东西都违反了封装原则。 :) 封装是一种必须小心对待的温柔原则。
那么,到底有哪些东西违反了封装原则呢?
继承会违反。
“因为继承将子类暴露给父类实现的细节,所以通常说‘继承破坏封装’”。(Gang of Four 1995:19)
面向方面编程会违反。例如,你注册onMethodCall()回调就会给你一个很好的机会来注入代码到正常的方法评估中,添加奇怪的副作用等。
C++中的友元声明会违反。
Ruby中的类扩展会违反。只需在完全定义字符串类之后在某个地方重新定义一个字符串方法即可。
嗯,很多东西会违反封装原则。
封装是一项好的和重要的原则。但不是唯一的原则。
switch (principle)
{
case encapsulation:
if (there_is_a_reason)
break!
}
是的,依赖注入(DI)违反了封装性(也被称为“信息隐藏”)。
但真正的问题在于开发者将其用作违反KISS(保持简单和简洁)和YAGNI(你不会需要它)原则的借口。
个人而言,我更喜欢简单有效的解决方案。我通常使用“new”操作符在需要和任何地方实例化有状态依赖项。这是简单、良好封装、易于理解和测试的。那么,为什么不呢?
它不违反封装。您提供了一个协作者,但类可以决定如何使用它。只要遵循告诉别问的原则,一切都很好。我发现构造函数注入更可取,但是设置器也可以,只要它们是智能的。也就是说,它们包含维护类所代表的不变量的逻辑。