实际上是私有成员还是公共成员(封装有多重要?)
面向对象编程的最大优势之一就是封装,我们(或者至少我已经学到)的"真相"之一就是,应该始终使成员成为私有成员,并通过访问器和增幅器使成员可用方法,从而确保了验证和确认更改的能力。
不过,我很好奇这在实践中到底有多重要。特别是,如果我们有一个更复杂的成员(例如集合),那么将其公开而不是使用大量方法来获取集合的键,从集合中添加/删除项目,可能会非常诱人等等。
我们总体上遵守规则吗?答案会根据是为自己编写的代码还是为他人使用的代码而变化?我是否还因为这种混淆而遗漏了更微妙的原因?
解决方案
一切都是关于控制人们可以用我们给他们的东西做什么。控制得越多,我们所做的假设就越多。
此外,从理论上讲,我们可以更改基础实现或者其他内容,但是由于在大多数情况下,它是:
private Foo foo; public Foo getFoo() {} public void setFoo(Foo foo) {}
证明这一点有些困难。
在实践中,我始终只遵循一个规则,即"没有大小适合所有人"的规则。
封装及其重要性是项目的产物。哪些对象将访问界面,他们将如何使用它,如果他们对成员具有不必要的访问权限,这会很重要吗?这些问题以及诸如此类的问题,我们在进行每个项目实施时都需要问自己。
我的决定基于模块中《准则》的深度。
如果我写的是模块内部的代码,并且不与外界交互,那么我不会用私有方式封装很多东西,因为这会影响我的程序员性能(我编写和重写代码的速度)。
但是对于作为服务器与用户代码接口的对象,我坚持严格的隐私模式。
由于过去必须要维护许多人使用的已有数年历史的代码,所以对我来说很清楚,如果将成员属性公开,则最终会被滥用。我什至听说有人不同意存取器和变异器的想法,因为它仍然没有真正达到封装的目的,即"隐藏类的内部工作原理"。这显然是一个有争议的话题,但是我的观点是"将每个成员变量都设为私有,首先考虑类必须做什么(方法),而不是如何让人们更改内部变量"。
当然,无论是编写内部代码还是供他人使用(甚至由我们自己使用,但作为一个包含的单元使用),这都将有所不同。要在外部使用的任何代码都应具有定义明确/文档化的接口,希望尽可能少地改变。
对于内部代码,根据难度,我们可能会发现现在以简单的方式执行操作的工作量减少了,以后要付出一点点代价。当然,墨菲定律将确保在以后需要进行广泛更改的地方(需要更改未能封装的类的内部)进行多次广泛更改,从而可以消除短期收益。
我倾向于严格遵循规则,即使只是我自己的代码也是如此。因此,我真的很喜欢C中的Properties。它非常容易控制给出的值,但是我们仍然可以将它们用作变量。或者将集合设为私有并公开,等等。
这取决于。这是必须务实决定的那些问题之一。
假设我有一个代表点的课程。我可以使用X和Y坐标的getter和setter,也可以将它们公开,并允许对数据的自由读/写访问。在我看来,这是可以的,因为该类的行为就像是一个荣耀的结构,它可能添加了一些有用的功能。
但是,在许多情况下,我们不想提供对内部数据的完全访问权限,而不想依靠类提供的方法与对象进行交互。一个示例是HTTP请求和响应。在这种情况下,让任何人通过电线发送任何东西都必须由类方法进行处理和格式化是一个坏主意。在这种情况下,该类被视为实际对象,而不是简单的数据存储。
这实际上取决于动词(方法)是否驱动结构或者数据是否驱动结构。
当以下至少一种情况成立时,封装很重要:
- 除了我们之外的任何人都将使用类(否则它们将破坏不变式,因为他们不阅读文档)。
- 任何不阅读文档的人都将使用类(否则他们将破坏我们精心记录的不变式)。请注意,此类别包括我们距现在两年。
- 在将来的某个时候,有人将从类继承(因为当字段的值更改时可能需要执行额外的操作,因此必须有一个setter)。
如果它只适合我,并且在几个地方使用,并且我不会继承它,并且更改字段不会使该类假定的任何不变式无效,只有这样,我才会偶尔将字段公开。
我的趋势是尽可能将所有内容都设为私有。这样可以尽可能清晰地定义对象边界,并尽可能使对象分离。我之所以喜欢这样,是因为当我不得不重写第一次(第二次,第五次)损坏的对象时,它可以将损坏保持在较少数量的对象上。
如果将对象紧密结合在一起,将它们组合成一个对象可能会更简单。如果充分放松了耦合约束,我们将返回结构化编程。
可能是,如果我们发现一堆对象只是访问器函数,则应重新考虑对象划分。如果我们不对该数据执行任何操作,则它可能属于另一个对象。
当然,如果要编写类似库的内容,则需要尽可能清晰和清晰的界面,以便其他人可以针对该界面进行编程。
我更喜欢将成员保持尽可能长时间的私密性,并且即使是在同一类中,也只能通过getter访问em。我还尝试尽可能避免使用setter作为提倡值样式对象的初稿。与依赖项注入一起使用时,我们经常会有setter而没有getter,因为客户端应该能够配置对象,但是(其他)则不知道实际配置的对象,因为这是实现细节。
问候,
奥利
CProperties"模拟"公共字段。看起来很酷,语法确实加快了创建这些get / set方法的速度
特别是对于使用要返回的集合的示例,这种集合的实现可能会发生变化(与更简单的成员变量不同),从而使封装的实用性更高。
话虽如此,我有点喜欢Python的处理方式。成员变量默认情况下是公共的。如果要隐藏它们或者添加验证,则提供了一些技术,但是这些技术被认为是特殊情况。
使工具适合工作...最近,我在当前的代码库中看到了一些类似这样的代码:
private static class SomeSmallDataStructure { public int someField; public String someOtherField; }
然后在内部使用此类来轻松传递多个数据值。这并不总是很有意义,但是如果我们只有DATA,没有任何方法,并且没有将其公开给客户,我会发现它是一个非常有用的模式。
我对此的最新使用是一个JSP页面,其中显示了一个数据表,该数据表以声明方式定义在顶部。因此,最初是在多个数组中,每个数据字段一个数组...这最终导致代码难以遍历,因为字段在定义上不会彼此相邻,因此将它们一起显示...所以我创建了一个简单的代码像上面这样的类可以将其组合在一起...结果是真正可读的代码,比以前要多得多。
道德...有时候,只要我们仔细考虑并考虑后果,就应该考虑使用"可接受的不良"替代方案,只要它们可以使代码更简单易读,就可以...不要盲目接受所听到的一切。
就是说……公共获取和设置者几乎等同于公共领域……至少在本质上(有一点点灵活性,但将其应用于我们拥有的每个领域仍然是一个糟糕的模式)。
甚至Java标准库也有一些公共字段。
我几乎一直都遵守规则。对于我而言,基本上有四种情况,即规则本身和一些异常(均受Java影响):
- 可用于当前类之外的任何东西,可通过getter / setter访问
- 内部到类的用法通常以" this"开头,以表明它不是方法参数
- 意味着要保持极小的物体,如运输物体-基本上是属性的直截了当;所有公众
- 需要是非私有的,以进行某种扩展
是的,封装很重要。公开基础实现确实(至少)有两件事是错误的:
- 混合职责。调用者不需要或者不想了解底层实现。他们应该只希望全班做好自己的工作。通过公开基础实现,我们就不会在上课。相反,它只是将责任推给了呼叫者。
- 将我们与基础实现联系起来。公开了基础实现之后,便会受到约束。如果我们告诉呼叫者,例如,在下面有一个集合,则无法轻松地将集合交换为新的实现。
无论我们是直接访问基础实现还是仅复制所有基础方法,这些(和其他)问题均适用。我们应该公开必要的实现,仅此而已。将实施保持私有状态可以使整个系统更易于维护。
这里存在一个实际问题,大多数现有答案都没有解决。封装以及将干净,安全的接口暴露于外部代码总是很不错的选择,但是如果要在空间和/或者时间上占用大量"用户"的基础上使用我们正在编写的代码,则这尤为重要。我的意思是,如果我们计划某个人(甚至我们)计划在将来很好地维护代码,或者如果我们正在编写一个模块,以与众多其他开发人员的代码交互,则我们需要做更多的思考而不是编写一次性或者全部由我们编写的代码。
老实说,我知道这是什么令人不快的软件工程实践,但是我经常会一开始将所有内容公开,这样会使事情记住和键入的速度稍快一些,然后添加有意义的封装。如今,当今最流行的IDE中的重构工具使我们所使用的方法(添加封装或者移除封装)的相关性远低于以往。
当我使对象有意义时,它们将更易于使用和维护。
例如:Person.Hand.Grab(howquick,howuchuch);
诀窍不是将成员视为简单的值而是将对象本身视为对象。
我认为这个问题确实将封装的概念与"信息隐藏"混为一谈
(这不是批评者,因为它似乎与对"封装"概念的常见解释相匹配)
但是对我来说,"封装"是:
- 将几个项目重新组合到一个容器中的过程
- 容器本身将物品重新分组
假设我们正在设计纳税人系统。对于每个纳税人,我们可以将孩子的概念封装为
- 代表孩子的孩子的清单
- 的地图要考虑到来自不同父母的孩子
- 一个将提供所需信息(例如儿童总数)的儿童(非儿童)对象
在这里,我们有三种不同类型的封装,两种由低级容器(列表或者映射)表示,一种由对象表示。
通过做出这些决定,我们不会
- 使封装公开,受保护或者私有:仍然需要选择"隐藏信息"
- 进行完整的抽象(我们需要优化对象Child的属性,并且可以决定创建对象Child,这将从纳税人系统的角度仅保留相关信息)抽象是选择哪些属性的过程对象的内容与系统有关,必须完全忽略。
所以我的意思是:
该问题的标题可能是:
实践中的私人成员与公共成员(信息隐藏的重要性有多大?)
不过,只有我的2美分。我完全尊重一个人,可以将封装视为包括"信息隐藏"决策的过程。
但是,我总是尝试区分"抽象","封装","信息隐藏或者可见性"。
基本上,信息隐藏与代码清晰度有关。它旨在使其他人更容易扩展代码,并防止他们在使用类的内部数据时意外地创建错误。它基于以下原则:没人会阅读评论,尤其是其中有说明的评论。
示例:我正在编写更新变量的代码,并且我需要绝对确保Gui进行更改以反映更改,最简单的方法是添加访问器方法(也称为" Setter"),而不是调用该方法更新数据已更新。
如果我公开该数据,并且在不通过Setter方法的情况下更改了变量(这种情况在每个发誓的时间都发生),那么有人将需要花费一个小时的调试时间来找出为什么不显示更新。在较小程度上,这同样适用于"获取"数据。我可以在头文件中添加注释,但很可能没人会读它,直到出现严重的,极其严重的错误。用private强制执行它意味着不会犯错误,因为它将显示为易于定位的编译时错误,而不是运行时错误。
根据经验,我们唯一想公开一个成员变量,而又不使用Getter和Setter方法的情况是,如果我们想明确表明更改它不会带来任何副作用。尤其是在数据结构简单的情况下,例如仅将两个变量成对保存的类。
这应该是相当罕见的情况,因为通常我们会希望产生副作用,并且如果我们创建的数据结构是如此简单以至于我们不需要(例如,配对),那么将会有一个效率更高的书面形式在标准库中。
话虽这么说,对于大多数一站式无扩展的小型程序,例如我们上大学的程序,它是比任何东西都更"好的实践",因为我们会在编写过程中记得,然后请把它们交上,再也不要碰代码了。另外,如果我们正在编写数据结构来了解它们如何存储数据,而不是作为发布代码,那么就有一个很好的论点,即Getters和Setters将无济于事,并且会妨碍学习体验。
只有当我们到达工作场所或者大型项目时(可能由不同人员编写的对象和结构调用代码),使这些"提醒"变得更加重要。不管是单人项目,这都无关紧要,原因很简单,因为"从现在起六个星期"与同事是不同的人。而且"我六个星期前"经常被证明是懒惰的。
最后一点是,有些人非常热衷于信息隐藏,如果数据不必要地公开,他们会感到恼火。最好给他们幽默。
@VonC
我们可能会发现国际标准化组织的"开放式分布式处理参考模型"很有趣。它定义:"封装:仅通过对象支持的接口上的交互才能访问对象中包含的信息的属性。"
我试图说明信息隐藏在这里是此定义的关键部分:
http://www.edmundkirwan.com/encap/s2.html
问候,
埃德
我发现很多getter和setter都是代码的味道,说明程序的结构设计得不好。我们应该查看使用这些getter和setter的代码,并查找确实应该属于该类的功能。在大多数情况下,类的字段应为私有实现细节,并且只有该类的方法可以操纵它们。
同时具有吸气剂和吸气剂等于公开的领域(当吸气剂和吸气剂是微不足道的/自动生成的)。有时,最好将字段声明为public,这样代码会更简单,除非我们需要多态性或者框架需要get / set方法(并且我们不能更改框架)。
但是在某些情况下,使用吸气剂和吸气剂是一种很好的模式。一个例子:
创建应用程序的GUI时,我尝试将GUI的行为保留在一个类(FooModel)中,以便可以轻松对其进行单元测试,并在另一类(FooView)中使GUI可视化仅手动。视图和模型通过简单的粘合代码连接;当用户更改字段" x"的值时,视图将调用模型上的" setX(String)",从而可能引发一个事件,表明模型的其他部分已更改,并且该视图将获取更新后的值从带有吸气剂的模型中获取。
在一个项目中,有一个包含15个getter和setter的GUI模型,其中只有3个get方法是微不足道的(以便IDE可以生成它们)。所有其他所有内容都包含一些功能或者非平凡的表达式,例如:
public boolean isEmployeeStatusEnabled() { return pinCodeValidation.equals(PinCodeValidation.VALID); } public EmployeeStatus getEmployeeStatus() { Employee employee; if (isEmployeeStatusEnabled() && (employee = getSelectedEmployee()) != null) { return employee.getStatus(); } return null; } public void setEmployeeStatus(EmployeeStatus status) { getSelectedEmployee().changeStatusTo(status, getPinCode()); fireComponentStateChanged(); }
请记住在对象上调用方法的语义。方法调用是一种非常高级的抽象,可以通过各种不同的方式在编译器或者运行时系统中实现。
如果我们要调用的方法的对象存在于相同的进程/内存映射中,则编译器或者VM可以很好地优化方法以直接访问数据成员。另一方面,如果对象位于分布式系统中的另一个节点上,则无法直接访问其内部数据成员,但仍可以在向其发送消息时调用其方法。
通过对接口进行编码,我们可以编写代码,而不必关心目标对象的位置或者目标方法的调用方式,即使它是用同一语言编写的。
在示例中,该对象实现了集合的所有方法,那么可以确定该对象实际上是一个集合。因此也许在这种情况下,继承比封装好。