“猴子补丁”真的那么糟糕吗?

29

一些像 Ruby 和 JavaScript 这样的语言具有开放类(open classes)的特性,这使得你可以修改甚至是核心类中如数字、字符串、数组等的接口。 很显然这样做可能会让熟悉 API 的其他人感到困惑,但如果你只是添加接口而不改变现有行为,那么是否有充分的理由避免这样做呢?

例如,为没有实现 ECMAScript 第五版的 Web 浏览器添加Array.map 实现可能很好(如果你不需要所有的 jQuery)。或者你的 Ruby 数组可能受益于使用“inject”方法的“sum”便捷方法。只要更改仅限于你的系统(例如不是发布用于分发的软件包的一部分),是否存在不利用此语言特性的充分理由呢?


2
顺便说一句,在 Ruby 2.0 中有一个提案,称为“细化”,可以缓解与猴子补丁相关的陷阱。只需搜索“ruby refinements”,就会出现几篇文章。 - Mladen Jablanović
6个回答

32
猴子补丁(Monkey-patching)和其他编程工具一样,可用于善良或邪恶之用。问题在于,在整体上,这种工具倾向于被用在哪里。在我使用Ruby的经验中,它往往会偏向于“邪恶”的一面。
那么什么是猴子补丁的“邪恶”用途呢?总的来说,猴子补丁让你的代码容易产生冲突,而这些冲突可能无法被诊断。例如,我有一个类A,还有一个猴子补丁模块MB,它可以让A包含method1、method2和method3。另外还有一个猴子补丁模块MC,它也可以让A包含method2、method3和method4。现在我的问题是:我调用instance_of_A.method2时,哪个方法会被调用?答案可能取决于很多因素:
1. 我导入猴子补丁模块的顺序是什么? 2. 补丁是立即应用还是在某种条件下应用? 3. 啊啊啊啊啊啊啊!蜘蛛正在从我的内部吃掉我的眼球!
好吧,#3可能有点过度了......
总之,这就是猴子补丁的问题:可能会产生可怕的冲突问题。考虑到支持猴子补丁的语言通常具有高度动态性,你已经面临着很多潜在的“鬼打墙”问题,而猴子补丁只会加剧这些问题。
如果你是一个负责任的开发者,那么拥有猴子补丁是不错的。不幸的是,在我看来,它往往被误用了。有人看到猴子补丁,就会说:“太好了!我可以直接使用猴子补丁,而不必考虑其他更合适的机制。” 这种情况大致相当于在编写Lisp代码库时,人们在思考将其作为函数进行处理之前,首先就开始使用宏。

我更好奇的是它什么时候可以被用于好处。硬币的邪恶面是显而易见的,但是什么时候它实际上比更改程序来接受你正在修补的任何东西作为参数更有用呢?我的意思是,我们有这些架构模式,似乎像猴子补丁这样的东西只是为了颠覆它们而存在。 - weberc2
首先,除了纯函数式语言之外,大多数语言都是命令式的,因此每种编程语言在某种形式上都存在“远程操作”,尽管在不需要时不应使用它。Monkey patching 对于测试或模拟行为非常有用。它们可以在工厂/类装饰器/元类中进行本地化,从另一个对象创建一个修补过的新类/对象,以帮助处理跨越所有方法的“横切关注点”,例如日志记录/记忆化/缓存/数据库/持久性/单位转换。 - aoeu256
1
@weberc2 你好!我知道这已经是七年后的事了,但这里有一个使用猴子补丁的用例:修改第三方库!也许“好”不是正确的词。但是,如果你绝对必须使用该库,并且它不能直接做到你想要的功能,那么这是一种可行的选择。 - Charles Wood

23

维基百科简短概述了猴子补丁的陷阱:

http://en.wikipedia.org/wiki/Monkey_patch#Pitfalls

万物皆有其时,也适用于猴子补丁。经验丰富的开发者掌握许多技术,并学会何时使用它们。很少有技术本身是“邪恶”的,只是它的不考虑使用方式。


1
嗯,这是那种“如果每个人都这样做”的事情,就像在高速公路上乱扔垃圾一样。 - Alexander Mills
有经验的开发人员掌握了许多技巧,并且知道何时使用它们。我不确定是否有“猴子补丁”的时间和地点。例如,将可变的东西作为参数传递比使用猴子补丁更好,这时候猴子补丁会更好吗? - weberc2
@weberc2 不错的猴子补丁示例是第三方库函数的基准测试。添加一个在函数执行前启动并在执行后结束的计时器,为什么不呢? - S Panfilov
除非我误解了你的意思,否则我不认为有任何理由进行补丁。只需在调用函数之前启动计时器,然后在调用之后停止它即可。也许您有一些更微妙的用例? - weberc2
@weberc2 当然可以。我曾经使用猴子补丁来修补第三方套接字客户端。这些客户端使用回调函数,但我希望它能够与承诺一起工作。就像 client.success().error() 而不是 client(callback)。我需要这样做是因为我想以与项目中使用的 rest 客户端相同的方式使用这些套接字客户端。除了所有好的文章之外:http://me.dt.in.th/page/JavaScript-override/ - S Panfilov
显示剩余2条评论

6
关于Javascript:

除非你是添加界面而不是改变现有行为,否则是否有充分的理由避免使用它?

是的。最坏的情况是,即使您不修改 现有 行为,也可能损坏语言的 未来 语法。
这正是 Array.prototype.flattenArray.prototype.contains 所发生的事情。简而言之,这些方法的规范被编写出来,它们的提案到达了 第三阶段,然后浏览器开始发布它们。但在两种情况下,发现有古老的库将内置的 Array 对象与其自己的方法 使用相同的名称 进行了修补,并且具有不同的行为;结果导致网站崩溃,浏览器不得不退出新方法的实现,并且必须编辑规范。(方法被重新命名。)
如果您在自己的浏览器、自己的电脑上改变内置对象(如Array),那么这是可以的,特别是对于用户脚本来说非常有用。但是,如果您在公共网站上改变内置对象,那就不太好了——这可能最终会导致像上面提到的问题一样的麻烦。如果您恰好控制着一个大型网站(比如stackoverflow.com),并且改变了内置对象,那么几乎可以保证浏览器将拒绝实现破坏您网站的新功能/方法(因为那么该浏览器的用户将无法使用您的网站,并且他们更有可能迁移到其他浏览器)。(请参见此处,了解规范编写者和浏览器制造商之间这种交互的解释)
总之,关于您问题中的具体例子:
例如,向那些没有实现ECMAScript第5版的Web浏览器添加一个Array.map实现可能是不错的。

这是一种非常常见和可靠的技术,称为polyfill

一个 polyfill 是一段代码,用于在不支持该功能的 web 浏览器上实现该功能。通常,它指的是一种 JavaScript 库,用于在旧版浏览器上实现 HTML5 网页标准,可以是某些浏览器支持的已建立标准,也可以是现有浏览器上未受任何浏览器支持的拟议标准

例如,如果您为 Array.prototype.map(或者以更新的例子,为 Array.prototype.flatMap)编写了一个 polyfill,它与官方阶段4规范完全一致,然后在没有这个功能的浏览器上运行定义了 Array.prototype.flatMap 的代码:

if (!Array.prototype.flatMap) {
  Array.prototype.flatMap = function(...
    // ...
  }
}

如果您的实现是正确的,那么这是完全可以的,并且在整个网络上非常普遍,以便过时的浏览器可以理解较新的方法。 polyfill.io 是这种情况下常见的服务。

所以...人们在过去违背最佳实践的方式就是我不小心写成.contains()然后要花一点时间意识到在JS中应该用.includes()。真是太好了。 - DexieTheSheep

4
只要更改是隔离在你的系统中(例如,不是发布用于分发的软件包的一部分),就没有理由不利用这个语言功能。作为一个孤独的开发者解决问题时,扩展或修改本机对象没有问题。对于更大的项目,这是一个团队选择应该做出的决定。个人而言,我不喜欢在javascript中更改本机对象,但这是一种常见的做法,也是一种有效的选择。如果你要编写一个库或代码,供他人使用,我会强烈建议避免这样做。然而,允许用户设置配置标志以便覆盖本机对象与方便方法之间的选择,是一种有效的设计选择,因为它们非常方便。下面是一个JavaScript特定的陷阱示例:
Array.protoype.map = function map() { ... };

var a = [2];
for (var k in a) {
    console.log(a[k]);
} 
// 2, function map() { ... }

通过使用ES5,您可以向对象注入非枚举属性,从而避免此问题。

这主要是一个高级设计选择,并且每个人都需要意识到并同意这一点。


使用数组表达式的 for...in - user422039
2
@user422039 是的,这是一个典型的陷阱。在数组上调用for in是否是良好的实践是另一个问题。 - Raynos
3
不,这里使用属性枚举来遍历数组是主要问题。 - user422039
@user422039,这只是一个例子,说明当您使用猴子补丁时,代码可能会出现问题。个人认为,使用属性枚举遍历“Object”是有效的,而“Array”就是一种“Object”。通用函数不应该对“Object”的子类型进行保护。 - Raynos

4
使用"猴子补丁"来解决已知问题是完全合理的,如果等待修复程序的话,它将是替代方案。这意味着暂时承担修复某个东西的责任,直到您能够部署“适当”的,正式发布的修复程序。
Gilad Bracha 对 Monkey Patching 的看法如下:http://gbracha.blogspot.com/2008/03/monkey-patching.html

2
您所描述的条件 -- 添加(而不是更改)现有行为,并且不将您的代码发布到外部世界 -- 看起来相对安全。但是,如果 Ruby、JavaScript 或 Rails 的下一个版本更改了它们的 API,就可能出现问题。例如,如果 jQuery 的某个未来版本检查 Array.map 是否已定义,并假设它是 EMCA5Script 版本的 map,而实际上它是您的 monkey-patch,那会怎样呢?
同样,如果您在 Ruby 中定义了“sum”,有一天您决定要在 Rails 中使用那段 Ruby 代码,或者将 Active Support gem 添加到您的项目中。Active Support 也定义了一个 sum 方法(在 Enumerable 上),因此会发生冲突。

1
如果未来的jQuery版本检查Array.map是否已定义,并假定它是EMCA5Script版本的map,那么它就做出了不合理的假设,应当自食其果。 - ChrisJJ

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接