17

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

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

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

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

那么在松散耦合的 DI 世界中,为什么我们将逻辑分组到类中?

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

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

4

4 回答 4

8

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

高凝聚力

在计算机编程中,凝聚力是衡量单个模块的职责相关性或集中程度的指标。应用于面向对象编程时,如果为给定类服务的方法在许多方面趋于相似,则称该类具有高内聚性。在高度内聚的系统中,代码可读性和重用的可能性增加,而复杂性保持在可控范围内。

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

* 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)和命名不佳的实现类。

于 2011-03-13T14:54:13.433 回答
4
  • 为什么我们要在类中分组,原则应该是什么?

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

如果您问我们为什么要对事物进行分组,那么我会第二次选择 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) 我们不仅使用类:我们还使用命名空间/包进行封装。

问候,

埃德。

于 2009-11-27T06:59:54.423 回答
3

我可以想到两个原因。

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

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

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

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

于 2009-01-09T12:01:45.047 回答
2

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

成本因素

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

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

于 2009-01-09T11:40:01.367 回答