本文译自 Semantic Import Versioning, Go & Versioning 的第 3 部分, 版权@归原文所有.
如何将不兼容的更改部署到现有软件包 ? 这是任何包管理系统中的根本挑战和决断. 问题的答案决定了所产生的系统的复杂性, 它决定了如何轻松或难以使用包管理. (它还决定如何轻松或难以实现包管理, 但用户体验更重要.)
为了回答这个问题, 这篇文章首先介绍了 Go 的导入兼容性规则:
如果旧包和新包具有相同的导入路径, 新软件包必须向后兼容旧软件包.
我们从 Go 一开始就主张这个原则, 但我们没有给它一个名字或者这样一个直接的陈述.
导入兼容性规则大大简化了使用不兼容版本的软件包的体验. 当每个不同版本具有不同的导入路径时, 关于给定导入语句的预期语义没有歧义. 这使开发人员和工具更容易理解 Go 程序.
今天的开发人员希望使用语义版本来描述软件包, 因此我们将它们应用到模型中.
具体来说, 模块 my/thing
被导入成 my/thing
作为 v0, 该阶段预计会发生破坏性更改, 并且不会受到保护, 并且持续到第一个稳定的主要版本 V1.
但是当添加 v2 的时候, 我们不再重新定义现在稳定的 my/thing
的所代表的含义, 而是给它一个新的名字: my/thing/v2
.
我将这种约定称为语义导入版本控制, 这是在使用语义版本控制时遵循导入兼容性规则的结果.
一年前, 我相信把版本放入这样的导入路径是丑陋的, 不可取的, 并且可能是可以避免的. 但是在过去的一年里, 我已经开始理解它们为系统带来多少清晰和简单. 在这篇文章中, 我希望能让你了解我为什么改变主意.
一个依赖的故事
为了使讨论具体化, 请考虑以下故事. 这个故事当然是虚构的, 但它是由一个真正的问题所驱动. 当 dep 发布时, Google 编写 OAuth2 软件包的团队问我, 他们应该如何引入他们长期以来都想做的一些不兼容的改进. 我越想它, 越是意识到这不像听起来的那么容易, 至少不像没有语义导入版本的那样.
序幕
从包管理工具的角度来看, 分为代码作者和代码用户. Alice, Anna 和 Amy 是不同代码包的作者. Alice 在 Google 工作并编写 OAuth2 软件包. Amy 在微软工作并编写了 Azure 客户端库. Anna 在亚马逊工作并撰写了 AWS 客户端库. Ugo 是所有这些软件包的用户. 他正在开发最终的云应用 Unity, 并使用所有这些软件包和其他软件包.
作为作者, Alice, Anna 和 Amy 需要能够编写和发布他们软件包的新版本. 软件包的每个版本都为其每个依赖项指定了所需的版本.
作为用户, Ugo 需要能够用这些其他软件包一起构建 Unity. 他需要精确控制在特定构建中使用哪些版本; 当他选择时他需要能够更新到新版本.
当然, 我们的朋友可能期望从包管理工具中获得更多, 特别是在发现, 测试, 可移植性和有用的诊断方面, 但这些与故事无关.
随着我们的故事逐渐打开, Ugo 的 Unity 构建依赖关系看起来像这样:
章节 1
每个人都独立编写软件.
在谷歌, Alice 一直在为 OAuth2 软件包设计一个新的, 更简单, 更易于使用的 API. 它仍然可以完成旧软件包可以完成的所有工作, 但只需要一半的 API 接口.
她将其发布为 OAuth2 r2
. (这里的 r
代表修订. 目前, 修订号并不表示除顺序之外的任何内容: 特别是, 它们不是语义版本)
在微软, Amy 正处于应有的长假期中, 她的团队决定在她回来之前不做任何与 OAuth2 r2
相关的更改. Azure 包现在将继续使用 OAuth2 r1
.
在亚马逊, Anna 发现使用 OAuth2 r2
可以让她在实现 AWS r1
的过程中删除许多难看的代码, 因此她将 AWS 更改为使用 OAuth2 r2
. 她一路修复了一些错误, 并将结果发布为 AWS r2
.
Ugo 获取了有关 Azure 行为的 bug 报告, 并追踪到 Azure 客户端库的一个 bug. 在休假之前, Amy 已经在 Azure r2
中发布了该 bug 的修复程序.
Ugo 向 Unity 添加了一个测试用例, 确认它失败, 并要求包管理工具更新到 Azure r2
.
更新之后, Ugo 的构建看起来像这样:
他确认新的测试通过, 并且他所有的旧测试仍然通过. 他锁定 Azure 更新并发布更新后的 Unity.
章节 2
Amazon 大张旗鼓地推出了他们新的云服务: Amazon Zeta Functions. 为了准备发布, Anna 给 AWS 软件包添加了 Zeta 支持, 她现在将它发布为 AWS r3
.
当 Ugo 听到有关 Amazon Zeta 的消息时, 他写了一些测试程序, 并对他们工作的效果感到非常兴奋, 因此他跳过午餐更新去 Unity. 今天的更新不如最后一次.
Ugo 希望使用 Azure r2
和 AWS r3
(每个版本的最新版本)来构建包含 Zeta 支持的 Unity. 但 Azure r2
需要 OAuth2 r1
(而不是 r2 ), 而 AWS r3
需要 OAuth2 r2
(而不是 r1).
经典的菱形依赖, 对吧? Ugo 不在乎它是什么. 他只是想构建 Unity.
更糟的是, 这似乎不是任何人的错. Alice 写了一个更好的 OAuth2 包. Amy 修复了一些 Azure bug 并去度假. Anna 觉得 AWS 应该使用新的 OAuth2 (内部实现细节), 并且后来增加了 Zeta 支持. Ugo 希望 Unity 使用最新的 Azure 和 AWS 软件包. 很难说他们中的任何一个做错了什么. 如果这些人没有错, 那么也许包管理错了. 我们一直假设在 Ugo 的 Unity 构建中只能有一个版本的 OAuth2. 也许这就是问题所在: 也许包管理器应该允许在单个构建中包含不同的版本. 这个例子似乎表明它必须这样.
Ugo 仍然卡住了, 所以他搜索了 StackOverflow 并找到了包管理器的 -fmultiverse标志, 它允许多个版本, 以便他的程序构建为:
Ugo 试了. 它不起作用. 进一步深入研究这个问题, Ugo 发现 Azure 和 AWS 都在使用名为 Moauth 的流行 OAuth2 中间件库, 它简化了部分 OAuth2 处理. Moauth 不是一个完整的 API 替代品: 用户仍然直接导入 OAuth2, 但他们使用 Moauth 来简化一些 API 调用.
Moauth 参与的细节没有从 OAuth2 r1
改为 r2, 因此 Moauth r1
(唯一存在的版本)与两者兼容. Azure r2
和 AWS r3
都使用 Moauth r1
. 在仅使用 Azure 或仅使用 AWS 的程序中, 这种方式效果很好, 但 Ugo 的 Unity 构建看起来像这样:
Unity 需要两个 OAuth2 拷贝, 但 Moauth 导入哪个拷贝 ?
为了使构建工作, 我们似乎需要两个完全相同的Moauth副本: 一个导入 OAuth2 r1
供 Azure 使用, 另一个导入 OAuth2 r2
供 AWS 使用.
一个快速的 StackOverflow 搜索显示软件包管理器有一个标志(flag): -fclone
. Ugo 的程序使用这个标志来构建为:
这实际上可以工作并通过了测试, 虽然 Ugo 现在想知道是否还有更多的潜在问题. 他需要回家吃晚饭.
章节 3
Amy 已经结束度假回到了微软. 她觉得 Azure 可以继续使用 OAuth2 r1
一段时间, 但她意识到它可以帮助用户直接传递 Moauth 令牌到 Azure API.
她以向后兼容的方式将其添加到 Azure 包中, 并发布 Azure r3
. 在亚马逊这边, Anna 喜欢 Azure 包基于 Moauth 的新 API, 并向 AWS 包添加了类似的 API, 发布为 AWS r4.
Ugo 看到了这些变化, 并决定更新到最新版本的 Azure 和 AWS, 以便使用基于 Moauth 的 API. 这次卡了他一个下午. 首先他暂时更新 Azure 和 AWS 包, 而不修改 Unity. 它的程序可以构建!
令人兴奋的是, Ugo 将 Unity 改变为使用基于 Moauth 的 Azure API, 而且也可以构建. 但是, 当他将 Unity 更改为使用基于 Moauth 的 AWS API
时, 构建失败.
困惑的是, 他恢复了他的 Azure 更改, 只留下 AWS 更改, 构建成功. 他将 Azure 更改放回, 构建再次失败. Ugo 返回到 StackOverflow (继续搜索).
Ugo 了解到, 当通过 -fmultiverse -fclone
仅使用一个基于 Moauth 的 API (在本例中为 Azure) 时, Unity 隐式地构建为:
但是当他使用两个基于 Moauth 的 API 时, Unity 中的单个导入 "moauth" 是不明确的. 由于 Unity 是主要的软件包, 它不能被克隆(与 Moauth 本身相反):
一个 StackOverflow 评论建议将 Moauth 导入移动到两个不同的包中, 然后让 Unity 导入它们. Ugo 尝试了这一点, 令人难以置信的是, 它可以工作:
Ugo 按时回了家. 他对包管理并不满意, 但他现在是 StackOverflow 的忠实粉丝.
基于语义版本的复述
假设包管理使用它们而不是原始故事的 'r' 数字, 让我们挥动一把魔杖并用语义版本复述故事.
以下是一些变化:
OAuth2 r1
变为OAuth2 1.0.0
Moauth r1
变为Moauth 1.0.0
Azure r1
变为Azure 1.0.0
AWS r1
变为AWS 1.0.0
OAuth2 r2
变为OAuth2 2.0.0
(部分不兼容的 API)Azure r2
变为Azure 1.0.1
(bug 修复)AWS r2
变为AWS 1.0.1
(bug 修复, 内部使用OAuth2 2.0.0
)AWS r3
变为AWS 1.1.0
(功能更新: 添加 Zeta)Azure r3
变为Azure 1.1.0
(功能更新: 添加 Moauth API)AWS r4
变为AWS 1.2.0
(功能更新: 添加 Moauth API)
故事没有任何变化. Ugo 仍然遇到相同的构建问题, 他仍然不得不转向使用 StackOverflow 来了解构建标志(flag)和重构技术, 以保持 Unity 成功构建. 根据 semver, Ugo 应该没有任何更新的麻烦: 在故事中没有一个 Unity 导入的包改变了主要版本. 只有 OAuth2 深入 Unity 的依赖树. Unity 本身不会导入 OAuth2. 什么地方出了错 ?
这里的问题是, semver 规范实际上不仅仅是选择和比较版本字符串的方式. 它没有说别的. 特别是, 在增加主版本号后, 如何处理不兼容的更改也没有提及.
semver 最有价值的部分是鼓励在可能的情况下进行向后兼容的更改. FAQ 正确的记录到:
"不兼容的更改不应该轻微引入具有大量相关代码的软件. 升级必须承担的成本可能很大. 不得不通过增加主版本号来发布不兼容的更改意味着你将会考虑更改的影响并评估涉及的 成本/收益 率."
我当然同意 "不应该轻易引入不兼容的变化". 我认为 semver 缺乏的地方是: "不得不通过增加主版本号" 是促使你 "思考你更改的影响并评估涉及的 成本/收益 率" 的一个步骤. 恰恰相反: 读取 semver 太容易了, 因为这意味着只要你在进行不兼容的更改时递增主版本, 其他所有操作都可以解决. 这个例子表明情况并非如此.
从 Alice 的角度来看, OAuth2 API
需要向后兼容的变更, 并且当她做出这些更改时, semver 似乎承诺发布不兼容的 OAuth2 软件包会很好, 前提是她给了它 2.0.0 版本.
但是这种经过 semver 认可的改变引发了 Ugo 和 Unity 的一系列问题.
语义版本是作者向用户传达期望的重要方式, 但这就是他们的全部. 就其本身而言, 不能期望解决这些较大的构建问题. 相反, 让我们看看解决构建问题的方法. 之后, 我们可以考虑如何在这种方法中恰当的使用 semver.
导入版本控制复述
再一次, 让我们使用导入兼容性规则重新讲述故事:
在 Go 中, 如果旧包和新包具有相同的导入路径, 新软件包必须向后兼容旧软件包.
现在情节变化更加显著. 故事以同样的方式开始, 但在第 1 章中, 当 Alice 决定创建一个部分不兼容的新 OAuth2 API
时, 她不能使用 "oauth2" 作为其导入路径. 相反, 她将新版本命名为 Pocoauth, 并为其提供导入路径 "pocoauth".
面对两个不同的 OAuth2 软件包, Moe (Moauth 的作者) 必须为 Pocoauth 编写第二个软件包 Moauth, 他命名为 Pocomoauth 并给出了导入路径 "pocomoauth".
当 Anna 将 AWS 软件包更新为新的 OAuth2 API
时, 她还将该代码中的导入路径从 "oauth2" 更改为 "pocoauth", 并将 "moauth" 中的导入路径更改为 "pocomoauth". 然后, 随着 AWS r2
和 AWS r3
的发布, 故事会继续进行.
在第 2 章中, 当 Ugo 热切地采用 Amazon Zeta 时, 一切正常. 所有软件包代码中的导入都完全匹配需要构建的代码. 他不必在 StackOverflow 上查找特殊标志(flag), 而且他午餐仅仅晚了五分钟.
在第 3 章中, Amy 将基于 Moauth 的 API 添加到 Azure, 而 Anna 则将相同的基于 Pocomoauth 的 API 添加到 AWS.
当 Ugo 决定更新 Azure 和 AWS 时, 再次没有问题. 他更新的程序不需要任何特殊的重构:
在这个故事版本的末尾, Ugo 甚至都没有想到他的包管理器. 它正常工作; 他几乎没有注意到它在那里.
与故事的语义版本翻译相比, 这里使用导入版本化改变了两个关键细节. 首先, 当 Alice 介绍了她向后兼容的 OAuth2 API
时, 她不得不将其作为一个新包发布 (Pocoauth). 其次, 由于 Moe 的封装包 Moauth 在其 API 中公开了 OAuth2 包的类型定义, Alice 发布了一个新包迫使 Moe 也发布了一个新包 (Pocomoauth).
Ugo 最终的 Unity 构建进展顺利, 因为 Alice 和 Moe 的软件包拆分创建保持了 Unity 等客户端构建和运行所需的结构.
取而代之的是, Ugo 和像他一样的用户不再需要诸如 -fmultiverse -fclone
这样外来重构辅助的不完整包管理的复杂性, 导入兼容性规则将少量额外的工作推给包作者, 从而让所有用户收益.
需要为每个向后不兼容的 API 更改引入一个新名称肯定会有成本, 但正如 semver FAQ 所述, 该成本应鼓励作者更清楚地考虑这些变化的影响以及它们是否真的有必要. 而在导入版本控制的情况下, 成本会为用户带来显著的收益.
导入版本控制 (Import Versioning) 的一个优点是程序包名称和导入路径是 Go 开发人员能很好理解的概念. 如果你告诉一个包作者, 做出向后不兼容的更改需要创建具有不同导入路径的不同包, 那么 - 即便没有任何版本控制的知识 - 作者可以通过对客户端包的影响推理: 客户端 需要更改他们的导入一次; Moauth 不再适用于新的软件包, 等等.
能够更清楚地预测对用户的影响, 作者可能会对他们的变化做出不同的, 更好的决策. Alice 可能会寻求将新的更清晰的 API 与现有 API 一起引入最初的 OAuth2 包中, 以避免包拆分. Moe 可能会更仔细地考虑是否可以使用接口来使 Moauth 支持 OAuth2 和 Pocoauth, 从而避免使用新的 Pocomoauth 包. Amy 可能会认为更新到 Pocoauth 和 Pocomoauth 是值得的, 而不是暴露 Azure API 使用过时的 OAuth2 和 Moauth 包的事实. Anna 可能会尝试让 AWS API 允许 Moauth 或 Pocomoauth 使 Azure 用户更容易切换.
相比之下, semver "主版本升级 (bump)" 的含义远不是那么清晰, 并且不会对作者施加同样的设计压力. 需要清楚的是, 这种方法为作者创造了更多的工作, 但通过为用户带来显著的好处, 这项工作是合理的. 总的来说, 这种权衡是有道理的, 因为软件包的目标是拥有比作者更多的用户, 并且希望所有软件包至少拥有与作者一样多的用户.
语义导入版本控制
上一节展示了在更新期间, 导入版本如何带来简单, 可预测的构建. 但是, 在每次向后兼容的更改中选择一个新名字对用户来说都很困难并且没有任何帮助. 鉴于 OAuth2 和 Pocoauth 之间的选择, Amy 应该使用哪个 ? 没有进一步调查, 就没有办法知道.
相比之下, 语义版本化使得这很容易: OAuth2 2.0.0
显然是 OAuth2 1.0.0
的预期替代品.
我们可以使用语义版本控制, 并通过在导入路径中包含主版本来遵循导入兼容性规则. Alice 可以用新的导入路径 "oauth2/v2" 调用她新的 API OAuth2 2.0.0
, 而不需要创建一个可爱但不相关的新名称 Pocoauth.
Moe 也一样: Moauth 2.0.0
(导入为 "moauth/v2" ) 也可以成为 OAuth2 2.0.0
的辅助包, 就像 Moauth 1.0.0
是 OAuth2 1.0.0
的辅助包一样.
当 Ugo 在第 2章中添加 Zeta 支持时, 他的构建看起来像这样:
因为 "moauth" 和 "moauth/v2" 只是不同的软件包, 所以 Ugo 完全清楚他需要如何使用 Azure 的 "moauth" 以及使用 AWS 的 "moauth/v2": 导入两者.
为了兼容现有的 Go 用法, 并作为不做向后不兼容的 API 更改的小鼓励, 我在此假定主要版本 1 从导入路径中省略: import "moauth", 而不是 "moauth/v1". 同样, 主要版本 0 明确拒绝兼容性, 也从导入路径中省略. 这里的想法是, 通过使用 v0 依赖关系, 用户明确承认破坏的可能性并在选择更新时承担处理它的责任. (当然, 更新不会自动发生是很重要的. 我们将在下一篇文章中看到如何最小化版本选择对此有所帮助.)
功能性名称和不可变的含义
二十年前, Rob Pike 和我正在修改 Plan 9 C 库的内部结构, Rob 教会了我这样一个经验法则, 当你改变一个函数的行为时, 你也改变它的名字. 旧的名字有一个含义. 通过对新名称使用不同的含义并删除旧名称, 我们确保编译器会大声抱怨需要检查和更新的每段代码, 而不是静静地编译不正确的代码. 如果人们有他们自己的程序在使用这个函数, 他们会得到编译时失败, 而不是长时间的调试会话. 在当今分布式版本控制的世界中, 最后一个问题被放大了, 这使得名称变得更加重要. 并发编写的旧代码所期望的旧的语义不应该在合并的时候被新的语义替代.
当然, 删除旧函数只有在可以找到所有用到它的地方, 或者当用户了解他们有责任跟上变化时才会起作用, 例如在 Plan 9
等研究系统中. 对于导出的 API, 通常会更好地保留旧名称和旧行为, 并只添加新行为的新名称.
Rich Hickey 在 2016 年的 "Spec-ulation" 演讲中提到了这一点, 即只添加新名称和行为, 不删除旧名称或重新定义其含义, 正是函数式编程鼓励的个体变量或数据结构.
功能性方法在小规模编程中带来了明确性和可预测性方面的好处, 并且在应用时效益更大, 就像导入兼容性规则一样, 对于整个 API: 依赖地狱实际上只是可变性而已. 这只是演讲中的一个小点; 整个演讲值得一看.
在 "go get" 的早期, 当人们询问有关做出向后不兼容的变化时, 我们的回应是 - 基于多年来对这些软件变化的经验得出的直觉 - 是给出导入版本规则, 但没有明确的解释, 为什么这种方法比不把主版本放在导入路径中更好. Go 1.2 添加了一个关于软件包版本控制的 FAQ 条目, 它提供了基本的建议 (Go 1.10 也是):
打算供公众使用的软件包应该尽量保持向后兼容性. Go 1 兼容性准则在这里是一个很好的参考: 不要删除导出的名称, 鼓励标记的复合字面量等等. 如果需要不同的功能, 请添加新名称而不是更改旧名称. 如果需要完全破坏兼容性, 请使用新的导入路径创建新的程序包.
这篇博文的一个动机是用一个清晰可信的例子来展示为什么遵循规则是如此重要.
避免单例 (Singleton) 问题
对语义导入版本管理方法的一个普遍反对意见是, 包作者今天预计在给定的构建中只有一个包的副本. 在不同主要版本中允许多个包可能会由于单例的意外重复而导致问题.
一个例子是注册一个 HTTP handler. 如果 my/thing
为 /debug/my/thing
注册了一个 HTTP handler, 那么拥有该程序包的两个副本将导致重复的注册, 这在注册时会引起恐慌.
另一个问题是如果程序中有两个 HTTP 堆栈. 显然, 只有一个 HTTP 堆栈可以在端口 80 上监听; 我们不希望一半的程序注册不会被使用的 handlers. Go 开发者已经由于 vendored 包出现这样的问题.
迁移到 vgo 和语义导入版本可以澄清并简化当前的情况. 通过取代 vendoring 导致的不可控的重复问题, 包作者将保证每个主版本的软件包只有一个实例.
通过将主要版本包含在导入路径中, 包作者应该更清楚 my/thing
和 my/thing/v2
是不同的并且需要能够共存. 或许这意味着在 /debug/my/thing/v2
上导出 v2 调试信息. 或者这也许意味着协调.
也许 v2 可以负责注册 handler, 但也可以为 v1 提供一个钩子(hook)来提供信息以显示在页面上. 这意味着 my/thing
导入 my/thing/v2
或反之亦然; 具有不同的导入路径, 这很容易做到并且易于理解.
相反, 如果 v1 和v2 都是 my/thing
, 那么很难理解导入自己的导入路径并获取其他导入路径的含义.
自动 API 更新
允许程序包的 v1 和 v2 共存于一个大型程序中的关键原因之一是可以一次升级该程序包的客户端, 并且仍然具有可构建的结果. 这是逐步修复代码的更一般问题的具体实例. (请参阅我的 2016 年文章 "Codebase Refactoring (with help from Go)," 了解更多关于该问题的信息.)
除了保持程序的构建外, 语义导入版本控制对逐步修复代码有很大的好处, 我在前面的章节中提到过: 代码包的一个主版本可以导入并用另一个版本编写. 将 v2 API 编写为 v1 实现的封装很简单, 反之亦然. 这让他们能够共享代码, 并且通过适当的设计选择和使用类型别名, 甚至可以允许使用 v1 和 v2 的客户端进行互操作. 它还可以帮助解决定义自动 API 更新中的关键技术问题.
在 Go 1 之前, 我们严重依赖于 go fix
, 在更新到新的 Go 版本后, 用户运行它并找到不再编译的程序. 更新编译不过的代码使得我们无法使用大多数我们的程序分析工具, 这些工具要求其输入是有效的程序.
另外, 我们想知道如何允许 Go 标准库之外的包的作者提供特定于其自己的 API 更新的 "修复". 在单个程序中命名和处理多个不兼容版本的软件包的能力提示了一种可能的解决方案: 如果 v1 API 函数可以作为 v2 API 的包装器来实现, 则包装器实现是修订规范的两倍.
例如, 假设 API 的 v1 函数具有 EnableFoo 和 DisableFoo 函数, v2 用一个 SetFoo(enabled bool)
替换该函数对. v2 发布后, v1 可以作为 v2 的包装实现:
package p // v1
import v2 "p/v2"
func EnableFoo() {
//go:fix
v2.SetFoo(true)
}
func DisableFoo() {
//go:fix
v2.SetFoo(false)
}
特别的 //go:fix
注释会指示去修正后面的包装体应该被内联到调用中. 然后运行 go fix
将重写调用 v1 EnableFoo
为 v2 SetFoo(true)
. 重写很容易指定和类型检查, 因为它是简单的 Go 代码.
更好的是, 重写显然是安全的: v1 EnableFoo
已经在调用 v2 SetFoo(true)
, 所以重写调用显然不会改变程序的含义.
合理的做法可能是使用符号执行来修复反向 API 更改, 从使用 SetFoo 的 v1 到使用 EnableFoo 和 DisableFoo 的 v2. v1 SetFoo
实现可以读取:
package q // v1
import v2 "q/v2"
func SetFoo(enabled bool) {
if enabled {
//go:fix
v2.EnableFoo()
} else {
//go:fix
v2.DisableFoo()
}
}
然后 go fix
会更新 SetFoo(true)
为 EnableFoo()
, SetFoo(false)
为 DisableFoo()
. 这种修补程序甚至会应用于单个主要版本中的 API 更新.
例如, v1 可能会被弃用(但保留) SetFoo, 并引入 EnableFoo 和 DisableFoo. 同样的修复将帮助调用者摆脱已弃用的 API.
要清楚的是, 今天这还没有实现, 但它看起来很有前景, 而且通过赋予不同的东西以不同的名称, 使得这种工具成为可能. 这些示例演示了将持久的, 不可变的名称附加到特定代码行为的能力. 我们只需遵循这样的规则: 当你改变某些东西时, 你也改变它的名字。
致力于兼容性
语义导入版本控制对包的作者来说更有用. 他们不能只是决定发布 v2, 远离 v1, 并留下像 Ugo 这样的用户来应对这种后果. 但那些这么做的包作者正在伤害用户. 在我看来, 如果系统难以伤害用户, 并且自然而然地包作者也不会作出伤害用户的行为, 那么这似乎是件好事.
更一般地说, Sam Boyer 在 GopherCon 2017 上谈到了软件包管理如何调节我们的社交互动, 以及人们构建软件的协作. 我们可以决定. 我们是否希望在一个围绕系统创建的社区中工作, 该系统可以优化兼容性, 平滑过渡以及一起很好的工作 ? 或者我们是否希望在一个围绕系统创建的社区中工作, 该系统可以优化创建和描述不兼容性, 这使得作者破坏用户程序也可以接受 ? 导入版本控制, 特别是通过将语义主版本提升到导入路径来处理语义版本控制, 就是我们如何确保在第一种社区中工作.
让我们致力于兼容性.