【问题标题】:DI: Composition root decompositionDI:组合根分解
【发布时间】:2018-01-21 10:38:27
【问题描述】:

Composition root 看起来很奇怪的模式。我们有一个非常大的上帝对象,它对任何事情都了如指掌。

将组合根拆分为一些模块的正确方法是什么,这些模块将封装对象图自身部分的初始化?

hierarchical dependency injection 呢?

【问题讨论】:

  • “拆分组合根的正确方法是什么”。这可能取决于您使用的技术。您指的是 Angular 文档。你的问题是关于 Angular 上下文中的 DI 吗?
  • @Steven,我需要概念性的答案。你可以提供任何技术的样本,如果它有助于你解释的话。
  • 这里有一篇相关文章:dotnetcurry.com/patterns-practices/1285/…

标签: dependency-injection dependencies inversion-of-control code-injection


【解决方案1】:

我不同意组合根是God Object 的前提。虽然从概念上讲,Composition Root 可能包含多行代码,但它只有一个 single responsibility: compose an object graph。

Composition Root 可以包含很多数据,并且它本身与整个应用程序耦合,但它在概念上只有 一个 方法。它只有一个改变的理由,那就是如果你想改变你的应用程序的组成方式。

此外,虽然组合根与应用程序的其余部分耦合,但应用程序代码对组合根一无所知。因此,根据Dependency Inversion Principle,耦合只在一个方向上进行。

一种说法是,God 对象违反了所有五个SOLID principles,而 Composition Root 至少遵循 SRP、ISP 和 DIP。我认为OCPLSP 不适用于此处。

话虽如此,如果一个组合根组成一个大型应用程序,它仍然可能包含很多代码。如果你发现它变得无法管理,那么你应该如何将它分解成更小的代码块?

与分解任何其他代码的方式相同。视情况而定。

我希望我从来没有写过或说过一个 Composition Root 只能是一个类。如果我这样做了,我想收回它。在编写 Composition Roots 时,我经常将其职责划分为多个类(尽管很少超过少数)。您可以应用任何您想要的设计模式或其他 OOD 技术来实现该目标。然后,Composition Root 在您的实现上变为 Facade

我不能提供更多的指导,因为它取决于应用程序架构。如果您进行垂直切片,您很可能需要以不同于进行水平分层的方式来构建 Composition Root,等等。

也就是说,作为一般规则,我发现尝试将依赖关系图分解为子树很有用。首先因为依赖图是一个graph,而不是树,还因为你经常需要交错多个独立的关注点。例如,您可以拥有负责应用程序的各个子部分的子图,但是您需要在整个过程中交错相同的日志记录机制、相同的缓存机制或相同的安全机制等整个图形。如果你试图将它们分开,你就不能轻易做到这一点。

【讨论】:

  • 谢谢。 Composition Root 有很多改变的理由——其中列出的每一个类都是一个理由。而且它的变化非常频繁。分解的问题非常困难,并导致了全新模式的出现。看看 Matt 的回答——他介绍了几个新概念——工厂、股票策略、经纪人界面……它是 Composition Root 吗?或查看 Angular 实现。我认为需要考虑问题以防止出现新的反模式。
  • "Composition Root 有很多改变的理由——每一个类,列在里面都是一个理由。" - 仅当您在组合根中显式调用每个类的构造函数(可能会更改)而不是自己连接各个类并让容器将它们放在一起时,这才是正确的。或者更好的是,按照惯例注册你的大部分类,这样你就只有很少的组合代码,然后找到一种方法来组织它就不是什么大问题了。
  • @NightOwl888 即使您使用 DI 容器,添加、删除、重命名和移动类也会引起组合根的更改。顺便说一句,DI 容器的自动实例化可能会导致不明显的错误,例如使用未注册的服务(只能在运行时发现)。因此,使用 DI 容器也会降低组合根的可管理性。
  • 我知道这是一篇旧文章,但我仍然想听听您对服务器端应用程序的组合根的意见。我遇到的问题是,对于每个请求,将创建一些有状态对象(并形成一个图),而应用程序对象保持无状态,但是在没有组合根的情况下构建这个每个请求图变得很复杂。可以有一个每个请求的组合根吗?还是有更好的方法?
  • @JasonZang 我从来没有暗示组合根代码只能执行一次。它绝对可以根据请求构建对象图。参见例如以blog.ploeh.dk/2014/06/03/compile-time-lifetime-matching 为例。
【解决方案2】:

我认为社区对于 Composition Root 的外观并没有完全一致。

例如,您可以通过练习Pure Dependency Injection (Pure DI) 手动连接您的对象,或者您可以使用依赖注入容器并手动注册您的映射,或者您可以使用依赖注入容器并围绕类和接口名称创建一些约定实现所谓的约定优于配置。或者您可以混合使用这三种方法。

另一个例子:你可以选择在 Composition Root 中连接所有内容(我认为更好的选择),或者你可以选择为每个组件(例如 .NET 中的类库)设置一个“小型 Composition Root”系统,然后将这些根连接到“主合成根”中。

根据您选择的方法,Composition Root 的知识会有所不同。

例如,如果您使用 Pure DI 并且只有一个 Composition Root,那么 Composition Root 将需要非常了解应用程序。

另一方面,如果您遵循约定优于配置的方法,那么关于哪些类应该映射到哪些接口的一些知识将分布在各处(在类和接口名称等内部)。

根据我的经验,对于任何足够大的应用程序,具有单个组合根的 Pure DI + 是最佳选择。这为您提供了一个了解应用程序的中心位置,并使您的Composition Root navigable

这意味着 Composition Root 的责任非常大。我不确定我们是否可以称它为上帝的对象。我认为创造这个词的背景是不同的。

我不会说合成根“太多”了。虽然它做了很多,我认为它应该。如果您像我一样,那么您需要一个可以导航和了解您的应用程序的位置。

要分解您的 Composition Root,您可以将其拆分为多个方法。我有时会在 C# 中创建一个部分类(多个文件中的单个类)并将相关方法放入同一个文件中。

虽然应用图是一个图,但是如果它是一棵树,分解起来会容易得多。我尽力使图树状。如果两个对象需要类似的依赖关系,我会尝试为它们提供这种依赖关系的不同实例,以保持图的树状结构。

我已经写了一篇关于这个主题的文章,你可以阅读它here

Here 也是另一篇文章,提供了关于 Composition Root 职责的一些观点。

【讨论】:

  • 谢谢,你列出了所有显而易见的事情,但不要触及问题的核心。如果我们拆分组合根,那么一些依赖性将是常见的,一些特定于模块(或模块树?)。主要问题是如何组织公共部分和特定部分之间的交互,使其可预测、可维护且易于管理。
  • @Pavel,唯一需要共享的是共享的有状态对象和共享资源(例如打开和锁定的文件)。对于这些对象,您可以在最后可能的位置创建它们,然后将它们作为参数传递给 Composition Root 方法。所有其他无状态对象都不需要共享。我在linked article 的“代码共享与资源/状态共享”部分详细解释了这一点。
  • @Pavel,顺便说一句,最大化无状态“对象”的数量并最小化有状态“对象”的数量是很有意义的。这将极大地帮助您维护您的合成根(除其他外)。这是一篇相关文章:dotnetcurry.com/patterns-practices/1367/…
  • 这是个好主意,但看起来它来自理想世界。您在真正的大型应用程序中使用它是否已过期?
  • @Pavel,是的。我写的文章是基于我构建大型应用程序的经验,这些应用程序需要多年维护。
【解决方案3】:

组合根并不是真正的上帝对象,因为它只知道对象图的未封装部分。

在组件层次结构中,组合根具有一定数量的子级,它创建的对象图具体化了这些子级之间的相互依赖关系。交易门户可能配置有股票策略、共同基金策略和经纪人界面。这些策略需要一个代理界面,而门户 UI 需要一个或多个策略。组合根将它们连接在一起。

现在,这些注入的依赖项中的任何一个——交易策略或经纪人接口,都可能有很多内部功能,这些功能也可以通过依赖注入进行配置,但组合根不应该有任何关于这些内部的硬编码知识。相反,可以使用 factory 实现配置组合根以构建复杂的依赖关系。

工厂实现可能需要来自组合根的资源。例如,交易策略工厂都需要代理接口实现。但是它们都需要来自组合根的相同的东西,因此组合根不需要知道它们的细节。

如有必要,传递给工厂的资源可以包括对组合根不透明的工厂的 DI 配置。因此,工厂可以创建任何适合实现它们应该创建的对象的对象图。

需要注意的是,工厂接口——应用程序调用来构造它需要的对象的东西——是特定于应用程序的。在满足特定类型依赖的所有可配置方式中相同。上面会有一个TradingStrategyFactory 接口,例如,共同基金(使用共同基金模块)和股票(使用股票模块)的不同实现。

除非工厂接口以某种方式标准化,否则,工厂接口和实现是应用程序的一部分,而不是实现它们使用的功能的模块的一部分。

有时,需求会发生变化,某些新模块将需要组合根未提供的资源或连接。这将需要更改工厂接口及其所有实现。它应该需要对其他模块进行更改,因此这些工厂实现是应用程序本身的适当部分。

所以,最后...您的问题的答案是,一个庞大而复杂的组合根分解为多个不同种类的工厂实现。

您还提到了 Angular 中的分层 DI。这是一种将配置的资源自动传递给组件工厂的机制。我不熟悉 Angular,但我猜它是必需的,因为 Angular 试图为所有组件标准化一个工厂接口。这具有以难以理解的方式将对象图本身的构造移动到 DI 配置中的效果。如果您在 .net 中编程,则不需要这个,我会在那种环境中避免它。

【讨论】:

  • 谢谢,但你能提供一些链接来阅读这种向工厂分解的技术吗?很难从你的文字中理解整个画面。
  • 抱歉,没有链接。这个答案来自经验,而不是书本学习。不过,我会考虑其中可能难以理解的内容。同时,您在上面迷住的@steven 是您参考的书的合著者,所以也许您会从他那里得到一个黄金答案。
猜你喜欢
  • 1970-01-01
  • 2011-02-20
  • 1970-01-01
  • 2011-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多