虽然Mark的答案对于Web场景非常好,但将其应用于所有架构(即富客户端 - 即WPF、WinForms、iOS等)的关键缺陷在于假设所有操作所需的所有组件都可以/应该一次性创建。对于Web服务器来说,这是有意义的,因为每个请求的生命周期非常短暂,每个请求都会由基础框架(没有用户代码)创建一个ASP.NET MVC控制器。因此,控制器及其所有依赖项可以轻松地由DI框架组合,并且几乎没有维护成本。请注意,Web框架负责管理控制器的生命周期以及其所有依赖项的生命周期(DI框架将在控制器创建时为您创建/注入)。依赖项在请求的持续时间内存在并且您的用户代码不需要自己管理组件和子组件的生命周期,这是完全可以接受的。还要注意,Web服务器在不同请求之间是无状态的(除了会话状态,但这与本讨论无关),您永远不会有多个需要同时存在的控制器/子控制器实例来服务单个请求。
在富客户端应用程序中,情况则非常不同。如果使用MVC/MVVM架构(你应该这么做!),用户的会话是长期存在的,并且控制器在用户浏览应用程序时创建子控制器/同级控制器(请参见底部有关MVVM的说明)。在Web世界中的类比是,在富客户端应用程序中,每个用户输入(按钮单击、操作执行)都相当于Web框架接收到的请求。然而,最大的区别在于,你希望富客户端应用程序中的控制器在操作之间保持活动状态(很可能用户在同一屏幕上执行多个操作-由特定控制器管理),并且随着用户执行不同的操作,子控制器被创建和销毁(想想一个选项卡控件,如果用户导航到它,就会懒惰地创建选项卡,或者一个只需要在用户对屏幕执行特定操作时加载的UI片段)。
这两个特点意味着
用户代码需要管理控制器/子控制器的生命周期,而控制器的依赖关系不应该全部预先创建(即:子控制器、视图模型、其他表示组件等)。如果您使用DI框架执行这些职责,您将不仅在不应存在的地方增加大量代码(参见:
构造函数过度注入反模式),而且还需要在大多数表示层中传递依赖容器,以便您的组件可以在需要时使用它来创建其子组件。
为什么我的用户代码可以访问DI容器是不好的?
1)依赖容器持有应用程序中许多组件的引用。将此“坏男孩”传递给需要创建/管理另一个子组件的每个组件,相当于在您的架构中使用全局变量。更糟糕的是,任何子组件也可以向容器注册新组件,因此很快它也将成为全局存储。开发人员将对象放入容器中,只是为了在组件之间传递数据(无论是在兄弟控制器之间还是在深层控制器层次结构之间 - 即:祖先控制器需要从祖父控制器获取数据)。请注意,在Web世界中,容器不会传递给用户代码,因此这永远不是问题。
2) 依赖容器与服务定位器/工厂/直接对象实例化相比的另一个问题是,从容器中解析使得无法确定您是正在创建组件还是仅重用现有组件。相反,由一个集中的配置(即:引导程序/组合根)来确定组件的生命周期。在某些情况下,这是可以接受的(例如: web控制器,在这种情况下,需要管理组件生命周期的不是用户代码,而是运行时请求处理框架本身)。然而,当您的组件的设计应指示它们是否负责管理组件以及其生命周期应该是什么时,这就变得非常棘手(例如:电话应用程序弹出一个要求用户输入一些信息的视图控制器。这通过控制器创建一个子控制器来实现,该子控制器管理覆盖视图。一旦用户输入了一些信息,表格被撤消,并将控制权返回给最初的控制器,该控制器仍保持先前用户所做的状态)。如果使用DI解析表格子控制器,则无法确定其生命周期或者谁应该负责管理它(发起控制器)。与其他机制的明确责任相比较。
场景A:
// not sure whether I'm responsible for creating the thing or not
DependencyContainer.GimmeA<Thing>()
情景 B:
Factory.CreateMeA<Thing>()
new Thing()
方案 C:
ServiceLocator.GetMeTheExisting<Thing>()
ServiceLocator.Thing
正如您所看到的,依赖注入使得不清楚谁负责子组件的生命周期管理。
注意:严格来说,许多依赖注入框架确实有一些惰性创建组件的方法(参见:
如何不做依赖注入——静态或单例容器),这比传递容器要好得多,但您仍然需要为了到处传递创建函数而改变代码,缺乏在创建过程中传递有效构造函数参数的一级支持,最终你仍然在不必要地使用间接机制,在唯一的受益处是实现可测试性的地方,这可以通过更好、更简单的方式来实现(请参见下文)。
这意味着什么?
这意味着依赖注入适用于某些场景,而对于其他场景则不适用。在富客户端应用程序中,它往往具有DI的许多缺点,但却很少具备优点。随着应用程序复杂度的不断增加,维护成本也会越来越高。它还存在被误用的严重潜力,这取决于团队沟通和代码审查流程的严密程度,可能会导致技术债务成本的严重问题。有一种谣言流传开来,认为服务定位器、工厂或老式实例化机制是不好的和过时的机制,仅仅因为它们可能不是最佳机制在Web应用程序世界中,这里可能有很多人在玩耍。我们不应该将这些经验教训过度概括到所有情况,并将所有东西视为钉子,只因为我们学会了使用特定的锤子。
我的建议是,在编写富客户端应用程序时,对于每个组件使用最少的机制来满足要求。80% 的时间这应该是直接实例化。服务定位器可用于存储您的主要业务层组件(即:通常为单例的应用程序服务),当然工厂甚至单例模式也有它们的位置。
并不意味着您不能在服务定位器后面隐藏 DI 框架,以一次性创建您的业务层依赖项及其所有依赖项 - 如果这样做可以使您在该层更轻松地工作,并且该层不表现出富客户端展示层所普遍展示的惰性加载。只需确保将用户代码屏蔽,以防止传递 DI 容器可能创建的混乱。
那测试性呢?
“即使没有依赖注入框架,也完全可以实现可测试性。” 我建议使用拦截框架,例如
UnitBox(免费)或
TypeMock(价格较高)。这些框架提供了您需要解决问题的工具(如何在 C# 中模拟实例化和静态调用),并且不需要更改整个架构以解决它们(不幸的是,在 .NET/Java 世界中,这已经成为趋势)。更明智的做法是找到解决问题的方法,并使用最适合底层组件的自然语言机制和模式,而不是试图把每个方形钉子塞进圆形依赖注入孔中。一旦您开始使用这些更简单、更具体的机制,您会发现如果有的话,您的代码库中几乎不需要使用依赖注入。"
注意:对于MVVM架构
在基本的MVVM架构中,视图模型实际上承担了控制器的职责,因此在所有情况下,考虑上述“控制器”措辞适用于“视图模型”。基本的MVVM适用于小型应用程序,但是随着应用程序的复杂性增加,您可能希望使用MVCVM方法。 视图模型变成了大多数愚笨的DTO,以促进数据绑定到视图,而与业务层之间和代表屏幕/子屏幕的视图模型组之间的交互则封装在显式的控制器/子控制器组件中。 在任何一种架构中,控制器的责任都存在,并展现了上述相同的特征。