我应该避免使用依赖注入和 IoC 吗?
Should I avoid using Dependency Injection and IoC?
在我的中型项目中,我将静态 classes 用于存储库、服务等,它实际上工作得很好,即使大多数程序员的期望相反。我的代码库非常紧凑、干净且易于理解。现在我尝试重写所有内容并使用 IoC
(控制反转),但我非常失望。我必须在每个 class、控制器等中手动初始化十几个依赖项,为接口添加更多项目等等。我真的没有看到我的项目有任何好处,而且似乎它造成的问题多于解决的问题。我在 IoC
/DI
中发现了以下缺点:
- 更大的代码大小
- 馄饨代码而不是意大利面条代码
- 性能较慢,即使我要调用的方法只有一个依赖项,也需要在构造函数中初始化所有依赖项
- 不使用 IDE 时更难理解
- 一些错误被推送到运行-time
- 添加额外的依赖项(DI 框架本身)
- 新员工必须先学习 DI 才能使用它
- 大量样板代码,这对有创造力的人来说很糟糕(例如将实例从构造函数复制到属性...)
我们不测试整个代码库,只测试某些方法并使用真实数据库。那么,当测试不需要模拟时,是否应该避免依赖注入?
如果您必须在代码中手动初始化依赖项,那您就做错了。 IoC
的一般模式是构造函数注入,或者可能是 属性 注入。 Class 或者控制器根本不应该知道 DI 容器。
一般来说,您所要做的就是:
- 配置容器,如
Interface = Class in Singleton scope
- 使用它,喜欢
Controller(Interface interface) {}
- 受益于在一处控制所有依赖项
我没有看到任何样板代码或较慢的性能或您描述的任何其他内容。如果没有它,我真的无法想象如何编写或多或少复杂的应用程序。
但一般来说,您需要决定什么更重要。取悦 "creative people" 或构建可维护且健壮的应用程序。
顺便说一句,要创建 属性 或从构造函数归档,您可以在 R# 中使用 Alt+Enter
,它会为您完成所有工作。
有很多 'textbook' 支持使用 IoC 的论据,但根据我个人的经验,收益 are/were:
可以只测试项目的一部分,并模拟其他部分。例如,如果您有一个从 DB 返回配置的组件,则很容易模拟它,这样您的测试就可以在没有真实 DB 的情况下工作。使用静态 类 这是不可能的。
更好地了解和控制依赖项。使用 static 类 可以很容易地添加一些依赖项,甚至不会引起注意,这可能会在以后产生问题。使用 IoC,这更加明确和可见。
更明确的初始化顺序。对于静态 类 这通常是一个黑盒子,并且由于循环使用可能存在潜在问题。
对我来说唯一的不便是,通过将所有内容都放在接口之前,无法从用法 (F12) 直接导航到实现。
但是,项目的开发人员最能判断特定情况下的利弊。
您的大部分顾虑似乎都归结为误用或误解。
更大的代码量
这通常是正确遵守单一职责原则和接口隔离原则的结果。它大得多吗?我怀疑没有你说的那么大。但是,它所做的很可能是将 classes 归结为特定功能,而不是让 "catch-all" classes 做任何事情。在大多数情况下,这是关注点健康分离的标志,而不是问题。
馄饨代码而不是意大利面条代码
再一次,这很可能导致您思考堆栈而不是难以看到的依赖关系。我认为这是一个很大的好处,因为它导致了适当的抽象和封装。
性能较慢 只需使用快速容器即可。我最喜欢的是 SimpleInjector 和 LightInject。
甚至需要在构造函数中初始化所有依赖
如果我要调用的方法只有一个依赖项
这再次表明您违反了单一职责原则。这是一件好事,因为它迫使您从逻辑上思考您的架构,而不是随意添加。
当没有使用 IDE 时更难理解一些错误被推送到 运行-time
如果您仍然不使用 IDE,真丢人。现代机器对此没有很好的论据。此外,如果您愿意,某些容器 (SimpleInjector) 将首先验证 运行。您可以通过简单的单元测试轻松检测到这一点。
添加额外的依赖(DI 框架本身)
你必须选择你的战斗。如果学习一个新框架的成本低于维护意大利面条代码的成本(我怀疑会如此),那么这个成本是合理的。
新员工必须先学习 DI 才能使用它
如果我们回避新模式,我们就永远不会成长。我认为这是丰富和发展团队的机会,而不是伤害他们的方式。此外,权衡是学习意大利面条代码,这可能比选择行业范围的模式要困难得多。
大量样板代码对有创造力的人不利(例如将实例从构造函数复制到属性...)
这是完全错误的。强制依赖项应始终通过构造函数传入。只有可选的依赖项应该通过属性设置,并且只应该在非常特殊的情况下这样做,因为通常它会违反单一职责原则。
我们不测试整个代码库,只测试某些方法并使用真实数据库。那么,当测试不需要模拟时,是否应该避免依赖注入?
我认为这可能是最大的误解。依赖注入不仅仅是为了让测试更容易。这样一来,您就可以浏览 class 构造函数的签名,并且 立即 知道需要什么才能使 class 打钩。这对于 static classes 是不可能的,因为 classes 可以在他们喜欢的任何时候在堆栈中向上和向下调用而没有押韵或理由。您的目标应该是增加代码的一致性、清晰度和区别性。这是使用 DI 的最大原因,也是我强烈建议您重新访问它的原因。
您没有选择使用 IOC 库(StructureMap、Ninject、Autofac 等)的原因是什么?
使用其中任何一个都会让你的生活更轻松。
Although David L has already made an excellent set of commentaries on your points, I'll add my own as well.
更大的代码量
我不确定你是如何得到一个更大的代码库的; IOC 库的典型设置非常小,并且由于您在 class 构造函数中定义了不变量(依赖项),因此您还删除了一些您不需要的代码(即“new xyz()”东西)不需要了。
馄饨代码而不是意大利面条代码
我正好很喜欢馄饨:)
性能较慢,即使我要调用的方法只有一个依赖项,也需要在构造函数中初始化所有依赖项
如果你这样做,那么你根本就没有真正使用依赖注入。您应该通过 class 本身的构造函数参数中声明的依赖参数接收现成的、完全加载的对象图——而不是在构造函数中创建它们!
大多数现代 IOC 库都快得离谱,而且永远不会成为性能问题。
Here's一个很好的视频证明了这一点。
不使用 IDE 时更难理解
的确如此,但这也意味着您可以借此机会从抽象的角度进行思考。例如,你可以看一段代码
public class Something
{
readonly IFrobber _frobber;
public Something(IFrobber frobber)
{
_frobber=frobber;
}
public void LetsFrobSomething(Thing theThing)
{
_frobber.Frob(theThing)
}
}
当您查看此代码并试图弄清楚它是否有效,或者它是否是问题的根本原因时,您可以忽略实际的 IFrobber
实现;它只是代表了 Frob 某些东西的抽象能力,您不需要在脑子里记住 如何 任何特定的 Frobber 可能会完成它的工作。你可以专注于确保这个 class 做它应该做的事情 - 即,将一些工作委托给某种 Frobber。
另请注意,您甚至不需要在这里使用接口;您也可以继续并注入具体的实现。然而,这往往会违反依赖倒置原则(它仅与我们在这里讨论的 DI 有切向关系),因为它迫使 class 依赖于具体而不是抽象。
一些错误被推送到运行-time
不多于或少于在构造函数中手动构建图形;
添加额外的依赖项(DI 框架本身)
这也是事实,但大多数 IOC 库都非常小且不引人注目,在某些时候,您必须决定权衡拥有稍大的生产工件是否值得(确实如此)
新员工必须先学习 DI 才能使用它
这与任何新技术的情况并没有什么不同 :) 学习使用 IOC 库往往会打开思路,接受其他可能性,例如 TDD、SOLID 原则等等,这从来都不是坏事!
大量样板代码,这对有创造力的人来说很糟糕(例如将实例从构造函数复制到属性...)
我不明白这一点,你怎么会得到这么多样板代码;我不认为将给定的依赖项存储在私有只读成员中作为值得讨论的样板 - 请记住,如果每个 class 有超过 3 或 4 个依赖项,则很可能违反了 SRP 并且应该重新考虑您的设计。
最后,如果您不相信这里提出的任何论点,我仍然建议您阅读 Mark Seeman 的“Dependency Injection in .Net”。 (或者实际上他必须在 DI 上说的任何其他内容,您可以在他的博客上找到)。
我保证你会学到一些有用的东西,我可以告诉你,它改变了我编写软件的方式。
尽管 IoC/DI 并不是适用于所有情况的灵丹妙药,但您可能没有正确应用它。依赖注入背后的一套原则需要时间来掌握,或者至少,它确实对我有用。如果应用得当,它可以带来(除其他外)以下好处:
- 改进的可测试性
- 提高灵活性
- 改进的可维护性
- 改进并行开发
从你的问题中,我已经可以提取出一些你的情况可能出错的地方:
I have to manually initialize dozen of dependencies in every class
这意味着您创建的每个 class 都负责创建它所需的依赖项。这是一种称为 Control Freak 的反模式。 class 不应该 new
增加其依赖项本身。您甚至可能已经应用了 Service Locator anti-pattern,其中您的 class 通过调用容器(或表示容器的抽象)来请求其依赖项以获取特定的依赖项。 class 应该只将它需要的依赖项定义为构造函数参数。
dozen of dependencies
此声明暗示您违反了 Single Responsibly Principle。这实际上并没有耦合到 IoC/DI,您的旧代码可能已经违反了单一职责原则,导致其他开发人员难以理解和维护它。原作者通常很难理解为什么其他人很难维护代码,因为你写的东西通常很适合你的想法。通常违反 SRP 会导致其他人难以理解和维护代码。测试违反 SRP 的 classes 通常更难。一个 class 最多应该有六个依赖项。
add more projects for interfaces and so on
这意味着您违反了 Reused Abstraction Principle. In general, the majority of components/classes in your application should be covered by a dozen of abstractions. For instance, all classes that implement some use case probably deserve one single (generic) abstraction. Classes that implement queries also deserve one abstraction。对于我编写的系统,80% 到 95% 的组件(class 包含应用程序行为的组件)由 5 到 12 个(主要是通用的)抽象覆盖。大多数时候,您不需要仅为接口创建新项目。
大多数时候,我将这些接口放在同一个项目的根目录中。
much bigger codesize
您编写的代码量最初不会有太大差异。然而,依赖注入的实践仅在应用 SOLID 时效果很好,并且 SOLID 促进小的集中 classes。 类 只负责一项工作。这意味着您将拥有许多易于理解且易于组合成灵活系统的小型 classes。并且不要忘记:我们不应该努力编写更少的代码,而是更易于维护的代码。
但是,有了良好的 SOLID 设计和正确的抽象,我实际上不得不编写比以前少得多的代码。例如,应用某些横切关注点(如日志记录、审计跟踪、授权等)可以通过在应用程序的基础设施层编写几行代码来应用,而不是让它分布在整个应用程序中应用。它甚至让我能够做以前不可行的事情,因为它们迫使我对整个代码库进行彻底的更改,这非常耗时,以至于管理层不允许我这样做。
ravioli-code instead of spaghetti-code
harder to understand when no IDE is used
这是真的。依赖注入促进 classes 彼此解耦。这有时会使浏览到代码库变得更加困难,因为 class 通常依赖于抽象而不是具体的 classes。在过去,我发现 DI 给我的灵活性远远超过了寻找实现的成本。使用 Visual Studio 2015 我可以简单地执行 CTRL + F12 来查找接口的实现。如果只有一个实现,Visual Studio 将直接跳转到该实现。
slower performance
这不是真的。性能与使用仅静态方法调用的代码库没有任何不同。然而,您选择让您的 classes 具有 Transient 生活方式,这意味着您可以在所有地方新建实例。在我上一个应用程序中,我只为每个应用程序创建了一次所有 classes,这提供了与仅具有静态方法调用大致相同的性能,但应用程序的好处是非常灵活和可维护。但请注意,即使您决定 new
为每个(网络)请求完成对象图,性能成本也很可能比任何 I/O(数据库、文件系统和网络服务)低几个数量级调用)您在该请求期间执行的次数,即使使用最慢的 DI 容器也是如此。
some errors are pushed to run-time
adding additional dependency (DI framework itself)
这些问题都暗示使用了 DI 库。 DI 库在 运行 时间进行对象组合。然而,DI 库 不是 练习依赖注入时的必需工具。小型应用程序可以在没有工具的情况下受益于依赖注入;一种叫做 Pure DI 的做法。您的应用程序可能不会从使用 DI 容器中受益,但大多数应用程序实际上受益于使用依赖注入(如果使用正确)作为实践。再次强调:工具是可选的,编写可维护的代码不是。
但即使您使用 DI 库,也有一些库具有内置工具,可让您验证和诊断您的配置。它们不会为您提供编译时支持,但它们允许您在应用程序启动或使用单元测试时 运行 此分析。这可以防止您对整个应用程序进行回归,只是为了验证您的容器是否正确连接。我的建议是选择一个 DI 容器来帮助您检测这些配置错误。
new staff have to learn DI first in order to work with it
这是对的,但依赖注入本身并不难学。真正难学的是如何正确应用 SOLID 原则,当你想要编写需要由多个开发人员维护相当长一段时间的应用程序时,你无论如何都需要学习这一点。我宁愿投资于教我团队中的开发人员编写 SOLID 代码,而不是让他们编写代码;以后肯定会导致维护困难。
a lot of boilerplate code
当我们查看用 C# 6 编写的代码时,有一些样板代码,但这实际上并没有那么糟糕,尤其是当您考虑它提供的优势时。未来版本的 C# 将删除样板文件,这主要是由于必须定义构造函数,这些构造函数接受经过 null 检查并分配给私有变量的参数。当引入记录类型和非空引用类型时,C# 7 或 8 肯定会解决这个问题。
which is bad for creative people
对不起,这个论点纯属胡扯。我已经看到这个论点被不想学习设计模式和软件原则和实践的开发人员一遍又一遍地用作编写错误代码的借口。有创造力不是编写别人无法理解的代码或无法测试的代码的借口。我们需要应用公认的模式和实践,并且在该边界内有足够的空间来发挥创造力,同时编写出好的代码。 写代码不是一门艺术;这是一门手艺。
正如我所说,DI 并非在所有情况下都适用,围绕它的实践需要时间来掌握。我可以建议你看书Dependency Injection in .NET by Mark Seemann;它将给出许多答案,并让您很好地了解如何以及何时应用它,以及何时不应用它。
请注意:我讨厌 IoC。
这里有很多令人欣慰的好答案。根据史蒂文(非常有力的回答)的主要好处是:
- Improved testability
- Improved flexibility
- Improved maintainability
- Improved scalability
我的经历非常不同,这里是为了平衡:
(奖金)愚蠢的存储库模式
很多时候,这与 IoC 一起包含在内。存储库模式应该只用于访问外部数据,并且可互换性是核心期望。
当您使用此功能时,与 Entity Framework 一起使用时,您会禁用 Entity Framework 的所有功能,服务层也会发生这种情况。
例如。呼叫:
var employees = peopleService.GetPeople(false, false, true, true); //Terrible
应该是:
var employees = db.People.ActiveOnly().ToViewModel();
在这种情况下使用扩展方法。
谁需要灵活性?
如果您没有更改服务实现的计划,则不需要它。如果您认为将来会有多个实现,那么也许可以添加 IoC,并且仅针对该部分。
但是"Testability"!
Entity Framework(可能还有其他 ORM),允许您更改连接字符串以指向内存数据库。当然,这仅从 EF7 开始可用。但是,它可以只是临时环境中的一个新的(适当的)测试数据库。
你们还有其他特殊的考试资源和服务点吗?在这个时代,它们可能是不同的 WebService URI 端点,也可以在 App.Config / Web.Config.
中配置
自动化测试使您的代码易于维护
TDD - 如果它是 Web 应用程序,请使用 Jasmine 或 Selenium 并进行自动行为测试。这会一直测试到用户的所有内容。这是一项随着时间的推移而进行的投资,首先要涵盖关键特性和功能。
DevOps/SysOps - 维护用于配置整个环境的脚本(这也是最佳实践),启动暂存环境和 运行 所有测试。您还可以在那里克隆您的生产环境和 运行 您的测试。不要以 "maintainable" 和 "testable" 作为选择 IoC 的借口。从这些要求开始,找到满足这些要求的最佳方法。
可扩展性 - 以何种方式?
(我可能需要看书了)
- 对于编码器的可扩展性,分布式代码版本控制是常态(尽管我讨厌合并)。
- 为了人力资源的可扩展性,您不应该浪费时间为您的项目设计额外的抽象层。
- 对于生产并发用户可伸缩性,您应该构建、测试然后改进。
- 对于服务器吞吐量的可扩展性,您需要考虑比 IoC 更高层次的问题。您要 运行 客户 LAN 上的服务器吗?你能复制你的数据吗?您是在数据库级别还是应用程序级别进行复制?移动时离线访问重要吗?这些都是实质性的架构问题,而 IoC 很少是答案。
试试 F12
如果您使用的是 IDE(您应该这样做),例如 Visual Studio 社区版,那么您就会知道 F12 在浏览代码时是多么方便。
使用 IoC,您将被带到界面,然后您需要找到所有使用特定界面的引用。只是多了一个步骤,但是对于一个用了这么多的工具来说,这让我很沮丧。
史蒂文在接球
With Visual Studio 2015 I can simply do CTRL + F12 to find the
implementations of an interface.
是的,但是您必须随后浏览两种用法和声明的列表。 (其实我觉得在最新的VS里,声明单独列出来了,但还是多了一个鼠标点击,把你的手从键盘上拿开。而且我应该说这是Visual Studio的限制,不能带你直接到唯一的接口实现。
在我的中型项目中,我将静态 classes 用于存储库、服务等,它实际上工作得很好,即使大多数程序员的期望相反。我的代码库非常紧凑、干净且易于理解。现在我尝试重写所有内容并使用 IoC
(控制反转),但我非常失望。我必须在每个 class、控制器等中手动初始化十几个依赖项,为接口添加更多项目等等。我真的没有看到我的项目有任何好处,而且似乎它造成的问题多于解决的问题。我在 IoC
/DI
中发现了以下缺点:
- 更大的代码大小
- 馄饨代码而不是意大利面条代码
- 性能较慢,即使我要调用的方法只有一个依赖项,也需要在构造函数中初始化所有依赖项
- 不使用 IDE 时更难理解
- 一些错误被推送到运行-time
- 添加额外的依赖项(DI 框架本身)
- 新员工必须先学习 DI 才能使用它
- 大量样板代码,这对有创造力的人来说很糟糕(例如将实例从构造函数复制到属性...)
我们不测试整个代码库,只测试某些方法并使用真实数据库。那么,当测试不需要模拟时,是否应该避免依赖注入?
如果您必须在代码中手动初始化依赖项,那您就做错了。 IoC
的一般模式是构造函数注入,或者可能是 属性 注入。 Class 或者控制器根本不应该知道 DI 容器。
一般来说,您所要做的就是:
- 配置容器,如
Interface = Class in Singleton scope
- 使用它,喜欢
Controller(Interface interface) {}
- 受益于在一处控制所有依赖项
我没有看到任何样板代码或较慢的性能或您描述的任何其他内容。如果没有它,我真的无法想象如何编写或多或少复杂的应用程序。
但一般来说,您需要决定什么更重要。取悦 "creative people" 或构建可维护且健壮的应用程序。
顺便说一句,要创建 属性 或从构造函数归档,您可以在 R# 中使用 Alt+Enter
,它会为您完成所有工作。
有很多 'textbook' 支持使用 IoC 的论据,但根据我个人的经验,收益 are/were:
可以只测试项目的一部分,并模拟其他部分。例如,如果您有一个从 DB 返回配置的组件,则很容易模拟它,这样您的测试就可以在没有真实 DB 的情况下工作。使用静态 类 这是不可能的。
更好地了解和控制依赖项。使用 static 类 可以很容易地添加一些依赖项,甚至不会引起注意,这可能会在以后产生问题。使用 IoC,这更加明确和可见。
更明确的初始化顺序。对于静态 类 这通常是一个黑盒子,并且由于循环使用可能存在潜在问题。
对我来说唯一的不便是,通过将所有内容都放在接口之前,无法从用法 (F12) 直接导航到实现。
但是,项目的开发人员最能判断特定情况下的利弊。
您的大部分顾虑似乎都归结为误用或误解。
更大的代码量
这通常是正确遵守单一职责原则和接口隔离原则的结果。它大得多吗?我怀疑没有你说的那么大。但是,它所做的很可能是将 classes 归结为特定功能,而不是让 "catch-all" classes 做任何事情。在大多数情况下,这是关注点健康分离的标志,而不是问题。
馄饨代码而不是意大利面条代码
再一次,这很可能导致您思考堆栈而不是难以看到的依赖关系。我认为这是一个很大的好处,因为它导致了适当的抽象和封装。
性能较慢 只需使用快速容器即可。我最喜欢的是 SimpleInjector 和 LightInject。
甚至需要在构造函数中初始化所有依赖 如果我要调用的方法只有一个依赖项
这再次表明您违反了单一职责原则。这是一件好事,因为它迫使您从逻辑上思考您的架构,而不是随意添加。
当没有使用 IDE 时更难理解一些错误被推送到 运行-time
如果您仍然不使用 IDE,真丢人。现代机器对此没有很好的论据。此外,如果您愿意,某些容器 (SimpleInjector) 将首先验证 运行。您可以通过简单的单元测试轻松检测到这一点。
添加额外的依赖(DI 框架本身)
你必须选择你的战斗。如果学习一个新框架的成本低于维护意大利面条代码的成本(我怀疑会如此),那么这个成本是合理的。
新员工必须先学习 DI 才能使用它
如果我们回避新模式,我们就永远不会成长。我认为这是丰富和发展团队的机会,而不是伤害他们的方式。此外,权衡是学习意大利面条代码,这可能比选择行业范围的模式要困难得多。
大量样板代码对有创造力的人不利(例如将实例从构造函数复制到属性...)
这是完全错误的。强制依赖项应始终通过构造函数传入。只有可选的依赖项应该通过属性设置,并且只应该在非常特殊的情况下这样做,因为通常它会违反单一职责原则。
我们不测试整个代码库,只测试某些方法并使用真实数据库。那么,当测试不需要模拟时,是否应该避免依赖注入?
我认为这可能是最大的误解。依赖注入不仅仅是为了让测试更容易。这样一来,您就可以浏览 class 构造函数的签名,并且 立即 知道需要什么才能使 class 打钩。这对于 static classes 是不可能的,因为 classes 可以在他们喜欢的任何时候在堆栈中向上和向下调用而没有押韵或理由。您的目标应该是增加代码的一致性、清晰度和区别性。这是使用 DI 的最大原因,也是我强烈建议您重新访问它的原因。
您没有选择使用 IOC 库(StructureMap、Ninject、Autofac 等)的原因是什么? 使用其中任何一个都会让你的生活更轻松。
Although David L has already made an excellent set of commentaries on your points, I'll add my own as well.
更大的代码量
我不确定你是如何得到一个更大的代码库的; IOC 库的典型设置非常小,并且由于您在 class 构造函数中定义了不变量(依赖项),因此您还删除了一些您不需要的代码(即“new xyz()”东西)不需要了。
馄饨代码而不是意大利面条代码
我正好很喜欢馄饨:)
性能较慢,即使我要调用的方法只有一个依赖项,也需要在构造函数中初始化所有依赖项
如果你这样做,那么你根本就没有真正使用依赖注入。您应该通过 class 本身的构造函数参数中声明的依赖参数接收现成的、完全加载的对象图——而不是在构造函数中创建它们! 大多数现代 IOC 库都快得离谱,而且永远不会成为性能问题。 Here's一个很好的视频证明了这一点。
不使用 IDE 时更难理解
的确如此,但这也意味着您可以借此机会从抽象的角度进行思考。例如,你可以看一段代码
public class Something
{
readonly IFrobber _frobber;
public Something(IFrobber frobber)
{
_frobber=frobber;
}
public void LetsFrobSomething(Thing theThing)
{
_frobber.Frob(theThing)
}
}
当您查看此代码并试图弄清楚它是否有效,或者它是否是问题的根本原因时,您可以忽略实际的 IFrobber
实现;它只是代表了 Frob 某些东西的抽象能力,您不需要在脑子里记住 如何 任何特定的 Frobber 可能会完成它的工作。你可以专注于确保这个 class 做它应该做的事情 - 即,将一些工作委托给某种 Frobber。
另请注意,您甚至不需要在这里使用接口;您也可以继续并注入具体的实现。然而,这往往会违反依赖倒置原则(它仅与我们在这里讨论的 DI 有切向关系),因为它迫使 class 依赖于具体而不是抽象。
一些错误被推送到运行-time
不多于或少于在构造函数中手动构建图形;
添加额外的依赖项(DI 框架本身)
这也是事实,但大多数 IOC 库都非常小且不引人注目,在某些时候,您必须决定权衡拥有稍大的生产工件是否值得(确实如此)
新员工必须先学习 DI 才能使用它
这与任何新技术的情况并没有什么不同 :) 学习使用 IOC 库往往会打开思路,接受其他可能性,例如 TDD、SOLID 原则等等,这从来都不是坏事!
大量样板代码,这对有创造力的人来说很糟糕(例如将实例从构造函数复制到属性...)
我不明白这一点,你怎么会得到这么多样板代码;我不认为将给定的依赖项存储在私有只读成员中作为值得讨论的样板 - 请记住,如果每个 class 有超过 3 或 4 个依赖项,则很可能违反了 SRP 并且应该重新考虑您的设计。
最后,如果您不相信这里提出的任何论点,我仍然建议您阅读 Mark Seeman 的“Dependency Injection in .Net”。 (或者实际上他必须在 DI 上说的任何其他内容,您可以在他的博客上找到)。 我保证你会学到一些有用的东西,我可以告诉你,它改变了我编写软件的方式。
尽管 IoC/DI 并不是适用于所有情况的灵丹妙药,但您可能没有正确应用它。依赖注入背后的一套原则需要时间来掌握,或者至少,它确实对我有用。如果应用得当,它可以带来(除其他外)以下好处:
- 改进的可测试性
- 提高灵活性
- 改进的可维护性
- 改进并行开发
从你的问题中,我已经可以提取出一些你的情况可能出错的地方:
I have to manually initialize dozen of dependencies in every class
这意味着您创建的每个 class 都负责创建它所需的依赖项。这是一种称为 Control Freak 的反模式。 class 不应该 new
增加其依赖项本身。您甚至可能已经应用了 Service Locator anti-pattern,其中您的 class 通过调用容器(或表示容器的抽象)来请求其依赖项以获取特定的依赖项。 class 应该只将它需要的依赖项定义为构造函数参数。
dozen of dependencies
此声明暗示您违反了 Single Responsibly Principle。这实际上并没有耦合到 IoC/DI,您的旧代码可能已经违反了单一职责原则,导致其他开发人员难以理解和维护它。原作者通常很难理解为什么其他人很难维护代码,因为你写的东西通常很适合你的想法。通常违反 SRP 会导致其他人难以理解和维护代码。测试违反 SRP 的 classes 通常更难。一个 class 最多应该有六个依赖项。
add more projects for interfaces and so on
这意味着您违反了 Reused Abstraction Principle. In general, the majority of components/classes in your application should be covered by a dozen of abstractions. For instance, all classes that implement some use case probably deserve one single (generic) abstraction. Classes that implement queries also deserve one abstraction。对于我编写的系统,80% 到 95% 的组件(class 包含应用程序行为的组件)由 5 到 12 个(主要是通用的)抽象覆盖。大多数时候,您不需要仅为接口创建新项目。 大多数时候,我将这些接口放在同一个项目的根目录中。
much bigger codesize
您编写的代码量最初不会有太大差异。然而,依赖注入的实践仅在应用 SOLID 时效果很好,并且 SOLID 促进小的集中 classes。 类 只负责一项工作。这意味着您将拥有许多易于理解且易于组合成灵活系统的小型 classes。并且不要忘记:我们不应该努力编写更少的代码,而是更易于维护的代码。
但是,有了良好的 SOLID 设计和正确的抽象,我实际上不得不编写比以前少得多的代码。例如,应用某些横切关注点(如日志记录、审计跟踪、授权等)可以通过在应用程序的基础设施层编写几行代码来应用,而不是让它分布在整个应用程序中应用。它甚至让我能够做以前不可行的事情,因为它们迫使我对整个代码库进行彻底的更改,这非常耗时,以至于管理层不允许我这样做。
ravioli-code instead of spaghetti-code harder to understand when no IDE is used
这是真的。依赖注入促进 classes 彼此解耦。这有时会使浏览到代码库变得更加困难,因为 class 通常依赖于抽象而不是具体的 classes。在过去,我发现 DI 给我的灵活性远远超过了寻找实现的成本。使用 Visual Studio 2015 我可以简单地执行 CTRL + F12 来查找接口的实现。如果只有一个实现,Visual Studio 将直接跳转到该实现。
slower performance
这不是真的。性能与使用仅静态方法调用的代码库没有任何不同。然而,您选择让您的 classes 具有 Transient 生活方式,这意味着您可以在所有地方新建实例。在我上一个应用程序中,我只为每个应用程序创建了一次所有 classes,这提供了与仅具有静态方法调用大致相同的性能,但应用程序的好处是非常灵活和可维护。但请注意,即使您决定 new
为每个(网络)请求完成对象图,性能成本也很可能比任何 I/O(数据库、文件系统和网络服务)低几个数量级调用)您在该请求期间执行的次数,即使使用最慢的 DI 容器也是如此。
some errors are pushed to run-time adding additional dependency (DI framework itself)
这些问题都暗示使用了 DI 库。 DI 库在 运行 时间进行对象组合。然而,DI 库 不是 练习依赖注入时的必需工具。小型应用程序可以在没有工具的情况下受益于依赖注入;一种叫做 Pure DI 的做法。您的应用程序可能不会从使用 DI 容器中受益,但大多数应用程序实际上受益于使用依赖注入(如果使用正确)作为实践。再次强调:工具是可选的,编写可维护的代码不是。
但即使您使用 DI 库,也有一些库具有内置工具,可让您验证和诊断您的配置。它们不会为您提供编译时支持,但它们允许您在应用程序启动或使用单元测试时 运行 此分析。这可以防止您对整个应用程序进行回归,只是为了验证您的容器是否正确连接。我的建议是选择一个 DI 容器来帮助您检测这些配置错误。
new staff have to learn DI first in order to work with it
这是对的,但依赖注入本身并不难学。真正难学的是如何正确应用 SOLID 原则,当你想要编写需要由多个开发人员维护相当长一段时间的应用程序时,你无论如何都需要学习这一点。我宁愿投资于教我团队中的开发人员编写 SOLID 代码,而不是让他们编写代码;以后肯定会导致维护困难。
a lot of boilerplate code
当我们查看用 C# 6 编写的代码时,有一些样板代码,但这实际上并没有那么糟糕,尤其是当您考虑它提供的优势时。未来版本的 C# 将删除样板文件,这主要是由于必须定义构造函数,这些构造函数接受经过 null 检查并分配给私有变量的参数。当引入记录类型和非空引用类型时,C# 7 或 8 肯定会解决这个问题。
which is bad for creative people
对不起,这个论点纯属胡扯。我已经看到这个论点被不想学习设计模式和软件原则和实践的开发人员一遍又一遍地用作编写错误代码的借口。有创造力不是编写别人无法理解的代码或无法测试的代码的借口。我们需要应用公认的模式和实践,并且在该边界内有足够的空间来发挥创造力,同时编写出好的代码。 写代码不是一门艺术;这是一门手艺。
正如我所说,DI 并非在所有情况下都适用,围绕它的实践需要时间来掌握。我可以建议你看书Dependency Injection in .NET by Mark Seemann;它将给出许多答案,并让您很好地了解如何以及何时应用它,以及何时不应用它。
请注意:我讨厌 IoC。
这里有很多令人欣慰的好答案。根据史蒂文(非常有力的回答)的主要好处是:
- Improved testability
- Improved flexibility
- Improved maintainability
- Improved scalability
我的经历非常不同,这里是为了平衡:
(奖金)愚蠢的存储库模式
很多时候,这与 IoC 一起包含在内。存储库模式应该只用于访问外部数据,并且可互换性是核心期望。
当您使用此功能时,与 Entity Framework 一起使用时,您会禁用 Entity Framework 的所有功能,服务层也会发生这种情况。
例如。呼叫:
var employees = peopleService.GetPeople(false, false, true, true); //Terrible
应该是:
var employees = db.People.ActiveOnly().ToViewModel();
在这种情况下使用扩展方法。
谁需要灵活性?
如果您没有更改服务实现的计划,则不需要它。如果您认为将来会有多个实现,那么也许可以添加 IoC,并且仅针对该部分。
但是"Testability"!
Entity Framework(可能还有其他 ORM),允许您更改连接字符串以指向内存数据库。当然,这仅从 EF7 开始可用。但是,它可以只是临时环境中的一个新的(适当的)测试数据库。
你们还有其他特殊的考试资源和服务点吗?在这个时代,它们可能是不同的 WebService URI 端点,也可以在 App.Config / Web.Config.
中配置自动化测试使您的代码易于维护
TDD - 如果它是 Web 应用程序,请使用 Jasmine 或 Selenium 并进行自动行为测试。这会一直测试到用户的所有内容。这是一项随着时间的推移而进行的投资,首先要涵盖关键特性和功能。
DevOps/SysOps - 维护用于配置整个环境的脚本(这也是最佳实践),启动暂存环境和 运行 所有测试。您还可以在那里克隆您的生产环境和 运行 您的测试。不要以 "maintainable" 和 "testable" 作为选择 IoC 的借口。从这些要求开始,找到满足这些要求的最佳方法。
可扩展性 - 以何种方式?
(我可能需要看书了)
- 对于编码器的可扩展性,分布式代码版本控制是常态(尽管我讨厌合并)。
- 为了人力资源的可扩展性,您不应该浪费时间为您的项目设计额外的抽象层。
- 对于生产并发用户可伸缩性,您应该构建、测试然后改进。
- 对于服务器吞吐量的可扩展性,您需要考虑比 IoC 更高层次的问题。您要 运行 客户 LAN 上的服务器吗?你能复制你的数据吗?您是在数据库级别还是应用程序级别进行复制?移动时离线访问重要吗?这些都是实质性的架构问题,而 IoC 很少是答案。
试试 F12
如果您使用的是 IDE(您应该这样做),例如 Visual Studio 社区版,那么您就会知道 F12 在浏览代码时是多么方便。
使用 IoC,您将被带到界面,然后您需要找到所有使用特定界面的引用。只是多了一个步骤,但是对于一个用了这么多的工具来说,这让我很沮丧。
史蒂文在接球
With Visual Studio 2015 I can simply do CTRL + F12 to find the implementations of an interface.
是的,但是您必须随后浏览两种用法和声明的列表。 (其实我觉得在最新的VS里,声明单独列出来了,但还是多了一个鼠标点击,把你的手从键盘上拿开。而且我应该说这是Visual Studio的限制,不能带你直接到唯一的接口实现。