Getter 消除者
2006 年 2 月 22 日
当他们看到一个 getter 方法时,你可以从他们嘴角左侧的抽搐中认出他们,他们会迅速拔出战斧,并发出一声满足的喊叫,因为另一个 getter 被无情地从一个类中砍了下来,这个类立即在英勇的 Getter 消除者脚下,陶醉于感激的狂喜中。
好吧,也许我回归英国啤酒对我的影响有点大,但 Chris 的 温和调整触及了我心中一个挥之不去的烦恼。我经常遇到一些人告诉你避免在类中使用 getter 方法,将这种行为视为对封装的违反,Allen Holub 的文章 是最著名的文章之一。
通常的理由是 getter 违反了封装。如果我有一个保龄球类,其中包含 overs、runs 和 wickets 的字段,那么添加 getter(getOvers、getRuns、getWickets)与直接将字段设为 public 没什么区别。
这个论点有一定的道理,我当然建议你只有在真正需要时才编写访问器,但这也会带来错过封装要点的风险。对我来说,封装的重点不是隐藏数据,而是隐藏设计决策,尤其是在这些决策可能需要改变的领域。内部数据表示是其中一个例子,但并非唯一例子,也并非总是最佳例子。用于与外部数据存储进行通信的协议就是一个很好的封装示例 - 它更多地与该存储的消息有关,而不是与任何数据表示有关。当你思考封装时,我认为最好问问自己“你隐藏了什么可变性,以及为什么”,而不是“我是否暴露了数据”。(Craig Larman 为我写了一篇关于此的 不错的专栏。)
虽然对封装的辩护是 getter 消除者常用的集结号,但我认为他们真正的动机更加合理和务实。在面向对象语言中,有大量的代码在设计上是过程式的。面向对象社区可能已经“获胜”,因为现代语言以对象为主导,但他们尚未最终获胜,因为面向对象编程还没有得到广泛应用。因此,我们仍然经常看到从对象中提取数据以执行某些操作的过程,而这种行为更适合在对象本身中执行 - 违反了务实程序员的“告诉,不要问”原则。只有在你有 getter 的情况下才能进行这种过程式编程,因此告诉人们摆脱 getter 有助于推动他们将行为移到正确的位置。
我非常同情这种动机,但我担心仅仅告诉人们避免 getter 是一种过于粗暴的工具。在许多情况下,对象确实需要通过交换数据进行协作,这会导致对 getter 的真正需求。
如果我们正在寻找一个简单的经验法则,我更喜欢一个我第一次从 Kent Beck 那里听到的经验法则,那就是始终警惕一些代码对同一个对象调用多个方法的情况。这发生在访问器和更合理的命令中。如果你向一个对象请求两部分数据,你能用一个请求来代替你正在计算的数据吗?如果你告诉一个对象做两件事,你能用一个命令来代替它们吗?当然,有很多情况你做不到,但始终值得问问自己这个问题。
另一个预示着麻烦的良好迹象是数据类 - 一个只有字段和访问器的类。这几乎总是预示着麻烦,因为它缺乏行为。如果你看到这样的类,你应该始终保持怀疑。看看谁使用了这些数据,并尝试看看是否可以将一些行为移到对象中。在这些情况下,问问自己“我能否摆脱这个 getter?”可能会有所帮助。即使你做不到,提出这个问题也可能导致一些良好的行为迁移。
对象之间行为的分配是面向对象设计的核心,因此与任何设计一样,没有硬性规定 - 而是权衡取舍的判断。将行为放在与数据相同的类中,Craig Larman 称之为“信息专家”,是首选。但这并非唯一途径。分层通常胜过这一点,许多四人帮模式为了特定需求将数据与行为分离。一个好的经验法则是,一起变化的东西应该放在一起。数据和使用它的行为通常一起变化,但你经常会看到其他更重要的分组。