依赖注入是否必须以封装为代价?


128

如果我理解正确,则依赖注入的典型机制是通过类的构造函数或类的公共属性(成员)进行注入。

这暴露了要注入的依赖项,并且违反了封装的OOP原理。

在确定这种折衷方案时我是否正确?您如何处理这个问题?

另请在下面查看我对自己问题的回答。


7
这是一个非常聪明的问题,恕我直言
— dfa

5
首先回答这个问题需要一个关于封装含义的争论。;)
— Jeff Sternal

2
封装由接口维护。它们公开了对象的基本特征,并隐藏了诸如依赖项之类的细节。这使我们可以在某种程度上“开放”类,以提供更灵活的配置。
— 劳伦斯·瓦格菲尔德

Answers:


62

您可能会发现解决此问题的另一种方法。

当我们使用IoC /依赖注入时,我们并没有使用OOP概念。诚然,我们使用OO语言作为“宿主”,但是IoC背后的思想来自面向组件的软件工程,而不是OO。

组件软件就是关于管理依赖关系的。常用的例子是.NET的Assembly机制。每个程序集都会发布其引用的程序集列表,这使得将运行的应用程序所需的各个部分放在一起(并验证)变得更加容易。

通过IoC在我们的OO程序中应用类似的技术,我们旨在使程序更易于配置和维护。发布依赖项(作为构造函数参数或其他)是其中的关键部分。封装并不是真正适用的,因为在面向组件/服务的世界中,没有“实现类型”用于泄漏细节。

不幸的是,我们的语言目前没有将细粒度的,面向对象的概念与粗粒度的面向组件的概念区分开,因此这是您仅需牢记的区别:)


18
封装不仅仅是一种奇特的术语。这是具有实际好处的真实事物,并且您认为程序是“面向组件”还是“面向对象”都没有关系。封装被认为可以保护您的对象/组件/服务/任何状态,以免以意想不到的方式更改,并且IoC确实消除了其中的一些保护,因此绝对需要进行权衡。
— 罗恩·英巴尔

1
通过构造函数提供的参数仍属于对象“更改” 的预期方式的范围:显式公开它们,并强制围绕它们的不变量。信息隐藏是您所指的那种隐私的更好的称呼,@RonInbar,它不一定总是有益的(这使面食更难以解开;-)。
— Nicholas Blumhardt'7

2
OOP的全部要点是将意大利面缠结分离为单独的类,并且只有当您要修改该特定类的行为时,您才必须将其弄乱(这就是OOP减轻复杂性的方式)。类(或模块)在公开方便的公共接口的同时封装了其内部结构(这是OOP促进重用的方式)。通过接口公开其依赖项的类为其客户端增加了复杂性,因此,其重用性较低。从本质上讲,它也更脆弱。
— Neutrino

1
无论我以哪种方式看待,在我看来,DI都严重破坏了OOP的某些最有价值的好处,而我还没有遇到过这样一种情况:我发现它实际上可以解决实际问题。存在的问题。
— Neutrino

1
这是解决问题的另一种方法:假设.NET程序集是否选择了“封装”并且没有声明它们依赖的其他程序集。这将是一个疯狂的情况,阅读文档并希望加载后能够正常工作。在该级别上声明依赖关系可使自动化工具处理该应用程序的大规模组合。您必须斜视才能看到类比,但是类似的作用力确实会在组件级别施加。权衡取舍,YMMV一如既往:-)
— Nicholas Blumhardt

29

这是一个很好的问题-但在某些时候,封装在其最纯粹的形式需要,如果对象是永远有它的依赖性履行受到侵犯。依赖项的某些提供者必须知道所讨论的对象需要一个Foo,并且提供者必须具有一种Foo向该对象提供的方式。

通常,后一种情况是按照您所说的通过构造函数参数或setter方法处理的。但是,这不一定是正确的-例如,我知道Java中的Spring DI框架的最新版本可让您注释私有字段(例如使用@Autowired),并且将通过反射来设置依赖关系,而无需通过以下方式公开依赖关系任何类的公共方法/构造函数。这可能是您正在寻找的解决方案。

话虽如此,我也不认为构造函数注入不是很大的问题。我一直觉得对象在构造之后应该是完全有效的,因此无论如何需要通过构造函数提供它们执行其角色(即处于有效状态)所需的任何东西。如果您有一个需要协作者才能工作的对象,那么对我来说,构造函数公开发布此要求并确保在创建类的新实例时可以满足要求就可以了。

理想情况下,在处理对象时,无论如何都要通过接口与对象进行交互,并且这样做越多(并且通过DI关联了依赖项),则实际需要自己处理的构造函数就越少。在理想情况下,您的代码不会处理甚至无法创建类的具体实例;因此,仅IFoo通过DI 即可获得它,而不必担心的构造函数FooImpl表示其需要完成工作,甚至不用担心它FooImpl的存在。从这个角度来看,封装是完美的。

这当然是一种意见,但是在我看来,DI不一定违反封装,实际上可以通过将内部的所有必要知识集中到一个地方来帮助它。这本身不仅是一件好事,而且更好的是,此地方不在您自己的代码库之外,因此您编写的所有代码都无需了解类的依赖关系。


2
好点。我建议不要在私有字段上使用@Autowired;这使全班很难测验;然后如何注入模拟或存根?
— lumpynose

4
我不同意。DI确实违反了封装,可以避免这种情况。例如,通过使用ServiceLocator,显然不需要了解有关客户端类的任何信息。它只需要了解Foo依赖项的实现。但是,在大多数情况下,最好的方法是简单地使用“ new”运算符。
— 罗杰里奥

5
@Rogerio-可以说任何DI框架的行为都与您描述的ServiceLocator完全相同;客户端对Foo实现一无所知,而DI工具也不知道与该客户端有关的一丁点。而且使用“ new”对于违反封装的情况要差得多,因为您不仅需要知道确切的实现类,而且还需要知道它所需的所有依赖项的确切类和实例。
— Andrzej Doyle

4
使用“ new”实例化一个通常不公开的助手类,可以促进封装。DI的替代方法是公开帮助程序类,并在客户端类中添加公共构造函数或设置器。这两个更改都会破坏原始帮助程序类提供的封装。
— 罗杰里奥

1
“这是一个很好的问题-但在某个时候,如果要满足对象的依赖关系,就必须违反其最纯粹形式的封装。”答复的成立前提是完全不正确的。正如@Rogério声明在内部更新依赖关系一样,对象内部满足其依赖关系的任何其他方法都不会违反封装。
— Neutrino

17

这暴露了要注入的依赖项,并且违反了封装的OOP原理。

好吧,坦率地说,一切都违反了封装。:)这是一种温柔的原则,必须加以妥善处理。

那么,什么违反封装?

继承确实。

“因为继承将子类公开给其父级实现的详细信息,所以经常说“继承破坏封装”。(四人帮1995:19)

面向方面的编程 确实可以做到。例如,您注册onMethodCall()回调,这为您提供了一个将代码注入到常规方法评估中的绝好机会,从而增加了奇怪的副作用等。

C ++ 中的 Friend声明可以。

Ruby 中的类扩展确实可以。在完全定义字符串类之后,只需在某个地方重新定义字符串方法。

好吧,很多东西都可以。

封装是一个很好的重要原则。但不是唯一的。

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
“这些是我的原则,如果您不喜欢它们……那么,我还有其他原则。” (格劳乔·马克思)
— 罗恩·英巴尔

2
我认为这很重要。这取决于注入与封装。因此,仅在产生明显好处的地方使用依赖注射。正是DI到处都给DI起了坏名
— Richard Tingle

不知道这个答案想说什么...做DI时可以违反封装,可以还是“永远可以”,因为无论如何都会违反封装,还是DI 可能是违反封装的原因?另一方面,如今,不再需要依赖公共构造函数或属性来注入依赖项。取而代之的是,我们可以将其注入私有的带注释的字段,该字段更简单(代码更少)并保留封装。因此,我们可以同时利用这两个原则。
— 罗杰里奥

继承原则上不会违反封装,但是如果父类编写不当,继承可能会违反封装。您提出的其他要点是一个相当边缘的编程范例,以及几种与体系结构或设计无关的语言功能。
— Neutrino

13

是的,DI违反了封装(也称为“信息隐藏”)。

但是,真正的问题出在开发人员以它为借口违反KISS(保持简短和简单)和YAGNI(您不需要它)原则的时候。

就个人而言,我更喜欢简单有效的解决方案。我主要使用“ new”运算符在任何时候,任何地方实例化有状态依赖。它简单,封装好,易于理解且易于测试。那么,为什么不呢?


前瞻性思考并不可怕,但我同意保持简单,愚蠢,尤其是如果您不需要它时!我已经看到开发人员浪费了周期,因为他们过分地设计了某些东西以致于无法过时,并且基于这种预感,甚至没有已知/怀疑的业务需求。
— 雅各布·麦凯

5

良好的依赖注入容器/系统将允许构造函数注入。依赖对象将被封装,并且根本不需要公开。此外,通过使用DP系统,您的代码都不会“知道”对象的构造细节,甚至可能不包括正在构造的对象。在这种情况下,存在更多的封装,因为几乎所有代码都不仅不了解封装对象,而且甚至不参与对象构造。

现在,我假设您正在与创建对象创建自己的封装对象(最有可能在其构造函数中)的情况进行比较。我对DP的理解是,我们希望将这种责任从对象上移开,然后将其交给其他人。为此,“其他人”(在这种情况下为DP容器)确实具有“违反”封装的深入了解;好处是它将知识从对象本身中拉出来。有人必须拥有它。您的应用程序的其余部分则没有。

我会这样想:依赖注入容器/系统违反了封装,但是您的代码没有这样做。实际上,您的代码比以往更加“封装”。


3
如果您遇到客户端对象可以直接实例化其依赖关系的情况,那为什么不这样做呢?这绝对是最简单的事情,并不一定会降低可测试性。除了简单和更好的封装之外,这还使拥有状态对象而不是无状态单例变得更加容易。
— 罗热里奥

1
除了@Rogério所说的之外,它的效率也可能大大提高。并非世界历史上曾经创建的每个类都需要在其拥有对象的整个生命周期中实例化其每个依赖关系。使用DI的对象将失去对其自身依赖项(即生存期)的最基本控制。
— Neutrino

5

正如Jeff Sternal在对该问题的评论中指出的那样,答案完全取决于您如何定义封装。

封装的含义似乎有两个主要阵营:

  1. 与对象有关的所有事物都是对象上的方法。所以,一个File对象可能有方法Save,Print,Display,ModifyText,等。
  2. 一个对象是它自己的小世界,并且不依赖于外部行为。

这两个定义彼此直接矛盾。如果File对象可以自行打印,它将在很大程度上取决于打印机的行为。另一方面,如果它仅知道可以为其打印的内容(一个IFilePrinter或某些这样的接口),则该File对象不必了解任何有关打印的信息,因此使用它可以减少对对象的依赖。

因此,如果使用第一个定义,依赖项注入将破坏封装。但是,坦率地说,我不知道我是否喜欢第一个定义-它显然无法扩展(如果这样,MS Word将是一大类)。

另一方面,如果您使用封装的第二种定义,则依赖注入几乎是必需的。


对于第一个定义,我绝对同意。它也违反了SoC,这可以说是编程的主要罪过之一,并且可能是它无法扩展的原因之一。
— Marcus Stade'9

4

它不违反封装。您正在提供一个协作者,但是该类可以决定如何使用它。只要您遵循“告诉”,不要问一切都很好。我发现最好使用构造器注入,但是只要是聪明的二传手就可以。也就是说,它们包含维护类表示的不变式的逻辑。


1
因为...不是吗?如果您有一个记录器,并且有一个需要记录器的类,则将记录器传递给该类不会违反封装。这就是依赖注入。
— jrockway

3
我认为您误解了封装。例如,上一个简单的日期类。在内部,它可能具有日,月和年实例变量。如果将这些作为简单的setter公开而没有逻辑,则将破坏封装,因为我可以执行诸如将month设置为2并将day设置为31的操作。另一方面,如果这些setter很聪明并检查不变量,那么一切都很好。 。还要注意,在后一版本中,我可以将存储更改为自1970年1月1日以来的天数,并且只要我适当地重写了日/月/年方法,使用该接口的人员就无需知道这一点。
— 杰森·沃特金斯

2
DI确实确实违反了封装/信息隐藏。如果将私有内部依赖项转换为类的公共接口中公开的内容,那么根据定义,您将破坏该依赖项的封装。
— 罗热里奥

2
我有一个具体的示例,我认为封装受到DI的损害。我有一个FooProvider从数据库中获取“ foo数据”,还有一个FooManager对其进行缓存并在该提供程序之上计算内容。我让我的代码的使用者错误地去了FooProvider来获取数据,而我宁愿将它们封装起来,所以他们只知道FooManager。这基本上是我提出原始问题的触发因素。
— urig 2010年

1
@Rogerio:我认为构造函数不是公共接口的一部分,因为它仅在组合根目录中使用。因此,依赖关系仅由合成根“看到”。组合根的唯一职责是将这些依赖关系连接在一起。因此,使用构造函数注入不会破坏任何封装。
— 杰伊·沙利文

4

这类似于被投票的答案,但我想大声思考-也许其他人也这样看。

  • 经典OO使用构造函数为该类的使用者定义公用的“初始化”协定(隐藏所有实现细节;也称为封装)。该合同可以确保实例化之后,您具有一个随时可用的对象(即,用户无需记住(忘记)其他任何初始化步骤)。

  • (构造函数)DI 通过此公共构造函数接口通过泄漏实现细节,无疑地破坏了封装。只要我们仍然认为公共构造函数负责为用户定义初始化合同,我们就已经造成了可怕的封装违规。

理论示例:

班富有4个方法,并且需要一个整数来进行初始化,因此其构造函数看起来像Foo(int size),并且对于Foo类的用户来说,他们必须立即提供一个大小,这是很清楚的在实例化时才能使Foo工作。

说这个特定的Foo实现可能还需要IWidget来完成其工作。此依赖项的构造方法注入将使我们创建类似Foo(int size,IWidget小部件)的构造方法

令我烦恼的是,现在我们有一个混合的构造函数初始化数据与依赖项函数-一个输入对类(size)的用户来说是令人感兴趣的,另一个输入是一个内部依赖项,仅用于混淆用户,并且是一种实现。详细信息(小部件)。

size参数不是依赖项-它是每个实例的简单初始化值。IoC是外部依赖项(如小部件)的花花公子,但内部状态初始化不是。

更糟糕的是,如果仅对此类的4种方法中的2种使用Widget,该怎么办?即使未使用Widget,我可能也会产生实例化开销!

如何妥协/调和呢?

一种方法是专门切换到接口以定义操作合同。并取消用户对构造函数的使用。为了保持一致,所有对象都只能通过接口访问,并且只能通过某种形式的解析器(例如IOC / DI容器)实例化。只有容器可以实例化事物。

这就解决了Widget的依赖性,但是如何在不借助Foo接口上单独的初始化方法的情况下初始化“大小”呢?使用此解决方案,我们失去了确保Foo实例在您获得实例之前已完全初始化的能力。Bummer,因为我真的很喜欢构造函数注入的想法和简单性。

当初始化不仅仅是外部依赖时,如何在这个DI世界中实现有保证的初始化?


更新:我刚刚注意到Unity 2.0支持为构造函数参数(例如状态初始化器)提供值,而在resolve()期间仍使用常规机制来实现依赖关系的IoC。也许其他容器也支持这一点?这就解决了在一个构造函数中混合状态init和DI的技术难题,但仍然违反了封装!
— shawnT 2010年

我听到了。我之所以问这个问题,是因为我也觉得两个好东西(DI和封装)以牺牲另一个为代价。顺便说一句,在您的示例中,只有4个方法中有2个需要IWidget,这表明其他2个方法属于不同的组件IMHO。
— 2010年

3

纯封装是永远无法实现的理想选择。如果所有依赖项都隐藏了,那么您根本就不需要DI。这样考虑一下,如果您确实拥有可以在对象内部内部化的私有值,例如汽车对象速度的整数值,那么您就没有外部依赖关系,也不需要反转或注入该依赖关系。这些类型的内部状态值仅由私有函数进行操作,因此您总是希望对其进行封装。

但是,如果您要建造一辆想要某种引擎对象的汽车,那么您将具有外部依赖性。您可以在汽车对象的构造函数内部内部实例化该引擎(例如,新的GMOverHeadCamEngine()),保留封装但创建与具体类GMOverHeadCamEngine的更隐蔽的耦合,也可以注入它,以允许Car对象运行例如,在没有具体依赖性的情况下,对接口IEngine进行不可知论的(并且更加健壮)。不管您使用IOC容器还是简单的DI来实现这一点都不是问题-关键是您拥有一辆可以使用多种引擎而无需与之耦合的Car,从而使您的代码库更加灵活,较少出现副作用。

DI并不违反封装,它是在几乎每个OOP项目中都必然要破坏封装时将耦合最小化的一种方式。从外部将依赖项注入接口可以最大程度地减少耦合副作用,并使您的类对实现保持不可知论。


3

这取决于依赖项是真正的实现细节,还是客户端希望/需要以某种方式了解的某种方式。相关的一件事是该类所针对的抽象级别。这里有些例子:

如果你有一个方法,它使用引擎盖下的缓存,以加快呼叫,那么缓存对象应该是一个单身或东西,应该不会被注入。完全使用高速缓存的事实是类的客户端不必关心的实现细节。

如果您的类需要输出数据流,则可能需要注入输出流,以便该类可以轻松地将结果输出到数组,文件或其他可能要发送数据的地方。

对于灰色区域,假设您有一个进行蒙特卡洛模拟的类。它需要随机性的来源。一方面,它需要一个事实是一个实现细节,因为客户端实际上并不在乎随机性来自何处。另一方面,由于现实世界中的随机数生成器会在客户端可能希望控制的随机度,速度等之间进行权衡,并且客户端可能希望控制播种以获得可重复的行为,因此注入可能很有意义。在这种情况下,我建议提供一种无需指定随机数生成器即可创建类的方法,并使用线程局部的Singleton作为默认值。如果/当需要更精细的控制时,请提供另一个构造函数,以允许注入随机性源。


2

我相信简单。在Domain类中应用IOC / Dependecy注入没有任何改进,只是通过使用描述关系的外部xml文件使代码更难以维护。诸如EJB 1.0 / 2.0和struts 1.1之类的许多技术正在通过减少XML中的内容并尝试将其作为注释等方式放入代码中来进行逆转。因此,对您开发的所有类应用IOC都会使代码变得毫无意义。

当从属对象在编译时尚未准备好创建时,IOC就有好处。在大多数基础架构抽象级别体系结构组件中都可能发生这种情况,尝试建立一个通用的基础框架,该框架可能需要在不同的场景下工作。在那些地方使用IOC更有意义。但这仍然不能使代码更加简单/可维护。

与所有其他技术一样,它也具有优点和缺点。我担心的是,无论最佳使用环境如何,我们都会在所有地方实施最新技术。


2

仅当类既负责创建对象(需要了解实现细节)又负责使用类(不需要了解这些细节)时,封装才被破坏。我将解释原因,但首先快速分析一下汽车:

当我驾驶旧的1971 Kombi时,我可以踩油门,它的行驶速度(稍微)更快。我不需要知道为什么,但是在工厂制造Kombi的人确切知道为什么。

但是回到编码。 封装是“从使用该实现的东西中隐藏实现细节”。封装是一件好事,因为实现细节可以在类的用户不知情的情况下更改。

使用依赖项注入时,构造函数注入用于构造服务类型对象(与建模状态的实体/值对象相反)。服务类型对象中的任何成员变量都表示不应泄漏的实现细节。例如套接字端口号,数据库凭据,要执行加密的另一个类,缓存等。

该构造在最初创建的类时是相关的。这种情况发生在施工阶段,而您的DI容器(或工厂)将所有服务对象连接在一起。DI容器仅知道实现细节。它了解所有实施细节,例如Kombi工厂的人员也了解火花塞。

在运行时,创建的服务对象被称为apon以完成一些实际工作。此时,对象的调用者不了解任何实现细节。

那是我开车将我的Kombi送到海滩。

现在,回到封装。如果实现细节发生更改,则在运行时使用该实现的类无需更改。封装没有损坏。

我也可以开车去海滩。封装没有损坏。

如果实现细节发生更改,则DI容器(或工厂)确实需要更改。您从来没有试图首先从工厂隐藏实现细节。


您将如何对工厂进行单元测试?这意味着客户需要了解工厂才能得到一辆可以工作的汽车,这意味着您需要为系统中的每个其他对象提供工厂。
— 罗德里戈·鲁伊斯

2

经过与问题的进一步斗争后,我现在认为依赖注入确实(此时)在某种程度上违反了封装。不过请不要误会我的意思-我认为在大多数情况下使用依赖注入是值得进行权衡的。

当您正在处理的组件要交付给“外部”参与者(想为客户编写库)时,DI违反封装的原因就变得很清楚。

当我的组件要求通过构造函数(或公共属性)注入子组件时,则无法保证

“防止用户将组件的内部数据设置为无效或不一致的状态”。

同时不能说

“该组件(其他软件)的用户只需要知道该组件的功能,而不必依赖于它如何执行的细节”。

这两个引文均来自维基百科。

举一个具体的例子:我需要提供一个客户端DLL,以简化和隐藏与WCF服务(本质上是远程外观)的通信。因为它依赖于3个不同的WCF代理类,所以如果我采用DI方法,则必须通过构造函数公开它们。这样,我就暴露了我试图隐藏的通信层的内部。

通常,我全力以赴。在这个特殊(极端)的例子中,它使我感到非常危险。


2

DI违反了非共享对象的封装-期限。共享对象的寿命超出正在创建的对象的范围,因此必须将其聚合到正在创建的对象中。对于要创建的对象私有的对象应该被组合到创建的对象中-当创建的对象被销毁时,它会带走组成的对象。让我们以人体为例。什么组成,什么汇总。如果使用DI,则人体构造函数将具有100个对象。例如,许多器官(可能)是可替换的。但是,它们仍然组成身体。血细胞每天在体内产生(并被破坏),而无需外部影响(蛋白质除外)。因此,血细胞是由人体内部产生的-新的BloodCell()。

DI的提倡者认为,一个对象永远不要使用新的运算符。这种“纯粹的”方法不仅违反了封装,而且还违反了创建对象的人的《里斯科夫替代原理》。


1

我也为这个想法而苦恼。最初,使用DI容器(例如Spring)来实例化对象的“要求”就像跳铁环一样。但是实际上,这实际上并不是一个障碍-只是创建我需要的对象的另一种“发布”方式。当然,封装是“破坏”的,因为某个人在“课堂外”知道了它的需求,但实际上并不是系统的其余部分知道它-DI容器。没有什么神奇的事情发生了,因为DI“知道”一个物体需要另一个物体。

实际上,它甚至变得更好-通过专注于工厂和存储库,我什至不必知道DI完全参与其中!对我来说,这将使盖子重新封装。ew!


1
只要DI负责整个实例化链,那么我同意封装会发生。之所以这样,是因为依赖项仍然是公共的,并且可以被滥用。但是,当链中某个“向上”的地方有人需要实例化一个对象而无需使用DI(也许他们是“第三方”)时,就会变得混乱。它们暴露于您的依赖关系中,并可能会滥用它们。否则他们可能根本不想了解他们。
— urig

1

PS。通过提供依赖注入,您不必破坏封装。例:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

客户端代码仍然不知道实现细节。


在您的示例中,如何注入任何东西?(除了您的setter函数的命名)
— foo

1

也许这是一种幼稚的思考方式,但是采用整数参数的构造函数和采用服务作为参数的构造函数之间有什么区别?这是否意味着在新对象之外定义整数并将其馈送到对象中会破坏封装?如果仅在新对象中使用该服务,则看不到如何破坏封装。

另外,通过使用某种自动装配功能(例如,用于C#的Autofac),它使代码变得非常干净。通过为Autofac构建器构建扩展方法,我能够切出大量的DI配置代码,随着依赖关系列表的增加,这些配置代码将随着时间的推移而必须维护。


这与价值与服务无关。它与控制的反转有关-是类的构造函数设置类,还是该服务是否接管设置类。它需要了解该类的实现细节,因此您还有另一个地方可以维护和同步。
— foo

1

我认为很明显,DI至少会大大削弱封装。除此以外,DI还需要考虑其他一些缺点。

  1. 它使代码更难重用。客户端无需显式提供依赖项就可以使用的模块显然比客户端必须以某种方式发现该组件的依赖项是什么然后使它们可用的方式容易使用。例如,最初创建用于ASP应用程序的组件可能希望由DI容器提供其依赖项,该DI容器为对象实例提供与客户端http请求相关的生存期。要在不具有与原始ASP应用程序相同的内置DI容器的另一个客户端中进行复制,可能并不容易。

  2. 它可以使代码更脆弱。接口规范提供的依赖关系可以以意想不到的方式实现,这会引起一类运行时错误,而这些错误在静态解析的具体依赖关系中是不可能的。

  3. 从某种意义上来说,它可能会使代码的灵活性降低,从而使您最终对自己的工作方式仅有较少的选择。在拥有实例的整个生命周期中,并不是每个类都需要拥有其所有依赖项,但是对于许多DI实现,您别无选择。

考虑到这一点,我认为接下来最重要的问题是:“ 是否需要从外部指定特定的依赖项? ”。实际上,我很少发现有必要为了支持测试而从外部提供依赖项。

如果确实需要外部提供依赖项,则通常表明对象之间的关系是协作而不是内部依赖项,在这种情况下,适当的目标是封装每个类,而不是将一个类封装在另一个内部。

以我的经验,有关使用DI的主要问题是,是从内置DI的应用程序框架开始,还是在代码库中添加DI支持,出于某些原因,人们认为,既然您拥有DI支持,那么它必须是正确的。实例化所有内容的方法。他们甚至从未问过“是否需要在外部指定此依赖关系”这个问题。更糟糕的是,他们也开始试图迫使其他人来使用的DI支持一切了。

这样的结果是,您的代码库不可避免地开始陷入一种状态,即在您的代码库中创建任何实例的任何实例都需要大量的钝化DI容器配置,并且调试任何事情的难度都是原来的两倍,因为您有额外的工作量试图确定如何以及任何实例化的地方。

所以我对这个问题的回答是这样。使用DI可以确定要解决的实际问题,而其他任何方法都无法简单地解决。


0

我同意极端地说,DI可能会违反封装。通常,DI公开从未真正封装过的依赖项。这是从米什科·赫弗里(MiškoHevery)的《单身人士是病态的骗子》中借来的简化示例:

您从CreditCard测试开始,然后编写一个简单的单元测试。

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

下个月,您会收到100美元的账单。为什么要收费?单元测试影响了生产数据库。在内部,信用卡通话Database.getInstance()。重构CreditCard,使其DatabaseInterface在其构造函数中采用,就暴露出存在依赖性的事实。但是我要说的是,从不封装依赖关系,因为CreditCard类会导致外部可见的副作用。如果要在不进行重构的情况下测试CreditCard,则可以肯定会发现依赖关系。

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

我认为不必担心将数据库公开为依赖项会减少封装,因为这是一个很好的设计。并非所有DI决策都会如此直接。但是,没有其他答案显示反例。


1
单元测试通常是由类作者编写的,因此从技术的角度讲,可以在测试用例中阐明依赖性。当后来的信用卡类别更改为使用Web API(例如PayPal)时,如果用户死亡,则用户将需要更改所有内容。单元测试通常是在对测试对象有深入了解的基础上进行的(不是全部吗?),因此我认为测试比典型示例更是一个例外。
— kizzx2 2010年

1
DI的目的是避免您描述的更改。如果您从Web API切换到PayPal,则不会更改大多数测试,因为它们将使用MockPaymentService,而CreditCard将由PaymentService构建。您只需要进行一些测试,即可查看CreditCard和真实的PaymentService之间的真实交互,因此将来的更改非常孤立。对于更深的依赖关系图(例如,依赖于CreditCard的类),其好处更大。
— Craig P. Motlin 2010年

@克雷格 Motlin如何将CreditCard对象从WebAPI更改为PayPal,而无需更改类外部的内容?
— 伊恩·博伊德

@Ian我提到CreditCard应该进行重构,以在其构造函数中使用DatabaseInterface,以保护它免受DatabaseInterfaces实现的更改。也许这需要更加通用,并采用一个StorageInterface,而WebAPI可能是另一个实现。贝宝(PayPal)处于错误的级别,因为它是信用卡的替代品。PayPal和CreditCard都可以实现PaymentInterface来屏蔽此示例之外的应用程序的其他部分。
— Craig P. Motlin

@ kizzx2换句话说,说信用卡应该使用PayPal是胡说八道。它们是现实生活中的替代品。
— Craig P. Motlin

0

我认为这是范围问题。在定义封装(不让其知道如何)时,必须定义什么是封装功能。

  1. 照原样进行类:您封装的是类的唯一责任。它知道该怎么办。例如,排序。如果您注入一些比较器进行订购(比方说客户),那不属于封装的内容:quicksort。

  2. 配置的功能:如果您想提供即用型功能,则您不是在提供QuickSort类,而是提供了一个由Comparator配置的QuickSort类的实例。在这种情况下,负责创建和配置的代码必须在用户代码中隐藏。这就是封装。

在对类进行编程时,就是将单个职责实施到类中,即使用选项1。

当您对应用程序进行编程时,就是要进行一些有用的具体工作,然后重复使用选项2。

这是配置实例的实现:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

这是其他一些客户端代码使用它的方式:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

它被封装是因为如果您更改实现(更改clientSorterBean定义),则不会中断客户端的使用。也许,当您将xml文件一起编写时,您会看到所有详细信息。但请相信我,客户端代码(ClientService) 不知道任何关于它的分拣机。


0

可能值得一提的是,它Encapsulation在某种程度上取决于透视图。

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

从某人在A课堂上工作的角度来看,在第二个示例中 A,人们对this.b

而没有DI

new A()

与

new A(new B())

查看此代码的A人员对第二个示例的本质了解更多。

使用DI,至少所有泄漏的知识都集中在一个地方。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.