啥是Tell,Don't Ask?
在这里,我把Alec Sharp大神的话摘抄在这里:
Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.
— Alec Sharp
译:面向过程编程是先获取状态信息再(根据条件判断)选择做什么事情,面向对象编程是直接告诉对象要做什么事情。
我觉得这句话很好的将面向过程和面向对象区分了出来。也就是说,对于对象来说,我应该直接告诉它你该做什么,而不是先去问它的状态,然后根据这个状态条件再去告诉对象你需要做什么什么,怎么怎么做,至于具体如何去做,不应该是我考虑的事情,而是对象自己的事情,你只发出命令,然后对象做完后返回给你结果即可,你不需要知道拿到这个结果的具体过程是如何实现的。
仔细回想一下,我还真在实际编程时做过这种事情:先去调用一个对象的getter方法得到某个状态,然后判断一下这个状态,最后根据这个状态调用对象的另外某个方法来让对象去完成某个操作。其实,仔细想想,对象的状态是什么样,该不该做,以及如何做都是对象自己该处理的,而不是我该操心的,这么做只能破坏了对象的封装,让调用它的代码和这个对象紧耦合起来。
那么,这个原则真的很重要么?
可以设想这样一种情景:我在程序中调用一个对象的某个getter方法,得到一个该对象的内部状态,比如得到的是一个String类型的东西。如果我接下来把这个String类型的东西用在了程序其它的地方,这时就产生了一种问题,我必须知道这个String类型的东西代表什么意思。比如说这个String类型的值是”RED”,那么,这个RED是什么意思?是一种颜色?还是一个人名?还是某种缩写?对于这个东西的意思,这个东西所在的那个对象再清楚不过了,而离开了那个对象,它就会被滥用和错用。
我们常说,一个对象就是数据和方法的抽象与封装。在我理解来看,就是使这写状态和方法有了上下文和语义,脱离了上下文,就破坏了它的封装。
好吧,我把该让对象做的,都移到对象内部去。
这样没问题了吧。我不在对象外边去查询它的状态了。我让对象内部自己去做。那么,是不是在对象内部我可以随便来查询和调用对象的方法了?
不行,我们要遵守迪米特法则(Law of Demeter)
迪米特法则也称为最小知识原则(Least Knowledge Principle, LKP),简单说,如果两个对象不必直接通信,那么这两个对象就不应当发生直接的相互作用。如果一个对象需要调用另外一个对象的某个方法的话,那么应该通过第三个对象来转发调用。迪米特法则可以简单的说成:Talk only to your immediate friends。
迪米特法则强调一个问题:如果我们在实现某个方法时调用的对象越多,那么我们的程序耦合度就越高。一旦某个地方需要修改,都会带来麻烦。所以根据迪米特法则,我们在实现对象O的方法M时,M能够调用的对象应该只有:
对象O自己
M的参数
M内部创建的对象
O的直接组件对象
举个例子,我曾经这样写过某段代码:
BookList bookList = bookStore.getAllBooks();
if(!bookList.has(book.getID())
{
bookList.addBook(book.getID(), book);
}
代码很简单,我只是单纯的想往我们的商店中增加一本新的书。但是上面的这段简单的代码,依赖了bookStore、BookList甚至book对象,其实我们只是想单纯的在书店中加一本新书而已,为什么不能这么用呢?
bookStore.addNewBook(book);
这样,我们调用的代码仅仅以来bookStore一个对象了。
所以,请记住Tell,Don't Ask
这个真的可以帮助我,让我写出更加抽象的、松耦合的代码。
代码中,我看到的最多被违反的原则是“命令,不要去询问(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方法,你的代码最终会写成这样:
1 | if (person.getAddress().getCountry() == "Australia" ) { |
这违反了得墨忒耳定律,因为这个调用者跟Person过于亲密。它知道Person里有一个Address,而Address里还有一个country。它实际上应该写成这样:
1 | if (person.livesIn( "Australia" )) { |
这并不违反得墨忒耳定律,因为这个调用者只跟它的直接朋友Person通话,而且它不知道内部情况。从“命令,不要询问”的视角来看,这也是更好,因为确定这个person是否居住在Australia的逻辑被隐藏到了Person里,如果我们改变Person里存储国家信息的方式,这将不会影响任何依赖这个信息的其它类。