必须dependency injection是以牺牲封装性为代价的吗?
如果我理解正确,dependency injection的典型机制是通过类的构造函数或通过类的公共属性(成员)注入。
这暴露了被注入的依赖关系,违反了封装的OOP原则。
我是否正确地认定这个权衡? 你如何处理这个问题?
请看下面我自己的问题的答案。
还有另一种方法来看待这个问题,你可能会觉得有趣。
当我们使用IoC /dependency injection时,我们不使用OOP概念。 无可否认,我们使用OO语言作为“主机”,但IoC背后的思想来自于面向组件的软件工程,而不是面向对象。
组件软件都是关于pipe理依赖关系的 – 常用的一个例子是.NET的组装机制。 每个程序集都会发布它所引用的程序集列表,这样可以更容易地将正在运行的应用程序所需的代码块组合在一起(并validation)。
通过IoC在我们的OO程序中应用类似的技术,我们的目标是使程序更易于configuration和维护。 发布依赖关系(作为构造函数参数或其他)是这个的关键部分。 封装并不是真正适用的,就像在面向组件/服务的世界里,没有“实现types”的细节可以泄漏。
不幸的是,我们的语言目前并没有将细粒度,面向对象的概念与粗粒度的面向组件的概念分离开来,所以这是一个区别,你必须牢牢记住:)
这是一个很好的问题 – 但是在某种程度上,如果对象要满足其依赖性,那么就需要以最纯粹的forms进行封装。 某些依赖关系的提供者必须知道所讨论的对象需要一个Foo
,而提供者必须有一种方法将Foo
提供给对象。
按照惯例,后一种情况是通过构造函数参数或setter方法来处理的。 然而,这不一定是正确的 – 例如,我知道Java中Spring DI框架的最新版本允许注释私有字段(例如使用@Autowired
),并且通过reflection设置依赖关系,而不需要公开依赖任何类的公共方法/构造函数。 这可能是你正在寻找的解决scheme。
也就是说,我不认为构造函数注入也是一个很大的问题。 我一直觉得,施工结束后对象应该是完全有效的,这样他们所需要的任何事情(即处于有效状态)都应该通过施工人员来提供。 如果你有一个需要协作者的对象,我认为这个构造器公开地通告了这个需求,并且确保在创build这个类的一个新的实例的时候它被满足了。
理想情况下,在处理对象时,无论如何都要通过接口与它们进行交互,并且越多(通过DI连接依赖关系),实际上自己处理构造函数就越less。 在理想的情况下,你的代码不会处理,甚至不会创build类的具体实例。 所以它只是通过DI得到一个IFoo
,而不用担心FooImpl
的构造FooImpl
表明它需要做什么工作,事实上甚至没有意识到FooImpl
的存在。 从这个angular度来看,封装是完美的。
这当然是一种意见,但在我看来,DI并不一定违反封装,实际上可以通过将内部的所有必要知识集中到一个地方来帮助实现。 这不仅本身是一件好事,而且更好的是这个地方在你自己的代码库之外,所以你写的代码都不需要知道类的依赖关系。
这暴露了被注入的依赖关系,违反了封装的OOP原则。
坦率地说,一切都违反封装。 :)这是一种温和的原则,必须妥善处理。
那么,什么违反封装?
inheritance。
“因为inheritance暴露了一个子类到它的父实现的细节,所以常常说”inheritance破坏了封装“。 (四人帮1995:19)
面向方面的编程呢 。 例如,你注册onMethodCall()callback,这给你一个很好的机会注入代码,以正常的方法评估,添加奇怪的副作用等
在C ++ 中的朋友声明呢。
Ruby 中的类inheritance。 在完全定义了一个string类之后,重新定义一个string方法。
那么,很多东西呢。
封装是一个好的和重要的原则。 但不是唯一的一个。
switch (principle) { case encapsulation: if (there_is_a_reason) break! }
是的,DI违反封装(也称为“信息隐藏”)。
但真正的问题出现在开发人员以此为借口来违反KISS(Keep It Short and Simple)和YAGNI(你不会需要它)原则的时候。
我个人更喜欢简单有效的解决scheme。 我主要使用“新”运算符来实例化状态依赖关系,无论何时何地需要它们。 它简单,封装好,易于理解,易于testing。 那么,为什么不呢?
它不违反封装。 你正在提供一个协作者,但是class级可以决定如何使用它。 只要你按照Tell不要求事情没有问题。 我更喜欢constructer injection,但是setter可以很好,只要他们聪明。 这是他们包含逻辑来维护不变式的类表示。
一个好的dependency injection容器/系统将允许构造器注入。 依赖对象将被封装,不需要公开暴露。 此外,通过使用DP系统,您的代码甚至不会“知道”对象的构造细节,甚至可能包括正在构build的对象。 在这种情况下有更多的封装,因为几乎所有的代码不仅不被封装的对象所知,而且甚至不参与对象的构build。
现在,我假设你正在比较创build的对象创build自己的封装对象,最有可能在其构造函数中的情况。 我对DP的理解是,我们要把这个责任远离对象,并把它交给别人。 为此,在这种情况下,作为DP容器的“他人”确实具有“违反”封装的知识; 好处就是把知识从对象中抽出来, 有人必须拥有它。 其余的应用程序不会。
我会这样想:dependency injection容器/系统违反封装,但你的代码不。 事实上,你的代码更“封装”了。
纯封装是一个永远无法实现的理想。 如果所有的依赖关系都隐藏起来,那么根本就不需要DI。 这样想一下,如果你真的有私有的价值,可以在对象内部化,比如汽车对象速度的整数值,那么你就没有外部依赖,也不需要反转或注入这个依赖。 这些纯粹由私有函数操作的内部状态值就是你总想要封装的东西。
但是,如果你正在build造一辆想要某种引擎对象的汽车,那么你有一个外部的依赖。 您可以在汽车对象的构造函数内部实例化该引擎 – 例如新的GMOverHeadCamEngine() – 保留封装,但创build更具潜在的耦合到具体的类GMOverHeadCamEngine,或者可以注入它,让您的Car对象操作对于例如一个没有具体依赖性的接口IEngine来说,是非常强烈地(更加强大的)。 无论是使用IOC容器还是简单的DI来实现这一点都不是重点 – 关键是你有一个Car,可以使用多种引擎,而无需耦合到任何引擎,从而使您的代码库更加灵活,不太容易出现副作用。
DI不是封装的侵犯,当封装必然在每个OOP项目中都被打断时,这是一种最小化耦合的方式。 从外部向接口注入依赖性可以最大限度地减less耦合副作用,并使您的类可以不需要执行。
这与upvoted的答案类似,但我想大声思考 – 也许别人也是这样看待事情的。
-
古典的OO使用构造函数为类的用户定义公共的“初始化”契约(隐藏所有的实现细节,也就是封装)。 这个契约可以确保在实例化之后,你有一个随时可以使用的对象(即没有额外的初始化步骤被用户记住(呃,忘记))。
-
(构造函数)通过这个公共构造函数接口不可否认地破坏了封装的出血实现细节 。 只要我们仍然考虑负责为用户定义初始化合同的公共构造函数,我们已经创build了一个可怕的封装违规行为。
理论例子:
Foo类有4个方法,需要一个用于初始化的整数,所以它的构造函数看起来像Foo(int size) ,对Foo类的用户来说,他们必须立即清楚它们必须在实例化时提供一个大小 ,以便Foo工作。
说这个Foo的特定实现可能也需要一个IWidget来完成它的工作。 这个依赖关系的构造器注入将使我们创build一个像Foo(int size,IWidget widget)
现在我们有一个构造函数将初始化数据与依赖关系混合在一起 – 一个input对类的用户( 大小 )感兴趣,另一个是内部依赖,仅用于混淆用户,是一个实现细节( 小工具 )。
大小参数不是一个依赖项 – 它是简单的每个实例的初始化值。 IoC对于外部依赖(比如窗口部件)来说是很好看的,但对于内部状态初始化来说则不是。
更糟糕的是,如果Widget仅仅是这个类的四种方法中的两种, 即使可能不会使用Widget,也可能会导致实例化开销!
如何妥协/协调呢?
一种方法是专门切换到界面来定义运营合同; 并取消用户使用构造函数。 为了保持一致性,所有的对象只能通过接口访问,并且只能通过某种forms的parsing器(如IOC / DI容器)来实例化。 只有容器才能实例化事物。
这照顾了Widget的依赖关系,但是我们如何初始化“size”,而不是在Foo接口上使用单独的初始化方法呢? 使用这个解决scheme,我们失去了确保在获得实例的时候Foo的一个实例被完全初始化的能力。 Bummer,因为我非常喜欢构造函数注入的想法和简单 。
在初始化不仅仅是外部依赖性时,如何在这个DI世界中实现有保证的初始化?
正如Jeff Sternal在对这个问题的评论中指出的那样,答案完全取决于你如何定义封装 。
似乎有两个封装手段的主要阵营:
- 与对象有关的所有东西都是对象的方法。 所以,一个
File
对象可能有Save
,Print
,Display
,ModifyText
等方法。 - 一个客体是它自己的小世界,并不依赖于外在的行为。
这两个定义是相互矛盾的。 如果一个File
对象可以自己打印,它将在很大程度上取决于打印机的行为。 另一方面,如果它仅仅知道可以打印的东西(一个IFilePrinter
或者一些这样的接口),那么File
对象不需要知道任何关于打印的信息,所以使用它就会减less对于打印的依赖目的。
所以,如果使用第一个定义,dependency injection将破坏封装。 但是,坦率地说,我不知道我是否喜欢第一个定义 – 它显然没有规模(如果是的话,MS Word将是一个大的类)。
另一方面,如果使用封装的第二个定义,dependency injection几乎是必须的。
这取决于依赖关系是否真的是实现细节,或者是客户想要/需要以某种方式知道的事情。 有一件事是相关的,就是这个类的抽象级别是什么。 这里有些例子:
如果你有一个方法在引擎盖下使用caching来加速调用,那么caching对象应该是一个单例或什么东西,不应该被注入。 caching正在被使用的事实是你的类的客户端不必关心的实现细节。
如果您的类需要输出数据stream,那么注入输出stream可能是有意义的,这样类可以很容易地将结果输出到数组,文件或别人可能想要发送数据的任何地方。
对于一个灰色地带,假设你有一个class级做一些蒙特卡罗模拟。 它需要一个随机的来源。 一方面,它需要这个事实是一个实现细节,客户端真的不关心随机性来自哪里。 另一方面,由于现实世界的随机数发生器在客户端可能想要控制的随机程度,速度等之间进行权衡,并且客户端可能想要控制种子以获得可重复的行为,所以注入可能是有意义的。 在这种情况下,我build议提供一种创build类的方法,而不指定随机数生成器,并使用线程本地Singleton作为默认值。 如果/当需要更精细的控制时,提供另一个构造函数,允许注入一个随机源。
如果一个类既有创build对象的责任(需要知道实现细节),又使用了类(不需要知道这些细节),则封装只能被破坏。 我会解释为什么,但首先是一个快速汽车的学科:
当我驾驶我的1971年Kombi时,我可以按下加速器,它稍微快一点。 我不需要知道为什么,但在工厂build造Kombi的人确切知道为什么。
但是回到编码。 封装是“使用该实现隐藏实现细节”。 封装是一件好事,因为实现的细节可以在class级的用户不知情的情况下改变。
当使用dependency injection时,构造函数注入被用来构造服务types对象(而不是模型状态的实体/值对象)。 服务types对象中的任何成员variables都表示不应泄漏的实现细节。 例如套接字端口号,数据库凭证,调用执行encryption的另一个类,caching等。
构造函数在初始创build类时是相关的。 这发生在施工阶段,而您的DI容器(或工厂)将所有服务对象连接在一起。 DI容器只知道实现细节。 它知道所有关于实施的细节,比如Kombi工厂的人知道火花塞。
在运行时,创build的服务对象被称为apon来做一些真正的工作。 此时,对象的调用者对实现细节一无所知。
那是我驾驶我的Kombi去海边
现在回到封装。 如果实现细节发生变化,那么在运行时使用该实现的类不需要改变。 封装不被破坏。
我也可以把我的新车开到海边去。 封装不被破坏。
如果实施细节更改,DI容器(或工厂)确实需要更改。 你从来没有试图从工厂隐藏实施细节。
一直在争论这个问题,我现在认为dependency injection(在这个时候)确实在一定程度上违反了封装。 不要误解我的意思 – 我认为在大多数情况下使用dependency injection非常值得。
当你正在从事的组件被交付给一个“外部”组织时(考虑为客户编写一个库),DI为什么违反封装的情况就变得很清楚了。
当我的组件需要通过构造函数(或公共属性)注入子组件时,不能保证
“防止用户将组件的内部数据设置为无效或不一致的状态”。
同时也不能这么说
“组件的用户(其他软件)只需要知道组件的function,而不能依赖于它的具体细节” 。
两个引号都来自wikipedia 。
举一个具体的例子:我需要提供一个客户端DLL,它简化并隐藏了WCF服务(本质上是一个远程外观)的通信。 因为它取决于3个不同的WCF代理类,所以如果我采用DI方法,我不得不通过构造函数公开它们。 我用这个方法公开了我想要隐藏的通信层的内部。
一般来说,我都是为了DI。 在这个特别的(极端的)例子中,它使我感到危险。
我也为这个想法而努力。 首先,使用DI容器(如Spring)来实例化一个对象的“需求”就像是跳过箍环一样。 但实际上,这实际上不是一个箍环 – 它只是另一种“发表”的方式来创build我需要的对象。 当然,封装是“破裂的”,因为“课外”有人知道它需要什么,但是系统的其他部分并不知道这是DI容器。 没有什么不可思议的事情发生,因为DI'知道'一个物体需要另一个物体
事实上,它变得更好 – 通过关注工厂和知识库,我甚至不需要知道DI是如何参与的! 对我来说,把盖子放回封装。 呼!
我相信简单。 在域类中应用IOC / Dependecy注入并没有任何改进,只是通过外部的xml文件来描述关系使得代码变得更加困难。 EJB 1.0 / 2.0&struts 1.1等许多技术都是通过减less放在XML中的东西来逆转,并尝试将它们作为代码来使用,所以将IOC应用于您开发的所有类将会使代码变得没有意义。
IOC在编译时依赖对象没有准备好创build的时候有好处。 这在大多数基础设施抽象层次体系结构组件中可能会发生,试图build立一个可能需要适用于不同场景的通用基础框架。 在那些地方使用国际奥委会更有意义。 尽pipe如此,这并不能使代码更加简单/可维护。
像所有其他技术一样,PROs&CON也是如此。 我担心的是,我们在所有地方实施最新的技术,而不考虑其最佳用法。
PS。 通过提供dependency injection,您不一定会破坏封装 。 例:
obj.inject_dependency( factory.get_instance_of_unknown_class(x) );
客户端代码仍然不知道实现细节。
也许这是一个天真的想法,但接受一个整数参数的构造函数和一个服务作为参数的构造函数之间有什么区别? 这是否意味着在新对象之外定义一个整数并将其馈送到对象中会破坏封装? 如果服务只用于新的对象,我不明白这是如何破坏封装。
另外,通过使用某种自动assemblyfunction(例如Autofac for C#),它使代码非常干净。 通过为Autofac构build器构build扩展方法,我能够删除很多DIconfiguration代码,随着依赖项列表的增长,我将不得不维护一段时间。
我同意,采取一个极端,DI可以违反封装。 通常DI暴露了从未真正封装的依赖关系。 这里有一个简单的例子,从MiškoHevery's Singletons借用病理性说谎者 :
你从一个CreditCardtesting开始,写一个简单的unit testing。
@Test public void creditCard_Charge() { CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008); c.charge(100); }
下个月你会收到100美元的账单。 你为什么被指控? unit testing影响了生产数据库。 CreditCard在内部调用Database.getInstance()
。 对CreditCard进行重构,以便在其构造函数中使用DatabaseInterface
公开存在依赖关系的事实。 但是我会争辩说,依赖从来没有封装开始,因为CreditCard类导致外部可见的副作用。 如果你想在没有重构的情况下testingCreditCard,你当然可以观察依赖关系。
@Before public void setUp() { Database.setInstance(new MockDatabase()); } @After public void tearDown() { Database.resetInstance(); }
我认为不值得担心将数据库公开为依赖关系是否会减less封装,因为这是一个很好的devise。 并不是所有的DI决定都会如此简单。 但是,没有其他答案显示反例。
我认为这是一个范围问题。 当你定义封装(不知道如何)时,你必须定义什么是封装function。
-
类是这样的 :你正在封装的是类的唯一责任。 它知道该怎么做。 例如,sorting。 如果你注入一些比较订单,比方说,客户,这不是封装的东西的一部分:快速sorting。
-
configuration的function :如果您想提供即用function,则不提供QuickSort类,而是使用Comparatorconfiguration的QuickSort类的实例。 在这种情况下,负责创build和configuration的代码必须隐藏在用户代码中。 这就是封装。
当你正在编程的时候,就是把单一的职责实现到类中,你正在使用选项1。
When you are programming applications, it is, making something that undertakes some useful concrete work then you are repeteadily using option 2.
This is the implementation of the configured instance:
<bean id="clientSorter" class="QuickSort"> <property name="comparator"> <bean class="ClientComparator"/> </property> </bean>
This is how some other client code use it:
<bean id="clientService" class"..."> <property name="sorter" ref="clientSorter"/> </bean>
It is encapsulated because if you change implementation (you change clientSorter
bean definition) it doesn't break client use. Maybe, as you use xml files with all written together you are seeing all the details. But believe me, the client code ( ClientService
) don't know nothing about its sorter.
It's probably worth mentioning that Encapsulation
is somewhat perspective dependent.
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; } }
From the perspective of someone working on the A
class, in the second example A
knows a lot less about the nature of this.b
Whereas without DI
new A()
VS
new A(new B())
The person looking at this code knows more about the nature of A
in the second example.
With DI, at least all that leaked knowledge is in one place.
I think it's self evident that at the very least DI significantly weakens encapsulation. In additional to that here are some other downsides of DI to consider.
-
It makes code harder to reuse. A module which a client can use without having to explicitly provide dependencies to, is obviously easier to use than one where the client has to somehow discover what that component's dependencies are and then somehow make them available. For example a component originally created to be used in an ASP application may expect to have its dependencies provided by a DI container that provides object instances with lifetimes related to client http requests. This may not be simple to reproduce in another client that does not come with the same built in DI container as the original ASP application.
-
It can make code more fragile. Dependencies provided by interface specification can be implemented in unexpected ways which gives rise to a whole class of runtime bugs that are not possible with a statically resolved concrete dependency.
-
It can make code less flexible in the sense that you may end up with fewer choices about how you want it to work. Not every class needs to have all its dependencies in existence for the entire lifetime of the owning instance, yet with many DI implementations you have no other option.
With that in mind I think the most important question then becomes, " does a particular dependency need to be externally specified at all? ". In practise I have rarely found it necessary to make a dependency externally supplied just to support testing.
Where a dependency genuinely needs to be externally supplied, that normally suggests that the relation between the objects is a collaboration rather than an internal dependency, in which case the appropriate goal is then encapsulation of each class, rather than encapsulation of one class inside the other.
In my experience the main problem regarding the use of DI is that whether you start with an application framework with built in DI, or you add DI support to your codebase, for some reason people assume that since you have DI support that must be the correct way to instantiate everything . They just never even bother to ask the question "does this dependency need to be externally specified?". And worse, they also start trying to force everyone else to use the DI support for everything too.
The result of this is that inexorably your codebase starts to devolve into a state where creating any instance of anything in your codebase requires reams of obtuse DI container configuration, and debugging anything is twice as hard because you have the extra workload of trying to identify how and where anything was instantiated.
So my answer to the question is this. Use DI where you can identify an actual problem that it solves for you, which you can't solve more simply any other way.
- 依赖倒置原则(SOLID)与封装(OOP的支柱)
- 企业图书馆Unity与其他IoC容器
- 什么是dependency injection上下文中的组合根
- 使用Unity如何将一个命名的dependency injection到构造函数中?
- Ioc / DI – 为什么我必须引用入口应用程序中的所有层/组件?
- MEF(pipe理可扩展性框架)与IoC / DI
- MEF和IoC容器的区别(如Unity,Autofac,SMap,Ninject,Windsor.Spring.net等)
- DAL – > BLL < – GUI +组合根。 如何设置DI绑定?
- 我应该使用哪种dependency injection工具?