面向对象编程:这里我说了算!

自 9 岁拥有第一台 Commodore 64 家用电脑起,我就开始编程了。然而,面对“如何写出好代码”这个问题,我仍然感觉自己有很多需要学习的地方。

在探索如何提高编程水平的过程中,我学习了很多种语言,其中大多数是以面向对象(Object-Oriented, OO)为主的。

然而,令人惊讶的是,在我读过的大多数书本、杂志和网络文章中,存在着大量糟糕透顶的代码,但它们却被当作面向对象的范例。

在这些代码中,我看到被违反得最频繁的原则是 “命令,不要去询问”(Tell, Don't Ask) 原则。这个原则的核心在于:一个对象应该命令其它对象该做什么,而不是去查询其它对象的状态来决定自己要做什么。查询其它对象的状态来决定做什么,也被称作 “功能嫉妒”(Feature Envy)。在面向对象编程中,一个对象被定义为由“对象状态”和“操作这个状态的方法”组成。

在《Holub on Patterns: Learning Design Patterns By Looking At Code》这本书里,Allen Holub 在第一章中有一节的标题是“为什么 getter 和 setter 方法有害”。他在 JavaWorld 上的一篇文章里也谈论了这个问题。对所有的面向对象程序员来说,这应该是一篇“必读”文章。

我有一些程序员同事,他们在为一个对象声明了属性后,第一步就是添加 getter 和 setter 方法。JavaBean 规范对于这种文化的推广负有很大责任。人们认为这是一种能让你写出可复用的模块化组件的好方法,但这已是很多年前的事了,时过境迁。

编写带有 getter 和 setter 方法的类会导致过程式代码。通过 getter 和 setter 来获取数据进行操作的逻辑最终会遍布整个应用,进而经常导致应用内的重复(这违反了另外一个原则:DRY——不要自我重复 (Don't Repeat Yourself))。这会致使产生很难维护的代码,当你对一个类做任何修改时,都会在整个应用内造成连锁式的牵连。

用这种方式来暴露数据还会妨碍你重构你的类,因为对这样的属性的任何修改都意味着会影响到访问了这个属性的其它类。

违反“命令,不要去询问”原则的另外一个副作用是,你的探询最终会变成严重依赖状态信息并带有很多前提条件。这会让人很难理解你究竟询问的是什么。

你很可能会最终违反的第三个原则是,最少知识(Least Knowledge) 原则,也叫作 得墨忒耳定律(Law Of Demeter)。这个定律可以总结为下面一句话:

一个类应该只跟它的直接朋友通话,不要跟陌生人说话。

在类里面加入 getter 方法,你的代码最终会写成这样:

if (person.getAddress().getCountry() == "Australia") {

这违反了得墨忒耳定律,因为这个调用者跟 Person 过于亲密。它知道 Person 里有一个 Address,而 Address 里还有一个 country。它实际上应该写成这样:

if (person.livesIn("Australia")) {

这并不违反得墨忒耳定律,因为这个调用者只跟它的直接朋友 Person 通话,而且它不知道内部情况。从“命令,不要询问”的视角来看,这也是更好的做法,因为确定这个 person 是否居住在 Australia 的逻辑被隐藏到了 Person 里。如果我们改变 Person 里存储国家信息的方式,这将不会影响任何依赖这个信息的其它类。

另外一个这样写代码的好处是,它把我们的意图显示得很清楚。这是我会在以后的时间里讨论的另外一个话题。

为了避免违反“命令,不要去询问”原则,有几样事情你要记在心里:

  • 在 IDE 里敲击快捷生成键前,询问自己 ‘五个为什么’,问问自己为什么一上来就添加 getter 和 setter 方法。
  • 不要询问对象的状态。要做什么,告诉它们该怎么做。
  • 操作所属的对象拥有这些操作的数据。

进一步阅读

我的一个朋友是这个思想的大力倡导者,他用 East Oriented 方式精彩地解释了这个原则。

《Tell Don't Ask and the effect on testing》,出自 Steve Freeman 和 Nat Pryce,他们也是《Growing Object-Oriented Software》、《Guided by Tests》两本书的作者。

[英文原文:I give the orders around here! ]


说明:本文原文发表于 2012 年左右,文中提到的部分观点(如 JavaBean 规范的影响)具有特定的时代背景,但核心设计原则(Tell, Don't Ask、得墨忒耳定律)在当今面向对象设计中依然适用。