许多小型组件的特定缺点?

jer*_*jvl 19 .net dependency-injection ninject

我正在计划一些工作,将依赖注入引入当前的大型单片库,试图使库更容易进行单元测试,更容易理解,并可能更灵活地作为奖励.

我决定使用NInject,我真的很喜欢Nate的"做一件事,做得好"(转述)的座右铭,而且它似乎在DI的背景下特别好.

我现在想知道的是,我是否应该将当前单个大型装配拆分为具有不相交特征集的多个较小装配.这些较小的程序集中的一些将具有相互依赖性,但远非所有这些程序集,因为代码的体系结构已经非常松散地耦合.

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

我看到前一个问题:什么是更好的,许多小型装配,或一个大装配?有点解决这个问题,但是看起来更精细的粒度,这让我想知道那里的答案是否仍适用于这种情况?

此外,在接近这个主题的各种问题中,一个常见的答案是,"太多"集会导致了未指明的"痛苦"和"问题".我真的想要具体了解这种方法可能存在的缺点.

我同意在仅需要1个之前添加8个组件是"有点痛苦",但是必须为每个应用程序包含一个大的单片库也不是完全理想的...加上8个组件只是你做的事情曾经,所以我对这个论点很少有同情心(即使我最初可能会和其他人一起抱怨).

附录:
到目前为止,我已经看到没有针对小型装配的任何理由,所以我想我现在将继续进行,好像这是一个非问题.如果有人能够用可验证的事实来考虑好的可靠理由来支持它们,我仍然会非常有兴趣了解它们.(我会尽快增加赏金以提高知名度)

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

jer*_*jvl 30

由于性能分析比预期的要长一些,我已经把它放在它自己的单独答案中.我将接受彼得的官方答案,尽管它缺乏测量,因为它最激励我自己进行测量,并且因为它给了我最值得测量的灵感.

分析:
到目前为止提到的具体缺点似乎都集中在一种另一种的性能上,但缺少实际的定量数据,我已经做了以下测量:

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

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

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

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

这些类沿着2个维度以8种不同的配置分布在程序集中:

  • 组件数量:10,20,50,100
  • 切割方向:跨越继承层次结构(AE中的任何一个都不在同一个程序集中),并且沿着继承层次结构

测量结果并非完全准确; 有些是通过秒表完成的,并且误差较大.测量结果如下:

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

结果:
在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
Run Code Online (Sandbox Code Playgroud)

观察:

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

因此,从这一切看来,更多装配的负担主要由开发人员承担,然后主要以编译时间的形式承担.正如我已经说过的那样,这些项目非常简单,每次编译都需要不到一秒钟的时间来编译,导致每个程序集的编译开销占主导地位.我可以想象,跨越大量程序集的亚秒级程序集编译强烈表明这些程序集已进一步拆分而不是合理的.此外,当使用预编译的程序集时,分裂(编译时间)的主要开发人员参数也将消失.

在这些测量中,为了运行时性能,我可以看到很少有证据表明不能拆分成较小的组件.唯一需要注意的(在某种程度上)是尽可能避免切断继承; 我认为大多数理智的设计无论如何都会限制它,因为继承通常只发生在功能区域内,通常最终会在一个组件内.


Pet*_*yer 14

我将给你一个真实世界的例子,其中许多(非常)小组件的使用产生了.Net DLL Hell.

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

因此,快进几年和以后的一些维护周期,发生的事情是DLL和它们支持的应用程序的相互依赖性造成了严重破坏.在每台生产机器上都是machine.config文件中的一个巨大的程序集重定向部分,它确保无论请求何种程序集,都可以通过Fusion加载"正确"的程序集.这是因为重建每个依赖于已修改或升级的依赖框架和应用程序程序集所遇到的困难.采取了很大的痛苦(通常)以确保在修改组件时不会对组件进行任何重大更改.重建了程序集,并在machine.config中创建了一个新的或更新的条目.

这是我会停下来听一声巨大的集体呻吟声和喘息声!

这种特殊情况是不做的事情的典型代表.确实在这种情况下,你会陷入完全无法维持的境地.我记得当我第一次使用它时,花了两天的时间让我的机器设置用于开发这个框架 - 解决我的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可能更好的结合,除非有一个非常有说服力的论据允许它们保持分离.Enterprise Library就是这样的经典示例,您几乎总是需要引用3个程序集才能使用库的单个方面 - 但在Enterprise Library的情况下,能够构建核心功能和代码之上重用是它的架构的原因.

看建筑是另一个很好的指导方针.如果你有一个很好的干净堆叠架构,你的程序集依赖关系是堆栈的形式,比如说"垂直",而不是"web",当你在每个方向都有依赖关系时会开始形成,然后分离程序集功能边界是有道理的.否则,请将事物放入一个或寻找重新设计.

无论哪种方式,祝你好运!

  • +1是一个很好的例子.我确实发现,在很大程度上,这种情况下的问题不是源于许多小型装配本身,而是源于这种粒度使得其他事情更加错误的事实?......我并不是说这不是一个问题,因为风险显然应该尽可能地受到限制. (2认同)