uri*_*rig 123 oop encapsulation dependency-injection inversion-of-control
如果我理解正确,依赖注入的典型机制是通过类的构造函数或通过类的公共属性(成员)注入.
这暴露了注入的依赖性并违反了封装的OOP原则.
我在确定这种权衡时是否正确?你是如何处理这个问题的?
另请参阅下面我对自己问题的回答.
Nic*_*rdt 60
还有另一种看待这个问题的方法,你可能会感兴趣.
当我们使用IoC /依赖注入时,我们没有使用OOP概念.不可否认,我们使用OO语言作为"主机",但IoC背后的思想来自面向组件的软件工程,而不是OO.
组件软件就是关于管理依赖关系 - 常用的一个例子是.NET的汇编机制.每个程序集都会发布它引用的程序集列表,这样可以更容易地将正在运行的应用程序所需的部分组合在一起(并进行验证).
通过IoC在我们的OO程序中应用类似的技术,我们的目标是使程序更易于配置和维护.发布依赖项(作为构造函数参数或其他)是其中的关键部分.封装并不真正适用,因为在面向组件/服务的世界中,没有"实现类型"可以泄漏细节.
不幸的是,我们的语言目前没有将细粒度的,面向对象的概念与粗粒度的面向组件的概念隔离开来,所以这是一个区别,你必须在脑海中只有:)
And*_*yle 29
这是一个很好的问题 - 但是在某些时候,如果对象永远要满足其依赖性,则需要违反其最纯粹的形式.某些依赖项提供者必须知道所讨论的对象需要一个Foo,并且提供者必须有一种方法来提供Foo该对象.
经典地说,后一种情况可以通过构造函数参数或setter方法来处理.但是,这不一定是真的 - 例如,我知道Java中的Spring DI框架的最新版本允许您注释私有字段(例如with @Autowired),并且将通过反射设置依赖项,而无需通过反映来公开依赖项任何类公共方法/构造函数.这可能是您正在寻找的那种解决方案.
也就是说,我认为构造函数注入也不是问题.我一直认为对象在构造之后应该是完全有效的,因此无论如何,它们为了执行它们的角色(即处于有效状态)所需的任何东西都应该通过构造函数提供.如果你有一个需要协作者工作的对象,我觉得构造函数公开地宣传这个要求并确保在创建一个新的类实例时完成它.
理想情况下,在处理对象时,无论如何都要通过接口与它们进行交互,并且执行此操作的次数越多(并且通过DI连接依赖关系),实际上您自己必须处理构造函数的次数就越少.在理想情况下,您的代码不会处理甚至创建类的具体实例; 所以它只是IFoo通过DI 得到了,而不用担心构造函数FooImpl表明它需要做什么工作,事实上甚至没有意识到它FooImpl的存在.从这个角度来看,封装是完美的.
这当然是一种观点,但在我看来,DI并不一定违反封装,事实上可以通过将所有必要的内部知识集中到一个地方来帮助它.这不仅仅是一件好事,但更好的是这个地方在你自己的代码库之外,所以你编写的代码都不需要知道类的依赖性.
top*_*dev 17
这暴露了注入的依赖性并违反了封装的OOP原则.
好吧,坦率地说,一切都违反了封装.:)这是一种必须妥善对待的温柔原则.
那么,什么违反了封装?
继承确实如此.
"因为继承将子类暴露给其父实现的细节,所以通常会说'继承会破坏封装'".(Gang of Four 1995:19)
面向方面的编程 确实如此.例如,您注册onMethodCall()回调,这为您提供了一个很好的机会,可以将代码注入正常的方法评估,添加奇怪的副作用等.
C++ 中的朋友声明确实如此.
Ruby 中的类扩展确实如此.在完全定义字符串类之后,只需在某处重新定义字符串方法.
好了,很多东西呢.
封装是一个很好而重要的原则.但不是唯一的.
switch (principle)
{
case encapsulation:
if (there_is_a_reason)
break!
}
Run Code Online (Sandbox Code Playgroud)
Rog*_*rio 13
是的,DI违反了封装(也称为"信息隐藏").
但真正的问题出现在开发人员将其作为违反KISS(保持简短和简单)和YAGNI(你不需要它)原则的借口时.
就个人而言,我更喜欢简单有效的解决方案.我主要使用"new"运算符来随时随地实例化有状态依赖项.它简单,封装良好,易于理解,易于测试.那么,为什么不呢?
一个好的依赖注入容器/系统将允许构造函数注入.依赖对象将被封装,并且根本不需要公开公开.此外,通过使用DP系统,您的代码甚至都不会"知道"对象的构造细节,甚至可能包括正在构造的对象.在这种情况下有更多的封装,因为几乎所有的代码不仅不受封装对象的了解,而且甚至不参与对象构造.
现在,我假设您正在与创建的对象创建自己的封装对象的情况进行比较,很可能是在其构造函数中.我对DP的理解是,我们希望将此责任从对象中移除并将其交给其他人.为此,"其他人",在这种情况下是DP容器,确实具有"违反"封装的亲密知识; 好处是它将这些知识从对象中拉出来.有人必须拥有它.你的应用程序的其余部分没有.
我会这样想:依赖注入容器/系统违反了封装,但是你的代码没有.事实上,您的代码更加"封装".
小智 5
这与投票的答案类似,但我想大声思考 - 也许其他人也这么看问题。
经典面向对象使用构造函数为类的使用者定义公共“初始化”契约(隐藏所有实现细节;也称为封装)。该契约可以确保实例化后您拥有一个随时可用的对象(即用户无需记住(呃,忘记)额外的初始化步骤)。
(构造函数) DI 通过这个公共构造函数接口泄露实现细节,无可否认地破坏了封装。只要我们仍然考虑负责为用户定义初始化契约的公共构造函数,我们就造成了可怕的违反封装性的情况。
理论例子:
Foo类有 4 个方法,需要一个整数进行初始化,因此它的构造函数看起来像Foo(int size) ,并且Foo类的用户立即清楚他们必须在实例化时提供一个大小,以便 Foo 工作。
假设 Foo 的这个特定实现可能还需要IWidget来完成其工作。此依赖项的构造函数注入将使我们创建一个类似Foo(int size, IWidget widget) 的构造函数
令我烦恼的是,现在我们有一个将初始化数据与依赖项混合在一起的构造函数- 一个输入是类的用户感兴趣的(size),另一个是内部依赖项,它只会让用户感到困惑,并且是一种实现详细信息(小部件)。
大小参数不是依赖项 - 它只是每个实例的初始化值。IoC 对于外部依赖项(如小部件)来说是花哨的,但对于内部状态初始化却不是。
更糟糕的是,如果该类的 4 个方法中只有 2 个需要 Widget,该怎么办?即使可能不使用 Widget,我也可能会产生实例化开销!
如何妥协/调和?
一种方法是专门切换到接口来定义操作契约;并废除用户对构造函数的使用。为了保持一致,所有对象都必须仅通过接口进行访问,并且仅通过某种形式的解析器(如 IOC/DI 容器)进行实例化。只有容器才能实例化事物。
这就解决了 Widget 依赖关系,但是我们如何在不诉诸 Foo 接口上单独的初始化方法的情况下初始化“size”呢?使用此解决方案,我们无法确保 Foo 实例在您获取实例时已完全初始化。真糟糕,因为我真的很喜欢构造函数注入的想法和简单性。
当初始化不仅仅是外部依赖项时,如何在这个 DI 世界中实现有保证的初始化?
正如Jeff Sternal在对该问题的评论中指出的那样,答案完全取决于您如何定义封装。
封装的含义似乎有两个主要阵营:
File对象可能有方法Save,Print,Display,ModifyText,等。这两个定义彼此直接矛盾。如果File对象可以自行打印,它将在很大程度上取决于打印机的行为。另一方面,如果它仅知道可以为其打印的内容(一个IFilePrinter或某些此类接口),则该File对象不必了解任何有关打印的信息,因此使用它可以减少对对象的依赖。
因此,如果使用第一个定义,依赖项注入将破坏封装。但是,坦率地说,我不知道我是否喜欢第一个定义-它显然无法扩展(如果这样,MS Word将是一大类)。
另一方面,如果您使用封装的第二种定义,则依赖注入几乎是必需的。