循环依赖是邪恶的

2021-01-31 00:45:50

关于F#的最常见的抱怨之一是,它要求代码按依赖关系顺序排列。也就是说,您不能使用前向引用来引用编译器尚未看到的代码。

“ .fs文件的顺序使其难以编译……我的F#应用程序仅包含50行代码,但是即使编译最微小的非常规应用程序,它的工作量也超过了其价值。有没有办法使F#编译器更像C#编译器,从而使其与文件传递到编译器的顺序没有那么紧密地联系在一起?” [fpish.net]

“在尝试使用F#构建一个略高于玩具的项目后,我得出的结论是,使用当前的工具,即使维护一个中等复杂性的项目也非常困难。” [www.ikriv.com]

“ F#编译器太线性了。 F#编译器应自动处理所有类型解析问题,而与声明顺序无关” [www.sturmnet.org]

“在该论坛上已经讨论了F#项目系统令人讨厌(且恕我恕我直言不必要的)限制的主题。我在说控制编译顺序的方式” [fpish.net]

好吧,这些抱怨是没有根据的。您当然可以使用F#构建和维护大型项目。 F#编译器和核心库是两个明显的示例。

实际上,大多数这些问题归结为“为什么F#不能像C#”。如果您来自C#,则习惯于使编译器自动连接所有内容。必须显式地处理依赖关系是非常烦人的,甚至是老式的和回归的。

这篇文章的目的是解释(a)依赖管理为什么很重要,以及(b)一些可以帮助您解决它的技术。

我们都知道依赖是我们生存的祸根。程序集依存关系,配置依存关系,数据库依存关系,网络依存关系–总是存在。

因此,作为专业人士,我们的开发人员往往会付出很多努力来使依赖项更易于管理。这个目标以许多不同的方式体现出来:接口隔离原理,控制反转和依赖注入;使用NuGet进行软件包管理;用人偶/厨师进行配置管理;等等。从某种意义上说,所有这些方法都在试图减少我们必须了解的事物的数量以及可能破坏的事物的数量。

当然,这不是一个新问题。经典书籍“大规模C ++软件设计”的很大一部分致力于依赖性管理。正如作者约翰·拉科斯(John Lakos)所说:

“通过避免组件之间不必要的依赖关系,可以大大降低子系统的维护成本”

这里的关键词是“不必要的”。什么是“不必要的”依赖?当然要看情况。但是几乎总是不需要一种特定的依赖关系-循环依赖关系。

要了解循环依赖为何有害,让我们重新回顾一下“组件”的含义。

组件是好东西。无论您将它们视为软件包,程序集,模块,类还是其他类,它们的主要目的都是将大量代码分解为更小且更易于管理的部分。换句话说,我们正在采用分而治之的方法来解决软件开发问题。

但是,为了对维护,部署等有用,一个组件不应该只是随机的东西集合。它应该(当然)仅将相关代码分组在一起。

因此,在理想世界中,每个组件都将完全独立于其他任何组件。但是通常(当然),总是需要一些依赖项。

但是,现在我们有了带有依赖项的组件,我们需要一种方法来管理这些依赖项。一种标准的方法是使用“分层”原理。我们可以有“高级”层和“低级”层,关键规则是:每一层应仅依赖于其下的层,而不应依赖于其上的层。

我敢肯定,您对此非常熟悉。这是一些简单图层的示意图:

但是现在,当您像下面这样引入从底层到顶层的依赖关系时,会发生什么?

通过从下到上具有依赖性,我们引入了邪恶的“循环依赖性”。

从逻辑角度来看,此替代分层与原始分层相同。

发生严重错误!很明显,我们真的搞砸了。

实际上,一旦组件之间有了任何形式的循环依赖关系,您唯一可以做的就是将它们全部放入同一层。

换句话说,循环依赖已完全破坏了我们的“分而治之”的方法,这就是首先拥有组件的全部原因。现在,我们不再只有三个组成部分,而只有一个“超级组成部分”,它比所需的要大三倍,而且更复杂。

有关此主题的更多信息,请参见StackOverflow答案和有关Patrick(NDepend)的Patrick Smacchia进行分层的文章。

首先,我们来看一下.NET程序集之间的循环依赖关系。以下是布莱恩·麦克纳马拉(Brian McNamara)的一些战争故事(我的重点):

.Net Framework 2.0出现了这个问题。 System.dll,System.Configuration.dll和System.Xml.dll彼此无望地纠缠在一起。这以各种丑陋的方式表现出来。例如,我在VS调试器中发现了一个简单的[bug],在尝试加载符号时遇到断点时,调试对象实际上会崩溃,这是由这些程序集之间的循环依赖性引起的。另一个故事:我的一个朋友是Silverlight初始版本的开发人员,任务是尝试精简这三个程序集,而第一个艰巨的任务是尝试解开循环依赖关系。 “免费相互递归”在小范围内非常方便,但会在很大程度上破坏您的利益。

VS2008比计划的交货时间晚了一周,因为VS2008依赖于SQL Server,而SQL Server依赖于VS,哎呀!最终,他们无法生产出所有产品都具有相同内部版本号的完整产品版本,因此必须争先恐后才能使其正常运行。 [fpish.net]

因此,有大量证据表明程序集之间的循环依赖关系很糟糕。实际上,程序集之间的循环依赖关系被认为非常糟糕,以至于Visual Studio甚至都不允许您创建它们!

您可能会说:“是的,我可以理解为什么循环依赖关系对程序集不利,但是为什么要麻烦程序集内的代码呢?”

好吧,出于完全相同的原因!分层允许更好的分区,更容易的测试和更整洁的重构。您可以在有关“狂野”的依赖周期的相关文章中看到我的意思,其中我比较了C#项目和F#项目。 F#项目中的依赖项不像意大利面条。

我在这里宣扬一个不受欢迎的职位,但是我的经验是,当您被迫考虑和管理系统各个级别的“软件组件之间的依赖顺序”时,世界上的一切都会变得更好。针对F#的特定UI /工具可能还不理想,但我认为该原则是正确的。这是您想要的负担。这是更多的工作。 “单元测试”还需要更多工作,但我们已经达成共识,认为工作“值得”,因为从长远来看,它可以节省您的时间。对于“订购”,我有同样的感觉。系统中的类和方法之间存在依赖关系。您将忽略这些依赖关系,后果自负。一个强迫您考虑此依赖关系图(大致是组件的拓扑结构)的系统可能会引导您开发具有更简洁的体系结构,更好的系统分层以及更少的不必要依赖关系的软件。

如果您喜欢我用图片解释事物的方式,请查看我的“域建模使功能”这本书是对域驱动设计,类型建模和功能编程的友好介绍。

好的,我们同意循环依赖性不好。那么,我们如何检测到它们然后将其清除呢?

让我们从检测开始。 有许多工具可帮助您检测代码中的循环依赖关系。 如果您使用的是C#,则需要使用诸如不可估量的NDepend之类的工具。 但是,如果您使用的是F#,那么您会很幸运! 您可以免费获得循环依赖项检测! 您可能会说:“非常有趣,我已经了解F#的循环依赖禁令-这真让我发疯! 我该怎么做才能解决问题并使编译器满意?” 由Disqus提供动力的博客评论