今天我读了一本书,作者写道,在一个设计良好的类中,访问属性的唯一方法是通过其中一个类方法。这是一个被广泛接受的想法吗?为什么封装属性如此重要?不做会有什么后果?我早些时候在某处读到这可以提高安全性或类似的东西。PHP 或 Java 中的任何示例都会非常有帮助。
6 回答
这是一个被广泛接受的想法吗?
在面向对象的世界中,是的。
为什么封装属性如此重要?不做会有什么后果?
对象旨在成为包含数据和行为的内聚实体,其他对象可以通过公共接口以受控方式访问这些数据和行为。如果一个类不封装它的数据和行为,它就不再控制被访问的数据,也不能履行它与公共接口隐含的其他对象的契约。
这样做的一个大问题是,如果一个类必须在内部进行更改,那么公共接口就不必更改。这样它就不会破坏任何代码,其他类可以像以前一样继续使用它。
PHP 或 Java 中的任何示例都会非常有帮助。
这是一个 Java 示例:
public class MyClass {
// Should not be < 0
public int importantValue;
...
public void setImportantValue(int newValue) {
if (newValue < 0) {
throw new IllegalArgumentException("value cannot be < 0");
}
}
...
}
这里的问题是,因为我没有importantValue
通过将其private
公开而不是进行封装,所以任何人都可以绕过我在 setter 中进行的检查,以防止对象具有无效状态。importantValue
应该永远不会小于 0,但是由于缺乏封装,所以无法阻止它。
不做会有什么后果?
封装背后的整个想法是,所有与类相关的知识(除了它的接口)都在类本身之内。例如,允许直接访问属性意味着确保任何分配在执行分配的代码上都是有效的。如果什么是有效的定义发生变化,您必须使用该类检查并审核所有内容以确保它们符合。将规则封装在“setter”方法中意味着您只需在一个地方更改它,任何尝试任何有趣事情的调用者都可以得到抛出异常作为回报。当属性更改时,您可能还想做很多其他的事情,而 setter 就是这样做的地方。
是否允许直接访问没有任何规则来绑定它们的属性(例如,任何适合整数的东西都可以)是好的做法是有争议的。我想为了一致性起见,使用 getter 和 setter 是一个好主意,也就是说,你总是知道你可以调用setFoo()
来改变foo
属性,而不必查看你是否可以直接这样做。它们还允许你对你的类进行未来验证,这样如果你有额外的代码要执行,放置它的地方就已经存在了。
就个人而言,我认为必须使用 getter 和 setter 看起来很笨拙。我宁愿写作也x.foo = 34
不愿x.setFoo(34)
期待有一天某种语言为成员提供相当于数据库触发器的功能,允许您定义在分配之前、之后或代替分配时触发的代码。
关于如何实现“良好的 OOD”的意见不一而足,而且经验丰富的程序员和设计师往往对设计选择和理念存在分歧。如果您询问具有多种语言背景和范式的人们,这可能是一场激烈的战争。
是的,理论上理论和实践是一样的,所以语言选择不应该对高级设计产生太大影响。但在实践中,它们确实如此,因此会发生好事和坏事。
让我补充一点:这取决于。封装(在支持语言中)使您可以控制类的使用方式,因此您可以告诉人们:这是 API,您必须使用它。在其他语言(例如 python)中,官方 API 和非正式(可能会更改)接口之间的区别仅在于命名约定(毕竟,我们在这里都是同意的成年人)
封装不是安全功能。
另一个需要思考的想法
使用访问器进行封装还可以在未来提供更好的可维护性。在上面 Feanor 的回答中,强制执行安全检查非常有用(假设您的 instvar 是私有的),但它可以有更进一步的好处。
考虑以下场景:
1)您完成了您的应用程序,并将其分发给一组用户(内部、外部等)。
2) BigCustomerA 接近您的团队并希望将审计跟踪添加到产品中。
如果每个人都在他们的代码中使用访问器方法,那么实现起来几乎是微不足道的。像这样:
MyAPI 版本 1.0
public class MyClass {
private int importantValue;
...
public void setImportantValue(int newValue) {
if (newValue < 0) {
throw new IllegalArgumentException("value cannot be < 0");
}
importantValue = newValue;
}
...
}
MyAPI V1.1(现在带有审计跟踪)
public class MyClass {
private int importantValue;
...
public void setImportantValue(int newValue) {
if (newValue < 0) {
throw new IllegalArgumentException("value cannot be < 0");
}
this.addAuditTrail("importantValue", importantValue, newValue);
importantValue = newValue;
}
...
}
API 的现有用户不会更改他们的代码,并且现在可以使用新功能(审计跟踪)。
如果不使用访问器进行封装,您将面临巨大的迁移工作。
第一次编码时,看起来工作量很大。它的打字速度要快得多:但如果每个人都采取简单的方法,那么为class.varName = something
BigCustomerA class.setVarName(something);
的功能请求获得报酬将是一项巨大的努力。
在面向对象编程中有一个被称为 (http://en.wikipedia.org/wiki/Open/closed_principle) 的原则: POC --> Principle of Open and Closed。这个原则坚持:一个好的类设计应该为可扩展性(继承)开放,但对内部成员的修改(封装)关闭。这意味着您无法在不关心对象的情况下修改它的状态。
因此,新语言仅通过属性(C++ 或 Java 中的 getter 和 setter 方法)修改内部变量(字段)。在 C# 中,属性编译为 MSIL 中的方法。
C#:
int _myproperty = 0;
public int MyProperty
{
get { return _myproperty; }
set { if (_someVarieble = someConstantValue) { _myproperty = value; } else { _myproperty = _someOtherValue; } }
}
C++/Java:
int _myproperty = 0;
public void setMyProperty(int value)
{
if (value = someConstantValue) { _myproperty = value; } else { _myproperty = _someOtherValue; }
}
public int getMyProperty()
{
return _myproperty;
}
采取这些想法(来自Head First C#):
- 想想这些字段可能被滥用的方式。如果设置不正确,可能会出现什么问题。
- 你班上的一切都是公开的吗?花一些时间考虑封装。
- 哪些字段需要处理或计算?他们是主要候选人。
- 仅在需要时才公开字段和方法。如果您没有理由公开某事,请不要。