依赖注入必须以牺牲封装为代价吗?
如果我理解正确,依赖注入的典型机制是通过类的构造函数或通过类的公共属性(成员)注入。
这暴露了注入的依赖关系并违反了 OOP 封装原则。
我对这种权衡的认识正确吗? 您如何处理这个问题?
另请参阅下面我对自己问题的回答。
If I understand correctly, the typical mechanism for Dependency Injection is to inject either through a class' constructor or through a public property (member) of the class.
This exposes the dependency being injected and violates the OOP principle of encapsulation.
Am I correct in identifying this tradeoff? How do you deal with this issue?
Please also see my answer to my own question below.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(21)
您可能会发现另一种看待这个问题的有趣方式。
当我们使用 IoC/依赖注入时,我们没有使用 OOP 概念。 诚然,我们使用 OO 语言作为“宿主”,但 IoC 背后的思想来自面向组件的软件工程,而不是 OO。
组件软件就是管理依赖关系 - 一个常见的例子是 .NET 的 Assembly 机制。 每个程序集都会发布它引用的程序集列表,这使得将正在运行的应用程序所需的各个部分组合在一起(并验证)变得更加容易。
通过 IoC 在我们的 OO 程序中应用类似的技术,我们的目标是使程序更易于配置和维护。 发布依赖项(作为构造函数参数或其他形式)是其中的关键部分。 封装并不真正适用,因为在面向组件/服务的世界中,没有可供泄漏细节的“实现类型”。
不幸的是,我们的语言目前并没有将细粒度的面向对象的概念与粗粒度的面向组件的概念分开,所以这是一个你必须牢记在心的区别:)
There is another way of looking at this issue that you might find interesting.
When we use IoC/dependency injection, we're not using OOP concepts. Admittedly we're using an OO language as the 'host', but the ideas behind IoC come from component-oriented software engineering, not OO.
Component software is all about managing dependencies - an example in common use is .NET's Assembly mechanism. Each assembly publishes the list of assemblies that it references, and this makes it much easier to pull together (and validate) the pieces needed for a running application.
By applying similar techniques in our OO programs via IoC, we aim to make programs easier to configure and maintain. Publishing dependencies (as constructor parameters or whatever) is a key part of this. Encapsulation doesn't really apply, as in the component/service oriented world, there is no 'implementation type' for details to leak from.
Unfortunately our languages don't currently segregate the fine-grained, object-oriented concepts from the coarser-grained component-oriented ones, so this is a distinction that you have to hold in your mind only :)
这是一个很好的问题 - 但在某些时候,如果对象要满足其依赖性,则需要违反最纯粹形式的封装。 某些依赖项提供者必须知道相关对象需要
Foo
,并且提供者必须有一种提供Foo
的方法到对象。通常,后一种情况是通过构造函数参数或 setter 方法处理的。 然而,这不一定是真的 - 例如,我知道 Java 中的 Spring DI 框架的最新版本允许您注释私有字段(例如使用
@Autowired
),并且依赖项将通过以下方式设置反射,而无需通过任何类公共方法/构造函数公开依赖关系。 这可能就是您正在寻找的解决方案。也就是说,我认为构造函数注入也不是什么大问题。 我一直认为对象在构造后应该是完全有效的,这样它们执行其角色所需的任何东西(即处于有效状态)都应该通过构造函数提供。 如果您有一个需要协作者才能工作的对象,那么在我看来,构造函数公开宣传此要求并确保在创建该类的新实例时满足该要求似乎很好。
理想情况下,在处理对象时,无论如何,您都可以通过接口与它们交互,并且您这样做的次数越多(并且通过 DI 连接依赖项),您实际上需要自己处理构造函数的次数就越少。 在理想情况下,您的代码不处理甚至不创建类的具体实例; 所以它只是通过 DI 获得一个
IFoo
,而不用担心FooImpl
的构造函数表明它需要完成它的工作,事实上甚至不知道>FooImpl
的存在。 从这一点来看,封装是完美的。这当然是一种观点,但在我看来,DI 并不一定违反封装性,事实上,可以通过将所有必要的内部知识集中到一个地方来帮助实现封装性。 这本身不仅是一件好事,而且更好的是这个地方位于您自己的代码库之外,因此您编写的代码都不需要了解类的依赖关系。
It's a good question - but at some point, encapsulation in its purest form needs to be violated if the object is ever to have its dependency fulfilled. Some provider of the dependency must know both that the object in question requires a
Foo
, and the provider has to have a way of providing theFoo
to the object.Classically this latter case is handled as you say, through constructor arguments or setter methods. However, this is not necessarily true - I know that the latest versions of the Spring DI framework in Java, for example, let you annotate private fields (e.g. with
@Autowired
) and the dependency will be set via reflection without you needing to expose the dependency through any of the classes public methods/constructors. This might be the kind of solution you were looking for.That said, I don't think that constructor injection is much of a problem, either. I've always felt that objects should be fully valid after construction, such that anything they need in order to perform their role (i.e. be in a valid state) should be supplied through the constructor anyway. If you have an object that requires a collaborator to work, it seems fine to me that the constructor publically advertises this requirement and ensures it is fulfilled when a new instance of the class is created.
Ideally when dealing with objects, you interact with them through an interface anyway, and the more you do this (and have dependencies wired through DI), the less you actually have to deal with constructors yourself. In the ideal situation, your code doesn't deal with or even ever create concrete instances of classes; so it just gets given an
IFoo
through DI, without worrying about what the constructor ofFooImpl
indicates it needs to do its job, and in fact without even being aware ofFooImpl
's existance. From this point of view, the encapsulation is perfect.This is an opinion of course, but to my mind DI doesn't necessarily violate encapsulation and in fact can help it by centralising all of the necessary knowledge of internals into one place. Not only is this a good thing in itself, but even better this place is outside your own codebase, so none of the code you write needs to know about classes' dependencies.
好吧,坦率地说,一切都违反了封装性。 :) 这是一种必须好好对待的温柔原则。
那么,什么违反了封装呢?
继承确实。
面向方面编程 确实。 例如,您注册了 onMethodCall() 回调,这为您提供了一个很好的机会将代码注入到正常的方法评估中,添加奇怪的副作用等。C
++ 中的友元声明可以。
Ruby 中的类扩展确实。 只需在字符串类完全定义后在某个地方重新定义字符串方法即可。
嗯,很多事情都可以。
封装是一个很好且重要的原则。 但不是唯一的一个。
Well, frankly speaking, everything violates encapsulation. :) It's a kind of a tender principle that must be treated well.
So, what violates encapsulation?
Inheritance does.
Aspect-oriented programming does. For example, you register onMethodCall() callback and that gives you a great opportunity to inject code to the normal method evaluation, adding strange side-effects etc.
Friend declaration in C++ does.
Class extention in Ruby does. Just redefine a string method somewhere after a string class was fully defined.
Well, a lot of stuff does.
Encapsulation is a good and important principle. But not the only one.
是的,DI 违反了封装(也称为“信息隐藏”)。
但当开发人员以此为借口违反 KISS(保持简短)和 YAGNI(你不需要它)原则时,真正的问题就出现了。
就我个人而言,我更喜欢简单有效的解决方案。 我主要使用“new”运算符在需要时随时随地实例化有状态依赖项。 它简单、封装良好、易于理解、易于测试。 那么,为什么不呢?
Yes, DI violates encapsulation (also known as "information hiding").
But the real problem comes when developers use it as an excuse to violate the KISS (Keep It Short and Simple) and YAGNI (You Ain't Gonna Need It) principles.
Personally, I prefer simple and effective solutions. I mostly use the "new" operator to instantiate stateful dependencies whenever and wherever they are needed. It is simple, well encapsulated, easy to understand, and easy to test. So, why not?
一个好的依赖注入容器/系统将允许构造函数注入。 依赖对象将被封装,根本不需要公开暴露。 此外,通过使用 DP 系统,您的代码甚至都不会“知道”如何构造对象的细节,甚至可能包括正在构造的对象。 在这种情况下有更多的封装,因为几乎所有代码不仅不了解封装对象的知识,而且甚至不参与对象构造。
现在,我假设您正在与创建的对象创建自己的封装对象(很可能在其构造函数中)的情况进行比较。 我对DP的理解是,我们想把这个责任从对象身上拿走,交给别人。 为此,“其他人”(在本例中是 DP 容器)确实拥有“违反”封装的深入知识; 好处是它将知识从对象本身中提取出来。 必须有人拥有它。 您的应用程序的其余部分则不然。
我会这样想:依赖注入容器/系统违反了封装,但您的代码没有。 事实上,您的代码比以往更加“封装”。
A good depenancy injection container/system will allow for constructor injection. The dependant objects will be encapsulated, and need not be exposed publicly at all. Further, by using a DP system, none of your code even "knows" the details of how the object is constructed, possibly even including the object being constructed. There is more encapsulation in this case since nearly all of your code not only is shielded from knowledge of the encapsulated objects, but does not even participate in the objects construction.
Now, I am assuming you are comparing against the case where the created object creates its own encapsulated objects, most likely in its constructor. My understanding of DP is that we want to take this responsibility away from the object and give it to someone else. To that end, the "someone else", which is the DP container in this case, does have intimate knowledge which "violates" encapsulation; the benefit is that it pulls that knowledge out of the object, iteself. Someone has to have it. The rest of your application does not.
I would think of it this way: The dependancy injection container/system violates encapsulation, but your code does not. In fact, your code is more "encapsulated" then ever.
这与投票的答案类似,但我想大声思考 - 也许其他人也这么看问题。
经典面向对象使用构造函数为类的使用者定义公共“初始化”契约(隐藏所有实现细节;也称为封装)。 这个契约可以确保在实例化之后你有一个随时可用的对象(即用户不需要记住(呃,忘记)额外的初始化步骤)。
(构造函数)DI 通过此公共构造函数接口渗透实现细节,无可否认地破坏了封装。 只要我们仍然考虑负责为用户定义初始化契约的公共构造函数,我们就造成了可怕的违反封装性的情况。
理论示例:
类 Foo 有 4 个方法,并且需要一个整数进行初始化,因此它的构造函数看起来像 Foo(int size),并且类 的用户可以立即清楚Foo 他们必须在实例化时提供大小才能使 Foo 工作。
假设 Foo 的这个特定实现可能还需要一个 IWidget 来完成其工作。 此依赖项的构造函数注入将使我们创建一个类似于 Foo(int size, IWidget widget) 的构造函数,令
我烦恼的是现在我们有一个将初始化数据混合的构造函数依赖项 - 一个输入是类的用户感兴趣的(大小),另一个输入是内部依赖项,只会让用户感到困惑,并且是实现细节(小部件 >)。
大小参数不是依赖项 - 它只是每个实例的初始化值。 IoC 对于外部依赖项(如小部件)来说是花哨的,但对于内部状态初始化却不是。
更糟糕的是,如果该类的 4 个方法中只有 2 个需要 Widget,该怎么办? 即使可能不使用 Widget,我也可能会产生实例化开销!
如何妥协/调和?
一种方法是专门切换到接口来定义操作契约; 并废除用户对构造函数的使用。
为了保持一致,所有对象都必须仅通过接口进行访问,并且仅通过某种形式的解析器(如 IOC/DI 容器)进行实例化。 只有容器才能实例化事物。
这就解决了 Widget 依赖关系,但是我们如何在不诉诸 Foo 接口上单独的初始化方法的情况下初始化“size”呢? 使用此解决方案,我们无法确保 Foo 实例在您获取实例时已完全初始化。 真糟糕,因为我真的很喜欢构造函数注入的想法和简单性。
当初始化不仅仅是外部依赖项时,如何在这个 DI 世界中实现有保证的初始化?
This is similar to the upvoted answer but I want to think out loud - perhaps others see things this way as well.
Classical OO uses constructors to define the public "initialization" contract for consumers of the class (hiding ALL implementation details; aka encapsulation). This contract can ensure that after instantiation you have a ready-to-use object (i.e. no additional initialization steps to be remembered (er, forgotten) by the user).
(constructor) DI undeniably breaks encapsulation by bleeding implemenation detail through this public constructor interface. As long as we still consider the public constructor responsible for defining the initialization contract for users, we have created a horrible violation of encapsulation.
Theoretical Example:
Class Foo has 4 methods and needs an integer for initialization, so its constructor looks like Foo(int size) and it's immediately clear to users of class Foo that they must provide a size at instantiation in order for Foo to work.
Say this particular implementation of Foo may also need a IWidget to do its job. Constructor injection of this dependency would have us create a constructor like Foo(int size, IWidget widget)
What irks me about this is now we have a constructor that's blending initialization data with dependencies - one input is of interest to the user of the class (size), the other is an internal dependency that only serves to confuse the user and is an implementation detail (widget).
The size parameter is NOT a dependency - it's simple a per-instance initialization value. IoC is dandy for external dependencies (like widget) but not for internal state initialization.
Even worse, what if the Widget is only necessary for 2 of the 4 methods on this class; I may be incurring instantiation overhead for Widget even though it may not be used!
How to compromise/reconcile this?
One approach is to switch exclusively to interfaces to define the operation contract; and abolish the use of constructors by users.
To be consistent, all objects would have to be accessed through interfaces only, and instantiated only through some form of resolver (like an IOC/DI container). Only the container gets to instantiate things.
That takes care of the Widget dependency, but how do we initialize "size" without resorting to a separate initialization method on the Foo interface? Using this solution, we lost the ability to ensure that an instance of Foo is fully initialized by the time you get the instance. Bummer, because I really like the idea and simplicity of constructor injection.
How do I achieve guaranteed initialization in this DI world, when initialization is MORE than ONLY external dependencies?
正如 Jeff Sternal 在对该问题的评论中指出的那样,答案完全取决于您如何定义封装。
封装的含义似乎有两个主要阵营:
File
对象可能具有Save
、Print
、Display
、ModifyText
方法>等。这两个定义是直接矛盾的。 如果
File
对象可以打印自身,它将在很大程度上取决于打印机的行为。 另一方面,如果它仅仅知道可以为其打印的东西(IFilePrinter
或某些此类接口),那么File
对象不需要了解有关打印的任何信息,因此使用它会给对象带来更少的依赖。因此,如果使用第一个定义,依赖注入将破坏封装。 但是,坦率地说,我不知道我是否喜欢第一个定义 - 它显然无法扩展(如果可以的话,MS Word 将是一个大类)。
另一方面,如果您使用封装的第二个定义,则依赖项注入几乎是强制。
As Jeff Sternal pointed out in a comment to the question, the answer is entirely dependent on how you define encapsulation.
There seem to be two main camps of what encapsulation means:
File
object may have methods toSave
,Print
,Display
,ModifyText
, etc.These two definitions are in direct contradiction to each other. If a
File
object can print itself, it will depend heavily on the printer's behavior. On the other hand, if it merely knows about something that can print for it (anIFilePrinter
or some such interface), then theFile
object doesn't have to know anything about printing, and so working with it will bring less dependencies into the object.So, dependency injection will break encapsulation if you use the first definition. But, frankly I don't know if I like the first definition - it clearly doesn't scale (if it did, MS Word would be one big class).
On the other hand, dependency injection is nearly mandatory if you're using the second definition of encapsulation.
它不违反封装性。 您提供了一个协作者,但班级可以决定如何使用它。 只要您遵循告诉不要问,一切就都很好。 我发现构造函数注入更可取,但只要 setter 足够聪明,它们也可以。 也就是说,它们包含维护类所代表的不变量的逻辑。
It doesn't violate encapsulation. You're providing a collaborator, but the class gets to decide how it is used. As long as you follow Tell don't ask things are fine. I find constructer injection preferable, but setters can be fine as well as long as they're smart. That is they contain logic to maintain the invariants the class represents.
纯粹的封装是一个永远无法实现的理想。 如果所有依赖项都被隐藏,那么您根本就不需要 DI。 这样想,如果您确实拥有可以在对象内内化的私有值,例如汽车对象速度的整数值,那么您就没有外部依赖项,也不需要反转或注入该依赖项。 这些纯粹由私有函数操作的内部状态值是您始终想要封装的。
但是,如果您正在建造一辆需要某种类型的引擎对象的汽车,那么您就需要外部依赖。 您可以在汽车对象的构造函数内部实例化该引擎(例如 new GMOverHeadCamEngine()),保留封装,但创建与具体类 GMOverHeadCamEngine 的更隐蔽的耦合,或者您可以注入它,允许您的汽车对象运行例如,在没有具体依赖的接口 IEngine 上,是不可知的(并且更加稳健)。 无论您使用 IOC 容器还是简单的 DI 来实现这一点都不是重点——重点是您拥有一辆可以使用多种引擎而无需耦合到任何引擎的 Car,从而使您的代码库更加灵活和不太容易出现副作用。
DI 并不是对封装的破坏,它是一种在几乎每个 OOP 项目中必然会破坏封装时最小化耦合的方法。 从外部将依赖项注入到接口中可以最大限度地减少耦合副作用,并允许您的类对实现保持不可知。
Pure encapsulation is an ideal that can never be achieved. If all dependencies were hidden then you wouldn't have the need for DI at all. Think about it this way, if you truly have private values that can be internalized within the object, say for instance the integer value of the speed of a car object, then you have no external dependency and no need to invert or inject that dependency. These sorts of internal state values that are operated on purely by private functions are what you want to encapsulate always.
But if you're building a car that wants a certain kind of engine object then you have an external dependency. You can either instantiate that engine -- for instance new GMOverHeadCamEngine() -- internally within the car object's constructor, preserving encapsulation but creating a much more insidious coupling to a concrete class GMOverHeadCamEngine, or you can inject it, allowing your Car object to operate agnostically (and much more robustly) on for example an interface IEngine without the concrete dependency. Whether you use an IOC container or simple DI to achieve this is not the point -- the point is that you've got a Car that can use many kinds of engines without being coupled to any of them, thus making your codebase more flexible and less prone to side effects.
DI is not a violation of encapsulation, it is a way of minimizing the coupling when encapsulation is necessarily broken as a matter of course within virtually every OOP project. Injecting a dependency into an interface externally minimizes coupling side effects and allows your classes to remain agnostic about implementation.
这取决于依赖关系是否确实是实现细节,还是客户端希望/需要以某种方式了解的内容。 相关的一件事是该类所针对的抽象级别。 以下是一些示例:
如果您有一个方法在底层使用缓存来加速调用,那么缓存对象应该是 Singleton 或其他东西,并且不应该被注入。 缓存正在被使用这一事实是您的类的客户端不必关心的实现细节。
如果您的类需要输出数据流,那么注入输出流可能是有意义的,以便该类可以轻松地将结果输出到数组、文件或其他人可能想要发送数据的任何其他位置。
对于灰色区域,假设您有一个进行一些蒙特卡罗模拟的类。 它需要随机性的来源。 一方面,它需要这个的事实是一个实现细节,因为客户端实际上并不关心随机性到底来自哪里。 另一方面,由于现实世界的随机数生成器在客户端可能想要控制的随机性程度、速度等之间进行权衡,并且客户端可能想要控制播种以获得可重复的行为,因此注入可能是有意义的。 在这种情况下,我建议提供一种在不指定随机数生成器的情况下创建类的方法,并使用线程本地单例作为默认值。 如果/当需要更精细的控制时,请提供另一个允许注入随机源的构造函数。
It depends on whether the dependency is really an implementation detail or something that the client would want/need to know about in some way or another. One thing that is relevant is what level of abstraction the class is targeting. Here are some examples:
If you have a method that uses caching under the hood to speed up calls, then the cache object should be a Singleton or something and should not be injected. The fact that the cache is being used at all is an implementation detail that the clients of your class should not have to care about.
If your class needs to output streams of data, it probably makes sense to inject the output stream so that the class can easily output the results to an array, a file, or wherever else someone else might want to send the data.
For a gray area, let's say you have a class that does some monte carlo simulation. It needs a source of randomness. On the one hand, the fact that it needs this is an implementation detail in that the client really doesn't care exactly where the randomness comes from. On the other hand, since real-world random number generators make tradeoffs between degree of randomness, speed, etc. that the client may want to control, and the client may want to control seeding to get repeatable behavior, injection may make sense. In this case, I'd suggest offering a way of creating the class without specifying a random number generator, and use a thread-local Singleton as the default. If/when the need for finer control arises, provide another constructor that allows for a source of randomness to be injected.
在进一步解决这个问题之后,我现在认为依赖注入确实(此时)在某种程度上违反了封装。 但请不要误会我的意思 - 我认为在大多数情况下使用依赖注入是值得权衡的。
当您正在处理的组件要交付给“外部”方(想想为客户编写一个库)时,DI 违反封装的原因就变得很清楚了。
当我的组件需要通过构造函数(或公共属性)注入子组件时,无法保证
同时也不能说
这两句话均来自维基百科。
举一个具体的例子:我需要提供一个客户端 DLL,它可以简化并隐藏与 WCF 服务(本质上是远程外观)的通信。 因为它依赖于 3 个不同的 WCF 代理类,所以如果我采用 DI 方法,我将被迫通过构造函数公开它们。 这样我就暴露了我试图隐藏的通信层的内部结构。
一般来说,我完全支持 DI。 在这个特殊(极端)的例子中,我觉得它很危险。
Having struggled with the issue a little further, I am now in the opinion that Dependency Injection does (at this time) violate encapsulation to some degree. Don't get me wrong though - I think that using dependency injection is well worth the tradeoff in most cases.
The case for why DI violates encapsulation becomes clear when the component you are working on is to be delivered to an "external" party (think of writing a library for a customer).
When my component requires sub-components to be injected via the constructor (or public properties) there's no guarantee for
At the same time it cannot be said that
Both quotes are from wikipedia.
To give a specific example: I need to deliver a client-side DLL that simplifies and hides communication to a WCF service (essentially a remote facade). Because it depends on 3 different WCF proxy classes, if I take the DI approach I am forced to expose them via the constructor. With that I expose the internals of my communication layer which I am trying to hide.
Generally I am all for DI. In this particular (extreme) example, it strikes me as dangerous.
DI 违反了非共享对象的封装 - 就这样。 共享对象的生命周期在所创建的对象之外,因此必须聚合到所创建的对象中。 正在创建的对象私有的对象应该组合到创建的对象中 - 当创建的对象被销毁时,它会带走组合的对象。
我们以人体为例。 什么是组成的,什么是聚合的。 如果我们使用 DI,人体构造函数将拥有 100 个对象。 例如,许多器官(可能)是可替换的。 但是,它们仍然组成了身体。 血细胞每天在体内产生(并被破坏),不需要外部影响(蛋白质除外)。 因此,血细胞是由身体内部创建的 - new BloodCell()。
DI 的拥护者认为对象永远不应该使用 new 运算符。
这种“纯粹”方法不仅违反了封装,而且违反了创建对象的里氏替换原则。
DI violates Encapsulation for NON-Shared objects - period. Shared objects have a lifespan outside of the object being created, and thus must be AGGREGATED into the object being created. Objects that are private to the object being created should be COMPOSED into the created object - when the created object is destroyed, it takes the composed object with it.
Let's take the human body as an example. What's composed and what's aggregated. If we were to use DI, the human body constructor would have 100's of objects. Many of the organs, for example, are (potentially) replaceable. But, they are still composed into the body. Blood cells are created in the body (and destroyed) everyday, without the need for external influences (other than protein). Thus, blood cells are created internally by the body - new BloodCell().
Advocators of DI argue that an object should NEVER use the new operator.
That "purist" approach not only violates encapsulation but also the Liskov Substitution Principle for whoever is creating the object.
我也为这个想法而苦苦挣扎。 起初,使用 DI 容器(如 Spring)实例化对象的“要求”感觉就像是在跳圈。 但实际上,它实际上并不是一个箍 - 它只是创建我需要的对象的另一种“发布”方式。 当然,封装被“破坏”了,因为“类之外”的人知道它需要什么,但系统的其他部分实际上并不知道这一点——而是 DI 容器。 没有什么神奇的事情会发生不同,因为 DI“知道”一个对象需要另一个对象。
事实上,它变得更好 - 通过专注于工厂和存储库,我什至不需要知道 DI 的参与! 对我来说,这又重新盖上了封装的盖子。 哇!
I struggled with this notion as well. At first, the 'requirement' to use the DI container (like Spring) to instantiate an object felt like jumping thru hoops. But in reality, it's really not a hoop - it's just another 'published' way to create objects I need. Sure, encapsulation is 'broken' becuase someone 'outside the class' knows what it needs, but it really isn't the rest of the system that knows that - it's the DI container. Nothing magical happens differently because DI 'knows' one object needs another.
In fact it gets even better - by focusing on Factories and Repositories I don't even have to know DI is involved at all! That to me puts the lid back on encapsulation. Whew!
我相信简单。 在域类中应用 IOC/依赖注入不会带来任何改进,除了通过使用描述关系的外部 xml 文件使代码更难维护之外。 许多技术,如 EJB 1.0/2.0 和 EJB 1.0/2.0 struts 1.1 正在通过减少放入 XML 中的内容来进行逆转,并尝试将它们作为注释等放入代码中。因此,对您开发的所有类应用 IOC 将使代码变得毫无意义。
当依赖对象在编译时尚未准备好创建时,IOC 具有优势。 这可能发生在大多数基础设施抽象级架构组件中,尝试建立一个可能需要适用于不同场景的通用基础框架。 在这些地方使用 IOC 更有意义。 但这并没有使代码变得更简单/可维护。
与所有其他技术一样,这也有优点和缺点。 缺点。 我担心的是,我们在所有地方都实施了最新技术,而不管它们的最佳上下文使用情况如何。
I belive in simplicity. Applying IOC/Dependecy Injection in Domain classes does not make any improvement except making the code much more harder to main by having an external xml files describing the relation. Many technologies like EJB 1.0/2.0 & struts 1.1 are reversing back by reducing the stuff the put in XML and try put them in code as annoation etc. So applying IOC for all the classes you develope will make the code non-sense.
IOC has it benefits when the dependent object is not ready for creation at compile time. This can happend in most of the infrasture abstract level architecture components, trying establish a common base framework which may need to work for different scenarios. In those places usage IOC makes more sense. Still this does not make the code more simple / maintainable.
As all the other technologies, this too has PROs & CONs. My worry is, we implement latest technologies in all the places irrespective of their best context usage.
仅当类既负责创建对象(需要了解实现细节)又负责使用该类(不需要了解这些细节)时,封装才会被破坏。 我会解释原因,但首先做一个快速的汽车类比:
但回到编码。 封装是“对使用该实现的事物隐藏实现细节”。 封装是一件好事,因为实现细节可以在类的用户不知情的情况下发生变化。
使用依赖注入时,构造函数注入用于构造服务类型对象(与模型状态的实体/值对象相反)。 服务类型对象中的任何成员变量都表示不应泄漏的实现细节。 例如,套接字端口号、数据库凭据、调用以执行加密的另一个类、缓存等。
构造函数在最初创建类时相关。 这种情况发生在构建阶段,此时您的 DI 容器(或工厂)将所有服务对象连接在一起。 DI 容器只知道实现细节。 它了解所有实施细节,就像 Kombi 工厂的员工了解火花塞一样。
在运行时,创建的服务对象被调用 apon 来完成一些实际工作。 此时,对象的调用者对实现细节一无所知。
现在,回到封装。 如果实现细节发生变化,则在运行时使用该实现的类不需要更改。 封装没有被破坏。
如果实现细节发生变化,DI 容器(或工厂)确实需要更改。 您一开始就没有试图向工厂隐藏实现细节。
Encapsulation is only broken if a class has both the responsibility to create the object (which requires knowledge of implementation details) and then uses the class (which does not require knowledge of these details). I'll explain why, but first a quick car anaology:
But back to the coding. Encapsulation is "hiding an implementation detail from something using that implementation." Encapsulation is a good thing because the implementation details can change without the user of the class knowing.
When using dependency injection, constructor injection is used to construct service type objects (as opposed to entity/value objects which model state). Any member variables in service type object represent implementation details that should not leak out. e.g. socket port number, database credentials, another class to call to perform encryption, a cache, etc.
The constructor is relevant when the class is being initially created. This happens during the construction-phase while your DI container (or factory) wires together all the service objects. The DI container only knows about implementation details. It knows all about implementation details like the guys at the Kombi factory know about spark plugs.
At run-time, the service object that was created is called apon to do some real work. At this time, the caller of the object knows nothing of the implementation details.
Now, back to encapsulation. If implementation details change, then the class using that implementation at run-time does not need to change. Encapsulation is not broken.
If implementation details change, the DI container (or factory) does need to change. You were never trying to hide implementation details from the factory in the first place.
附言。 通过提供依赖注入,您不一定会破坏封装。 示例:
客户端代码仍然不知道实现细节。
PS. By providing Dependency Injection you do not necessarily break Encapsulation. Example:
Client code does not know implementation details still.
也许这是一种幼稚的思考方式,但是接受整数参数的构造函数和接受服务作为参数的构造函数有什么区别? 这是否意味着在新对象外部定义一个整数并将其输入到对象中会破坏封装? 如果该服务仅在新对象中使用,我不知道这会如何破坏封装。
此外,通过使用某种自动装配功能(例如 C# 的 Autofac),它使代码变得非常干净。 通过为 Autofac 构建器构建扩展方法,我能够删除大量随着依赖项列表的增长而不得不维护的 DI 配置代码。
Maybe this is a naive way of thinking about it, but what is the difference between a constructor that takes in an integer parameter and a constructor that takes in a service as a parameter? Does this mean that defining an integer outside the new object and feeding it into the object breaks encapsulation? If the service is only used within the new object, I don't see how that would break encapsulation.
Also, by using some sort of autowiring feature (Autofac for C#, for example), it makes the code extremely clean. By building extension methods for the Autofac builder, I was able to cut out a LOT of DI configuration code that I would have had to maintain over time as the list of dependencies grew.
我认为这是不言而喻的,至少 DI 显着削弱了封装性。 除此之外,还有 DI 的其他一些缺点需要考虑。
它使代码更难重用。 客户端无需显式提供依赖项即可使用的模块显然比客户端必须以某种方式发现该组件的依赖项是什么然后以某种方式使它们可用的模块更容易使用。 例如,最初创建用于 ASP 应用程序的组件可能期望由 DI 容器提供其依赖项,该容器提供具有与客户端 http 请求相关的生命周期的对象实例。 在另一个没有与原始 ASP 应用程序相同的内置 DI 容器的客户端中重现这一点可能并不容易。
它会使代码更加脆弱。 接口规范提供的依赖关系可能会以意想不到的方式实现,从而导致一整类运行时错误,而静态解析的具体依赖关系则不可能出现这些错误。
它可能会降低代码的灵活性,因为您最终可能对代码的工作方式有更少的选择。 并非每个类都需要在所属实例的整个生命周期内都存在其所有依赖项,但对于许多 DI 实现,您没有其他选择。
考虑到这一点,我认为最重要的问题就变成了“是否需要在外部指定特定的依赖项?”。 在实践中,我很少发现有必要从外部提供依赖项来支持测试。
如果依赖确实需要从外部提供,则通常表明对象之间的关系是协作而不是内部依赖,在这种情况下,适当的目标是封装每个类,而不是将一个类封装在另一个类中。
根据我的经验,使用 DI 的主要问题是,无论您是从内置 DI 的应用程序框架开始,还是向代码库添加 DI 支持,出于某种原因,人们认为既然您有 DI 支持,那么就一定是正确的实例化一切的方法。 他们只是从来没有费心去问“这种依赖关系是否需要在外部指定?”这个问题。 更糟糕的是,他们还开始试图强迫其他人也使用 DI 支持一切。
这样做的结果是,您的代码库开始不可避免地陷入这样一种状态:在代码库中创建任何内容的实例都需要大量迟钝的 DI 容器配置,并且调试任何内容都会加倍困难,因为您需要额外的工作量来尝试确定如何进行调试。以及任何东西被实例化的地方。
所以我对这个问题的回答是这样的。 使用 DI,您可以确定它可以为您解决的实际问题,而其他任何方式都无法更简单地解决该问题。
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.
我同意极端情况下,DI 可能会违反封装。 通常 DI 会公开从未真正封装的依赖项。 这是一个借自 Miško Hevery 的简化示例 单身人士是病态的骗子< /a>:
您从信用卡测试开始并编写一个简单的单元测试。
下个月你会收到一张 100 美元的账单。 为什么你被指控? 单元测试影响了生产数据库。 在内部,CreditCard 调用
Database.getInstance()
。 重构 CreditCard 使其在其构造函数中采用DatabaseInterface
暴露了存在依赖性的事实。 但我认为这种依赖关系从一开始就没有被封装,因为 CreditCard 类会导致外部可见的副作用。 如果你想在不重构的情况下测试 CreditCard,你当然可以观察依赖关系。我认为不值得担心将数据库暴露为依赖项是否会减少封装,因为这是一个很好的设计。 并非所有 DI 决策都会如此直接。 然而,其他答案都没有给出反例。
I agree that taken to an extreme, DI can violate encapsulation. Usually DI exposes dependencies which were never truly encapsulated. Here's a simplified example borrowed from Miško Hevery's Singletons are Pathological Liars:
You start with a CreditCard test and write a simple unit test.
Next month you get a bill for $100. Why did you get charged? The unit test affected a production database. Internally, CreditCard calls
Database.getInstance()
. Refactoring CreditCard so that it takes aDatabaseInterface
in its constructor exposes the fact that there's dependency. But I would argue that the dependency was never encapsulated to begin with since the CreditCard class causes externally visible side effects. If you want to test CreditCard without refactoring, you can certainly observe the dependency.I don't think it's worth worrying whether exposing the Database as a dependency reduces encapsulation, because it's a good design. Not all DI decisions will be so straight forward. However, none of the other answers show a counter example.
我认为这是一个范围问题。 当您定义封装(不让知道如何封装)时,您必须定义封装的功能是什么。
类原样:您所封装的内容是该类的唯一职责。 它知道怎么做。 举例来说,排序。 如果您注入一些用于排序的比较器,比如说客户端,那么这不是封装事物的一部分:快速排序。
配置的功能:如果您想提供即用型功能,那么您不是提供 QuickSort 类,而是提供配置了比较器的 QuickSort 类的实例。 在这种情况下,负责创建和配置的代码必须对用户代码隐藏。 这就是封装。
当您对类进行编程时,即在类中实现单一职责时,您正在使用选项 1。当您对
应用程序进行编程时,即制作一些东西来承担一些有用的具体工作,然后您重复使用选项 2.
这是已配置实例的实现:
这是其他一些客户端代码使用它的方式:
它是封装的,因为如果您更改实现(更改
clientSorter
bean 定义),它不会破坏客户端使用。 也许,当您使用所有一起编写的 xml 文件时,您会看到所有详细信息。 但相信我,客户端代码 (ClientService
)不知道关于它的排序器。
I think it's a matter of scope. When you define encapsulation (not letting know how) you must define what is the encapsuled functionality.
Class as is: what you are encapsulating is the only responsability of the class. What it knows how to do. By example, sorting. If you inject some comparator for ordering, let's say, clients, that's not part of the encapsuled thing: quicksort.
Configured functionality: if you want to provide a ready-to-use functionality then you are not providing QuickSort class, but an instance of QuickSort class configured with a Comparator. In that case the code responsible for creating and configuring that must be hidden from the user code. And that's the encapsulation.
When you are programming classes, it is, implementing single responsibilities into classes, you are using option 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:
This is how some other client code use it:
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.
可能值得一提的是,封装在某种程度上依赖于视角。
从处理
A
类的人的角度来看,在第二个示例中,A
对this.b
的本质知之甚少,而没有DI
与
查看此代码的人更了解第二个示例中
A
的性质。有了 DI,至少所有泄露的知识都集中在一处。
It's probably worth mentioning that
Encapsulation
is somewhat perspective dependent.From the perspective of someone working on the
A
class, in the second exampleA
knows a lot less about the nature ofthis.b
Whereas without DI
vs
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.