
2.4 管理包的依赖关系
Julia的生态系统拥有丰富的开源包。当以单一目标设计包时,可以更轻松地重用它们。但是,使用大型代码库并非易事,因为它更可能依赖于第三方包。为了避免依赖关系,开发人员需要花费大量时间和精力来维护和管理这些依赖关系。
重要的是要理解依赖关系不仅存在于包之间,而且还存在于特定版本的包之间。幸运的是,Julia语言对语义版本控制提供了强大的支持,可以帮助解决许多问题。
在本节中,我们将介绍以下几个主题:
·理解语义化版本控制方案
·指定Julia包的依赖关系
·避免循环依赖
现在让我们看一下语义化版本方案。
2.4.1 理解语义版本控制方案
语义版本控制(https://semver.org/)是由著名的GitHub联合创始人兼CTO Tom Preston-Werner开发的一种方案。语义版本控制有一个非常明确的目的,即提供版本号更改的含义(即语义)。
当我们使用第三方包并对其进行升级时,我们如何知道我们的应用程序是否需要更新?如果仅升级相关包而不对自己的应用程序进行任何测试,我们将承担什么样的风险?
在进行语义版本控制之前基本都是靠猜的。但是,勤奋且规避风险的开发人员至少会检查相关包的发行说明,尝试找出是否存在任何重大更改,然后采取适当的措施。
在这里,我们将快速总结语义版本控制的工作原理。版本号是由以下组件构成:

你也可以在版本号后跟一个发行标签和一个内部版本号,如下所示:

版本号的每个部分都表示一个含义:
·主要发行版本号(major)更改时,表示此版本中引入了与先前版本不兼容的重大更改。由于现有功能可能会中断,因此应用程序合并新版本的风险很大。
·次要发行版本号(minor)更改时,表示此发行版中有不间断的增强功能。合并新版本的应用程序具有适度的风险,因为至少在理论上,以前的功能应继续工作。
·补丁发行版本号(patch)更改时,表示此发行版中有不间断的错误修复。应用程序合并新版本的风险很小。
·预发行标签(pre-release)存在时,表示预发行候选(例如alpha,beta)或发行候选(RC)。该发行版被认为是不稳定的,应用程序永远不应在生产环境中使用它。
·构建(build)标签被认为是可以忽略的元信息。
需要注意的是,仅当所有包都正确使用语义版本控制时,它才有用。语义版本控制就像包开发人员可以用来在发行新版本时轻松表示其更改影响的一种通用语言。
Julia包生态系统鼓励语义版本控制。接下来,我们将研究Julia包管理器Pkg如何使用语义版本控制处理依赖关系。
尽管Julia鼓励语义版本控制,但许多开源包仍具有1.0之前的版本号,即使它们对于生产使用而言可能相当稳定。主要版本号为0表示特殊含义——基本上意味着每个新发行版都有所突破。
随着Julia语言的成熟,更多的包作者将其包标记为1.0,并且随着时间的推移,有关包兼容性的情况将越来越好。
2.4.2 指定Julia包的依赖关系
我们可以通过检查源文件中的using或import关键字来判断一个包何时依赖于另一个包。但是,Julia运行时环境旨在通过跟踪依赖关系来变得更加明确。此类信息存储在包目录中的Project.toml文件中。此外,在同一目录下Manifest.toml文件包含有关完整的依赖关系树的更多信息。这些文件以TOML文件格式写入。尽管手动编辑这些文件很容易,但是Pkg包管理器的命令行接口(CLI)可以更轻松地管理依赖关系。
要添加新的依赖包时只需执行以下步骤:
1)启动Julia REPL。
2)按]键进入Pkg模式。
3)使用activate命令激活项目环境。
4)使用add命令添加依赖包。
例如,将SaferIntegers包添加到Calculator包中。

让我们首先检查Project.toml文件的内容,如以下屏幕截图所示。看起来很有趣的散列码88634af6-177f-5301-88b8-7819386cfa38代表SaferIntegers包的唯一标识符(UUID)。请注意,即使从前面的输出中我们知道已经安装了版本2.5.0,也没有为SaferIntegers包指定版本号。

Manifest.toml文件包含包的完整依赖关系树。首先,我们找到以下有关SaferIntegers依赖关系的部分。

请注意,SaferIntegers包在manifest文件中有指定的版本:2.5.0,为什么呢?这是因为manifest旨在捕获所有直接依赖包和间接依赖包的确切版本信息。第二个观察结果是,正式捆绑的包(如Serialization、Sockets和Test)没有版本号:

这些包没有版本号,因为它们始终与Julia二进制文件一起发行。它们的实际版本在很大程度上取决于特定的Julia版本。
重要的一点是要认识到Project.toml和Manifest.toml都不包含任何版本兼容性信息,即使我们知道安装了2.5.0版的SaferInteger。要指定兼容性约束,我们可以使用语义版本控制方案手动编辑Project.toml文件。例如,如果我们知道Calculator与SaferIntegers1.1.1版本及更高版本兼容,则可以将此需求添加到Project.toml文件的[compat]部分,如下所示:

此兼容性设置为Julia包管理器提供了必要的信息,以确保至少安装了SaferIntegers版本1.1.1才能使用Calculator包。由于包管理器对语义版本控制敏感,因此上述设置意味着Calculator可以使用从1.1.1到最新的1.x.y版本(最高为2.0)的所有SaferIntegers版本。用数学符号表示,兼容版本的范围为[1.1.1,2.0.0),其中不包括2.0.0。
现在,如果对SaferIntegers进行改进并且决定发行2.0.0,该怎么办?好吧,因为主要版本号已经从1升级到2,所以我们必须期待重大的更改。如果我们不执行任何操作,则永远不会在Calculator环境中安装最新版本2.0.0,因为我们专门实现了2.0.0的互斥上限。
假设经过全面的检查和测试,我们得出的结论是,Calculator不受SaferInte-gers 2.0.0的任何重大更改的影响。在这种情况下,我们可以对Project.toml文件进行一些小的更改,如下所示:

上述代码指定两个兼容版本范围的并集:
·1.1.1规范表明该包与SaferIntegers版本[1.1.1,2.0.0]兼容。
·2.0规范表明该包与SaferIntegers版本[2.0.0,3.0.0]兼容
这样的信息很重要。如果Calculator包的环境由固定到SaferIntegers版本1.1.1的人使用,则我们知道Calculator在该环境中仍然兼容,并且可以在其中加载。
包管理器实际上非常灵活,它实现了更多的版本区分符格式。你可以参考Pkg参考手册以获取更多信息(https://julialang.github.io/Pkg.jl/v1/compatibility/#Version-specifier-format-1)。
指定包之间的兼容性很重要。通过使用Pkg接口并手动编辑Project.toml文件,我们可以正确地管理依赖关系,并且包管理器将帮助我们按工作顺序维护工作环境。
但有些时候,我们可能会遇到棘手的依赖问题,例如循环依赖。接下来,我们将研究如何处理这种情况。
2.4.3 避免循环依赖
循环依赖是有问题的。为了了解其原因,来看下面的例子。
假设我们有五个包(A、B、C、D和E),它们具有以下依赖关系:
·A依赖B和C
·C依赖D和E
·E依赖A
为了用图形化的方式说明这些依赖关系,我们可以创建一个图表,在其中可以使用箭头符号指示组件之间的依赖关系(如图2-4所示)。箭头的方向指示依赖关系的方向。

图 2-4
问题是什么
显然存在一个循环,因为A依赖C,C依赖E,E依赖A。这样的循环有什么问题?假设必须在包C中进行更改,该更改应该是向后兼容的。为了通过此更改正确地测试系统,我们必须确保C依从其依赖关系而继续具有适当的功能。现在,如果我们在依赖关系链中进行追溯,则必须使用D和E测试C,并且由于E依赖A,因此我们也必须包含A。现在包括了A,我们必须包括B和C。由于循环的原因,我们现在必须测试所有包!
怎么解决它
非循环依赖原则指出,包之间的依赖关系必须是有向非循环图(DAG),也就是说,依赖关系图必须没有循环。如果我们在图中确实看到一个循环,则表明存在设计问题。
遇到此类问题时,我们必须重构代码,以便将特定的依赖函数移到单独的包中。在上面的例子中,假定包A中有一些代码,这些代码由包内部使用,也由包E使用。这种依赖关系基本上是E->A。
我们可以获取此代码并将其移至新的包F。更改之后,包A和E都将依赖包F,从而有效地消除循环依赖如图2-5所示。

图 2-5
重构之后,当我们对C进行更改时,我们可以仅使用其依赖项(仅D、E和F)测试该包。包A和B都可以排除。
在本节中,我们学习了如何利用语义版本控制来清楚地传达包新版本的影响。我们可以使用Project.toml文件来指定当前包与其依赖包的兼容性。我们还回顾了一种解决循环依赖的技术。
现在知道了这些,我们将研究如何在Julia中设计和开发数据类型。