代码之家  ›  专栏  ›  技术社区  ›  jerryjvl

许多“小”组件的特定下侧面?

  •  19
  • jerryjvl  · 技术社区  · 15 年前

    我正在计划一些工作,将依赖注入引入到当前的大型单片库中,以使库更容易进行单元测试、更容易理解,并且可能作为一个额外的奖励,更加灵活。

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

    我现在想知道的是,我是否应该将当前单个大部件拆分为具有不相交特征集的多个小部件。其中一些较小的程序集将具有相互依赖性,但与所有程序集相差甚远,因为代码的体系结构已经相当松散地耦合在一起了。

    请注意,这些特性集本身也不是微不足道的。它包括客户机/服务器通信、序列化、自定义集合类型、文件IO抽象、公共例程库、线程库、标准日志记录等。

    我看到前面的一个问题: What is better, many small assemblies, or one big assembly? 某种程度上解决了这个问题,但是用更精细的粒度,这让我想知道这里的答案是否仍然适用于这个案例?

    此外,在与本主题接近的各种问题中,一个常见的答案是“太多”程序集导致了未指明的“疼痛”和“问题”。我真的很想具体地知道这种方法的潜在缺点是什么。

    我同意在只需要1个程序集之前添加8个程序集是“有点麻烦”,但必须为每个应用程序包含一个大的单片库也不是完全理想的…另外,添加8个程序集是你只做一次的事情,所以我对这一论点几乎没有任何同情心(即使我一开始可能会和其他人一起抱怨)。

    附录:
    到目前为止,我还没有看到任何令人信服的理由反对小规模的组装,所以我认为我将继续进行下去,就好像这是一个没有问题的问题一样。如果有人能用可证实的事实来支持他们,我还是很有兴趣听到他们。(我会尽快增加奖励以增加可见性)

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

    7 回复  |  直到 15 年前
        1
  •  14
  •   Peter Meyer    15 年前

    我将给出一个实际的例子,其中使用了许多(非常)小的程序集生成了.NET DLL地狱。

    在工作中,我们有一个大型的本土框架,它是一个很长的牙齿(.net 1.1)。除了常见的框架类型管道代码(包括日志记录、工作流、队列等),还有各种封装的数据库访问实体、类型化数据集和其他一些业务逻辑代码。我没有参与这个框架的初始开发和后续维护,但继承了它的使用。正如我提到的,整个框架产生了许多小的DLL。当我说很多的时候,我们说的是100个以上——不是你提到的可管理的8个左右。更为复杂的是,这些程序集都是经过stronly签名、版本控制并出现在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个程序集——然而,对于企业库来说,基于核心功能和代码重用构建的能力是其体系结构的原因。

    查看架构是另一个很好的指南。如果您有一个整洁的堆叠结构,您的程序集依赖关系是以堆栈的形式存在的,比如说“垂直”,而不是“Web”,当您在每个方向上都有依赖关系时,它就开始形成,那么在功能边界上分离程序集是有意义的。否则,就要把事情变成一个整体,或者重新设计。

    不管怎样,祝你好运!

        2
  •  28
  •   jerryjvl    15 年前

    由于性能分析比预期的要长一些,所以我把它放进了它自己的独立答案中。我将接受彼得的正式回答,尽管它缺乏测量,因为它是最有助于激励我自己进行测量的工具,而且它给了我对可能值得测量的东西最有启发。

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

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

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

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

    该应用程序包含1000个类,分为200组,每组5个类彼此继承。类的名称为axxx、bxxx、cxx、dxx和exxx。类A是完全抽象的,B-D是部分抽象的,覆盖了每个方法中的一个,E是具体的。这些方法的实现使得对e实例的一个方法调用将在层次结构链上执行多个调用。所有方法体都非常简单,理论上它们都应该是内联的。

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

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

    测量结果并非都是精确测量的;有些是用秒表测量的,误差范围较大。测量结果如下:

    • 在VS2008中打开解决方案(秒表)
    • 编译解决方案(秒表)
    • 在IDE中:从开始到第一行代码之间的时间(秒表)
    • 在IDE中:为IDE中的200个组中的每个组实例化一个exxx的时间(代码中)
    • 在IDE中:对IDE中的每个exxx执行100000次调用的时间(代码)
    • 最后三个“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
    

    观察:

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

    因此,从所有这些看来,更多程序集的负担主要由开发人员承担,然后主要以编译时间的形式承担。正如我已经说过的,这些项目非常简单,以至于每一个编译花费的时间都不到一秒钟,这就导致了每个程序集的编译开销占主导地位。我可以想象,跨大量程序集的次二级程序集编译强烈地表明这些程序集被拆分得比合理程度更高。另外,在使用预编译的程序集时,主要的developer参数 反对 拆分(编译时间)也会消失。

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

        3
  •  4
  •   Jonathan Rupp    15 年前

    加载每个程序集(如果它们被签名的话甚至更多)会对性能造成轻微的影响,因此这是在同一个程序集中将常用的东西聚集在一起的一个原因。我不认为加载东西后会有很大的开销(尽管在跨越装配边界时,JIT可能会遇到一些静态优化问题)。

    我尝试采用的方法是:名称空间用于逻辑组织。程序集将物理上应一起使用的类/命名空间分组。也就是说,如果你不希望得到A类而不是B类(反之亦然),它们属于同一个集合。

        4
  •  2
  •   Rune FS    15 年前

    单片怪兽使得重用代码的一部分在以后的工作中比原来更昂贵。并且会导致不需要耦合的类之间的耦合(通常是显式的),这会导致更高的维护成本,因为测试和错误更正将更加困难。

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

        5
  •  2
  •   kyoryu    15 年前

    装配组织中最大的因素应该是类和装配级别上的依赖关系图。

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

    相互依赖性最大的类应该在单个程序集中。

    如果A类依赖于B类,而B可能不直接依赖于A,那么它不太可能在A之外使用,那么它们应该共享一个组件。

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

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

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

        6
  •  1
  •   Benjol    15 年前

    我想如果你只说一打,你应该没事的。我正在开发一个100多个程序集的应用程序,它是 非常 痛苦的

    如果您没有某种方式来管理依赖关系——知道修改程序集X会破坏什么,那么您就有麻烦了。

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

    如果您想拥有一个自动化的构建,那么您将很有兴趣维护构建脚本(它需要按照依赖项的相反顺序构建),或者拥有“一个解决方案来管理所有脚本”,如果您拥有大量的程序集,那么在Visual Studio中几乎不可能使用该解决方案。

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

        7
  •  0
  •   leppie    15 年前

    就我个人而言,我喜欢单一的方法。

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

    我不知道装载组件的开销有多大。(也许有人能启发我们)