为什么方法和属性的可见性很重要?

44

为什么不应该将所有方法和属性都设置为全局可访问(即 public)?

如果我将一个属性声明为 public,你能给我举一个可能遇到的问题的例子吗?


2
我们无法提供一个简单的一行代码例子来说明封装的好处。请阅读相关资料,并自行决定。 - user229044
什么可能出错,就一定会出错。人们往往认为所有的按钮都是用来按的,即使标志上写着“不要按按钮”。当你想把钱包放在车里时,为什么会藏起来放在后备箱里呢?因为如果小偷看不到它,他们就不会去摸和打破他们没有权利触碰的东西。隐藏你的钱包可以降低被盗的几率。隐藏你的方法等同于“眼不见为净”。通过消除可能性,墨菲定律在最糟糕的时刻就无法攻击你了。 - Eric Leschinski
12个回答

145

把麦当劳看作一个对象,有一个众所周知的公开方法来点一份巨无霸汉堡。

内部会有数以千计的其他调用,用来获取制作那个巨无霸汉堡所需的材料。他们不希望您知道他们的供应链如何运作,所以您只能获得公开的 Gimme_a_BigMac() 调用,并且永远不会让您访问 Slaughter_a_cow()Buy_potatoes_for_fries() 方法。

如果是为自己编写代码,而且没有人会看到,那么可以将所有内容都设置为公开。但是,如果您正在为其他人重用的库编写代码,则需要保护内部细节。这样,麦当劳可以自由切换到使用传送器将牛肉传送过来,而不必打电话给货车公司送货。最终用户永远不会知道其中的区别-他们只会获得他们的巨无霸汉堡。但是,在内部,一切都可能发生根本性的变化。


4
尽管这个例子很荒谬,但它实际上是一个好的描述,特别是后面的库描述。在创建API时,我经常发现能够保持相同的外部控制(公共方法)非常有价值,同时彻底改造内部控制(私有或受保护方法),以提高网站性能并提供相同可靠的响应。 - stslavik
52
我认为这个比喻并不荒谬。毕竟,麦当劳是一个对象,而Gimme_a_BigMac()Slaughter_a_cow()确实是方法。 :) 此外,如果麦当劳决定使用自己的土豆农场,内部的私有/受保护方法Buy_potatoes_for_fries()会改为Pick_potatoes_for_fries(),公众甚至不会注意到这个变化。对于这个很好的比喻点赞。 - Herbert
44
应该给予负一分,因为它暗示麦当劳的汉堡是由真正的肉制成的。 - Anthony Pegram
4
有趣的是,这里有两个罗顿麦当劳正在进行翻新,两者都在升级下水道管线。我不确定是为了增加排污量,还是为了增加原材料的摄入量 :p - Marc B
2
很好的比喻,我想补充一点,即使没有其他人会看到你的代码,它仍然很重要,因为你自己会看到你的代码。它让你将应用程序分成组件,这些组件可以在不破坏应用程序的其余部分的情况下进行更改。这是为了防止在特定上下文之外调用你不想调用的方法。 - ForbesLindesay

45
为什么不应该让所有的方法和属性都可以从任何地方(即公共)访问?
因为这太过昂贵。每个我创建的公共方法都必须经过精心设计并由一组架构师批准,它必须被实现为在面对任意敌对或有缺陷的调用者时能够健壮运行,它必须经过充分测试,测试期间发现的所有问题都必须添加回归测试集,该方法必须被记录文档,并且该文档必须被翻译成至少十二种不同的语言。
但最大的成本是:该方法必须永远不变地得到维护,如果在下一个版本中我决定不喜欢该方法执行的操作,我无法更改它,因为客户现在依赖于它。破坏公共方法向后兼容性会给用户带来成本,我不愿意这样做。使用错误的公共方法设计或实现会给下一个版本的设计者、测试者和实现者带来高昂的成本。
一个公共方法很容易就会花费数千甚至数万美元。在一个类中创建一百个公共方法,那就是一个价值百万美元的类了。
私有方法没有这些成本。明智地使用股东的钱,尽可能地将所有内容设为私有。

1
然后你会看到有人使用 System.Reflection。 :) - Roman Royter
3
那些人利用其代码完全可信这一事实,揭示其他类的内部实现细节。如果你要明确使用特权来覆盖安全系统,则需要对可能导致严重故障的后果负责。 - Eric Lippert
@Erik Lippert,同意。我只希望这些人能够意识到他们行为的可能后果。不幸的是,很多人并没有意识到。 - Roman Royter
2
+1 我真的希望每个我不公开的方法都能被支付“数千甚至数万美元”:P - NikiC
3
你并非获得那些美元,而是储存它们。在这个梦幻世界里,如果你把所有东西都公开了,你会得到负薪水! - configurator

12

将可见性作用域视为信任的内部圆圈。

以自己为例,思考哪些活动是公开的,哪些是私人或受保护的。有许多事情是您不会授权给任何人代表您处理的。有一些是允许其他人触发的,还有一些具有有限的访问权限。

同样,在编程中,作用域为您提供了创建不同信任圆圈的工具。此外,将事物设为私有/受保护可以更好地控制发生的情况。例如,您可以允许第三方插件扩展您的某些代码,而它们可以被限制在其所能到达的范围内。

因此,总体而言,作用域为您提供了额外的安全级别,并使事物保持更加组织化。


10
因为这违反了面向对象编程的一个重要原则——封装的概念。

Javascript支持使用闭包进行封装。我相信你可以使用Smalltalk的消息系统来支持类似私有方法的功能。Python就有点奇怪了;)此外,人们有时会使用def _methodname符号来表示私有成员。即使在编译时没有强制执行,您仍然可以在代码中使用私有成员的概念。即使支持私有成员的语言也可以通过使用反射等方式来调用私有代码。 - wprl
1
我不了解Smalltalk和Python,但当人们说JavaScript是面向对象的时候,我会笑。也许在相反的世界里吧! - Maxim Gershkovich

9

你说存在风险?

<?php

class Foo
{
    /**
     * @var SomeObject
     */
    public $bar;
}

您的代码说明$bar应该包含一个SomeObject对象的实例。但是,使用您的代码的任何人都可以这样做。
$myFoo->bar = new SomeOtherObject();

如果代码依赖于Foo::$bar是一个SomeObject,那么这些代码将会中断。通过使用getter和setter以及受保护的属性,您可以强制执行这种期望:

<?php

class Foo
{
    /**
     * @var SomeObject
     */
    protected $bar;

    public function setBar(SomeObject $bar)
    {
        $this->bar = $bar;
    }
}

现在,您可以确定任何时候设置Foo::$bar,都将是一个 instanceof SomeObject的对象。

4
通过隐藏实现细节,它还可以防止对象进入不一致状态。
以下是一个虚构的堆栈示例(伪代码)。
public class Stack {

  public List stack = new List();
  public int currentStackPosition = 0;

  public String pop() {
    if (currentStackPosition-1 >= 0) {
      currentStackPosition--;
      return stack.remove(currentStackPosition + 1);
    } else {
      return null;
    }
  }

  public void push(String value) {
    currentStackPosition++;
    stack.add(value);
  }
}

如果将这两个变量都设为私有,则实现是正常的。但如果设置为公共的,只需为currentStackPosition设置错误值或直接修改List,就可以轻松破坏它。

只有暴露功能时,您才能提供可靠的契约供他人使用和信任。暴露实现只会使其成为一个可能无法正常运作的东西,除非没有人搞乱它。


2
封装在任何语言中都不是必需的,但它很有用。封装被用来最小化与变更传播概率最高的潜在依赖项的数量,同时有助于防止不一致性。
简单示例:假设我们创建了一个矩形类,其中包含四个变量 - 长度、宽度、面积和周长。请注意,面积和周长是由长度和宽度派生出来的(通常我不会为它们创建变量),所以修改长度会改变面积和周长。
如果您没有使用适当的信息隐藏(封装),那么使用该矩形类的另一个程序可以更改长度而不更改面积,因此矩形就会不一致。如果没有封装,将可能创建一个长度为1、宽度为3、面积为32345的矩形。
使用封装,我们可以创建一个函数,如果程序想要更改矩形的长度,那么对象将适当地更新其面积和周长而不会不一致。
封装消除了不一致的可能性,并将保持一致性的责任转移到了对象本身而不是使用它的程序上。
然而,同时封装有时也是一个坏主意,并且运动规划和碰撞(在游戏编程中)是特别容易发生这种情况的领域。
问题在于封装在需要时非常棒,但当应用于不需要的地方时则非常糟糕,例如需要维护一组封装的全局属性时。由于面向对象编程强制执行封装,因此您将被卡住。例如,许多对象的属性是非本地的,例如任何类型的全局一致性。在面向对象编程中经常发生的情况是每个对象都必须对全局一致性条件进行编码,并且尽其所能来帮助维护正确的全局属性。如果您真的需要封装以允许替代实现,那么这可能很有趣。但是,如果您不需要它,则最终会在多个地方编写大量非常棘手的代码,而这些代码基本上都是做同样的事情。一切似乎都封装了,但实际上完全相互依赖。

是的!我有一个全局对象传递到每个类,否则我就必须像这样做:this.parent.getChildByName("foo").score++ - apscience

2

其实你是可以把所有东西都公开的,并且当你明确说明了契约和正确使用对象的方式时,这并不会破坏封装性。也许不是属性,而是方法更容易隐藏。

请记住,打破封装性的不是API设计者,而是类的用户,在他们的应用程序中调用内部方法可能会这样做。您可以通过“私有”方式阻止他们这样做,或通过在非API方法前缀下划线(例如)将责任转移给他们。您真的在意某人是否通过您的库以其他方式使用它来破坏他的代码吗?我不在意。

几乎将所有内容设为私有或final-或没有API文档-另一方面-是鼓励开放源代码的可扩展性和反馈的一种方式。您的代码可以以您甚至没有想到的方式使用,如果一切都被锁定(例如C#中默认密封的方法),则可能不是这种情况。


1

为了避免自己犯错!

上面已经有一些很好的答案了,但我想再补充一点。这被称为最小权限原则。拥有更少的权限,更少的实体有权力破坏事物。破坏事物是不好的。

如果你遵循最小权限原则,最小知识原则(或Demeter法则)和单一职责原则也就不远了。由于你编写的用于下载最新足球比分的类遵循了这个原则,而且你必须轮询它的数据而不是直接将其转储到你的界面,所以你可以将整个类复制粘贴到下一个项目中,节省开发时间。节省开发时间是好的。

如果你很幸运,六个月后你会回到这段代码来修复一个小错误,此时你已经从中赚了gigaquads的钱。未来的自己会因为没有遵循上述原则而诅咒现在的自己,并且他将成为最少惊讶原则的受害者。也就是说,你的错误是足球比分模型的解析错误,但由于你没有遵循LOD和SRP,你会惊讶于你正在进行XML解析与输出生成的内联操作。生活中有比你自己的代码更令人惊讶的事情。相信我,我知道。

由于你遵循了所有原则并记录了你的代码,你每个星期四下午只需要工作两个小时进行维护编程,其余时间可以冲浪。


1

唯一的问题是,如果你不使用PrivateProtectedAbstract Static Final Interface等内容,人们可能会认为你不够“酷”。这些东西就像设计师服装或苹果设备一样——人们购买它们并不是因为需要,而只是为了跟上别人的步伐。

是的,封装是一个重要的理论概念,但在实践中,“private”和其他关键字很少有意义。它们在Java或C#中可能有些意义,但在像PHP这样的脚本语言中使用“private”或“protected”是愚蠢的,因为封装是由编译器检查的,而PHP中不存在编译器。更多细节

另请参见这个出色的回答以及@troelskn和@mario在这里的评论。


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