IOC容器违反了OOP原则


51

IOC容器的目的是什么?合并的原因可以简化为以下内容:

当使用OOP / SOLID开发原则时,依赖注入会变得混乱。您可能具有顶层入口点,这些入口点管理着位于其自身下方的多个级别的依赖关系,并通过构造递归传递依赖关系,或者您在工厂/构建器模式和接口中有一些重复的代码,可以根据需要构建依赖关系。

没有OOP / SOLID方式可以执行此操作拥有超级漂亮的代码。

如果上述陈述是正确的,那么IOC容器该如何做?据我所知,他们没有采用手动DI无法完成的未知技术,因此唯一的解释是IOC容器通过使用静态对象私有访问器来打破OOP / SOLID原则。

IOC容器是否在幕后打破了以下原则?这是真正的问题,因为我有很好的理解,但是感觉到其他人有更好的理解:

  1. 范围控制。范围是我对代码设计做出几乎所有决定的原因。块,本地,模块,静态/全局。范围应该非常明确,在块级别尽可能多,而在静态级别则尽可能少。您应该看到声明,实例化和生命周期的结尾。只要我明确指出,我相信该语言和GC可以管理范围。在我的研究中,我发现IOC容器将大多数或所有依赖项设置为静态,并通过幕后的一些AOP实现对其进行控制。所以没有什么是透明的。
  2. 封装。封装的目的是什么?我们为什么要保留私人会员呢?出于实际原因,因此我们的API的实现者无法通过更改状态(应由所有者类管理)来破坏实现。但是,出于安全原因,也不会发生注入超过我们的成员国并绕过验证和类控制的情况。因此,在编译之前以某种方式注入代码以允许外部访问私有成员的任何东西(模拟框架或IOC框架)都非常庞大。
  3. 单一责任原则。从表面上看,IOC容器似乎使事情更清洁。但是想象一下,如果没有帮助程序框架,您将如何完成相同的任务。您将传递带有十几个依赖项的构造函数。这并不意味着用IOC容器将其掩盖,这是一件好事!这是重构代码并遵循SRP的标志。
  4. 打开/关闭。就像SRP并非仅是类一样(我将SRP应用于单一职责范围,更不用说方法了)。打开/关闭不仅是不更改类代码的高级理论。这是一种了解程序配置并控制要更改和扩展的内容的实践。IOC容器可以部分地完全更改类的工作方式,因为:

    • 一种。主要代码没有确定切换出依赖项,而是框架配置。

    • b。范围可以在不受调用成员控制的时间进行更改,而是由静态框架在外部确定。

因此,该类的配置不是真正封闭的,它会根据第三方工具的配置自行更改。

这是一个问题,是因为我不一定是所有IOC容器的大师。尽管IOC容器的想法不错,但它们似乎只是掩盖了糟糕的实现的外观。但是,为了完成外部Scope控制,私有成员访问和延迟加载,必须进行许多非平凡的可疑事情。AOP很棒,但是通过IOC容器完成AOP的方式也值得怀疑。

我可以相信C#和.NET GC可以完成我期望的工作。但是我不能再信任第三方工具来改变我的已编译代码,以执行我无法手动执行的操作的变通办法。

EG:实体框架和其他ORM创建强类型对象并将其映射到数据库实体,并提供样板功能来执行CRUD。任何人都可以建立自己的ORM,并继续手动遵循OOP / SOLID原则。但是这些框架对我们有帮助,因此我们不必每次都重新发明轮子。看来,IOC容器可以帮助我们有目的地围绕OOP / SOLID原则开展工作并加以掩盖。


13
IOC和之间的+1相关Explicitness of code正是我遇到的问题。手动DI可以很容易地成为一件繁琐的事情,但是至少将独立的显式程序流程放在一个地方-“您得到所见即所得”。虽然IOC容器的绑定和声明可以轻松地自己成为隐藏的并行程序。
Eugene Podskal 2014年

6
这什至是个问题?
斯蒂芬

8
这似乎更像是博文而不是问题。
Bart van Ingen Schenau 2014年

4
我有一个内部问题类别,“过于争论”,ofc并不是正式的关闭理由,但我投票反对,有时甚至投票赞成关闭“不是真正的问题”。但是,它有两个方面,这个问题只满足第一个方面。(1)问题陈述论文并询问其是否正确,(2)发问者回答不同意的答案,捍卫最初的立场。马修的答案与问题的某些陈述相矛盾,因此除非提问者四处搜寻,否则我个人将其归类为一个善意问题,尽管形式为“我是对的,不是吗?”
史蒂夫·杰索普

3
@BenAaronson谢谢Ben。我正在尝试交流我从研究中学到的知识,并获得有关IOC容器性质的答案。作为研究的结果,我发现这些原则被打破了。对于我的问题的最佳答案将证实或否认IOC容器可以以某种方式完成我们无法手动完成的工作,而不会违反OOP / SOLID原则。由于您的好评,我通过更多示例重构了我的问题。
Suamere 2014年

Answers:


35

我将以数字方式详细说明您的观点,但首先,您需要特别注意以下几点:不要将消费者如何使用库与实现库的方法混为一谈。很好的例子就是实体框架(您自己会引用它作为一个好的库)和ASP.NET的MVC。两者都在幕后做了大量工作,例如反射,如果您通过日常代码进行传播,则绝对不会认为这是好的设计。

这些库在工作方式或幕后工作方面绝对不是 “透明的”。但这不是有害的,因为它们在其消费者中支持良好的编程原则。因此,每当谈到这样的库时,请记住,作为库的使用者,您不必担心其实现或维护。您只需要担心它如何帮助或阻碍您编写的使用该库的代码。不要混淆这些概念!

因此,要逐点进行:

  1. 立即我们可以得出我只能假设的上述例子。您说IOC容器将大多数依赖项设置为静态。好吧,也许它们如何工作的一些实现细节包括静态存储(尽管考虑到它们倾向于将像Ninject的对象的实例IKernel作为其核心存储,我什至对此表示怀疑)。但这不是您关心的问题!

    实际上,一个IoC容器在范围上与穷人的依赖注入一样明确。(我将继续将IoC容器与穷人的DI进行比较,因为将它们与根本没有DI的容器进行比较将是不公平和令人困惑的。而且,很明显,我没有使用“穷人的DI”作为贬义词。)

    在穷人的DI中,您将手动实例化一个依赖项,然后将其注入需要它的类中。在构造依赖项时,您将选择如何处理它-将其存储在局部作用域变量,类变量,静态变量中,无论如何都不要存储。您可以将同一实例传递给许多类,也可以为每个类创建一个新实例。随你。关键是要查看发生了什么,您可以查看可能在应用程序根目录附近的位置,在该位置创建依赖项。

    现在,一个IoC容器呢?好吧,你做的完全一样!此外,通过Ninject术语去,你看看绑定设置,以及知道它是否说像InTransientScopeInSingletonScope或什么的。如果有什么可能更清楚的话,因为您在那里有代码声明了它的作用域,而不必查看方法来跟踪某个对象发生了什么(一个对象可能作用域为一个块,但是它在其中多次使用了例如,阻止或仅阻止一次)。因此,也许您对必须使用IoC容器上的功能而不是原始语言功能来指示作用域的想法感到反感,但是只要您信任IoC库(您应该这样做),就没有真正的缺点。 。

  2. 我仍然真的不知道你在说什么。IoC容器是否将私有属性视为其内部实现的一部分?我不知道他们为什么会这样做,但是再次重申,如果这样做的话,您所关心的是如何实现所使用的库并不是您的问题。

    或者,也许,他们具有像注入私人二传手那样的能力?老实说,我从来没有遇到过这个问题,并且我怀疑这是否真的是一个常见功能。但是,即使它存在,这也是一种容易被滥用的工具的简单案例。请记住,即使没有IoC容器,也只需几行反射代码即可访问和修改私有属性。这是您几乎永远都不应做的事情,但这并不意味着.NET不利于公开此功能。如果有人如此明显地并且疯狂地滥用了工具,那是该人的过错,而不是工具的过错。

  3. 这里的最终点类似于2。在大多数情况下,IoC容器都使用构造函数!在非常特殊的情况下(由于特殊原因而不能使用构造函数注入)提供了Setter注入。一直使用setter注入来掩盖传入的依赖项的任何人都在大规模地使用该工具。那不是工具的错,是他们的错。

    现在,如果这是一个很容易犯的无辜错误,并且IoC容器鼓励这样做,那么好吧,也许您有意思。这就像让班上的每个成员都公开,然后在别人修改自己不应该做的事情时责备别人,对吗?但是,任何使用setter注入掩盖违反SRP的人要么故意忽略要么完全不了解基本设计原则。将此归咎于IoC容器是不合理的。

    这是特别正确的,因为您也可以使用穷人的DI轻松地做到这一点:

    var myObject 
       = new MyTerriblyLargeObject { DependencyA = new Thing(), DependencyB = new Widget(), Dependency C = new Repository(), ... };
    

    因此,实际上,这种担忧似乎与是否使用IoC容器完全正交。

  4. 更改类的协作方式并不违反OCP。如果是这样,那么所有依赖倒置都会鼓励违反OCP。如果是这种情况,它们将不会使用相同的SOLID首字母缩写!

    而且,a)和b)都与OCP无关。我什至不知道如何针对OCP回答这些问题。

    我唯一能猜到的是,您认为OCP与在运行时未更改行为有关,或者与在代码中控制依赖项生命周期的位置有关。不是。当添加或更改需求时,OCP不必修改现有代码。这全都与编写代码有关,而不是与您已经编写的代码如何粘合在一起(当然,松耦合是实现OCP的重要组成部分)。

最后一句话,您说:

但是我不能再信任第三方工具来改变我的已编译代码,以执行我无法手动执行的操作的变通办法。

是的,您可以。您绝对没有理由认为,比起其他任何第三方库,大量项目所依赖的这些工具更容易出错或发生意外行为。

附录

我只是注意到您的介绍段落也可以使用一些寻址方式。您讽刺地说,IoC容器“没有采用我们从未听说过的某些秘密技术”,以避免使用混乱且易于复制的代码来构建依赖关系图。而且您说的很对,他们实际上正在使用与程序员一样的基本技术来解决这些问题。

让我谈谈一个假设的情况。作为程序员,您将一个大型应用程序组合在一起,并且在构建对象图的入口处,您会发现代码很杂乱。有很多类被一次又一次地使用,并且每次您构建其中一个类时,都必须在其下再次构建整个依赖链。另外,您发现除声明代码或控制依赖项的生命周期外,没有任何表达方式,除非每个方法都有自定义代码。您的代码是非结构化的,并且充满重复性。这是您在介绍段落中谈到的混乱情况。

因此,首先,您开始重构位,其中一些重复的代码已足够结构化,可以将其拉入辅助方法,依此类推。但是随后您开始思考-这是我可能可以从一般意义上解决的一个问题,这个问题不是特定于该特定项目的,但可以在您将来的所有项目中为您提供帮助吗?

因此,您坐下来考虑一下,然后决定应该有一个可以解决依赖关系的类。然后,您将勾画出所需的公共方法:

void Bind(Type interfaceType, Type concreteType, bool singleton);
T Resolve<T>();

Bind说“在您看到类型的构造函数参数时interfaceType,传入的实例concreteType”。附加singleton参数说明是concreteType每次使用相同的实例,还是始终创建一个新实例。

Resolve将简单地尝试T使用任何可以发现其参数均为先前绑定类型的构造函数进行构造。它还可以递归调用自身,以完全解决依赖关系。如果由于未绑定所有内容而无法解析实例,则会引发异常。

您可以尝试自己实现这一点,您会发现需要一些反思,并且需要singleton为正确的绑定进行一些缓存,但是肯定不会有任何激烈或恐怖的事情。一旦完成,,您就拥有了自己的IoC容器的核心!真的那么恐怖吗?这与Ninject或StructureMap或Castle Windsor或您所喜欢的任何东西之间的唯一真正区别是,它们具有很多功能,可以满足基本版本不足的(很多!)用例。但从本质上讲,您拥有的是IoC容器的本质。


谢谢本。老实说,我只与IOC Containers合作了一年左右,尽管我没有这样做,但我周围的教程和同行确实专注于资产注入(包括私有)。这太疯狂了,它提出了我提出的所有观点。但是,即使其他人处于这种情况下,我认为他们可以做的最好的事情是了解有关IOC容器如何遵循原则的更多信息,并在代码审查中指出这些违反原则的人。但是...(续)
Suamere 2014年

我最大的担心不一定是在OOP / SOLID原则中列出的,而是范围的明确性。我仍然想把Block,Local,Module和Global级别的作用域扔到窗口之外,以了解关键字。使用块和声明来确定何时超出范围或将其处置的整个想法对我来说是巨大的,我认为这是IOC Containers对我来说最可怕的部分,用扩展关键字替换该非常基本的开发部分。
Suamere 2014年

因此,我将您的回答标记为答案,因为它确实可以解决这个问题。我的阅读方式是,您只需要相信IOC容器在幕后所做的一切,因为其他人都可以做到。至于可能的滥用的机会及其掩盖它的优雅方式,这是商店和代码审查所担心的,并且他们应该尽职调查以遵循良好的开发原则。
Suamere 2014年

2
关于4,Dependency Injection不是SOLID的缩写。'D'='依赖倒置原则',这是一个完全不相关的概念。
astreltsov

@astr好点。我已经用“反转”替换了“注入”,因为我认为那是我首先要写的意思。这仍然有点过分简化,因为依赖关系反转并不一定意味着有多个具体的实现,但是我认为含义已经很清楚了。如果退出实现违反了OCP,那么依靠多态抽象将很危险。
本·亚伦森

27

IOC容器不会违反很多这些原则吗?

理论上?不可以。IoC容器只是一种工具。您可以使对象承担单一责任,接口相互隔离,一旦编写就无法修改其行为,等等。

在实践中?绝对。

我的意思是,我听过不少程序员说他们使用IoC容器专门是为了允许您使用某些配置来更改系统的行为-违反了开放/封闭原则。过去曾经有过关于用XML编码的笑话,因为他们90%的工作都是在固定Spring配置。而且,您肯定是正确的,隐藏依赖项(诚然,并非所有IoC容器都这样做)是一种快速轻松地制作高度耦合且非常脆弱的代码的方法。

让我们以不同的方式来看待它。让我们考虑一个具有单个基本依赖性的非常基本的类。它依赖于不Action带任何参数并返回的委托或函子void。这个班级的责任是什么?你不知道 我可以将这种依赖命名为一个清晰的名称,例如CleanUpAction,它使您认为它可以实现您想要的功能。一旦IoC发挥作用,它就变成了任何东西。任何人都可以进入配置并修改您的软件以执行几乎任意的操作。

“与将an传递Action给构造函数有何不同?” 你问。这是不同的,因为一旦部署了软件(或在运行时!),就无法更改传递给构造函数的内容。这是不同的,因为您的单元测试(或消费者的单元测试)涵盖了将委托传递给构造函数的方案。

“但这是一个虚假的例子!我的东西不允许改变行为!” 你惊呼。抱歉告诉您,但确实如此。除非您要注入的接口只是数据。而且您的界面不仅仅是数据,因为如果只是数据,您将构建一个简单的对象并使用它。在注入点中有了虚拟方法后,使用IoC的类合同即为“我可以做任何事情 ”。

“与使用脚本来驱动事物有什么不同?那完全可以做到。” 你们有些人问。没什么不同 从程序设计的角度来看,没有区别。您正在调用程序来运行任意代码。可以肯定的是,这种游戏已经存在了很长时间。它以大量的系统管理工具完成。是OOP吗?并不是的。

他们当前无处不在没有任何借口。IoC容器有一个明确的目的-当您有很多组合时,将它们的依赖关系粘合在一起,或者它们移动得如此之快,以至于无法明确地显示这些依赖关系。因此,实际上很少有企业适合这种情况。


3
对。许多商店会声称他们的产品是如此复杂,以至于当他们不真正购买时,他们就属于这一类。在许多情况下都是如此。
Suamere 2014年

12
但是IoC与您所说的完全相反。它使用程序的打开/关闭。如果您可以更改整个程序的行为,而不必更改代码本身。
欣快2014年

6
@Euphoric-打开进行扩展,关闭进行修改。如果您可以更改整个程序的行为,则不会关闭它进行修改,是吗?IoC配置仍然是您的软件,它仍然是您的类用来执行工作的代码-即使它是XML而不是Java或C#或其他任何形式。
Telastyn 2014年

4
@Telastyn“为修改而关闭”表示无需更改(修改)代码以更改整体的行为。使用任何外部配置,您都可以将其指向一个新的dll,并具有整体更改的行为。
欣快2014年

12
@Telastyn这不是开放/封闭原则所说的。否则,采用参数的方法将违反OCP,因为我可以通过传递不同的参数来“修改”其行为。同样,在您的OCP版本中,它会与DI直接冲突,这会使两者都出现在SOLID首字母缩略词中,这很奇怪。
Ben Aaronson 2014年

14

使用IoC容器是否一定违反OOP / SOLID原则?没有

您是否可以以违反OOP / SOLID原则的方式使用IoC容器?是的

IoC容器只是用于管理应用程序中依赖项的框架。

您说过惰性注入,属性设置器以及我尚未感觉到需要使用的其他一些功能,我同意这些使用可能会给您带来不太理想的设计。但是,这些特定功能的使用是由于实现IoC容器的技术方面的限制。

例如,对于ASP.NET WebForms,您需要使用属性注入,因为无法实例化Page。这是可以理解的,因为WebForms从未在设计时考虑到严格的OOP范例。

另一方面,ASP.NET MVC完全可以通过非常友好的OOP / SOLID构造函数注入来使用IoC容器。

在这种情况下(使用构造函数注入),由于以下原因,我认为它促进了SOLID原则:

  • 单一责任原则-具有许多较小的类可以使这些类非常具体,并且有一个存在的原因。使用IoC容器使组织起来更加容易。

  • 打开/关闭-这是使用IoC容器的真正亮点,您可以通过注入不同的依赖项来扩展类的功能。通过注入依赖项,您可以修改该类的行为。一个很好的例子是通过编写装饰器以说添加缓存或日志记录来更改要注入的类。如果以适当的抽象级别编写,则可以完全更改依赖项的实现。

  • 里斯科夫换人原则-没什么关系

  • 接口隔离原理-如果需要,您可以将多个接口分配给单个具体时间,任何价值不菲的IoC容器都可以轻松实现此目的。

  • 依赖倒置-IoC容器使您可以轻松进行依赖倒置。

您定义了一堆依赖关系,并声明它们之间的关系和范围,并发出爆炸声:您神奇地拥有了一些插件,可以在某些时候更改代码或IL,并使事情正常进行。这不是明确或透明的。

它既透明又透明,您需要告诉您的IoC容器如何连接依赖项以及如何管理对象生存期。可以通过约定,配置文件或系统主应用程序中编写的代码来实现。

我同意,当我使用IOC容器编写或读取代码时,它摆脱了一些稍微不雅致的代码。

这是OOP / SOLID使系统易于管理的全部原因。使用IoC容器符合该目标。

该类的作者说:“如果我们不使用IOC容器,则构造函数将具有许多参数!!!这太可怕了!”

不管您在什么上下文中使用,具有12个依赖项的类都可能违反SRP。


2
极好的答案。另外,我认为所有主要的IOC容器也都支持AOP,这对OCP可以有很大帮助。对于跨领域的关注,AOP通常比Decorator更友好地使用OCP。
本·亚伦森2014年

2
@BenAaronson考虑到AOP将代码注入到一个类中,我不会称其为OCP友好型。您正在编译/运行时强行打开和更改类。
2014年

1
@Doval我看不到它是如何将代码注入到类中的。注意我在说的是Castle DynamicProxy样式,这里有一个拦截器,而不是IL编织样式。但是,拦截器实际上只是一个使用某种反射的装饰器,因此它不必将自身耦合到特定的接口。
Ben Aaronson 2014年

“依赖关系倒置-IoC容器使您可以轻松进行依赖关系倒置”-他们没有,它们只是利用了有益的东西,无论您是否拥有IoC。与其他原理相同。
2014年

实际上,我并没有得到解释的逻辑,即基于框架这一事实还不错。ServiceLocators也被当作框架,它们被认为是不好的:)。
ipavlu

5

IOC容器不会同时破坏并允许编码人员破坏许多OOP / SOLID原则吗?

staticC#中的关键字允许开发人员打破许多OOP / SOLID原则,但在正确的情况下它仍然是有效的工具。

IoC容器允许结构良好的代码并减轻开发人员的认知负担。IoC容器可用于使遵循SOLID原理变得更加容易。当然,它可能被滥用,但是如果我们排除了任何可能被滥用的工具的使用,我们将不得不完全放弃编程。

因此,尽管这种咆哮提出了一些关于编写不良代码的有效观点,但这并不是一个问题,也不是完全有用。


我对IOC容器的理解是,为了管理范围,惰性和可访问性,它更改了已编译的代码以执行破坏OOP / SOLID的操作,但将该实现隐藏在一个不错的框架下。但是,是的,它还为其他错误使用打开了大门。对于我的问题,最好的答案是对如何实现IOC容器进行一些澄清,这将增加或抵消我自己的研究成果。
Suamere 2014年

2
@Suamere-我认为您使用的IOC定义可能太宽泛。我使用的那个不会修改编译的代码。还有许多其他非IOC工具也可以修改已编译的代码。也许您的标题应该更像是:“是否修改了已编译的代码中断...”。
塞拉德2014年

@Suamere我对IoC并不了解,但是我从未听说过IoC会更改编译代码。您能否提供有关您正在谈论的平台/语言/框架的信息。
欣快2014年

1
“可以使用IoC容器使遵循SOLID原理更加容易。” -您能详细说明一下吗?解释它对每个原则的具体帮助将是有帮助的。当然,利用某些东西与帮助某些事情(例如DI)并不相同。
2014年

1
@Euphoric我不知道suamere在说什么.net框架,但是Spring确实在做底层代码修改。最明显的例子是它对基于方法的注入的支持(从而它覆盖了类中的抽象方法以返回其管理的依赖项)。它还广泛使用自动生成的代理,以包装您的代码以提供其他行为(例如,用于自动管理事务)。但是,其中很多实际上与其作为DI框架的角色无关。
Jules 2014年

3

在您的问题中,我看到许多错误和误解。第一个主要问题是缺少属性/字段注入,构造函数注入和服务定位器之间的区别。关于哪种是做事的正确方法,已经有很多讨论(例如,烈火战役)。在您的问题中,您似乎只假设属性注入而忽略构造函数注入。

第二件事是您假设IoC容器会以某种方式影响代码的设计。如果您使用IoC,则它们没有,或者至少没有必要更改代码的设计。IoC会在代码中隐藏真正的问题(最有可能破坏SRP的类),而隐藏的依赖关系是人们不鼓励使用属性注入和服务定位符的原因之一,这是因为IoC在代码中隐藏了真正的问题。

我不了解封闭原则的问题所在,因此我不会对此发表评论。但是您缺少SOLID的一部分,它对此有重大影响:依赖倒置原则。如果您遵循DIP,您会发现,反转依赖项会引入一个抽象,在创建类的实例时需要解决其实现。使用这种方法,您将获得许多类,这些类需要实现多个接口并在构造时提供这些接口才能正常运行。如果随后要手动提供这些依赖项,则必须做很多其他工作。IoC容器使这项工作变得更加容易,甚至允许您更改它而无需重新编译程序。

因此,您的问题的答案是否定的。IoC容器不会违反OOP设计原则。恰恰相反,它们解决了由遵循SOLID原则(尤其是Dependency-Inversion原则)引起的问题。


最近尝试将IoC(Autofac)应用于不重要的项目。意识到我必须对非语言结构(new()与API)进行类似的工作:指定应解析为接口的内容和应解析为具体类的内容,指定应为实例的实例和应为单例的实例,确保属性注入能够减轻循环依赖。决定放弃。虽然可以与ASP MVC中的Controller一起很好地工作。
2014年

@Den您的项目在设计时显然没有考虑依赖反转。而且我从没说过使用IoC很简单。
欣快2014年

实际上,我从一开始就在使用Dependency Inversion。我最大的担心就是IoC会影响设计。例如,为什么我会在所有地方都使用接口来简化IoC?另请参阅有关该问题的最高评论。
2014年

@Den从技术上讲您不需要“使用”界面。接口有助于模拟单元测试。您还可以标记需要模拟的事物。
Fred
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.