依赖倒置原则(SOLID)与封装(OOP的支柱)

最近我正在讨论依赖倒置原理控制反转dependency injection 。 关于这个话题,我们正在辩论这些原则是否违反了OOP的一个支柱,即封装

我对这些事情的理解是:

  • 依赖倒置原则意味着对象应该依赖于抽象而不是结核 – 这是实现控制反转模式和dependency injection的基本原理。
  • 控制反转是依赖反转原理的模式实现,其中抽象依赖关系取代具体的依赖关系,允许在对象之外指定依赖关系的结构。
  • dependency injection是实现控制反转并提供依赖性parsing的devise模式。 当一个依赖被传递给一个依赖的组件时就会发生注入。 实质上,dependency injection模式提供了一种将依赖抽象与具体实现耦合的机制。
  • 封装是高层对象所需要的数据和function被隔离开来并且不可访问的过程,因此程序员不知道如何实现对象。

辩论得到了一个关键点:

IoC不是OOP,因为它打破封装

就我个人而言,我认为所有的OOP开发者都应该遵守依存倒置原则和控制倒置的模式,而且我的观点如下:

如果有(可能)不只一个方法去剥皮,那就不要像只有一个。

例1:

class Program { void Main() { SkinCatWithKnife skinner = new SkinCatWithKnife (); skinner.SkinTheCat(); } } 

这里我们看到一个封装的例子。 程序员只需要调用Main() ,猫就会被剥皮,但是如果他想要剥皮猫,比如说一组锋利的razor呢?

例2:

 class Program { // Encapsulation ICatSkinner skinner; public Program(ICatSkinner skinner) { // Inversion of control this.skinner = skinner; } void Main() { this.skinner.SkinTheCat(); } } ... new Program(new SkinCatWithTeeth()); // Dependency Injection 

在这里,我们观察了依赖倒置原理和控制反转,因为提供了一个抽象( ICatSkinner )以允许程序员传入具体的依赖关系。 最后,有一种方法去皮肤猫!

这里的争吵是; 这是否打破封装? 技术上可以争辩说.SkinTheCat(); 仍然被封装在Main()方法调用中,所以程序员不知道这个方法的行为,所以我不认为这会破坏封装。

再深入一点,我认为IoC 容器因为使用了reflection而破坏了OOP,但是我不相信IoC破坏了OOP,我也不相信IoC破坏了封装。 其实我会尽可能地说:

控件的封装和反转相互愉快地相互重合,允许程序员只通过一个依赖的结构,同时通过封装隐藏整个实现。

问题:

  • IoC是依赖倒置原则的直接实现吗?
  • IoC总是打破封装,因此OOP?
  • IoC应该谨慎使用,虔诚还是恰当?
  • IoC和IoC容器有什么区别?

IoC总是打破封装,因此OOP?

不,这些是分级相关的问题。 封装是OOP中最被误解的概念之一,但我认为这种关系最好通过抽象数据types(ADT)来描述。 本质上,ADT是数据和相关行为的一般描述。 这个描述是抽象的; 它省略了实现细节。 相反,它根据前后 条件描述了一个ADT。

这就是Bertrand Meyer所称的合同devise 。 您可以在面向对象的软件构build中阅读关于OOD的详细描述。

对象通常被描述为具有行为的数据 。 这意味着没有数据的对象实际上不是一个对象。 因此,您必须以某种方式将数据导入对象。

例如,您可以通过其构造函数将数据传递给对象:

 public class Foo { private readonly int bar; public Foo(int bar) { this.bar = bar; } // Other members may use this.bar in various ways. } 

另一个select是使用setter函数或属性。 我希望我们可以同意,到目前为止,封装不受侵犯。

如果我们把一个整数变成另一个具体的类,会发生什么?

 public class Foo { private readonly Bar bar; public Foo(Bar bar) { this.bar = bar; } // Other members may use this.bar in various ways. } 

和以前相比唯一的区别就是bar现在是一个对象,而不是原始的。 但是,这是一个错误的区别,因为在面向对象的devise中, 整数也是一个对象 。 这只是因为在各种编程语言(Java,C#等)中的性能优化,原语(string,整数,布尔等)与“真实”对象之间存在实际差异。 从OOD的angular度来看,它们都是一样的。 string也有行为:你可以把它们变成全部大写字母,反转它们等等。

如果Bar是一个封闭/最后一个只有非虚拟成员的具体类,封装是否被违反?

bar只是行为的数据,就像一个整数,但除此之外,没有什么区别。 到目前为止,封装没有被侵犯。

如果我们允许Bar拥有一个虚拟成员,会发生什么?

是封装破了?

我们还可以expression关于Foo前后条件,因为Bar只有一个虚拟成员?

如果Bar坚持利斯科夫换人原则 (LSP),那就没有什么区别了。 LSP明确指出,改变行为不能改变系统的正确性。 只要合同得到履行,封装仍然完好无损。

因此,LSP(其中依赖倒置原则是另一个的SOLID原则之一)不违反封装; 它描述了在存在多态性的情况下保持封装的原理

如果Bar是一个抽象基类,结论是否会改变? 一个界面?

不,这不是:这些只是不同程度的多态性。 因此,我们可以将Bar重命名为IBar (以便表示它是一个接口),并将其传递到Foo作为其数据:

 public class Foo { private readonly IBar bar; public Foo(IBar bar) { this.bar = bar; } // Other members may use this.bar in various ways. } 

bar只是另一个多态对象,只要LSP成立,封装就成立了。

TL; DR

SOLID也被称为OOD的原则 。 封装(即按合同devise)定义了基本规则。 SOLID描述遵循这些规则的准则。

IoC是依赖倒置原则的直接实现吗?

这两者有关的方式,他们谈论抽象,但就是这样。 控制反转是:

一个计算机程序的自定义写入部分从一个通用的可重用的库( 源 )接收控制stream的devise,

控制反转允许我们将自定义代码挂接到可重用库的pipe道中。 换句话说,倒置控制是关于框架的。 一个不适用Inversion of Control的可重用库只是一个库。 框架是一个可重用的库, 它确实应用了Inversion of Control。

请注意,如果我们自己编写框架,我们作为开发人员只能应用控制反转; 作为应用程序开发人员不能应用控制反转。 我们可以(也应该)应用依赖倒置原则和dependency injection模式。

IoC总是打破封装,因此OOP?

由于IoC只是挂在框架的pipe道上,所以没有任何东西在泄漏。 所以真正的问题是:dependency injection是否破坏封装。

这个问题的答案是:不,事实并非如此。 它不会因为两个原因而破坏封装:

  • 由于依赖倒置原则声明我们应该对抽象进行编程,所以消费者将无法访问所使用的实现的内部,因此实现不会破坏封装到客户端。 这个实现在编译时甚至可能是不可知的或者是不可访问的(因为它生活在一个未被引用的程序集中),并且实现在这种情况下不会泄漏实现细节并破坏封装。
  • 尽pipe这个实现接受了它在整个构造函数中需要的依赖关系,但是这些依赖关系通常会存储在私有字段中,并且不能被任何人访问(即使消费者直接依赖于具体的types),也不会破坏封装。

IoC应该谨慎使用,虔诚还是恰当?

再一次,问题是“DIP和DI应该谨慎使用”。 在我看来,答案是:不,你应该在整个应用程序中使用它。 显然,你永远不应该虔诚地使用东西。 你应该应用固体原则,而DIP是这些原则的重要组成部分。 他们将使您的应用程序更灵活,更易于维护,在大多数情况下,应用SOLID原则是非常合适的。

IoC和IoC容器有什么区别?

dependency injection是一种可以使用或不使用IoC容器的模式。 IoC容器只是一个工具,可以帮助您更方便地构build对象图,以防您有一个正确应用SOLID原则的应用程序。 如果您有一个不适用SOLID原则的应用程序,则您将很难使用IoC容器。 你将很难应用dependency injection。 或者让我更广泛地说,无论如何,你将很难保持你的应用程序。 但绝不是IoC容器是必需的工具。 我正在开发和维护一个.NET的IoC容器,但是我并不总是为所有的应用程序使用一个容器。 对于大的BLOBAs(业务应用程序的无聊行),我经常使用一个容器,但对于较小的应用程序(或Windows服务),我并不总是使用一个容器。 但是我几乎总是使用dependency injection作为模式,因为这是坚持DIP的最有效方法。

注意:由于IoC容器可以帮助我们应用dependency injection模式,因此“IoC容器”对于这样的库来说是一个糟糕的名字。

但是,尽pipe我上面提到了任何东西,请注意:

在软件开发者的真实世界中,有用性胜过理论[来自Robert C. Martin的敏捷原则,模式和实践 ]

换句话说,即使DI会打破封装,也无所谓,因为这些技术和模式已被certificate是非常有价值的,因为它导致了非常灵活和可维护的系统。 实践胜过理论。

总结这个问题:

我们有能力让一个服务实例化它自己的依赖关系。

然而,我们也有能力让一个服务简单地定义抽象,并且要求一个应用程序知道相关的抽象,创build具体的实现,并将它们传递给它们。

问题不在于: “我们为什么这样做?” (因为我们知道有一个为什么的巨大列表)。 但问题是, “不select2破解封装?

我的“务实”答案

我认为马克是任何这样的答案最好的select,正如他所说:不,封装不是人们的想法。

封装隐藏了服务或抽象的实现细节。 依赖不是一个实现细节。 如果你把服务看成是一个合同,将其后续的子服务依赖关系看作是次合同(等等),那么你实际上最终会得到一个巨大的附加合同。

想象一下,我是一个来电者,我想用法律服务来起诉我的老板。 我的应用程序将不得不知道这样做的服务。 这就违背了理论,认识到完成我的目标所需的服务/合同是错误的。

这里的观点是…是的,但我只想聘请律师,我不在乎他使用什么书籍或服务。 我会从interwebz中得到一些随意的东西,而不关心他的实现细节…就像这样:

 sub main() { LegalService legalService = new LegalService(); legalService.SueMyManagerForBeingMean(); } public class LegalService { public void SueMyManagerForBeingMean(){ // Implementation Details. } } 

但事实certificate,需要其他服务来完成工作,比如了解工作场所的法律。 而事实certificate……我非常感兴趣的是律师签下我的名字的合同,以及他偷取我的钱的其他东西。 例如…为什么这个地狱是这个在韩国的互联网律师? 那该怎么帮助我!?!? 这不是一个实现细节,这是我很乐意pipe理的依赖性需求链的一部分。

 sub main() { IWorkLawService understandWorkplaceLaw = new CaliforniaWorkplaceLawService(); //IWorkLawService understandWorkplaceLaw = new NewYorkWorkplaceLawService(); LegalService legalService = new LegalService(understandWorkplaceLaw); legalService.SueMyManagerForBeingMean(); } public interface ILegalContract { void SueMyManagerForBeingMean(); } public class LegalService : ILegalContract { private readonly IWorkLawService _workLawService; public LegalService(IWorkLawService workLawService) { this._workLawService = workLawService; } public void SueMyManagerForBeingMean() { //Implementation Detail _workLawService.DoSomething; // { implementation detail in there too } } } 

现在,我所知道的是我有一个合同,其他合同可能有其他合同。 我对这些合同负责,而不是他们的实施细节。 虽然我很乐意签署那些与我的要求相关的结论。 再次,我不在乎这些结论如何工作,只要我知道我有一个具有约束力的合同,说我们以某种确定的方式交换信息。

我会尽量回答你的问题,根据我的理解:

  • IoC是依赖倒置原则的直接实现吗?

    我们不能将IoC标记为DIP的直接实现,因为DIP侧重于取决于抽象而不是更低级别模块的具体化来制作更高级别的模块。 而是IoC是dependency injection的实现。

  • IoC总是打破封装,因此OOP?

    我不认为IoC的机制将违反封装。 但可以使系统变得紧密耦合。

  • IoC应该谨慎使用,虔诚还是恰当?

    IoC可以被用作桥梁模式(Bridge Pattern)之类的许多模式,在这种模式中,从抽象中分离凝结物改善了代码。 因此可以用来实现DIP。

  • IoC和IoC容器有什么区别?

    IoC是Dependency Inversion的一种机制,但容器是使用IoC的容器。

封装与面向对象编程世界中的依赖倒置原则不矛盾。 例如,在汽车devise中,您将拥有一个外部封装的“内部引擎”,以及可以轻松更换的“车轮”,并将其视为汽车的外部组件。 汽车有规格(界面)旋转车轮的轴,车轮组件实施与轴相互作用的部分。

这里,内部引擎表示封装过程,而车轮部件表示汽车devise中的依赖性倒置原则(DIP)。 使用DIP,基本上我们防止构build一个单一的对象,而是使我们的对象组合。 你可以想象你build立一个汽车,在那里你不能更换车轮,因为它们是内置在车里。

你也可以在我的博客这里阅读更多关于依赖倒置原则的更多细节。

我只会回答一个问题,因为许多其他人已经回答了所有的问题。 请记住,没有正确或错误的答案,只是用户的喜好。

IoC应该谨慎使用,虔诚还是恰当? 我的经验使我相信,dependency injection只能用于一般的类,将来可能需要改变。 虔诚地使用它会导致一些类在构造函数中需要15个接口,这会花费很多时间。 这往往导致20%的发展和80%的pipe家。

有人提出了一个汽车的例子,汽车的build造者将如何改变轮胎。 dependency injection允许更换轮胎而不关心具体的实现细节。 但是,如果我们依靠注入dependency injection…那么我们需要开始build立与轮胎成分的接口…那么,轮胎的线程呢? 那些轮胎的缝合呢? 那些线程中的化学物质呢? 那些化学primefaces呢? 等等…好吧! 嘿! 在某些时候,你将不得不说“够了”! 让我们不要把每一件小事都变成一个界面……因为那样会花费太多的时间。 有一些类可以自己包含在类中,并在类中实例化,这是可以的! 开发速度更快,实例化类更容易。

只是我2美分。

当ioc和dependency injection破坏封装的时候,我发现了一个例子。 让我们假设我们有一个ListUtil类。 在那个类中有一个叫做删除重复的方法。 这个方法接受一个List。 有一个接口ISortAlgorith与sorting方法。 有一个名为QuickSort的类实现了这个接口。 当我们编写alogorithm去除重复项时,我们必须对列表进行sorting。 现在,如果RemoveDuplicates允许一个接口ISortAlgorithm作为参数(IOC /dependency injection)来允许其他人select另一个algorithm来删除重复的扩展性,我们将暴露删除ListUtil类的重复function的复杂性。 因此违反了Oops的基石。