【问题标题】:Going bananas with loose coupling and dependency injection松耦合和依赖注入的香蕉
【发布时间】:2009-01-09 10:45:32
【问题描述】:

随着我们依赖注入框架的最新添加(春季注释),创建 DI 管理组件的边际成本似乎已经达到了一些关键的新阈值。虽然以前有与 spring 相关的开销(大量的 XML 和额外的间接),但依赖注入似乎已经开始出现在许多模式所在的地方;他们进入引擎盖并“消失”。

这样做的结果是与large number 组件相关的概念开销变得可以接受。有争议的是,我们可以制作一个大多数类只公开的系统 一个单一的公共方法并通过疯狂地聚合这些部分来构建整个系统。在我们的例子中,给出了一些东西;应用程序的用户界面有一些功能需求,这些需求塑造了最顶层的服务。后端系统控制下部。但在这两者之间,一切都可以争夺。

我们一直在讨论为什么要在类中分组以及原则应该是什么?有几件事是确定的;立面图案已死并被掩埋。任何包含多个不相关功能的服务也往往会被拆分。 “不相关的特征”的解释比我之前所做的要严格得多。

在我们的团队中,这里有两种流行的思路:实现依赖限制分组;单个类中的任何功能最好是 all 注入依赖项的客户端。我们是一个 DDD 项目,而另一部分则认为域限制了分组(CustomerService 或更细粒度的 CustomerProductService、CustomerOrderService)——注入依赖项的规范化使用并不重要。

那么在松散耦合的 DI 世界中,我们为什么要在类中对逻辑进行分组?

edit: duffymo 指出这可能正在朝着函数式编程风格发展;这就提出了国家的问题。我们有相当多的“状态”对象代表相关应用程序状态的(小)片段。我们将这些注入到任何对该状态有合法需求的服务中。 (我们使用“状态”对象而不是常规域对象的原因是 spring 在未指定的时间构造这些对象。我认为这是一个轻微的解决方法或替代解决方案,让 spring 管理域对象的实际创建。可能有更好的解决方案在这里)。

因此,例如,任何需要 OrderSystemAccessControlState 的服务都可以注入它,而消费者并不容易知道这些数据的范围。一些与安全相关的状态通常用于许多不同的级别,但在中间的级别上完全不可见。我真的认为这从根本上违反了功能原则。从面向对象的角度来看,我什至很难适应这个概念——但只要注入的状态是精确的并且是强类型的,那么 need 就是合法的,也就是用例是合适的。

【问题讨论】:

    标签: dependency-injection


    【解决方案1】:

    良好的面向对象设计的首要原则不仅限于松散耦合,还包括高内聚,这在大多数讨论中都被忽略了。

    高内聚

    在计算机编程中,凝聚力是 衡量相关程度或 将责任集中在一个 单模块是。适用于 面向对象编程,如果 服务于给定类的方法 在很多方面趋于相似, 然后据说班级有高 凝聚。在一个高度内聚的系统中, 代码可读性和可能性 重用性增加,而复杂性 保持可管理性。

    如果出现以下情况,内聚力会降低:

    * The functionality embedded in a class, accessed through its methods,
      have little in common.
    * Methods carry out many varied activities, often using coarsely-grained or 
      unrelated sets of data.
    

    低内聚(或“弱内聚”)的缺点是:

    * Increased difficulty in understanding modules.
    * Increased difficulty in maintaining a system, because logical changes in 
      the domain affect multiple modules, and because changes in one module 
      require changes in related modules.
    * Increased difficulty in reusing a module because most applications
      won’t need the random set of operations provided by a module.
    

    当人们对 IoC 容器着迷时,丢失的一件事是失去了凝聚力,并且对某事的内容和方式的可追溯性成为日后自己弄清楚的噩梦,因为所有的关系都被一堆东西所掩盖XML 配置文件(我在看你的 Spring)和命名不佳的实现类。

    【讨论】:

    • +1 谈论耦合而不谈论内聚是没有意义的
    【解决方案2】:
    • 我们为什么要在类中对事物进行分组,原则应该是什么?

    您是在强调“分组”还是“类”?

    如果您要问我们为什么要对事物进行分组,那么我会支持 Medelt 的“可维护性”,尽管我会将其改写为“减少连锁反应的潜在成本”。

    暂时考虑一下,不是组件(类、文件,无论它们可能是什么)之间的实际耦合,而是潜在的耦合,即这些组件之间源代码依赖项的最大可能数量。

    有一个定理表明,给定一个组件链 a - b - c - d - e,使得 a 依赖于 b 等,改变 e 将改变 c 组件的概率不可能更大比改变 e 会改变 d 的可能性。而在真实的软件系统中,改变e影响c的概率通常小于改变e影响d的概率。

    当然,您可能会说,这很明显。但我们可以让它更加明显。在这个例子中,d 直接依赖于 e,而 c 间接(通过 d)依赖于 e。因此,我们可以说,从统计上讲,一个主要由直接依赖构成的系统将比一个主要由间接依赖构成的系统产生更大的连锁反应。

    鉴于在现实世界中,每个涟漪效应都需要花钱,我们可以说,主要由直接依赖关系形成的系统更新的涟漪效应成本将高于系统更新的涟漪效应成本主要由间接依赖构成。

    现在,回到潜在耦合。有可能表明,在绝对封装上下文中(例如 Java 或 C#,递归封装并未广泛使用)所有组件都可能通过直接依赖关系或与单个中间组件的间接依赖关系相互连接。从统计上看,一个系统可以最大限度地减少其组件之间的直接潜在耦合,从而最大限度地减少由于任何更新而产生的潜在涟漪效应。

    我们如何区分直接和间接潜在耦合(就好像我们还没有让猫从袋子里出来一样)?带封装。

    封装是一个属性,即模型化实体中包含的信息只能通过该模型化实体支持的接口上的交互来访问。无法通过这些接口访问的信息(可以是数据或行为)称为“隐藏信息”。通过组件内的信息隐藏行为,我们保证它只能被外部组件间接(通过接口)访问。

    这必然需要某种分组容器,其中可以隐藏某种更细粒度的功能。

    这就是我们为什么要“分组”的原因。

    至于我们为什么使用类来分组:

    A) 类提供语言支持的封装机制。

    B) 我们不仅使用类:我们还使用命名空间/包进行封装。

    问候,

    编。

    【讨论】:

      【解决方案3】:

      我能想到两个原因。

      可维护性:您自然会期望一些逻辑结合在一起。定义对特定外部服务(例如数据库)的操作的逻辑可能应该以逻辑方式组合在一起。您可以在命名空间或类中执行此操作。

      状态和身份:对象不仅包含逻辑,还维护状态。作为处理特定对象状态的接口的一部分的逻辑应该在该对象上定义。对象也保持身份,在问题域中建模一个实体的对象应该是您软件中的一个对象。

      作为侧节点:状态和身份参数主要适用于域对象。在大多数解决方案中,我主要将 IoC 容器用于这些服务。我的域对象通常是作为程序流的一部分创建和销毁的,我通常为此使用单独的工厂对象。然后可以通过 IoC 容器注入和处理工厂。我在创建工厂作为 IoC 容器的包装器方面取得了一些成功。这样容器也可以处理域对象的生命周期。

      这是一个非常有趣的问题。如果我回顾一下我过去实现事物的方式,我会看到一种趋势,即更小、更细化的接口和类。事情当然会这样好转。我不认为最佳解决方案每个班级都有一个功能。这实际上意味着您将 OO 语言用作函数式语言,虽然函数式语言非常强大,但将这两种范式结合起来还有很多话要说。

      【讨论】:

      • 很好的答案。我们按套餐对类似服务进行分组。我们不 DI 我们的域对象,但我们已经开始创建大量 small 状态对象(如果您愿意,可以作为场景上下文的一部分)注入到服务中。这些状态对象具有特定上下文所需的状态/域对象。
      • 我认为每个类的一个函数也不正确。这就是我要问的;)
      • 我发现大部分服务都是无状态的。如果我编写一个服务,它可能只有 DAO 作为私有数据成员。如果这对你来说是真的,我认为这会使“状态和身份”论点无效。
      • @duffymo 服务通常不需要身份。例如,我发现服务通常需要一些状态来维持连接。通常这不是一个真正的问题,因为服务的生命周期很简单,它们大多是单例。
      • @krosenvold 软件设计总是在平衡力量。似乎一股推动我们走向更大服务类别的力量已经随着简单的 DI 而消失了。现在看来,我们剩下的主要力量是许多小类的灵活性与将功能分组到大类的可维护性。
      【解决方案4】:

      纯DI完美宇宙,我觉得单类+方法设计比较理想。实际上,我们需要平衡降低可行性的成本。

      成本因素

      • DI 开销。为单一方法启动所有底层证券和相关底层证券是昂贵的。分组到一个类可以让我们抵消一些。
      • 技能 - DI 对许多人(尤其是我自己)来说是新的,因此了解如何做得更好或摆脱旧的/习惯性设计是很困难的
      • 已经拥有它们的棕地应用程序,使用它们更容易/更便宜/更快,并在未来的绿地应用程序中担心这一点

      希望我对 DI 的新手(是的,我充满了虚构的词)并没有让我完全错了。

      【讨论】:

      • 至少在 spring 中,布线开销主要是在应用程序启动时发生的一个恒定因素。运行时布线开销最小。所以我真的认为 DI 开销不是问题。概念问题更为重要。
      • 当我们与遗留界面交互时,我们仍然可以选择在这个界面之上创建许多微组件。可以说,您也可以在遗留应用程序中的很多地方执行此操作,一次从遗留 impl 中取出一个方法,这可能是发展遗留应用程序的一种聪明方法。..
      • @krosenvold:这取决于您使用 DI 容器的目的。如果您使用它来创建一些服务,则没有问题。如果您使用它来创建大量域对象,那么开销肯定是个问题。
      • @Mendelt 已接受。我们不 DI 域对象。
      • 没错。对我来说,域对象是根据输入参数在服务方法范围内创建的。他们没有被注射。问题开始听起来像“这个模型是否让我转向功能性开发方法并远离对象?”对我来说。
      猜你喜欢
      • 2013-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-16
      • 2014-03-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多