为什么继续使用具有不可变对象的getter?

时间:2013-01-11 05:45:04

标签: java c++ oop object immutability

使用不可变对象变得越来越普遍,即使手头的程序从不打算并行运行也是如此。然而,我们仍然使用getter,每个字段需要3行样板,每次访问需要5个额外字符(用你最喜欢的主流OO语言)。虽然这看似微不足道,而且许多编辑无论如何都会从程序员身上消除大部分负担,但这似乎仍然是不必要的努力。

继续使用访问器与不可变对象的直接字段访问有什么原因?具体来说,强制用户使用访问者(对于客户端或库编写者)是否有优势,如果是这样,他们是什么?


请注意,我指的是 immutable 对象,与question不同,后者通常引用对象。要清楚,不可变对象上没有setter。

8 个答案:

答案 0 :(得分:5)

我认为这实际上是语言依赖的。如果你能原谅我,我会稍微谈谈C#,因为我认为这有助于回答这个问题。

我不确定您是否熟悉C#,但它的设计,工具等非常直观且对程序员友好。
C#的一个特性(也存在于Python,D等中)有助于实现属性;一个属性基本上是一对方法(一个getter和/或一个setter),在外面看起来就像一个实例字段:你可以分配给它,你就可以像实例变量那样读取它。
当然,在内部,它是一种方法,它可以做任何事情。

但是C#数据类型有时也有GetXYZ()和SetXYZ()方法,有时它们甚至会直接暴露它们的字段......这就引出了一个问题:你如何选择在什么时候做?

Microsoft has a great guideline for C# properties and when to use getters/setters instead

  

属性的行为应该像字段一样;如果方法不能,则不应将其更改为属性。在以下情况下,方法优于属性:

     
      
  • 该方法执行耗时的操作。该方法明显慢于设置或获取字段值所需的时间。
  •   
  • 该方法执行转换。访问字段不会返回其存储的数据的转换版本。
  •   
  • Get方法具有可观察到的副作用。检索字段的值不会产生任何副作用。
  •   
  • 执行顺序很重要。设置字段的值不依赖于其他操作的发生。
  •   
  • 连续两次调用该方法会产生不同的结果。
  •   
  • 该方法是静态的,但返回一个可由调用者更改的对象。检索字段的值不允许调用者更改字段存储的数据。
  •   
  • 该方法返回一个数组。
  •   

请注意这些指南的整个目标是为了让所有属性看起来都像是外部字段。

因此,使用属性而不是字段的唯一真正原因是:

  1. 你想要封装,yada yada。
  2. 您需要验证输入。
  3. 您需要从其他地方检索数据(或将数据发送到其他地方。
  4. 您需要转发二进制(ABI)兼容性。我的意思是什么?如果你在某个时候,在某个时候,决定你需要添加某种验证(例如),然后将字段更改为属性并重新编译您的库将打破依赖于它的任何其他二进制文件。但是,在源代码级别,没有任何改变(除非你正在接收地址/引用,你可能不应该这样做。)
  5. 现在让我们回到Java / C ++和不可变数据类型。

    哪些要点适用于我们的情景?

    1. 有时它不适用,因为不可变数据结构的重点是存储数据,而不是(多态)行为(比如String数据类型)。
      如果您要隐藏数据并且不对其进行任何操作,那么存储数据的重点是什么? 但有时它确实适用(例如,假设你有一个不可变树) - 你可能不想暴露元数据 但在那种情况下,你显然会隐藏你不想公开的数据,而你首先不会问这个问题! :)
    2. 不适用;没有输入要验证,因为没有任何变化。
    3. 不适用,否则无法使用字段!
    4. 可能或可能不适用。
    5. 现在Java和C ++没有属性,但是方法取而代之 - 所以上面的建议仍然适用,并且没有属性的语言规则变为:

      如果(1)您不需要ABI兼容性,(2)您的getter就像一个字段(即它满足上面MSDN文档中的要求),那么你应该使用字段而不是getter。

      要认识到的重点是,这一切都不是哲学的;所有这些指南都基于程序员所期望的。显然,当天结束的目标是(1)完成工作,(2)保持代码可读/可维护。上面的指南被发现有助于实现这一目标 - 你的目标应该是做任何适合你的想法,以实现这一目标。

答案 1 :(得分:4)

封装有几个有用的目的,但最重要的是信息隐藏。通过将字段隐藏为实现细节,可以保护对象的客户端,使其不依赖于实际存在的字段。例如,对象的未来版本可能想要懒惰地计算或获取值,并且只有在您可以拦截读取字段的请求时才能执行此操作。

那就是说,没有理由让吸气者特别啰嗦。特别是在Java世界中,即使“get”前缀非常根深蒂固,你仍然会发现以值本身命名的getter方法(即方法foo()而不是getFoo()) ,这是保存一些角色的好方法。在许多其他OO语言中,您可以定义一个getter并仍然使用看起来像字段访问的语法,因此根本没有额外的详细程度。

答案 2 :(得分:2)

不可变对象应使用一致性的直接字段访问,因为它允许设计完全按照客户期望的方式执行的对象。

考虑一个系统,其中每个可变字段都隐藏在访问器后面,而每个不可变字段都不是。现在考虑以下代码段:

class Node {
    private final List<Node> children;

    Node(List<Node> children) {
        this.children = new LinkedList<>(children);
    }

    public List<Node> getChildren() {
        return /* Something here */;
    }
}

在不知道Node的确切实现的情况下,正如您在按合同设计时必须要做的那样,在您看到root.getChildren()的任何地方时,您只能假设正在发生以下三种情况之一:

  • 无。字段children按原样返回,您无法修改列表,因为您将破坏Node的不变性。为了修改List,你必须复制它,进行O(n)操作。
  • 它被复制,例如:return new LinkedList<>(children);。这是O(n)操作。您可以修改此列表。
  • 返回不可修改的版本,例如:return new UnmodifiableList<>(children);。这是O(1)操作。同样,为了修改这个List,你必须复制它,一个O(n)操作。

在所有情况下,修改返回的列表需要执行O(n)操作来复制它,而只读访问需要从O(1)或O(n)开始。这里要注意的重要一点是,通过按照合同进行设计,你无法知道库编写器选择哪个实现,因此必须假设最坏的情况,O(n)。因此, O(n)访问和O(n)创建自己的可修改副本。

现在考虑以下事项:

class Node {
    public final UnmodifiableList<Node> children;

    Node(List<Node> children) {
        this.children = new UnmodifiableList<>(children);
    }
}

现在,在您看到root.children的任何地方,都只有一种可能性,即它是UnmodifiableList因此您可以假设O(1)访问权限和O(n)来创建本地可变副本。

显然,在后一种情况下,可以得出关于访问该领域的性能特征的结论,而在前者中可以得出的唯一结论是在最坏的情况下的性能,因此我们必须假设,远比直接现场访问差。提醒一下,这意味着程序员必须在每次访问时考虑O(n)复杂度函数。


总之,对于这种类型的系统,无论何时看到getter,客户端都会自动知道getter对应于一个可变字段,或者getter执行某种操作,无论是耗时的O(n)防御性复制操作,延迟初始化,转换或其他。只要客户端看到直接字段访问,他们就会立即知道访问该字段的性能特征。

通过遵循这种风格,程序员可以推断出他/她正在与之交互的对象提供的合同的更多信息。这种风格还可以促进统一的不变性,因为只要您将上述代码段的UnmodifiableList更改为接口List,直接字段访问就可以使对象发生变异,从而强制将对象层次设计为从上到下是不可变的。

好消息是,您不仅可以获得不可变性的所有好处,而且无论在何处都能够推断出访问字段的性能特征,无需考虑实施情况,并且有信心它永远不会改变。

答案 3 :(得分:2)

Joshua Bloch,在Effective Java (2nd Edition)“第14项:在公共课程中,使用访问器方法,而不是公共字段,”有关于公开不可变字段的以下内容:

  

虽然公共类直接暴露字段绝不是一个好主意,但它确实如此   如果字段是不可变的,则危害较小。你不能改变表示   这样的类没有改变它的API,你不能在a时采取辅助动作   读取字段,但您可以强制执行不变量。

并总结了以下章节:

  

总之,公共类不应该公开可变字段。它少了   公共课程揭露不可变的领域是有害的,但仍然有问题。

答案 4 :(得分:1)

你可以拥有公共最终字段(模仿某种不变性),但这并不意味着引用的对象不能改变它们的状态。在某些情况下,我们仍需要防御性副本。

 public class Temp {
    public final List<Integer> list;

    public Temp() {
        this.list = new ArrayList<Integer>();
        this.list.add(42);
    }

   public static void foo() {
      Temp temp = new Temp();
      temp.list = null; // not valid
      temp.list.clear(); //perferctly fine, reference didn't change. 
    }
 }

答案 5 :(得分:1)

  

继续使用访问器与不可变对象的直接字段访问有什么原因?具体来说,强制用户使用访问者(对于客户端或库编写者)是否有优势,如果是这样,他们是什么?

你听起来像一个程序程序员,询问为什么你不能直接访问字段,但必须创建访问器。主要问题是即使你提出问题的方式也是错误的。这不是OO设计的工作原理 - 您通过它的方法设计对象行为并公开它。然后,根据需要创建内部字段,以便实现该行为。所以这样说:“我正在创建那些字段,然后通过一个getter暴露每个字段,这很详细”是OO设计不当的明显标志。

答案 6 :(得分:0)

封装字段然后仅通过getter方法公开它是一种OOP实践。如果直接暴露字段,则意味着您必须将其公开。公开领域并不是一个好主意,因为它暴露了对象的内在状态。

因此,将您的字段/数据成员公开并不是一个好习惯,它违反了OOP的封装原则。另外我会说它不是特定于Immutable对象;对于非不可变对象也是如此。

修改 正如@Thilo指出的那样;另一个原因:也许您不需要公开字段的存储方式。

感谢@Thilo。

答案 7 :(得分:0)

继续实践生成的一个非常实际的原因(我希望现在没有人用手写)在Java程序中获取,即使是不可变的&#34;值&#34;在我看来,这是不必要的开销:

许多库和工具都依赖于旧的JavaBeans约定(或者至少是getter和setter的一部分)。

这些使用反射或其他动态技术通过getter访问字段值的工具无法处理访问简单的公共字段。 JSP就是我想到的一个例子。

此外,现代IDE使得一次为一个或多个字段生成getter变得微不足道,并且在更改字段名称时更改getter的名称。

所以我们只是为不可变对象编写getter。