【问题标题】:What to know of implementation besides contract?除了合同之外,还需要了解哪些执行?
【发布时间】:2018-07-08 08:43:23
【问题描述】:

SOLID 中的 L 和 D 都鼓励我们根据合同而非实际实现进行编程。但这是否意味着接口使用者不应该知道实现,或者他不应该依赖实现来做合同中没有明确规定的事情?

我的意思是,我们可以有一个方法在 DB 中创建一个新的 NumericParameter,但它的接口只是这么说!它并没有说 NumericParameter 实际上是跨越 2 个表的继承实体,或者该方法还创建了一个更多相关实体以保存有关创建的实体的更多数据。这些是实现细节。

很清楚为什么不应该依赖实现来做某事。但也许依赖实现不做某事是合理的(即使合同中没有说明?)否则,你将如何修改你的代码?任何时候你添加一些活动,它都可能与你调用的其他一些方法和服务中发生的事情发生冲突。就像,你读了一个“更新”的数据库行,然后调用 foo() 然后写回一行并进行一些修改,但似乎 foo() 已经对同一行进行了一些更改——在这个事务中——他们将被覆盖。

另一方面,可以更改实现。如果某个消费者依赖它不做某事,那么在某些时候这种依赖可能会中断。这意味着,我们不仅要检查正在编辑的代码中调用了哪些代码,还要检查代码的可能调用者。我们必须详细了解触发此代码的任何事务中可能发生的一切。

不知何故,我认为那是一种气味,但没有它你怎么活?我通常会尝试忽略实现细节,但在很多情况下都会导致一些冲突。

【问题讨论】:

  • 你说的做一些没有在合同中规定的事情是什么意思?它收到的数据有什么问题吗?跟外面的世界有什么关系?与它的调用者?在foo() 示例中,我们希望依赖项不会做什么?
  • 一个更新的例子。一部分逻辑删除实体A;其他一些部分修改了由 A 引用的 B。由于 JPA API 的性质,如果引用的实体包含引用它的所有实体的集合,则该集合(缓存,一旦从 DB 中读取)必须与也缓存在此事务中的实际实体。因此,尽管这两个逻辑部分没有自然冲突,删除实体 A 然后保持 B 的当前状态(现在失去一致性,因为它仍然包含对 A 的引用)会引入冲突。
  • 这个例子向我们展示了这一点:任何修改某个 B 的代码都必须依赖它调用的其他代码才能不删除任何 A。如果它不能依赖它,那么它的作者就不能确定他不会通过编写这段代码来引入冲突。
  • 或者,更简单的是,任何从 DB 读取某个实体的代码都必须依赖它执行的任何其他服务都不会尝试从 DB 中删除该实体(这可能是后者的“内部实现细节”服务)。
  • 你误解了我的意思。 JPA,而不是你,必须尊重他们的合同。但并发访问问题与合约或 JPA 内部实现方式无关。它们与您(客户)如何使用 JPA 以及何时使用有关。

标签: oop architecture solid-principles


【解决方案1】:

好吧,让我总结一下收集的智慧。

1) 通常,方法的所有重要影响要么是其合同的一部分(或应该是),要么是由环境决定的(例如 JPA 的删除会干扰 JPA 对其他实体的修改)。如果是前者,那么合约告诉我们会发生什么,如果是后者,开发者应该知道他自己定义的环境的影响。因此,在这两种情况下,他都应该知道可能的冲突 不知道实现。一个设计合理的方法几乎不会产生一些与其合同和环境/设置/等完全无关的重大影响,或者至少我没有想到或在这里其他任何人提到过这样的例子。当然,除非该方法被非常熟悉并依赖于其实现的人使用;但在这种情况下,我当然必须知道实现,我的问题不适用。

2) 在我的问题中,我提到了诸如“跨越多个表的继承实体”和“再创建一个实体”之类的实现细节。好吧,特定于 JPA 继承的问题是环境的一部分;创建额外的实体应该是合同的一部分(如果该实体是由其他人引用的)或不会造成任何干扰(如果它是严格的私人事物),或者如果它是顶部配置引入的某些“方面”的产物,则应该是环境的一部分原始方法的逻辑。

因此,我最初的问题的答案是“不需要知道实现细节”(除非您必须使用紧密耦合的类),并且“给定正确定义的合同和正确设计的方法,您可以不用它,因为您应该知道合同和环境所需的一切”。

【讨论】:

    【解决方案2】:

    但这是否意味着接口使用者不应该知道 实施,或者只是他不应该依赖实施 做合同中没有明确说明的事情?

    前者。但是,依赖项是否可以避免接触他们甚至不知道客户关心的外部内容?合同与其说是说“不要动我的奶酪”,不如说是“把我期望的形状还给我,不管你怎么做”。

    我看到的唯一其他答案是使用纯(无副作用)函数,但我们不再真正处于 OOP 领域。

    【讨论】:

    • “但是,依赖项能否阻止自己接触他们甚至不知道客户关心的外部内容?” - 是的,这就是让我建议也许我,作为在某些时候执行该依赖的代码的作者,必须对依赖的实现有足够的了解,以判断在特定情况下使用它是否安全。这样,我可以依靠实现而不做任何与我的其余代码冲突的事情。因此是后一种选择。
    • 您提出的所有问题示例都是由于您的代码中的两个使用者共享可变状态和并发访问。依赖项的具体实现无关紧要。了解依赖项的底层实现不会改变竞争消费者的存在。
    • 了解两个消费者的实现至少会让人们看到他们在竞争!不了解消费者内部情况(其中一个人试图删除实体的事实 - 如果它是副作用而不是合同的一部分),一个人会合理地决定另一个消费者(修改另一个引用该实体的实体)不能与第一个消费者冲突.
    • 但是,由于我想不出一个合适的例子,我想可以假设通常方法的所有重要影响都是(或应该是)它的合同的一部分,或者由环境,就像 JPA 一样。如果是后者,开发者应该知道他自己定义的环境的影响。 (我的 JPA 示例演示了方法合同中未说明的干扰效果但它是 JPA 合同的一部分,JPA 是我自己提供的环境的一部分,所以我应该在这里期待干扰)。
    【解决方案3】:

    我认为 SOLID 的关键在于您应该尽可能地使用所有原则,而不是孤立地选择一两个。您使用 foo() 给出的示例是一个问题,因为您的代码的两个区域处理相同的数据库记录。我的解释是,这违反了 SOLID 中的 S。

    【讨论】:

    • 对于 foo(),我同意你的推理,但不完全同意:两个服务可能有理由触及同一个记录。一方面,他们最终使用相同的记录可能只是一个巧合:一个服务正在使用 X,而另一个服务在某些条件下查找某个 Y,并找到 Y=X。
    • 不过,因为你有一个观点,我在回复 guillaume31 时提供了另一个示例:在单个事务中,两个服务都没有访问数据库实体,但仍然是底层框架引发了冲突曲解情况。那么,在设计层面上,一切都很好。但是要知道这个问题,我必须知道被调用的服务会执行删除,甚至 如何 JPA 将实现该删除。
    • 另外,我相信 S 并不要求我们只有一个 class 负责某些功能。反过来说:S 需要一个类来负责一个feature;这并不意味着其他一些类不能参与实现相同的功能。包括访问同一行。或者,取决于设计更改的相同原因。此外,访问行可能不是方法的主要任务,而只是其分解的结果。
    • 我们甚至可以说我们将 foo() 方法分成几部分,根据我上面刚刚提到的分解。一部分做某事,另一部分被移动到另一个函数 bar() - 是的,单一职责 - 它删除了一个实体。即使在这种情况下,调用 foo() 的初始调用者仍然必须知道 foo() 会依次调用 bar(),否则他对冲突毫无准备。
    • 既然你提议两个有两个模块负责相同的功能,那么通用封闭原则可能更合适,它会告诉你在一个组件中将它们绑定在一起。无论如何,这两个模块是内聚和紧密耦合的,它们应该被设计成兼容的。
    【解决方案4】:

    这是在“固执己见”的边缘,但我认为事实的答案

    他不应该依赖执行来做合同中没有明确规定的事情吗?

    应该是是 - 完全正确。这就是这里的 core 元素。编写接口的人也会编写至少一个实现,这是很常见的。在写下接口时,可能已经设计了一些实现(至少在你的脑海中)。因此,“了解实现”是您在现实世界中最经常无法避免的事情。所以最好把注意力集中在你陈述的第二部分。

    除此之外:当您发现合同模棱两可时,不要花时间担心不同的解释。而是用你的精力澄清合同,使其变得“足够清楚”实施应该做什么或不做什么。

    【讨论】:

    • 好的,您对“也许依赖实施而不做某事是合理的”的回答是什么?我在上面描述了一个问题...
    猜你喜欢
    • 2023-02-13
    • 2012-12-07
    • 1970-01-01
    • 2017-07-28
    • 2023-03-15
    • 2011-05-12
    • 1970-01-01
    • 2016-04-20
    • 2018-01-14
    相关资源
    最近更新 更多