23

我正在计划一些工作,将依赖注入引入当前的大型单体库,以使该库更易于单元测试、更易于理解,并且可能更灵活。

我决定使用NInject,我真的很喜欢 Nate 的座右铭“做一件事,做好”(意译),而且在 DI 的背景下它似乎特别好。

我现在一直想知道的是,我是否应该将当前的单个大型组件拆分为具有不相交功能集的多个较小组件。其中一些较小的程序集将具有相互依赖关系,但远非全部,因为代码的体系结构已经非常松散耦合。

请注意,这些功能集本身也不是微不足道和小...它包含诸如客户端/服务器通信、序列化、自定义集合类型、文件 IO 抽象、通用例程库、线程库、标准日志记录等。

我看到之前的一个问题:多个小组件或一个大组件哪个更好?kind-of 解决了这个问题,但似乎比这更精细,这让我想知道那里的答案是否仍然适用于这种情况?

此外,在与该主题相关的各种问题中,一个常见的答案是“太多”组件会导致未指明的“痛苦”和“问题”。我真的很想具体了解这种方法可能存在的缺点。

我同意在只需要 1 个程序集之前添加 8 个程序集“有点痛苦”,但是必须为每个应用程序包含一个大的单体库也不是很理想......加上添加 8 个程序集是你只做的事情有一次,所以我对这个论点没有什么同情心(即使我一开始可能会和其他人一起抱怨)。

附录:
到目前为止,我还没有看到反对小型集会的令人信服的理由,所以我认为我现在会继续进行,就好像这不是问题一样。如果有人可以用可验证的事实来想出充分的理由来支持它们,我仍然很想听听它们。(我会尽快添加赏金以提高知名度)

编辑:将性能分析和结果移到单独的答案中(见下文)。

4

7 回答 7

33

由于性能分析比预期的要长一些,我把它放到了它自己的单独答案中。我将接受彼得的官方回答,尽管它缺乏测量,因为它最有助于激励我自己进行测量,并且因为它给了我最大的灵感来了解可能值得测量的东西。

分析:
目前提到的具体缺点似乎都集中在一种另一种的性能上,但缺少实际的定量数据,我对以下方面进行了一些测量:

  • 是时候在 IDE 中加载解决方案了
  • 是时候在 IDE 中编译了
  • 程序集加载时间(应用程序加载所需的时间)
  • 丢失的代码优化(算法运行所需的时间)

这种分析完全忽略了一些人在回答中提到的“设计质量”,因为我不认为质量是这种权衡中的一个变量。我假设开发人员将首先让他们的实现以获得最佳设计的愿望为指导。这里的权衡是,为了(某种程度的)性能,是否值得将功能聚合到比设计严格要求的更大的组件中。

应用程序结构
我构建的应用程序有点抽象,因为我需要大量的解决方案和项目来测试,所以我写了一些代码来为我生成它们。

该应用程序包含 1000 个类,分为 200 组,每组 5 个类相互继承。类被命名为 Axxx、Bxxx、Cxxx、Dxxx 和 Exxx。类 A 是完全抽象的,BD 是部分抽象的,分别覆盖 A 的方法之一,而 E 是具体的。这些方法的实现使得对 E 实例的一个方法的调用将在层次结构链上执行多个调用。所有方法体都足够简单,理论上它们都应该是内联的。

这些类以 8 种不同配置沿 2 个维度分布在组件中:

  • 组件数量:10、20、50、100
  • 切割方向:跨越继承层次(AE 从来都不是在同一个程序集中),沿着继承层次

测量结果并非都是精确测量的;有些是通过秒表完成的,误差较大。进行的测量是:

  • 在VS2008(秒表)中打开解决方案
  • 编译解决方案(秒表)
  • 在 IDE 中:开始和第一行代码执行之间的时间(秒表)
  • 在 IDE 中:是时候为 IDE 中的 200 个组中的每一个实例化一个 Exxx(在代码中)
  • 在 IDE 中:在 IDE 中对每个 Exxx 执行 100,000 次调用的时间(在代码中)
  • 最后三个“在 IDE 中”测量,但来自使用“发布”构建的提示

结果:
在VS2008中打开解决方案

                               ----- in the IDE ------   ----- from prompt -----
Cut    Asm#   Open   Compile   Start   new()   Execute   Start   new()   Execute
Across   10    ~1s     ~2-3s       -   0.150    17.022       -   0.139    13.909
         20    ~1s       ~6s       -   0.152    17.753       -   0.132    13.997
         50    ~3s       15s   ~0.3s   0.153    17.119    0.2s   0.131    14.481
        100    ~6s       37s   ~0.5s   0.150    18.041    0.3s   0.132    14.478

Along    10    ~1s     ~2-3s       -   0.155    17.967       -   0.067    13.297
         20    ~1s       ~4s       -   0.145    17.318       -   0.065    13.268
         50    ~3s       12s   ~0.2s   0.146    17.888    0.2s   0.067    13.391
        100    ~6s       29s   ~0.5s   0.149    17.990    0.3s   0.067    13.415

观察:

  • 装配的数量(但不是切割方向)似乎对打开解决方案所需的时间具有大致线性的影响。这并不让我感到惊讶。
  • 在大约 6 秒时,打开解决方案所需的时间在我看来并不是限制程序集数量的论据。(我没有衡量关联源代码控制是否对这个时间有重大影响)。
  • 在这个测量中,编译时间比线性增加一点。我想这大部分是由于编译的每个程序集开销,而不是程序集间符号解析。我希望不那么琐碎的组件沿着这个轴更好地扩展。即便如此,我个人并不认为 30 秒的编译时间是反对拆分的理由,尤其是当注意到大多数时候只有一些程序集需要重新编译时。
  • 启动时间似乎几乎无法测量,但明显增加。应用程序所做的第一件事是向控制台输出一行,“开始”时间是该行从执行开始到出现的时间(注意这些是估计值,因为即使在最坏的情况下也无法准确测量) .
  • 有趣的是,IDE 程序集的外部加载似乎(非常轻微)比 IDE 内部更有效。这可能与附加调试器或类似的工作有关。
  • 另请注意,在最坏的情况下,在 IDE 之外重新启动应用程序会进一步缩短启动时间。可能存在启动 0.3s 无法接受的情况,但我无法想象这在很多地方会很重要。
  • 无论程序集拆分如何,IDE 内的初始化和执行时间都是可靠的;这可能是因为它需要调试,使其更容易跨程序集解析符号。
  • 在 IDE 之外,这种稳定性继续存在,但需要注意的是……程序集的数量与执行无关,但是当跨越继承层次结构时,执行时间比沿着. 请注意,差异对我来说似乎太小而无法系统化;可能需要额外的时间来弄清楚如何进行相同的优化......坦率地说,虽然我可以进一步调查,但差异是如此之小,以至于我不想太担心。

因此,从这一切看来,更多程序集的负担主要由开发人员承担,然后主要以编译时间的形式出现。正如我已经说过的,这些项目非常简单,以至于每个项目的编译时间都远少于一秒,导致每个程序集的编译开销占主导地位。我想,跨大量程序集的亚秒级程序集编译强烈表明这些程序集已被进一步拆分,超出了合理范围。此外,当使用预编译程序集时,开发人员反对拆分(编译时间)的主要论点也将消失。

在这些测量中,我几乎看不到任何反对为了运行时性能而拆分成更小的程序集的证据。(在某种程度上)唯一需要注意的是尽可能避免跨越继承;我想大多数理智的设计无论如何都会限制这一点,因为继承通常只会发生在功能区域内,而这通常会在单个程序集中结束。

于 2009-08-02T05:21:10.303 回答
15

我会给你一个真实的例子,其中使用许多(非常)小程序集产生了 .Net DLL Hell。

在工作中,我们有一个成熟的大型本土框架(.Net 1.1)。除了通常的框架类型管道代码(包括日志记录、工作流、队列等)之外,还有各种封装的数据库访问实体、类型化数据集和一些其他业务逻辑代码。我不在此框架的初始开发和后续维护中,但确实继承了它的使用。正如我所提到的,整个框架产生了许多小的 DLL。而且,当我说很多时,我们说的是超过 100 个——而不是你提到的可管理的 8 个左右。更复杂的问题是,这些程序集都经过严格签名、版本控制并出现在 GAC 中。

所以,快进几年和之后的多个维护周期,所发生的事情是对 DLL 及其支持的应用程序的相互依赖造成了严重破坏。在每台生产机器上,machine.config 文件中都有一个巨大的程序集重定向部分,以确保无论请求什么程序集,Fusion 都能加载“正确”的程序集。这源于重建每个依赖框架和应用程序程序集所遇到的困难,这些框架和应用程序程序集依赖于已修改或升级的框架和应用程序程序集。(通常)付出了巨大的努力,以确保在修改程序集时不会对其进行重大更改。重新构建了程序集,并在 machine.config 中创建了新的或更新的条目。

在这里,我将停下来聆听巨大的集体呻吟和喘息声!

这种特殊情况是不该做什么的典型代表。实际上,在这种情况下,您会陷入完全无法维护的境地。我记得当我第一次开始使用它时,我花了 2 天时间来设置我的机器以针对这个框架进行开发——解决我的 GAC 和运行时环境的 GAC 之间的差异、machine.config 程序集重定向、编译时的版本冲突由于不正确的引用,或者更可能是由于直接引用组件 A 和组件 B 而导致的版本冲突,但组件 B 引用了组件 A,但版本与我的应用程序的直接引用不同。你明白了。

这种特定场景的真正问题是程序集内容过于细化。而且,这最终导致了相互依赖的错综复杂的网络。我的想法是,最初的架构师认为这将创建一个高度可维护的代码系统——只需重建对系统组件的非常小的更改。事实上,情况正好相反。此外,对于已经在此处发布的其他一些答案,当您获得这么多的程序集时,加载大量程序集确实会导致性能下降——肯定是在解决过程中,尽管我没有经验证据,但我猜想,在某些极端情况下,运行时可能会受到影响,特别是在反射可能发挥作用的情况下——在这一点上可能是错误的。

你会认为我会被嘲笑,但我相信程序集存在逻辑物理分离——当我在这里说“程序集”时,我假设每个 DLL 有一个程序集。这一切都归结为相互依赖关系。如果我有一个依赖于程序集 B 的程序集 A,我总是问自己是否需要在没有程序集 A 的情况下引用程序集 B。或者,这种分离是否有好处。查看如何引用程序集通常也是一个很好的指标。如果您要将大型库划分为程序集 A、B、C、D 和 E。如果您 90% 的时间都引用程序集 A,因此,您总是必须引用程序集 B 和 C,因为 A 依赖于它们, 那么将程序集 A、B 和 C 组合起来可能是一个更好的主意,除非有' 一个真正令人信服的论点,让他们保持分开。企业库是一个典型的例子,你几乎总是要引用 3 个程序集才能使用库的一个方面——然而,在企业库的情况下,能够在核心功能和代码之上构建重用是其架构的原因。

查看建筑是另一个很好的指导方针。如果您有一个干净整洁的架构,其中您的程序集依赖项是堆栈的形式,例如“垂直”,而不是“网络”,当您在各个方向都有依赖项时开始形成,然后分离程序集在功能边界上是有道理的。否则,请寻求将事物合二为一或寻求重新架构。

不管怎样,祝你好运!

于 2009-07-31T21:46:29.590 回答
4

加载每个程序集都会对性能造成轻微影响(如果它们已签名,则更甚),因此这是倾向于将常用的东西聚集在同一个程序集中的原因之一。我不相信一旦加载了东西就会有很大的开销(尽管可能有一些静态优化的东西,JIT 在跨越程序集边界时可能会更难执行)。

我尝试采用的方法是:命名空间用于逻辑组织。程序集是对应该一起物理使用的类/命名空间进行分组。IE。如果您不希望需要 ClassA 而不是 ClassB(反之亦然),它们属于同一个程序集。

于 2009-07-28T05:08:30.587 回答
2

单体怪物使得为以后的工作重用部分代码比必须的成本更高。并导致不需要耦合的类之间的耦合(通常是显式的),从而导致更高的维护成本,因为测试和纠错将因此变得更加困难。

拥有许多项目的缺点是(至少在 VS 中)与少数项目相比需要相当长的时间来编译。

于 2009-07-28T05:19:45.510 回答
2

您的装配组织中最大的因素应该是您的依赖关系图,在一个类以及一个装配级别。

程序集不应有循环引用。这应该是很明显的开始。

相互依赖最多的类应该在一个程序集中。

如果一个类 A 依赖于类 B,虽然 B 可能不直接依赖于 A,但它不太可能与 A 分开使用,那么它们应该共享一个程序集。

您还可以使用程序集来强制分离关注点——将您的 GUI 代码放在一个程序集中,而您的业务逻辑驻留在另一个程序集中,这将提供某种程度的业务逻辑强制执行与您的 GUI 无关。

基于代码运行位置的程序集分离是需要考虑的另一点 - 可执行文件之间的公共代码应该(通常)在一个公共程序集中,而不是让一个 .exe 直接引用另一个。

也许您可以使用程序集的更重要的事情之一是区分公共 API 和内部使用的对象,以使公共 API 能够工作。通过将 API 放在单独的程序集中,您可以强制执行其 API 的不透明性。

于 2009-08-01T08:11:15.963 回答
1

我想如果你只谈论十几个,你应该没问题。我正在开发一个包含 100 多个程序集的应用程序,这非常痛苦。

如果你没有办法管理依赖关系——知道如果你修改程序集 X 会破坏什么,你就有麻烦了。

我遇到的一个“好”问题是,当程序集 A 引用程序集 B 和 C,B 引用程序集 D 的 V1,而 C 引用程序集 D 的 V2 时。('Twisted diamond' 将是一个很好的名称)

如果您想进行自动构建,那么维护构建脚本会很有趣(这将需要以依赖项的相反顺序构建),或者有“一个解决方案来统治它们”,这几乎是不可能的如果您有很多程序集,请在 Visual Studio 中使用。

编辑 我认为你的问题的答案很大程度上取决于你的程序集的语义。不同的应用程序是否可能共享一个程序集?您希望能够分别更新这两个应用程序的程序集吗?您打算使用 GAC 吗?或者复制可执行文件旁边的程序集?

于 2009-07-28T05:42:28.630 回答
0

就个人而言,我喜欢整体方法。

但有时您无法帮助创建更多程序集。当您需要通用接口程序集时,.NET 远程处理通常负责此操作。

我不确定加载程序集的开销有多大。(也许有人可以启发我们)

于 2009-07-28T05:08:19.027 回答