让弃用警告真正生效

在弃用数月后,可设置为每百万次调用返回一次错误结果。这种频率大概不会触发任何人的午夜寻呼机警报,却能明确警示:依赖已弃用的功能是潜伏在代码中的缺陷。

 💬 157 条评论 |  编程 | 

塞斯·拉尔森发现人们往往无视弃用警告urllib库中的response.getheader方法自2023年起已被弃用,因为应改用response.headers字典。当该方法最终被移除时,大量代码因此失效。

弃用警告旨在解决向后不兼容的API变更带来的陡峭过渡问题,让开发者能分阶段规划维护工作,而非被迫一次性承担全部负担。但问题在于延迟处理的经济成本难以量化。开发者完全可以无视弃用警告直至API变更生效,而此时再推迟处理将付出高昂代价。

人类并不擅长应对突发变更。

元素周期表

若刻意让弃用函数偶尔返回错误结果呢?每次故意返回错误结果时,系统都会记录弃用警告。(对结果准确性要求极高的用户,或许会选择用人为延迟替代错误结果。)

初期应完全避免返回错误结果。但在弃用数月后,可设置为每百万次调用返回一次错误结果。这种频率大概不会触发任何人的午夜寻呼机警报,却能明确警示:依赖已弃用的功能是潜伏在代码中的缺陷。

再过数月,将错误频率提升至每万次调用一次。此时推迟维护可能开始带来些许痛楚。一年后,改为每千次调用返回一次错误结果。此时用户只能在非关键辅助场景延迟维护。最后临近截止期限时,每两次调用就返回错误结果——此时该功能已近乎废弃,如同被移除时一般。

这种机制能让API的弃用部分在移除前持续累积缺陷,并迫使用户更直接地权衡维护时机的经济成本。

若讽刺意味尚不明确:与其强行修复缺陷,不如保留瑕疵。但必须承认,就推动系统变革的有效性而言,警示标识和警告机制在分级列表中垫底。当它们失效时,我们不应感到意外。

本文文字及图片出自 Deprecate Like You Mean It

共有 157 条讨论

  1. 请反其道而行之。让所有弃用警告至少持续十年,只需在警告中注明该功能不再维护即可。

    但更重要的是,务必竭力避免破坏向后兼容性。若能通过其他方式实现相同功能,只需在后台将弃用函数改为使用新函数即可。

    我对静态类型化趋势最大的质疑在于:它让开发者误以为破坏向后兼容性是理所当然,而实际上维持兼容性往往轻而易举。

    编辑:并非避免破坏向后兼容性总是轻而易举,但多数情况下确实如此。

    1. > 只需在警告中注明该功能不再维护即可。

      我确信这在实践中不可行。无论你多少次声明某功能不再维护,一旦它影响到[更重要|业务关键]团队,维护工作就会立刻恢复。

      1. 若贵公司需要支持,可随时联系我洽谈服务合同。

        真正重要的功能他们会付费。很多时候你会发现问题并不关键,对方反而乐于自行解决。

        1. 你似乎在设想开源场景,而你回复的评论讨论的是企业内部依赖关系。

          1. 我认为企业内部代码的弃用是完全不同的问题。代码要么有商业价值,要么没有。若某项功能被弃用而下游项目仍需使用,该项目理应有预算支持(或绕过弃用功能进行开发)。

            从许多方面看,这个决策反而更简单——它应当基于商业用例或预算考量。

            1. 商业案例本身并不复杂,真正的困境在于协调不同团队达成共识:谁应承担支持责任?为何该案例比其他团队优先级更高?以及预算分配比例如何确定。团队规模小到人人相识时问题较轻,规模大到彼此漠视时则更棘手——即便由他们支持该方案比其他团队轻松十倍,他们仍对你的业务场景漠不关心。

              1. 哦。但这根本不是问题。库用户只需复制弃用前的代码,塞进不再维护的代码库里就完事了。问题解决。/s

      2. > 我确信这在实践中不可行。

        我不认同。某些编程语言已开始支持过时标记机制,该机制能在下游依赖项中触发自定义警告信息。这些仅需一行代码的标记不会改变任何功能。任何需要标记过时内容的人,都拥有底层实现机制。

      3. 我不明白这为何/如何构成问题?生活中其他事情不也如此处理吗?

        更何况,许多领域我们已主动决定不再使用某些技术,并强烈建议人们不要触碰曾使用过它们的旧系统。石棉就是典型案例——从建筑中清除石棉成本高昂且极具危险性。

      4. 这还会持续拖慢开发进度——即使获得全局编译通过,你仍需更新那些面临破坏性API变更的“已弃用”函数。

    2. > 我对静态类型化趋势最大的质疑在于:它让开发者误以为破坏向后兼容性是正当行为,而实际上维持兼容性本是轻而易举的事。

      我不明白你这里建立的关联。

      1. 声明:我强烈支持静态类型化。

        我完全理解这种关联。静态类型化的优势之一在于它能让大量重构变得轻而易举(或远比其他方式更简单)。任何操作变得更简单都会产生副作用:人们更倾向于贸然行动,而不会深思后果。在缺乏其他制约机制的情况下,将简单重构转化为意外的破坏性变更,这并不令人意外。

        更甚者,他们可能基于“对我而言重构很简单,下游适配也该简单”的逻辑,有意识地这么做。我甚至承认自己也曾用同样的逻辑做出过类似判断。当然,当破坏性变更影响到我日常不接触的开发者时,我会谨慎得多。但相较于许多同行,我对这种全面影响的警惕性仍显不足。

      2. 静态类型设计恰恰能保障向后兼容性,不是吗?

        因为它明确规定了接口必须满足的契约。没有契约?就无法强制兼容性。

        1. 这是向其他开发者宣告代码变更的方式,总比毫无说明要好。

          但这似乎让库开发者更敢于做出破坏性变更。他们似乎在想“反正文档里写着呢,用户更新时遇到类型检查器/代码检查器的报错,直接改代码就行了”。而我认为他们应该思考的是“我该怎么做才能让这次更新尽可能安静无声又轻松省事”。

          当然,我们各有目标,能免费使用这么多优质库我心怀感激。只是不得不花时间修改代码来迎合他人代码的美学价值,实在令人困扰。

          1. 但静态类型也能帮助库开发者更容易识别破坏性变更。没有静态类型时,你可能认为某个修改无伤大雅,却意外破坏了用户代码——因为用户依赖了你未预料到的特性。诚然静态类型无法完全解决问题,行为变更仍可能导致破坏,但它确实是识别特定破坏性变更类别的有效工具。

          2. 我唯一见过这种情况是在JS库中——每次大版本迭代几乎相当于重写整个库,如今这些库往往只是Rust实现的封装层。

            这并非JS独有。我认为问题根源在于语义化版本管理的强制执行,仿佛代码版本必须由一串“有意义的数字”构成。

        2. 若使用具备完整类型系统的语言,确实如此。但谁会用那种语言?

          在现实世界中人们实际使用的语言里,情况并非如此。考虑一个简单示例:契约要求返回1到10之间的整数值。在多数实际使用的语言中,你只能使用整数类型,其限制仅在于定义的位数——这可能被利用来返回11,而调用方对此毫无察觉。确实存在少数支持将数值类型限制在有限范围内的常用语言,但只要需求稍复杂些,它们立刻就会崩溃。

          这正是测试存在的意义。测试既能通过验证明确契约条款,又能为API使用者提供最佳使用示例,助其理解设计意图。

          1. 这更暴露了你对现代语言和类型系统的认知不足。

            以TypeScript为例,作为全球最广泛使用的语言之一,其拥有极其强大的类型系统,可用于建模大量不变量。通过“构造即正确”和“品牌化”等模式,你能够携带类型级别的证据——例如证明某个数字处于特定范围,或确认传递的字符串确实是UserId而非任意随机字符串。

            是否能刻意破坏这些保证?当然可以。但这无关紧要,正如any类型能破坏类型系统保证也无关紧要。实际应用中,类型验证发生在边界处,内部逻辑可完全依赖这些保证。有人能主动破坏保证的事实,在实践中并不重要。

            1. TypeScript是少数支持整数约束的实际使用语言之一,尽管实现方式相当笨拙:

                  type Decade = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
              

              但若要定义符合RFC规范的邮箱地址类型…

              某些具备完善类型系统的语言能定义完整契约,但TypeScript不在此列。缺乏此功能意味着你尚未真正定义出可用的契约(就本讨论而言)。你必须依赖测试来定义契约(例如断言“生成邮箱”函数的结果符合RFC规范)。

              而TypeScript确实如此。测试是TypeScript应用的核心。它或许拥有实际使用语言中最先进的类型系统之一,但该类型系统仍远未完善到能充当契约。正因如此,其生态系统中充斥着各类测试框架,以协助通过测试定义契约。

      3. 我能理解某些维护者受静态类型化鼓舞而引入破坏性变更——只要这些变更能确保编译时报错。但这仅适用于无动态链接的语言。真正促成这种行为的,是静态类型化与静态链接的结合。

      4. 确实如此。C#是静态类型语言,Python则不是。哪一种语言才有着臭名昭著的向后不兼容历史?

        1. .NET框架迁移至.NET Core时就引发了大量兼容性问题。Java 6到8的升级同样如此,而Java 11引入新模块系统时又造成了另一波破坏。即便在静态类型的Python中,你仍需修复字符串类型替换、核心API重命名等改动带来的问题。

          1. 得了吧,那些都是极其罕见且微不足道的小问题,很容易就能修复。

              1. 我们已将大量项目从.NET8和.NET9迁移至.NET10,几乎未出现任何问题。

    3. 我本想将其包装成环保议题。你总以“出于某些原因”为由改动东西。若在热门工具/库中这么做,你可能刚烧掉相当于一片小树林的能源——因为人们不得不投入大量精力处理这些改动。数十万用户因你制造的额外工作,错过了与孩子或爱人共进晚餐的时光。本该准时发布的产品延误了。你期待的功能未能实现,只因十万人被你制造的破坏牵扯精力。

    4. 静态类型 ≠ 强关系维护。

      虽然我很高兴看到Python和JavaScript(如TypeScript)引入类型系统,但更常见的是人们滥用这些类型的问题。

      99%的情况下,人们仅将变量定义为“string”,若无法覆盖则用“any”(或Map/Object等)。

      而这些变量多半是枚举键值或依赖库常量。每当看到region: stringstage: string这类定义,我内心就死掉一点。这些类型本应声明为region: Regionstage: Stage——其中“Region”和“Stage”应是具备明确值/变量/选项的规范枚举或接口。这能实现编译(或构建)时验证检查,防止问题蔓延至生产环境(甚至完全避免运行时错误)…

      1. 哇。何必移除呢?这不过是字典的最外层封装,既然字典已成为公开API的一部分,这些方法无需修改就能永久生效。

    5. 凡事皆有度。微软在转向Windows XP前就陷入可维护性危机——多年为迎合客户需求而不断调整API(短期确实留住了客户),最终导致API接口变得难以维护。当互联网时代来临,这些开放且缺乏监管的API瞬间成为蠕虫攻击的潜在载体,系统在压力下彻底崩溃。

      1. 实际上,我认为向后兼容性才是Windows至今仍占据主导地位的原因。它并非任何领域的最佳操作系统,甚至对用户充满敌意,但若你需要运行Windows应用程序…数十年的Windows应用积累几乎都能正常运行。

    6. 我始终厌恶“位腐蚀”这个概念。仿佛软件会随时间自然失效并丧失向后兼容性,如同某种自然法则。

      这绝非自然法则。位腐蚀的本质是:众多开发者刻意通过微小改动反复破坏向后兼容性。1995年编写的软件本应至今仍能运行。这是数字世界。它不会腐烂崩解,更不会自然退化。如今无法运行,完全源于平台和库维护者刻意作出的决策。就像楼主所言——废弃功能就该彻底废弃。这本就是选择!

      若要解决位腐问题,我们必须停止这种选择。

      1. 进步往往需要打破旧物才能实现。这不是选择题,而是唯一出路。我们必须为更美好的未来优化方案,即便这偶尔会让当下稍显艰难。

        这正是开源项目常远胜企业克隆版的关键原因:它们真正践行迭代与创新,而美国企业早已遗忘这种能力。

        1. 某些弃用是进步的必要代价,但更多弃用实属多余,而这些才最令人沮丧。

          例如Ruby将File.exists?弃用并改为File.exist?,只因多数人认为带s的版本语法不合逻辑(我虽不认同此观点,但这与我的论点无关)。

          很长一段时间以来,你总会收到警告说exists?已被弃用,应该替换为exist?……但为什么?为什么不能让exists?继续作为exist?的别名存在?这毫无成本,两个函数除了一个字母外完全相同。虽然修改起来微不足道,却徒增了无谓的烦恼。

          不过幸运的是,在 Ruby 中我可以自己创建 exists? 的别名,但为什么要让我这么做?!仅仅因为觉得小写 s 不对就移除存在已久的方法,这有什么意义?

          1. 假设我是 Ruby 新手。我想查文件是否存在,或者获取文件对象的可用方法列表。现在我必须同时研究exist?exists?,才能确定该选哪个。

            现在把这种情况乘以…所有历史API的演变,就会发现对新手而言,学习新事物变得极其困难。

            又名Common Lisp。

            1. 无论选择哪种写法都无妨,除了多出的s之外方法实现完全相同。根本不存在选错的情况。

              这类选择在Ruby中非常常见,相同功能可采用不同风格:代码块可用{}或do和end实现,条件语句既能写成“a if b”也能写成“if b then a”。方法调用可带括号也可不带。

              这些都是Ruby的风格选择,而我们已有像RuboCop这样的格式化工具,能强制特定项目遵循特定风格。

            2. 我并非在为Ruby的决策辩护,我明确表示自己不会这么做。这让我想起Python移除disttools的决定——若由我做主,绝不会采取如此激进的手段。如今多个发行版已通过补丁将其恢复。

              但这并非我能决定的事。这不是我的项目。我使用的是他人创建的项目,归根结底这是他们的决定,因为他们拥有所有权。除非无法绕开,否则我认为必须尊重这个决定,或者另寻替代方案。

              顺便说一句,你完全可以维护一个基于Ruby的补丁来添加别名,然后在自己的机器上运行。这应该相当简单,虽然肯定不如前述的sed命令那么简洁…

              1. 当然这是他们的决定,他们没有义务向我解释。

                但这里是讨论论坛,我只是希望认同该决定的人说明理由。重申:若不愿回应,他们完全不必回答。我只是说“若有支持此类变更的论据,我非常乐意听取”

                说他们无需解释理由确实没错,但这与我们的讨论无关。我问的不是他们,而是HN的读者。

          2. 你的示例只需一次sed调用就能修复整个代码库。诚然,若由我决定,我可能也会保留别名,但对最终用户的困扰确实微乎其微。..

            你得以免费使用开源项目,众多开发者持续维护这些项目,而你同样免费享受其成果。作为回报,有时需要修改依赖这些项目的代码——这能让维护者更轻松。

            个人认为这是非常合理的权衡。

            1. 第三种选择是使用不会自行崩溃的工具。Go语言的诸多乐趣之一在于:我能让个人项目闲置数年,重启时一切依然正常运行

            2. > 你举的例子只需在整个代码库执行一次sed命令就能修复

              我也得祈祷所有依赖库都这么做了。

              但我的核心疑问是:为何如此?为何非要我来做?

              1. 因为他们想这么做,这是他们的项目。成熟的标志之一就是接受并非所有影响你的决策都能认同。

                你本可以站出来反对,其他人同样如此。你做了吗?

                1. 好吧,我觉得你是在反驳我没说过的话。

                  我从未质疑他们修改的权利,也从未要求他们对我个人负有义务。我甚至没那么生气。Ruby依然是我最爱的语言,我通常认为他们的决策是正确的。

                  我只是对这个决定感到恼火。

                  没错,当初提案时我确实反对过(许多人也持反对意见)。显然他们并未被说服。

                  重申一次:我并非要求他们被迫满足我的意愿,也未指责他们做出此改动时存在阴暗或恶意。我根本不寻求任何补救措施。

                  我只是想说我不赞成这种改动方式(仅仅因为某些人觉得新名称在语法上更合理就更改方法名,却完全不改变方法本身)。我之所以评论,是因为这并非“为进步必须弃用旧法”的情形。当前方法并未造成任何限制,语法并不繁琐(且不会改变),维持该方法也未阻碍其他功能实现。这纯粹是“我读作’此文件是否存在?‘而非’此文件存在吗?’”的语义差异。

                  重申一次,他们当然有权反对我的观点——事实上他们确实反对。我只是主张:不应仅因个人偏好更简洁的语法形式就破坏现有语法结构。我希望持反对意见者(即你)能解释这种变更的必要性。

            3. 你这次的论点与先前相比已大幅偏移。最初你声称“进步往往需要打破旧规…这是唯一出路”。但cortesoft提供的Ruby示例并非进步的必要条件。即使取消弃用声明(甚至完全不做修改),该语言照样能正常运行。用“但这只是微不足道的改动”作为回应毫无意义——因为你最初辩护的依据并非“容易实现”,而是“必要性”。

              1. > 用“但这只是微不足道的改动”作为回应毫无意义

                当然相关。相较于本帖其他例子,这简直是可笑的微不足道。

        2. 这完全取决于选择。所有软件都能在保持向后兼容的同时实现进步。虽非易事,但绝非不可能。

          1. 但总会伴随性能代价,这在硬件成本极高的项目中是不可接受的(咳咳思科)。

          2. > 所有软件都能在保持向后兼容的同时实现功能升级

            这是个极其无知的论断。只需在glibc中运行“git log”,很快就能证明你错了。

            1. glibc不是几十年都保持着ABI兼容性吗?

              1. 过去二十年间确实存在大量破坏构建的变更,这些变更通常基于充分理由,且仅影响小众使用场景。

                1. 那么现有二进制文件还能继续运行吗?

                  1. 若追溯到存在nss问题的年代,未必能保证 🙂

      2. 天啊,要让我现在用1995年的编程方式写代码,那简直是噩梦。

        当然,现代协程确实让我怀念起当年支持协作式多任务的日子…

    7. 这难道不是在主张维持高维护成本并让设计停滞不前吗?

  2. > 这或许不会触发任何人的午夜寻呼机警报,但能明确警示:依赖已弃用的功能就是代码中潜伏的漏洞。

    你怎么知道?这纯属无稽之谈。这种想法太糟糕了。我以为众所周知的是:难以复现、看似随机的漏洞比编译器错误更难定位修复。

    若你准备好破坏API就直接破坏,别跟我玩花样。若更多人能及时移除弃用API,人们才会真正重视这个问题。

    1. 文章最后一段:

      > 若讽刺意味不够明确,保留这些瑕疵反而更好。但必须承认,就推动系统变革的有效性而言,警示标识和警告信息在分级列表中垫底。它们失效时我们不该感到意外。

      1. 确实让我恍然大悟。读到* * *时才恍然。这怪我 🙂

        1. 最后那段是在你评论后才添加的,因为作者意识到讽刺手法太隐晦,多数人没能领会。

      2. 多数HN访客不会读到最后一段,强调这一点很有必要。

        1. 我还以为自己读到了呢 🙂 原以为文末的三个* * *是在提示我即将开始阅读下一篇文章的推荐。所以对我来说绝对是个“嗖”的恍然大悟时刻 😀

    2. 是的,我同意——这种间歇性故障可能极其难以追踪,而且绝对会破坏人们对持续集成系统的信心——不稳定的测试绝对是最糟糕的测试类型。

  3. 无论废弃理由为何,这都不是好主意。

    若功能不再维护,应添加弃用警告并任其自然失效。修改已弃用的功能仅表明你可能有能力维护却不愿维护。

    若想强力推动用户迁移新版本,应制定清晰的开发路线图,并在弃用窗口期结束时强制抛出硬性错误——这样既能预先明确功能有效期,也能据此完善代码文档。

    这种模棱两可的半残废状态对谁都没好处

  4. 让已弃用的API随机返回错误简直是糟糕透顶的主意!

    更合理的方式是明确告知废弃功能/函数的移除时间表(未来版本及具体日期),同时若语言支持,应标记为废弃(如C++的[[deprecated]]属性),确保开发者即使未阅读发布说明也能通过编译警告获知。

    1. 没错。我承认自己面对硬性截止日期时比看到“未来某日”的提示更积极行动。也见过某些工具对弃用警告大做文章——跨行重复提示简直吵死人。请勿对每个实例重复记录相同警告。若需引起注意可使用颜色或表情符号,但一次提示已足够。当开发者无法即时处理时,在CI日志中排查其他问题却要筛选这些冗余噪音,实在令人烦躁。建议添加链接说明具体弃用细节及迁移方案。

    2. 我认为这并非严肃建议,文章结尾已标注为讽刺。

      但确实,这绝对是最糟糕的主意。

    3. 随机破坏不可取,但预定破坏窗口期值得尝试。

  5. 我找到的解决方案是:将API调用设为硬性错误,同时提供一个临时且名称惹人厌的变量作为绕行方案。

        WORKAROUND_URLLIB3_HEADER_DEPRECATION_THIS_IS_A_TEMPORARY_FIX_CHANGE_YOUR_CODE=1 python3 ...
    

    这种方案很显眼,能让急需代码立即运行的开发者找到退路,而当你最终处理弃用问题时,即使有人抱怨也站不住脚。

    当然可以先用警告层层铺垫,但最终要么采用此法,要么直接移除代码(或永远保留代码并承受由此带来的负担)。

    1. 我听闻谷歌某团队曾采用此法,他们将变量命名为:

        I_ACKNOWLEDGE_THAT_THIS_CODE_WILL_PERMANENTLY_BREAK_ON_2022_09_20_WITHOUT_SUPPORT_FROM_TEAM_X=1
      

      在截止日期一年前。若能添加

        _AND_MY_USER_ID_IS="<user_id>"
      
    2. 超赞的创意,某家FAANG公司的工具已实现此功能(全公司8万+软件工程师都在用)。我早已习惯在日志和终端看到这些内容,甚至大脑会自动屏蔽它们——就像鼻子对气味的无感一样。

  6. 如何应对顽固用户?我所在的机构正终止多个长期运行的API支持。所谓终止,就是关闭服务器,强制迁移至全新平台。

    我们已发布行业公告、更新文档并邮件通知所有用户。问题在于联系方式已失效——最初注册配置密钥的开发者早已离职。该服务在生产环境稳定运行多年且始终保持向后兼容性。

    难道直接关闭服务?我们已在响应中添加提示信息,但只要返回200ok状态码,就意味着没人会关注这些提示。我们讨论过实施限流策略:在明确告知故障原因的前提下,让所有请求失败一小时。

    是否有更优方案?我无法想象故意随机返回错误数据的做法,这简直荒谬至极。

    1. > 面对顽固用户该如何应对?

      保持服务器运行,但让那些顽固用户承担成本并额外支付。这其实是常见策略。大型迟缓的企业常在功能淘汰上遇到困难,但它们财力雄厚,乐意支付溢价以确保API至少在短期内保持稳定。

      若你索要费用,很可能也会引发更多反应。

    2. 第一步:停止将他们视为“顽固分子”。他们并非顽固。他们(很可能花钱)购买了产品,自然期望产品能持续运行至所需时限!他们不该承受供应商突然抽走地毯、破坏服务——仅仅因为API陈旧不堪,而软件工程师们厌倦了维护它。

      与其说“认真对待弃用声明”,这篇文章更该强调的是:“认真对待软件发布”——我的意思是:务必严肃对待。务必确保API设计万无一失,因为用户实际使用的周期往往远超你的预期。

      1. > 他们(很可能花钱)购买了产品,自然期望该产品能持续运行至所需时限!

        这取决于合同条款。通常服务终止条款会在许可协议中规定。若许可条款在相关司法管辖区合法有效,则不存在超出条款范围的根本性道德义务。某些特殊情况下严格遵循条款可能不妥,但这种例外也有其边界。

        1. 合同条款通常界定法律义务而非道德义务。它们规定当事人必须履行的责任,违者将承担法律后果——但不涉及当事人应尽的道德责任。

          1. 遵守法律义务是道德的重要组成部分。法律的宗旨之一正是减轻个人在复杂道德考量中的负担。这在法治民主国家尤为普遍。

            当然存在例外情况及对具体法规的争议。但只要法律站在你这边,就强烈暗示你的行为在道德层面基本无碍。当双方就某事项达成共识且协议条款毫无法律瑕疵时,很难断言其中一方在道德上严重失当。

            1. 我认为现代世界多数系统性恶行皆以合法手段实施。约翰迪尔压榨农民、科技巨头贩卖用户数据、亚马逊制造消费主义恶性竞争、联合健康保险随意拒保等行径,皆属合法却毫无道德可言。

            2. 尽管我持完全相反观点,仍欣赏你的见解!

      2. 万物皆无永恒。当你决定采用新的第三方API时,必须明白它可能在生产环境上线后一小时就消失——这很正常。

        1. 更要明白:有些API会突然抽走地毯,有些则不会。若你提供的是前者,在其他条件相同的情况下,用户终将流向竞争对手。

      3. 软件会随时间演进,商业需求亦然。曾经看似(甚至确实是!)好主意的方案,未来某刻必然失效。只要理由充分且管理得当,打破API完全没问题。

      4. 我同意,应尽可能完善API设计以避免废弃。

        但完美本就不现实。若未预先规划应对失误的方案,等于对必然发生之事毫无准备。

    3. 每月呈指数级增长的Sleep()调用似乎是个好方案。当API延迟达到10秒时,希望用户会开始质疑。若仍无动静,我认为实施限流是个不错的选择。

    4. > 我们讨论过实施服务降级方案:在明确错误提示下让所有请求失败一小时。

      这听起来是最佳选择。用户已习惯服务可能中断的情况,因此发生故障时他们会主动查看错误信息。

    5. 我在项目中采用过一种技巧:修改URL后,让旧地址返回426状态码,同时附上说明、新链接及API迁移的明确日期。这种方式能可靠地中断客户端的API调用,迫使他们重视变更,同时提供便捷的临时解决方案。

      虽然客户当时不满,但最终都完成了升级。最后升级的客户甚至付费要求我们在截止日期后继续开放API——他们延迟了9个月才升级,但支付了27万美元,这笔交易倒也无甚可抱怨。

      1. 那么你是否在返回426状态码时提供了正确数据?

        1. 不——目标是让API失效以引起用户注意,同时提供简单的修复方案。许多用户甚至不会检查HTTP状态码,因此必须停止返回数据才能确保API调用中断。

          我们确实提前一个月在测试环境部署了该变更,让测试环境用户能在正式环境上线前发现问题。但不出所料,那些过去一年无视警告的用户根本没用测试环境(即便用了,也直到破坏性变更上线后才发邮件告知我们)。

  7. 不要让它返回不同的结果。

    若实在需要,可在函数内部添加延迟:首次发布延迟1毫秒,第二次发布延迟2毫秒,依此类推。但或许更应改进工具链,确保弃用提示真正可见。

    1. 我理解初衷但这是糟糕的主意,和那篇文章一样

      若用户需依赖你的接口,就必须确保所有接口都可依赖

      总有人会忽略弃用通知,你唯一能做的就是提供可靠的预期信息——若他们屏蔽警告并遗忘,那是他们的事

      但故意制造问题却不表明是故意为之,只会让所有人(包括你自己的团队)感到沮丧并增加工作量

      你无法强迫他人更新代码,试图激怒他们反而会削弱产品信任度。即便舆论站在你这边,这种做法也达不到预期效果

      做好全面准备,真诚通知用户,然后应对必然出现的质疑——总会有漏网之鱼错过更新通知

    2. 这实际上是解决非技术性问题的合理技术方案——正是这类问题导致废弃代码被长期忽视

      让性能呈指数级下降(1毫秒、2毫秒、4毫秒、8毫秒…)必然催生“业务需求”,且不会直接破坏关键功能。若无性能退化,从商业角度看根本没有理由移除废弃代码。

      1. 应该让编译过程变慢而非运行时变慢。这样痛苦就由开发者承受,他们才能去修复。

    3. 有时是工具链问题,但更常见的是“少数团队具备消除所有工具链警告的纪律性”问题。

  8. 我理解废弃/移除功能的必要性。

    但故意以难以发现且令人恼火的方式破坏用户运行环境?作者还好吗?这简直像疯子所为。

  9. 若核心Python库如urllib、NumPy等能采用SemVer规范将大有裨益。移除函数属于破坏性变更。本文根源正是urllib在次要版本中破坏了兼容性。请废除这种伪SemVer机制——先废弃函数再在次要版本中破坏兼容性。或许之后开发者才能期待:只要不升级主版本号,代码就能长期稳定运行。

    1. 这只会鼓励人们继续使用陈旧、无人维护且存在安全隐患的旧版库。当他们仍在使用2.1.1版本时,若你维护的版本已更新至5.7.3,而有人在2.1版本中发现了重大安全漏洞,他们就会来向你抱怨要求发布2.1.2版本。

      未受维护的代码通常不适合使用,这是铁律。

      1. 库维护者无权监管他人如何使用其开源代码,这是基本原则。维护者也没有义务回溯移植安全修复。任何其他要求都实质上违背开源理念。

        值得注意的是,即便实施监管也无法消除抱怨。抱怨只会转向TFA所指出的问题。你只是转移了抱怨的焦点。

        这种做法也无法真正迫使用户升级。相反,用户可以直接将版本号限制在导致包损坏的小版本上。与其制造用户敌意,为何不让用户的工作更轻松?

        正确遵循SemVer规范能有效抑制不必要的破坏性变更,这对用户及软件包的长期健康都大有裨益。若不愿回溯安全修复,用户可选择付费、自行修复或停止使用该库。

      2. 此时可向用户提供支持合约,为已终止支持的版本开发更新

    2. Python本身就在次要版本间进行破坏性变更,生态系统跟进实属自然。

      我个人尚未遇到实际问题,但这种趋势令人忧虑。

      1. 尽管众人将3.0版本与2.3升级关联,Python从未宣称采用半版本号规范。

        1. 这恰恰说明问题由来已久,而非新近产生。

          它确实采用主版本号.次版本号.错误修复版本号的命名规则,但从未明确说明何时会出现破坏性变更。

          随着3.x版本的发布节奏加快,这个问题变得更加突出。

    3. Numpy确实遵循半版本号规范。

      仍有许多人对2.0版本提出抱怨。

        1. 他们是否在其他版本中对ABI结构进行过破坏性变更?

          1. 我不确定这与当前讨论有何关联。您是否想暗示NumPy实际上遵循SemVer规范,尽管其文档明确声明不采用?

    4. > 废除这种伪SemVer机制——先废弃函数,再在次要版本中破坏兼容性。

      我同意,但认为这背后存在更深层的文化根源。这是社区毒性文化的产物。

      Python 2到3的迁移是规范进行的,采用了真正的SemVer规范,并配备了完善的迁移工具。作为Python开发者,我曾有数年约25%的工作时间用于项目从2到3的迁移。每个项目耗时不超过两周(实际工作量不足40小时),多数仅需一天。

      遗憾的是,Python团队因此承受了大量仇恨(包括威胁)。作为自然反应,他们似乎患上了某种创伤后应激障碍,自3.0版本起便试图将破坏性变更分散引入,而非集中在4.0版本发布。

      我完全理解他们——虽然对Python用户体验确实更糟,但对开发者而言,让仇恨和威胁以可控速度涌入或许更有利。我认为解决方案在于:我们这些理解破坏性变更必要性的人,应该全力支持真正的版本号规范实践,用支持来平衡仇恨。

      2023年我曾服务过一个仍使用2.7.x版本的客户。当我在其代码中发现几个重大安全漏洞,并告知若不升级Python、Django及其他包,我基于职业道德无法继续维护其产品时,对方拒绝续签合同。据我所知,他们至今仍在使用2.7.x版本。:shrug:

      1. 这话有道理。我认为部分不满源于许多变更并未明显优化周边代码。最明显的例子就是将print从语句改为函数。这让语言稍显简洁,却破坏了现有代码,实际收益甚微。更隐蔽的是字符串与字节串的兼容性破坏。虽然这是必要且有益的变更,却可能引发隐蔽的静默故障。

        至少对我而言,真正的阻碍在于广泛的包支持问题。

        维护者应审慎评估变更是否会给用户带来大量后续工作。若用户察觉维护者未考虑此点,必然会感到愤怒。

  10. 不。破坏他人代码正是导致软件世界混乱的根源——新漏洞(包括安全漏洞)层出不穷,因为没有软件能摆脱每月重写的命运。我们需要建立这样的文化:在已发布的API中破坏向后兼容性几乎不可想象,绝非任何人愿被发现的行为。

  11. 弃用警告本质上就是警告——用户有权选择不采取行动。因此用户选择两种可用场景之一(即无所作为)并不令人意外。

    真正关注代码质量、追求最小化故障频率的资深用户会重视警告。他们不仅关注默认触发的警告,更借助额外工具获取更全面的警告信息,并切实跟进处理。

    弃用警告正是为这类人服务的。

    至于其他人,谁在乎呢?“我们多年来都慷慨地告知过你们这功能即将移除”。

    那些既无视兼容性警告,又采用未在项目配置文件中固定依赖版本的工作流——导致始终获取最新依赖项的人,是在主动选择给自己制造破坏。

  12. > 若我们故意让已弃用的函数偶尔返回错误结果呢?每次故意返回错误时,系统就会记录弃用警告。1

    假设我们发现高速公路立交桥的建筑材料存在缺陷,为促使人们使用更优质的材料,我们不时让混凝土块坠落地面,撞死过往司机?

    正因为我们认真对待弃用机制,人们会比预期更早更换那座立交桥。你以后会感谢我的。

    该帖开篇引用的https://sethmlarson.dev/deprecations-via-warnings-dont-work-…中写道:

    > 该API在下载量排名前三的Python包中持续发出警告长达三年之久,敦促库开发者和用户停止使用该API,但这仍不足以阻止问题发生。我们仍收到用户反馈称此次移除出乎意料,导致依赖库出现故障。

    完全在预料之中。

    即便许多看到弃用日志并刻意做出决策的人,也未曾料到你们会真的破坏API。

    > 最终我们不得不恢复API并紧急发布版本修复问题。

    完全在预料之中。

    省去不必要的痛苦,切勿随意破坏API。尽可能将其视为一种承诺。

    若确实影响持续开发,可参照链接文章建议采用SemVer规范和多版本策略。(弃用分支仅进行最小维护:或许仅修复特定漏洞,或仅处理关键安全问题,甚至对这些也设终止期限,并在完全停止支持时对整个库发布弃用警告。)

  13. 各类项目宣称功能已弃用却不公布移除时间表,或不断推迟移除(甚至明确表示永不移除,仅维持弃用状态),正是此类问题的根源。

    我认为任何弃用操作都应遵循以下步骤:

    1. 决定弃用该功能。此步骤还需包含迁移方案、替代方案及必要时的行为保留方案。同时需确定整体时间线,从决策开始直至最终移除。

    2. 使代码对弃用项发出强烈警告。若采用标准构建系统,应支持弃用警告功能。

    3. 以易于修复的方式破坏构建。若推荐步骤存在过多繁文缛节,则保留旧API,仅添加deprecated标记或路径。关键在于:此时修复构建无需修改依赖项或进行(重大)代码变更,仅需一行代码即可实现。

    4. 移除已废弃的组件。此步骤不可省略!必须实际删除。保留其在编译器/库等中的存在以触发错误,但需彻底清除。此时修复构建需定制代码或额外依赖,不再是简单操作(至少不应像前一步那样简单)。

    坦白说,构建系统本应提供相应工具。它应当支持:- 标记某项为弃用并触发警告- 设置标志后才允许访问弃用项- 移除项时显示“函数foo()已在v1.4.5版本中移除,详见链接…”而非简单报错“未找到函数foo()”

    若构建系统支持将警告视为错误,则应同时提供忽略特定警告的选项(以便在持续集成环境持续触发警告时仍能进行包更新)。警告本身不应被忽略。

  14. 我以为业界已通过将破坏性变更打包到主版本更新中解决了这个问题。

    V 1.0 – 引入 foo
    V 1.1 – foo 弃用,推荐使用 bar
    V 2.0 – foo 移除,仅保留 bar

    用户可无限期停留在 1.x 版本,即使该版本不再更新。开发工作持续推进至 2.x,最终进入 3.x 等版本。用户仅在手动执行主要版本升级时才会遭遇破坏性变更。

    1. 原文末句新增内容:

      > 若讽刺意味尚不明确,保留瑕疵或许更妥。但需认清:就推动系统变革的有效性而言,警示标识与警告提示在分级列表中垫底。它们失效时我们不应感到意外。

      看评论反应,大家似乎都无动于衷。

  15. 立意虽好,方案却有误。

    理想流程如下:

    1: 发出警告,若可行则设定截止期限(“将在下个主要版本中移除”)
    2: 经过X时间后,在截止期限前将其转化为编译器或代码检查器错误,但保留代码
    3: 通过相关渠道发布末日降临的正式通知
    4: 末日降临

    若执行得当,可将开发者对警告的认知从“警告?笑死!”转变为“警告是严肃的事”——这种认知在许多场景下都缺失

  16. 我有两个更优方案:

    1. 对弃用API收费。效仿Oracle在Java 8中的做法。

    – 或 –

    2. 在同一段代码路径中添加基于时间戳的RuntimeException。若日期大于(>)弃用日期,则对100%的请求始终抛出RuntimeException。

    没错,这会触发人员值班,但问题也会立即得到修复!

  17. 我负责维护某客户的一套软件系统。

    构建时会触发二三十条弃用警告。

    整个软件栈依赖于五年前就停止更新的软件包集群。

    该软件虽不面向终端用户,但构建时使用NPM而非预编译包。

    要消除这些警告,需用全新套件重写大量代码。

    但因系统运行正常,此事未被列为优先事项。

    从工匠视角看,这套软件存在诸多缺陷。

    客户只要功能正常,乐于忽略警告。

    修复陈旧系统既无收益又耗费精力。

    那些在包里添加弃用警告的人,其激励机制与包使用者并不一致。双方的时间线和动机截然不同。

  18. 我曾有过同样“聪明”的构想:设置弃用警告,并借助C宏在版本x发布时自动关闭警告,在此之前警告声会越来越响。

    假期归来那天,我刚搞砸了一个重大上线项目——版本号恰好超过了x。项目延期,下次机会要等几周。团队气炸了。

    没错,他们本可以修复警告。但因此导致上线失败,代价实在太高了。

    1. 客户是否能合理预见版本x的发布时间?

      他们难道不能回滚吗?

  19. 请别这么做。若要移除功能,就直接移除,即使有人抱怨也要坚持。别让用户逐渐陷入困境。

  20. 文章底部新增注释:

    若讽刺意味不够明确:保留缺陷反而更好。

    1. 哇,这注释最初没写吗?这解释了许多评论的缘由。

  21. (尽管这是个玩笑)或许是我多心,但我读出这是大型企业内部的解决方案——通过将痛苦/责任/技术债务分散到时间轴上,转嫁给可能人员流动率高的团队。即通过在代码中埋下定时炸弹(通过指标惩罚团队),在炸弹实际引爆前调整激励机制。

  22. 工作时千万别这么干。这叫企业破坏行为,会给你惹上大麻烦。而且本身就是个糟糕的主意。弃用功能的初衷是让他人自主选择切换时机。若不想维护就直接移除,这才是更体面的做法。别把他人逼入调试地狱,这会让你沦为恶人。

  23. 我认为弃用不该伴随如此敌意的信号,但若真要实施,就该保持一致性:在后续版本中逐步升级警告强度,对弃用路径施加性能退化(至少在弃用警告发布后一个版本开始),而非直接导致错误结果。

    废弃警告中应明确说明这些措施。

    (你既不想破坏系统,又希望让关心系统的人能主动调查,快速定位问题根源并理解解决方案。)

  24. 这是个糟糕的主意。但基于相同思路,有个次优方案:要求用户通过配置主动启用已弃用功能,同时设定明确的过期时间表(过期时触发硬性错误而非随机数据错误),迫使用户重新启用。此举虽造成不便但避免了随机性故障,更类似处理过期证书的方式——最终用户仍需寻求永久解决方案。

  25. > urllib中的response.getheader方法自2023年起已弃用,因为应改用response.headers字典。

    那么response.getheader本就该实现该功能。除非性能至关重要,否则没有理由向用户暴露底层实现。

  26. 当然,偶尔返回错误结果是不负责任的,但能否让弃用的API逐步更缓慢地淘汰?

  27. > urllib中的response.getheader方法自2023年起已弃用……当该方法最终移除时,大量代码因此失效。

    对于一个广泛使用的项目而言,两年支持周期在我看来并不算长,除非你提供长期支持版本供需要更高稳定性的用户使用,或者你明确声明API支持周期不超过两年。当然,API支持周期少于两年也是可以接受的,特别是对于免费项目而言。但就个人而言,我会从一开始就明确说明(事实上我对某些公开项目就是这么做的: “此为个人项目,可能随时变更或消失,若在依赖稳定性的工作流程中使用,风险自负”)。或者我的要求是否有些苛刻?

    若采用半版本号规范(semver)或类似机制,在重大版本发布时打破API兼容性是合理的——这正是重大版本的意义所在。不过最好不要立即终止对前一重大版本的所有支持。

    > 倘若我们故意让已弃用的函数偶尔返回错误结果呢?

    绝对不行。彻底断开连接才是上策。让整个API沦为一堆未定义(或模糊未定义)行为的集合,本质上是种恶行。这将导致所有依赖你项目的其他项目,同样沦为模糊定义行为的集合体。若你的API并非一成不变,请明确告知——除非用户主动要求保持稳定(此处“要求保持稳定”意指“愿意为此支付支持费用”),否则他们无权抱怨。

    > 对结果正确性极为敏感的用户…

    换言之:任何具备基本判断力的用户。

    > 或许更愿意用人为延迟替代错误结果。

    这种方案显然更易接受。起初设置极短延迟,直至最终弃用时逐步延长。具体时长需视具体场景而定,且难以精准判断:最短时长需足够长,让关注更新并测试的开发者能察觉依赖更新与项目发布之间的差异;但又不能过长,以免快速更新发布(例如为修复已影响项目的漏洞)时导致系统完全崩溃。

    在我看来这依然是种恶劣做法,但相对而言危害较小。我仍倾向彻底中断支持,不过我属于会关注弃用通知的类型——除非你是隐藏的嵌套依赖项,否则不会受影响。

  28. 这种做法荒谬至极,完全无视世间所有实践中“弃用”的常规操作。我们还会安装老式电线管吗?当然不会。但世上是否仍有人维护这类系统?当然存在。

    这是否意味着人们不该放弃旧有做法?并非如此。但每个人的优先级不同。诚然,我们可能认为“吱吱作响的轮子政策”是糟糕的主意,但坦白说这恰恰是最普遍的做法。

    因此,请别刻意要求他人将你的优先级视为普世标准。

  29. 在弃用版本中通过函数延缓过时方法的执行速度,使其耗时延长至约一天。

    随后移除该函数。

  30. « 当该方法最终被移除时,大量代码崩溃了。 »

    提前移除,不过意味着大量代码会更早崩溃罢了…

  31. > urllib中的response.getheader方法自2023年起已弃用,因为应改用response.headers字典。

    谁在乎?这简直是微不足道的例子——保留一个执行字典查找的占位方法根本不存在维护成本。我理解某些场景下维护成本可能很高,但拿这种例子说事实在令人难以信服。

    对新奇的痴迷,以及对不符合你追新观念的事物加以污名化,实在愚不可及。*你能相信它早在2023年就被弃用,居然还有人用?*2023年可不是什么遥远历史的黑暗时代。

  32. 唉,我实在难以表达对这种做法的厌恶。

    软件开发者本就身处无数技术栈的漩涡中,刻意以“淘汰旧技术”为名破坏向后兼容性,完全不必成为他们必须应对的难题。求求你们别当那种平台或库——先废弃功能再直接移除,逼得我不得不翻出2005年写的旧代码,只为迁移到新API才能维持运行。

  33. 不如直接随机触发自杀进程。这比随机返回错误安全多了。

  34. 哇,别告诉我你发布过重要/高使用率的软件却从未告知过这种事。

    我本以为这会建议采用“一版废弃、次版移除”的交替废弃周期,但这个点子绝对属于“entropicthoughts.com”的专属领域。

  35. 当然——毕竟我每年都要重写所有代码!

  36. 文笔精湛,但作者有触犯波尔定律的风险,尤其当读者未读完全文时。

    作者的观点是,他们提出的“谦卑建议”(及其姊妹方案——在解决弃用API组件时故意设置延迟)是个糟糕的主意。相反,作者认为“进行API变更本身”就是在回避核心问题:“这个API究竟服务于谁?”或许,如果旧系统永远不被弃用,其实也未尝不可

    1. > 尤其当读者未读完全文时。

      该注释是在文章发布后才添加的。

  37. 归档分类:试图用自身领域工具解决其他领域问题(这几乎总是糟糕的主意)

  38. 考虑采用延迟机制?每次版本更新时,该功能的弃用状态会使程序增加几毫秒的阻塞时间。这种方式破坏性更小,能实现渐进式过渡,产生真实但非破坏性的后果。

  39. 这简直是疯了。合理时间后直接移除弃用功能就行。

  40. 另一方案是让弃用警告的语气越来越吓人。

    1. “弃用警告:我真的会杀了你”

      1. “继续使用此功能是你的道德败坏,你该感到羞耻,你父母也该羞愧,耻辱将如影随形。”

        1. “若不迁移功能,我就告你妈”

        2. 抱歉,我才不怕这个。我照用不误。

  41. 代码改动是恶性负价值行为。在确定发布标准合约前,务必理性审慎设计。对外API的改动应仅用于解决核心关键问题,而非伪维护的无谓争论。更别拿“这是业余爱好”当借口。

    简而言之:别再无谓地改动破坏东西了。

  42. 天啊,这主意太糟糕了。光凭这篇帖子我就不会雇佣这个人。

    我对代码的要求是:a) 正常运行;b) 若无法实现,则要以可预测且明显的方式失败。

    返回错误结果不符合上述任何要求。它既未能如楼主所愿引起对弃用警告的关注,反而会引发神秘且不可预测的错误——这简直是调试中最棘手的类型。认为这种做法能奏效的想法,本身就令人质疑作者的判断力。究竟为何要故意在代码库中引入最难调试的错误类型?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

你也许感兴趣的: