“猴子补丁”真的那么糟糕吗? [关闭]

Posted

技术标签:

【中文标题】“猴子补丁”真的那么糟糕吗? [关闭]【英文标题】:Is "monkey patching" really that bad? [closed] 【发布时间】:2011-08-10 03:46:21 【问题描述】:

Ruby 和 javascript 等一些语言具有开放类,允许您修改数字、字符串、数组等核心类的接口。显然,这样做可能会使熟悉 API 的其他人感到困惑,但是否有充分的理由否则避免它,假设您要添加到界面而不更改现有行为?

例如,将Array.map 实现添加到不实现 ECMAScript 第 5 版的 Web 浏览器可能会很好(如果您不需要所有 jQuery)。或者您的 Ruby 数组可能会受益于使用“inject”的“sum”便捷方法。只要更改与您的系统隔离(例如,不是您为分发而发布的软件包的一部分),是否有充分的理由不利用此语言功能?

【问题讨论】:

FWIW,有一个建议是在 Ruby 2.0 中包含一些称为“改进”的东西,这将减轻与猴子修补相关的陷阱。只是谷歌“红宝石改进”,有几篇文章。 【参考方案1】:

Monkey-patching 就像编程工具箱中的许多工具一样,既可用于善也可用于恶。问题是,总的来说,这些工具最常被使用的地方。根据我使用 Ruby 的经验,天平在“邪恶”方面占了很大比重。

那么猴子补丁的“邪恶”用途是什么?好吧,猴子修补通常会让你对重大的、潜在的无法诊断的冲突敞开心扉。我有一堂课A。我有某种猴子修补模块MB 修补A 以包括method1method2method3。我有另一个猴子修补模块MC,它也修补A 以包括method2method3method4。现在我陷入了困境。我打电话给instance_of_A.method2:谁的方法被调用了?答案可能取决于很多因素:

    我是按什么顺序引入修补模块的? 补丁是立即应用还是在某种条件下应用? 啊啊啊啊!蜘蛛正在从里面吃掉我的眼球!

好的,所以#3 可能有点过于夸张了......

无论如何,这就是猴子修补的问题:可怕的冲突问题。鉴于通常支持它的语言的高度动态性,您已经面临许多潜在的“幽灵般的远距离动作”问题;猴子补丁只是增加了这些。

如果您是一个负责任的开发人员,那么提供猴子补丁是一件好事。不幸的是,IME,往往会发生的情况是,有人看到猴子补丁并说:“太好了!我只会猴子补丁,而不是检查其他机制是否更合适。”这种情况大致类似于 Lisp 代码库,这些代码库是由那些在考虑将宏作为一个函数来实现之前就使用它的人创建的。

【讨论】:

我更好奇什么时候可以永久使用它。硬币的坏处是显而易见的,但是什么时候比改变你的程序来接受你正在修补的任何东西作为参数更有用呢?我的意思是,我们有这些架构模式,似乎猴子补丁之类的东西的存在只是为了颠覆它们本身。 首先,除了纯粹的函数式语言之外,大多数语言都是命令式的,因此每种编程语言都有“远距离操作”,尽管是的,当你不使用它时,你不应该使用它。不需要它。 Monkey patching 有利于测试或模拟行为。它们可以在工厂/类装饰器/元类中本地化,在那里它们从另一个对象创建一个修补的新类/对象,以帮助处理 ALL 方法之间的“横切关注点”,例如日志记录/记忆/缓存/数据库/持久性/单位转换。【参考方案2】:

***对猴子修补的陷阱有一个简短的总结:

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

任何事情都有时间和地点,猴子修补也是如此。经验丰富的开发人员掌握了许多技术,并学习何时使用它们。它本身很少是一种“邪恶”的技术,只是不加考虑地使用它。

【讨论】:

嗯,这是“如果每个人都这样做的话”的事情之一,比如在高速公路上乱扔垃圾 “经验丰富的开发人员掌握了许多技术并学习何时使用它们” @weberc2 猴子补丁的好例子是第三方库功能的基准。添加在函数执行之前启动并在之后完成的计时器。为什么不呢? @SergeyPanfilov 除非我误解了你,否则我看不出有任何修补的理由。只需在调用该函数之前启动一个计时器,然后再停止它。也许您有一些更细微的用例? @weberc2 当然可以。曾经我使用猴子补丁来修补第三方套接字客户端。那些客户端使用回调,但我希望它能够使用 Promise。就像client.success().error() 而不是client(callback)。我之所以需要这个,是因为我想以与那些项目中使用的休息客户端相同的方式使用这些套接字客户端。除了所有好文章:me.dt.in.th/page/JavaScript-override【参考方案3】:

只要将更改隔离到 您的系统(例如,不属于 您发布的软件包 分布)是否有充分的理由 不要利用这种语言 功能?

作为孤立问题的独立开发人员,扩展或更改本机对象没有问题。同样在较大的项目中,这也是应该做出的团队选择。

我个人不喜欢修改 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 是的,这是一个典型的陷阱。在数组上调用for in 是否是一个好习惯是另一个问题。 不,这里使用属性枚举来遍历数组是主要问题 @user422039 这又是一个例子,说明当你使用猴子补丁时你的代码如何break。我个人会说使​​用属性枚举来遍历Object 是有效的。 ArrayObject。泛型函数不应该不必屏蔽Object的子类型【参考方案4】:

使用“猴子补丁”来纠正特定的已知问题是完全合理的,替代方法是等待补丁修复它。这意味着暂时承担修复某些问题的责任,直到有一个“适当的”、正式发布的修复程序可供您部署。

Gilad Bracha 关于猴子补丁的深思熟虑的意见:http://gbracha.blogspot.com/2008/03/monkey-patching.html

【讨论】:

【参考方案5】:

关于 Javascript:

假设您要添加到界面而不更改现有行为,是否有充分的理由避免它?

是的。最坏的情况是,即使您不更改 现有 行为,也可能会损坏该语言的 未来 语法。

这正是Array.prototype.flattenArray.prototype.contains 发生的情况。简而言之,为这些方法编写了规范,他们的提案被发送到stage 3,然后浏览器开始发布它。但是,在这两种情况下,都发现有一些古老的库用自己的方法修补了内置的Array 对象与新方法同名,并且具有不同的行为;结果,网站崩溃了,浏览器不得不退出他们对新方法的实现,并且必须编辑规范。 (方法已重命名。)

如果您在自己的浏览器上,在您自己的计算机上修改像Array 这样的内置对象,那很好。 (这对于用户脚本来说是一种非常有用的技术。)如果您在面向公众的网站上修改一个内置对象,那就不太好 - 它可能最终会导致类似上述的问题。如果你碰巧控制了一个大网站(比如 ***.com)并且你改变了一个内置对象,你几乎可以保证浏览器会拒绝实施破坏你网站的新功能/方法(因为那么该浏览器的用户将无法使用您的网站,他们更有可能迁移到其他浏览器)。 (请参阅 here 以了解规范编写者和浏览器制造商之间的此类交互)

所有这些,关于您问题中的具体示例

例如,将 Array.map 实现添加到不实现 ECMAScript 第 5 版的 Web 浏览器可能会很好

这是一种非常常见且值得信赖的技术,称为polyfill。

polyfill 是在不支持该功能的 Web 浏览器上实现该功能的代码。大多数情况下,它指的是实现 HTML5 Web 标准的 JavaScript 库,可以是旧浏览器上的既定标准(某些浏览器支持),也可以是现有浏览器上的提议标准(任何浏览器不支持)

例如,如果您为Array.prototype.map(或者,以较新的示例,为Array.prototype.flatMap)编写了一个polyfill,它与official Stage 4 specification完全一致,然后运行在尚未拥有 Array.prototype.flatMap 的浏览器上定义的代码:

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

如果你的实现是正确的,这很好,并且在整个网络上很常见,这样过时的浏览器就可以理解更新的方法。 polyfill.io 是这类事情的常用服务。

【讨论】:

【参考方案6】:

您所描述的条件——添加(而不是改变)现有行为,而不是向外界发布你的代码——似乎相对安全。但是,如果 Ruby 或 JavaScript 或 Rails 的下一版本更改了它们的 API,则可能会出现问题。例如,如果某个未来版本的 jQuery 检查 Array.map 是否已经定义,并假设它是 EMCA5Script 版本的 map 而实际上它是你的猴子补丁怎么办?

同样,如果您在 Ruby 中定义“sum”,并且有一天您决定要在 Rails 中使用该 ruby​​ 代码或将 Active Support gem 添加到您的项目中,该怎么办。 Active Support 还定义了 sum 方法(在 Enumerable 上),因此存在冲突。

【讨论】:

"如果某个未来版本的 jQuery 检查 Array.map 是否已经定义,并假设它是 EMCA5Script 版本的 map" 那么它做了一个不合理的假设,应该得到它。跨度>

以上是关于“猴子补丁”真的那么糟糕吗? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

CRC32 对文件完整性检查真的那么糟糕吗?

在数据库列中存储分隔列表真的那么糟糕吗?

在数据库列中存储分隔列表真的那么糟糕吗?

单身人士真的那么糟糕吗? [重复]

Javascript事件冒泡,没有想象中那么糟糕

Spark 中的 GroupByKey 函数有那么糟糕吗? [复制]