考虑到对象封装,吸气剂是否应返回不可变的属性?

时间:2020-03-06 14:32:04  来源:igfitidea点击:

当getter返回一个属性(例如返回其他相关对象的" List")时,该列表及其对象是否应该是不可变的,以防止类外部的代码改变这些对象的状态而又不知道主要的父对象?

例如,如果一个Contact对象有一个getDetails getter,它返回ContactDetails对象的List,那么任何调用该getter的代码:

  • 可以在没有" Contact"对象知道的情况下从该列表中删除" ContactDetail"对象。
  • 可以在不知道"接触"对象的情况下更改每个"接触细节"对象。

那么我们应该在这里做什么?我们应该只信任调用代码并返回容易可变的对象,还是艰难地为每个可变类创建一个不变的类?

解决方案

如果我们可以控制调用代码,那么最重要的是,我们所做的选择要在所有正确的位置得到正确记录。

我曾经返回列表的只读版本,或者至少返回一个副本。但是,列表中包含的每个对象都必须是可编辑的,除非它们在设计上是不可变的。

确实取决于上下文。但是通常,是的,应该编写尽可能防御的代码(返回数组副本,返回集合周围的只读包装器等)。无论如何,都应该清楚地记录下来。

这取决于我们是否应该在代码中"防御"。如果我们是班上的(唯一)用户,并且我们信任自己,那么就绝对不需要不变性。但是,如果此代码无论如何都需要工作,或者我们不信任用户,则使所有外部化的东西都是不可变的。

也就是说,我创建的大多数属性都是可变的。偶然的用户会对此造成破坏,但这又是他/她的错,因为有据可查地证明,不应通过通过吸气剂接收的可变对象发生突变。

我想我们会发现,每个可变表都是不可变的非常罕见。

我们可以做的是在此类对象中更改属性时引发事件。也不是完美的解决方案。

文档可能是最实用的解决方案;)

这取决于上下文。如果列表打算是可变的,那么当List拥有自己非常好的API时,将主类的API弄乱以对其进行变异的方法是没有意义的。

但是,如果主类无法应对突变,那么我们将需要返回一个不可变列表,并且列表中的条目本身也可能需要不可变。

不过,请不要忘记,我们可以返回一个自定义List实现,该实现知道如何通过触发事件或者直接执行任何必需的操作来安全地响应变异请求。实际上,这是使用内部类的好时机的经典示例。

当我刚开始时,我仍然深受HIDE YOUR DATA OO PRINCIPALS LOL的影响。我会坐下来思考一下,如果有人更改了属性公开的对象之一的状态,将会发生什么。我应该让它们仅对外部呼叫者只读吗?我不应该完全暴露它们吗?

收藏将这些忧虑带到了极致。我的意思是,有人可以在我不看时删除集合中的所有对象!

我最终意识到,如果对象对它们的外部可见属性和类型的依赖如此紧密,以至于如果有人在一个不好的地方触摸它们,那么架构就会出现缺陷。

有充分的理由使外部属性为只读且其类型不可变。但这是一个极端的案例,不是典型的案例,恕我直言。

首要任务应该是遵循Demeter法则或者Tell不要问。告诉对象实例该做什么,例如

contact.print( printer ) ;  // or
contact.show( new Dialog() ) ; // or
contactList.findByName( searchName ).print( printer ) ;

面向对象的代码告诉对象去做事情。程序代码获取信息,然后对该信息进行操作。要求对象揭示其内部细节将破坏封装,它是过程代码,而不是健全的OO编程,并且正如Will已经说过的那样,这是一个有缺陷的设计。

如果遵循Demeter法则,则对象状态的任何变化都将通过其定义的界面发生,因此副作用是已知的并且可以控制。问题就解决了。

在Java中的Collection,List,Set或者Map的特定情况下,很容易使用return returns Collections.unmodifiableList(list);将不变的视图返回给类。

当然,如果可能仍会修改后备数据,则需要制作列表的完整副本。

首先,setter和getter表示OO不好。通常,OO的想法是我们要求对象为我们做一些事情。设置和获取是相反的。 Sun应该想出了另一种实现Java Bean的方法,这样人们就不会选择这种模式并认为它是"正确的"。

其次,我们拥有的每个对象本身都应该是一个世界-通常,如果我们要使用setter和getter,它们应该返回相当安全的独立对象。这些对象可能是或者不是不变的,因为它们只是一流的对象。另一种可能性是它们返回总是不可变的本机类型。因此说"应该让设置者和获取者返回不可变的东西"没有太大意义。

至于自己制作不可变对象,除非我们有充分的理由不这样做,否则实际上应该始终使对象内部的成员成为最终成员(Final应该是默认值," mutable"应该是覆盖该默认值的关键字)。这意味着在任何可能的情况下,对象都是不可变的。

对于我们可能会传递的预定义的准对象事物,我建议我们将诸如集合和值组之类的东西包装在一起,并使用它们自己的方法合并到自己的类中。实际上,我从来没有绕过一个不受保护的集合,只是因为我们没有在明显使用精心设计的对象的地方就如何使用它提供任何指导/帮助。安全也是一个因素,因为允许某人访问我们班级内部的集合实际上几乎不可能确保该班级始终有效。

约书亚·布洛赫(Joshua Bloch)在其出色的"有效Java"一书中说,返回类似内容时,我们应该始终制作防御性副本。这可能有点极端,尤其是在ContactDetails对象不是Cloneable的情况下,但这始终是安全的方法。如有疑问,除非分析表明克隆是真正的性能瓶颈,否则始终偏向于代码安全胜于性能。

我们实际上可以添加几个保护级别。我们可以简单地返回该成员,这实际上是使其他任何类都可以访问类的内部。非常不安全,但是公平地广泛进行。如果我们想更改内部结构,以便将ContactDetails存储在Set中,这也会在以后给我们带来麻烦。我们可以返回一个新创建的列表,其中包含对内部列表中相同对象的引用。这样比较安全,另一个类无法删除或者添加到列表中,但是它可以修改现有对象。第三,返回一个新创建的列表,其中包含ContactDetails对象的副本。那是安全的方法,但可能会很昂贵。

我会做一个更好的方法。根本不返回任何列表,而是返回一个遍历列表的迭代器。这样,我们不必创建新列表(List具有获取迭代器的方法),但是外部类无法修改列表。它仍然可以修改项目,除非我们编写自己的迭代器以根据需要克隆元素。如果以后切换到内部使用另一个集合,它仍然可以返回迭代器,因此不需要进行外部更改。