【问题标题】:Why does "protection from changes" imply a direction of dependency?为什么“防止变化”意味着依赖的方向?
【发布时间】:2019-05-24 19:11:19
【问题描述】:

我的一些背景知识:我是一名自学成才的程序员,6 年前加入 MegaCorp(TM) 时,他开始使用 Python 并学习 Java。拥有数学学位,我在算法和批判性思维方面相当扎实(不是双关语),但我经常发现自己在数据结构、设计或其他 CompSci 基础知识方面存在差距,而我的同行在他们的计算机科学中学到了这些基础知识课程。

为此,我向团队中的一位高级工程师寻求一本书推荐来帮助填补我的空白,他建议Clean Architecture

我大约完成了三分之一,并且对这些建议的主要激励因素之一感到非常困惑。鲍勃叔叔提出了许多想法和原则(包括我以前听说过的SOLID 原则,尽管我仍在掌握里氏替换原则)旨在“保护”系统的某些部分从需求到改变。有几个例子,但最清楚的是第 73 页:

如果应该保护组件 A 免受组件 B 的更改,那么 组件 B 应该依赖于组件 A。

(我应该注意到,在没有任何我可以看到的实际定义的情况下,我认为“组件”等同于 Java 包,尽管我认为如果可以应用相同的思考过程, “组件”是一个单独的服务——两者都应该为用户提供稳定可用的接口,无论是本地调用还是通过网络调用)

这个说法没有任何证据,对我来说也不是不言而喻的。考虑组件(包)ComponentA 中的类ClassA 的情况,它在组件ClassB 中调用DoStuffReturn doStuff(DoStuffInput input, String someOtherArg) ComponentB - 其中调用是通过直接依赖或通过对接口的依赖in ComponentBnot in ComponentA,正如 Clean Architecture 所建议的那样)

  • 如果 B 中的更改是功能更改,而不是签名更改(即 - 对于相同的 DoStuffInputString 输入,返回不同的 DoStuffReturn),则 A 中不需要更改:
    • ClassAClassB.doStuff 的调用仍然有效(相同的参数和返回类型)
    • ClassA 的单元测试(应该使用模拟的 ClassBs)应该仍然通过
    • 任何测试ClassAClassB 如何协作的功能/集成测试都需要更新他们的期望,但这并不是对ComponentA 的更改(除非这些测试在 @ 987654340@ - 我通常在ComponentAIntegrationTests 包中看到它们外部化,但我想它们也可以放在一起。不过,这似乎不是本书所暗示的 - 它似乎是在谈论代码的更改,而不是测试)
  • 如果 B 中的更改是对 doStuff 以外的方法的签名更改,则 A 不需要任何更改
  • 如果 B 中的更改是对 doStuff 的签名更改,那么 A 需要更改 - 但如果接口也在 A 中,情况也是如此。

(请注意,在清洁架构提倡的设置下,其中提供类的接口在消费组件(A)中,只有第一种情况会构成 B 的变化 - 所以这是我们真正需要的唯一一个关心)

我错过了什么?如果 ComponentA 依赖于 ComponentB,那么在什么情况下 ComponentB 中的类的改变需要 ComponentA 的改变?

(请注意,我反对使用接口 - 它们还有许多其他好处,尤其是允许在合同的“双方”同时开发,并允许交换接口的各种实现)

【问题讨论】:

    标签: oop architecture solid-principles dependency-inversion


    【解决方案1】:

    为了解释我们如何通过反转依赖方向来保护一个组件免受另一个组件的更改,让我们首先定义 componentdependency。在 Clean Architecture 的上下文中,组件是 jar 文件,依赖项是类之间的链接。

    组件是部署的单元。它们是可以作为系统一部分部署的最小实体。在 Java 中,它们是 jar 文件。 --第 96 页

    首先要注意的是,所有的依赖都是源代码的依赖。 A 类指向 B 类的箭头表示 A 类的源代码中提到了 B 类的名称,而 B 类没有提及 A 类。 --第 72 页

    那么问题就变成了,我们想要保护组件(jar)免受哪些变化?答案是:重新编译、重新部署和传递依赖。

    如果组件A 依赖于组件B,那么B 中的任何更改(即使该更改不影响A 使用的API)都需要A 重新编译。此外,如果 B 中的更改添加、删除或修改了 B 的依赖项,则 A 必须协调一个新的传递依赖项,因此更改会传播。

    这种依赖性意味着对 [B] 源代码的更改将强制 [A] 重新编译和重新部署,即使它所关心的实际上并没有改变。 --第 84 页

    传递依赖违反了软件实体不应依赖于它们不直接使用的东西的一般原则。 --第 75 页

    我在这里将多个 SOLID 原则中的元素混为一谈;但在处理变更时,它们具有潜在的共性。

    【讨论】:

    • “如果组件 A 依赖于组件 B,那么 B 中的任何更改(即使该更改不影响 A 使用的 API)都需要重新编译 A。”那是我缺少的一点-谢谢!我确实在第 84 页的书中看到了这一行,但我认为这是“流程要求”(“我们必须这样做才能保持最新”),而不是 Java 实现的实际事实。
    【解决方案2】:

    就我的背景而言,我确实拥有计算机科学学位和 20 多年的专业经验。

    首先要做的事情。该领域没有什么是真正明确定义的(好吧,一些与数学相关的东西是)。甚至是面向对象本身或封装、单一职责等基本事物,以及领域驱动设计、REST 等更复杂的事物。对所有事情都有截然不同的看法。有时流行的解释或这些东西是最糟糕的选择。

    我想说的是,即使面对压倒性的权威,也要保持怀疑。质疑事物并总是试图找到支持某事的正当理由。不仅仅是“这种方式更易于维护”,这也只是一个假设,而不是证据。

    我碰巧认为鲍勃叔叔是一个不太理想的面向对象的信息来源。 Here 是对我前段时间写的“清洁架构”(不是书,想法)的详细评论,如果您有兴趣的话。

    回到你的问题。引用本身基本上说依赖的方向与变化的传播背道而驰(here 又是我的一篇文章,带有一些漂亮的图片)。它并不意味着(也不应该)更改总是会通过依赖项回流,但“知识”确实如此。总是。有时更改不需要外观,例如签名更改。容量、性能的变化有时是破坏性的变化,但在签名中并不直接可见。方法的含义也可能在没有任何明显迹象的情况下发生变化。

    所以,您是对的,内部更改可能根本不会导致任何其他更改。但是避免潜在改变的唯一方法是首先不依赖于那个东西。

    第二好的方法是有一个很好的抽象和封装,不幸的是“清洁架构”做得不好。 Bob 大叔提倡使用贫血对象和纯数据作为接口破坏了组件可能具有的任何抽象或封装。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-28
      • 2023-03-10
      • 2019-02-21
      • 1970-01-01
      • 2013-04-11
      • 1970-01-01
      相关资源
      最近更新 更多