Ruby的“开放类”是否会破坏封装性?

9
在Ruby中,程序员被允许更改预定义类。因此,一个非常糟糕的程序员可能会做出这样的事情:
class String
  def ==(other)
    return true
  end
end

显然,几乎没有人会如此愚蠢,但是更微妙的对预定义类的更改可能会导致已经工作正常的代码出现问题,这似乎违反了封装原则。
四个问题:
1.首先,这是否确实违反了OO封装原则?
2.其次,作为程序员,我是否有办法在我的代码中保证我正在使用未修改过的类?
3.第三,我应该因为任何原因在我的代码中“打开”类吗?
4.最后,在大规模生产编码环境中如何处理这种情况?换句话说,在编程行业中,人们在其他人将要使用的代码中实际上会这样做吗?即使他们不这样做,你如何确保某个插件作者没有像这样做某些事情,会破坏您程序的重要部分?
我知道这是一个有点主观的问题,但我真的很想知道更广泛的编程社区对所谓的“猴子补丁”有何看法。
6个回答

10

首先,这是否违反了封装的面向对象原则?

是的。

其次,作为程序员,有没有一种方法可以在代码中保证我正在使用一个未修改的类版本?

目前还没有。Ruby 2.0 中的 Classboxes(希望如此)将是解决方案。

第三,我是否应该以任何理由“打开”类?

只有在万不得已的情况下才需要。

您绝对不应该猴子补丁(monkey-patch)您自己的类。这根本没必要。您能够控制它们,在一开始就可以让它们按照您的意愿运行。

您绝不应该猴子补丁库中的类。(唯一的例外是那些旨在进行猴子补丁的库,例如 Marc-André Lafortune 的 backports 库,它会在 Ruby 1.8.6、1.8.7、1.9.0 和 1.9.1 上进行猴子补丁,尽可能地提供来自 Ruby 1.9.2 的所有功能。)您可以提供一个附加库,其中包含一些猴子补丁,使使用您的库变得更加容易 (例如,您有一个加密库,可以提供一个 Kryptonite.encrypt(str) 方法和一个 String#encrypt 方法的附加库),但是该附加库应该是一个独立的库,用户需要明确地 require 它,并且应该是完全可选的。

不应该对核心类进行猴子补丁。这指的是 Ruby 中的 ArraySymbol 等类,但对于 Rails 库,我还会将诸如 ActiveRecord::Base 这样的类包括在“核心”标签下。(与上述相同警告。例如,在 3.0 版本之前的 Rails 中,没有明确定义的插件 API,猴子补丁是扩展 Rails 的唯一方法。如果没有违反此规则的人,就永远不会有任何插件,并且 Rails 永远不会达到现在的状态。)

首先尝试继承。首先尝试组合(包装器、代理、外观、适配器等)。首先尝试重构。首先尝试辅助对象。只有在这些方法都不起作用时,再考虑猴子补丁。

当进行猴子补丁时,请保持尊重:如果您正在添加新方法,请确保它不存在,并在出现时处理它(例如从您的方法中调用它)。如果您正在包装现有方法,请确保如果其他人已经包装它,则调用他们的包装器,并且当有人想要稍后再包装它时,您的包装器允许这样做。(特别是,这意味着您必须保留方法的现有约定。)

如果可能,请将猴子补丁放入混合组件中。这样,它们会在继承链中显示,这将为任何试图调试代码的人提供机会弄清发生了什么。将猴子补丁放入单独的、明显命名的文件中。

最后,在一个大规模的生产编码环境中如何处理这种情况?换句话说,程序员在实际使用其他人的代码时真的会这样做吗?或者即使他们不这样做,你如何确保某个插件作者没有做某些可能会破坏程序的重要部分的事情?

避免与“非常糟糕”的程序员合作。

尽管听起来简单,但这基本上就是要点。当然,您可以编写测试、进行代码审查、练习成对编程、使用静态分析工具、启用警告运行您的代码(例如,您在问题中发布的代码将生成一个warning:method redefined; discarding old ==)。但对于我来说,这些都是一个不算太差的程序员会做的事情。


4
  1. 在某些情况下是可以的。如果你遵循一个类只负责一个任务的范例,那么重新打开类的用法通常会(但不一定)破坏封装性。然而,在 Ruby 中似乎并不是传统做法。例如,数组类充当列表、数组和堆栈,因此标准库似乎也不遵守严格的封装性。我想这取决于个人喜好。
  2. 我不知道有什么方法。也许其他人会想到一些方法。
  3. 我的观点是,如果你正在编写供他人使用的库,最好避免这样做。如果你正在编写一个应用程序,需要这样做(一个微不足道的例子:你需要为数字数组添加平均值方法——这是在可读性和不使用 monkeypatching 之间做出选择),那么我会去做。
  4. 最(不)著名的实际 monkeypatcher 是 rails。因此,对核心类的更改特别要进行良好的文档记录。当然,测试也是有帮助的。

在2点:也许可以使用类似于String.freeze的方法。 - steenslag

3
首先,这是否违反了OO封装原则?
封装的目的是隐藏实现细节,而不是规定类应该如何使用。在Ruby中,通常会尊重私有变量,并且当您想要绕过它时,您知道自己正在进行一个可能会在升级库时出错的黑客。我会说,在测试情况下,我打破封装的时间大约90%,当我无法在其他语言中进行操作时,我感到非常恼火。
其次,作为程序员,我是否可以保证我的代码与未修改的类一起使用?
那会有点违反“开放类”的整个概念,不是吗;-)
第三,我是否应该在我的代码中“打开”类,出于任何原因?
把它看作是“最后的手段”。通常的答案是“否”,因为您控制类定义,所以不需要这样做。但是,将内容添加到特定实例的单例类中是完全不同的故事;-)
最后,这种情况在大规模、生产编码环境中如何处理?换句话说,编程行业的人们实际上会在别人使用的代码中这样做吗?即使他们没有,如何确保某个插件作者不会像这样做,从而破坏您程序的一个重要部分?
原则上,如果库正在打开另一个库,则应将其作为最后的手段进行(即不能通过正常的OO功能实现相同的操作),并且当他们这样做时,他们应该确保它没有被其他人打开过。您可以使用一些技巧使该过程更安全,例如旧的alias_method_chain或新的内容,围绕使用mixin和调用super。
话虽如此,在Ruby中这种情况经常发生,在Rails中,这是获取插件的方式。
我在一个具有250k loc代码库的产品上工作,并且我们对很多事情进行了猴子补丁。我们也实行TDD(测试代码比例为1:1.5),并在提交到主线存储库之前运行所有测试。所有的猴子补丁都在“config/initializers”文件夹中,其目的清楚地标记,所有猴子补丁都经过完整测试。我已经在那里工作了一年,在那段时间里,至少我们从未遇到过与猴子补丁相关的问题。
话虽如此,在这支团队中,我认为这是我所在过的最好的团队,并且我们都非常致力于极限编程。如果不是这两个条件中的任何一个,我认为Rails不是一个好主意。在像Ruby这样的语言中,您需要信任您的团队以采取正确的操作,并尽可能多地进行检查和平衡。

2
  1. 简短回答:是的。更详细的回答是:有点。封装的目的确实是防止这种事情发生,但是在其他语言中,封装可能会被违反,尽管需要使用更难的手段。

  2. 也许是测试用例,但同样地,在编写应用程序时,Ruby以其特有的怪癖而闻名,尤其是在使用像Rails这样的重型框架时,直到版本3才解决了污染全局命名空间并导致意外结果的问题。

  3. 我不确定你的问题是什么意思。

  4. 在现实世界中,开发人员决定使用哪个包,最好选择经过大量测试的包。

额外注释:其他开发人员可以并且经常会破坏他们使用的程序。封装不是一种锁定应用程序部分访问的软件功能,而是一种语言功能,有助于防止编码人员直接破坏您的代码。


1

我的Ruby编程经验告诉我:

  • 是的,因为你可以添加一个方法来返回外部类的私有属性:程序可以随意破坏封装。
  • 不,你无法阻止这种情况发生,这是一种语言特性。
  • 是的,有时候添加方法到现有类中似乎很有用或者至少能够产生好看的代码:例如在String或Array中添加应用过滤方法。无论如何,创建这些方法时要使用模块并将其包含进去。我特别喜欢ActiveRecord中的实现方式,阅读他们的源代码非常清晰明了。
  • 在大规模的代码中,除非你有良好的单元测试和遵守纪律的开发人员,否则考虑切换到更加稳定的语言(是的,我知道有些人会不同意)。

1
虽然我是 Ruby 粉丝,但我完全同意上面这句话。如果你有糟糕的开发人员,那就选择 Java 或 C#。只有当每个人都足够了解自己的行业并能做出正确选择时,Ruby 和Python 才有意义。 - Matt Briggs

0
对于第四部分,有一个“选择不会出错”的原则。如果很多人都在使用你正在使用的插件,那么如果插件出了问题,很可能会有人发现它。
另一方面,你可能正在使用其他人没有使用过的插件组合。

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