设计 - 使用Windsor时,对象应该在哪里注册?

49

我的应用程序将包含以下组件:

  • DataAccess
  • DataAccess.Test
  • Business
  • Business.Test
  • Application

我希望使用Castle Windsor作为IoC来将这些层粘合在一起,但我对粘合的设计有些不确定。

我的问题是谁应该负责将对象注册到Windsor中? 我有几个想法:

  1. 每个层可以注册自己的对象。为了测试BL,测试平台可以为DAL注册模拟类。
  2. 每个层可以注册其依赖项的对象,例如业务层注册数据访问层的组件。为了测试BL,测试平台需要卸载“真实”的DAL对象并注册模拟对象。
  3. 应用程序(或测试应用程序)注册所有依赖项的对象。

我正在寻求一些不同路径的想法和优缺点。

2个回答

78

通常情况下,应用程序中的所有组件都应该尽可能晚地进行组合,这可以确保最大化模块化,并使模块之间的耦合尽量松散。

实际上,这意味着您应该在应用程序的根处配置容器。

  • 在桌面应用程序中,这将位于 Main 方法中(或非常靠近它)
  • 在 ASP.NET(包括 MVC)应用程序中,这将位于 Global.asax 中
  • 在 WCF 中,这将位于 ServiceHostFactory 中
  • 等等。

容器只是将模块组合成工作应用程序的引擎。原则上,您可以手动编写代码(这称为“贫民版 DI”),但使用像 Windsor 这样的 DI 容器会更加轻松。

这样的 组合根 理想情况下应该是应用程序根中唯一的代码片段,使应用程序成为所谓的 Humble Executable(来自优秀的 xUnit Test Patterns 一书)。这样的应用程序本身不需要单元测试。

您的测试不应该需要容器,因为您的对象和模块应该是可组合的,并且您可以直接从单元测试中向它们提供 Test Doubles。最好设计所有模块都不依赖于容器。

此外,在 Windsor 中,您应该在安装程序(实现 IWindsorInstaller 接口的类型)中封装组件注册逻辑。有关更多详细信息,请参见文档


关于最后一段,这是指您不在测试中使用IOC吗?只是澄清一下。但是如果您要测试的代码需要使用IOC来构建自身呢? - Greg
1
是的,它就是这个意思:https://dev59.com/-XM_5IYBdhLWcg3wPAfT#1465896 - Mark Seemann
1
马克,你能建议一种框架或方法来在WinForms应用程序中连接IoC而不会出现Service Locator反模式吗?例如,在MVC中,存在管理控制器实例化的基础设施,因此在单个位置连接容器并避免Service Locator是微不足道的。但是,如何在WinForms应用程序中构建该基础设施呢? - Matt Kocaj
哦,亲爱的,我已经好几年没碰Windows Forms了...如果我没记错的话,它很难,但最终还是有可能的。我认为早已停用的Composite Application Block指出了一些合理的方向,尽管它非常复杂。最终,您需要找到适合您正在构建的模式,如MVC/MVP/MVVM/某种模式...您可以通过Windows Forms中的数据绑定功能取得很大进展... - Mark Seemann
@MarkSeemann,我一直在阅读你和其他人的各种帖子,上面的两条评论最终为我澄清了一个问题,即为什么你比其他一些人采取更加严格、更加纯粹的立场,而这些人有时认为你的一些想法在实践中是不切实际的。 - Brandon Moore
显示剩余2条评论

30
虽然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:
// responsibility is clear that this component is responsible for creation

Factory.CreateMeA<Thing>()
// or simply
new Thing()

方案 C:

// responsibility is clear that this component is not responsible for creation, but rather only consumption

ServiceLocator.GetMeTheExisting<Thing>()
// or simply
ServiceLocator.Thing

正如您所看到的,依赖注入使得不清楚谁负责子组件的生命周期管理。
注意:严格来说,许多依赖注入框架确实有一些惰性创建组件的方法(参见:如何不做依赖注入——静态或单例容器),这比传递容器要好得多,但您仍然需要为了到处传递创建函数而改变代码,缺乏在创建过程中传递有效构造函数参数的一级支持,最终你仍然在不必要地使用间接机制,在唯一的受益处是实现可测试性的地方,这可以通过更好、更简单的方式来实现(请参见下文)。
这意味着什么?
这意味着依赖注入适用于某些场景,而对于其他场景则不适用。在富客户端应用程序中,它往往具有DI的许多缺点,但却很少具备优点。随着应用程序复杂度的不断增加,维护成本也会越来越高。它还存在被误用的严重潜力,这取决于团队沟通和代码审查流程的严密程度,可能会导致技术债务成本的严重问题。有一种谣言流传开来,认为服务定位器、工厂或老式实例化机制是不好的和过时的机制,仅仅因为它们可能不是最佳机制在Web应用程序世界中,这里可能有很多人在玩耍。我们不应该将这些经验教训过度概括到所有情况,并将所有东西视为钉子,只因为我们学会了使用特定的锤子。
我的建议是,在编写富客户端应用程序时,对于每个组件使用最少的机制来满足要求。80% 的时间这应该是直接实例化。服务定位器可用于存储您的主要业务层组件(即:通常为单例的应用程序服务),当然工厂甚至单例模式也有它们的位置。并不意味着您不能在服务定位器后面隐藏 DI 框架,以一次性创建您的业务层依赖项及其所有依赖项 - 如果这样做可以使您在该层更轻松地工作,并且该层不表现出富客户端展示层所普遍展示的惰性加载。只需确保将用户代码屏蔽,以防止传递 DI 容器可能创建的混乱。

那测试性呢?

“即使没有依赖注入框架,也完全可以实现可测试性。” 我建议使用拦截框架,例如 UnitBox(免费)或TypeMock(价格较高)。这些框架提供了您需要解决问题的工具(如何在 C# 中模拟实例化和静态调用),并且不需要更改整个架构以解决它们(不幸的是,在 .NET/Java 世界中,这已经成为趋势)。更明智的做法是找到解决问题的方法,并使用最适合底层组件的自然语言机制和模式,而不是试图把每个方形钉子塞进圆形依赖注入孔中。一旦您开始使用这些更简单、更具体的机制,您会发现如果有的话,您的代码库中几乎不需要使用依赖注入。"
注意:对于MVVM架构
在基本的MVVM架构中,视图模型实际上承担了控制器的职责,因此在所有情况下,考虑上述“控制器”措辞适用于“视图模型”。基本的MVVM适用于小型应用程序,但是随着应用程序的复杂性增加,您可能希望使用MVCVM方法。 视图模型变成了大多数愚笨的DTO,以促进数据绑定到视图,而与业务层之间和代表屏幕/子屏幕的视图模型组之间的交互则封装在显式的控制器/子控制器组件中。 在任何一种架构中,控制器的责任都存在,并展现了上述相同的特征。

在我看来,即使是针对 Web 场景,Web 容器也应该仅用于获取正确的服务(如果有必要的话)。使用 container.GetService<T> 的危险太大了,在我看来,你最好使用旧的 VBA 或 RAD 代码而不是 container.GetService<T>,这会让重构和查找具体实现变得痛苦,并且编写起来更加昂贵。 - user1496062
2
我对这篇文章感到困惑。Web应用程序需要根据运行时值在运行时构建对象,这不仅适用于厚客户端应用程序。我们通过使用抽象工厂、Dispose模式、配置IoC作用域(将VM的作用域限定为关联的View实例)以及支持嵌套子容器来处理这个问题。此外,Unit Box似乎并不比服务定位器更好,因为它将创建逻辑与需要依赖项的类耦合在一起(与仅有DI违反SRP和DI相比)。但最让我困惑的是,如果没有容器,你如何保持你的设计符合SOLID原则? - Ryan Vice
1
我不明白你对自定义生命周期的担忧...创建工厂和抽象工厂非常简单(尤其是通过我的经验中的Castle Windsor)- 如果您希望在运行时根据需要使用依赖项(惰性加载和卸载所有内容),那么这是正确的方法。如果您将所有可能的直接依赖项注入构造函数中,那么您正在错误地进行操作... - Vivek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接