原文:http://www.pragprog.com/articles/tell-dont-ask
Alec Sharp, in the recent book Smalltalk by Example [SHARP], points up a very valuable lesson in few words:
引用
Procedural code gets information then makes decisions. Object-oriented code tells objects to do things.— Alec Sharp
That is, you should endeavor to tell objects what you want them to do; do not ask them questions about their state, make a decision, and then tell them what to do.
The problem is that, as the caller, you should not be making decisions based on the state of the called object that result in you then changing the state of the object. The logic you are implementing is probably the called object’s responsibility, not yours. For you to make decisions outside the object violates its encapsulation.
Sure, you may say, that’s obvious. I’d never write code like that. Still, it’s very easy to get lulled into examining some referenced object and then calling different methods based on the results. But that may not be the best way to go about doing it. Tell the object what you want. Let it figure out how to do it. Think declaratively instead of procedurally!
It is easier to stay out of this trap if you start by designing classes based on their responsibilities, you can then progress naturally to specifying commands that the class may execute, as opposed to queries that inform you as to the state of the object.
Just the Data
The main purpose of this exercise is to ensure a correct division of responsibility that places the right functionality in the right class without causing excess coupling to other classes.
The biggest danger here is that by asking for data from an object, you are only getting data. You’re not getting an object—not in the large sense. Even if the thing you received from a query is an object structurally (e.g., a String) it is no longer an object semantically. It no longer has any association with its owner object. Just because you got a string whose contents was “RED”, you can’t ask the string what that means. Is it the owners last name? The color of the car? The current condition of the tachometer? An object knows these things, data does not.
The fundamental principle of Object Oriented programming is the unification of methods and data. Splitting this up inappropriately gets you right back to procedural programming.
Invariants aren’t enough
Every class has invariants—things that must always be true. Some languages (such as Eiffel) provide direct support for specifying and checking invariants. Most languages do not, but that only means that the invariants are not stated—they still exist. For instance, an iterator has the invariant that (using Java as an example):
hasMoreElements() == true
// implies that:
nextElement()
// will return a value
In other words, if hasMoreElements() is true, then attempting to get the next element must succeed, or something is seriously broken. If you are running multi-threaded code without the proper synchronization (locking), it may well be that the above invariant doesn’t hold: some other thread grabbed the last element before you did.
The invariant doesn’t hold; so something is wrong—you have a bug.
According to Design by Contract, as long as your methods (queries and commands ) can be freely intermixed, and there is no way to violate the class invariant by doing so, then you are ok. But while you are maintaining the class invariant, you may have also dramatically increased the coupling between the caller and the callee depending on how much state you have exposed.
For instance, suppose you have a container object C. You could expose iterators for the held objects in this container, as many of the JDK core routines do, or you could provide a method that would run some function over all members in the collections for you. In Java you might declare this as something like:
public interface Applyable {
public void each(Object anObject);
}
...
public class SomeClass {
void apply(Applyable);
}
// Called as:
SomeClass foo;
...
foo.apply( new Applyable() {
public void each(Object anObject) {
// do what you want to anObject
}
});
(Forgive the neologic barbarism of “Apply-able”; we’ve found it handy to name interfaces as “-able”, but English isn’t as cooperative as one would like).
This is easier to code in languages with function pointers, and even easier in Perl or Smalltalk where such concepts are built in, but you should get the idea: “run this function over all contained items, I don’t care how.”
You can achieve the same results both ways, either via an apply sort of method or via iterators. The choice really comes down to how much coupling you are willing to have: To minimize coupling, expose the minimum amount of state necessary. As shown here, apply exposes less state than exposing an iterator does.
Law of Demeter
So we’ve decided to expose as little state as we need to in order to accomplish our goals. Great! Now within our class can we just starting sending commands and queries to any other object in the system will-nilly? Well, you could, but that would be a bad idea, according to the Law of Demeter. The Law of Demeter tries to restrict class interaction in order to minimize coupling among classes. (For a good discussion on this topic, see
[APPLETON]).
What that means is that the more objects you talk to, the more you run the risk of getting broken when one of them changes. So not only do you want to say as little as possible, you don’t want to talk to more objects than you need to either. In fact, according to the Law of Demeter for Methods, any method of an object should only call methods belonging to:
- itself.
- any parameters that were passed in to the method.
- any objects it created.
- any composite objects.
Specifically missing from this list is methods belonging to objects that were returned from some other call. For example (we’ll use Java syntax here):
SortedList thingy = someObject.getEmployeeList();
thingy.addElementWithKey(foo.getKey(), foo);
This is what we are trying to prevent. (We also have an example of Asking instead of Telling in foo.getKey()). Direct access of a child like this extends coupling from the caller farther than it needs to be. The caller is depending on these facts:
- someObject holds employees in a SortedList.
- SortedList’s add method is addElementWithKey()
- foo’s method to query its key is getKey()
Instead, this should be:
someObject.addToThingy(foo);
Now the caller is only depending on the fact that it can add a foo to thingy, which sounds high level enough to have been a responsibility, not too dependent on implementation.
The disadvantage, of course, is that you end up writing many small wrapper methods that do very little but delegate container traversal and such. The cost tradeoff is between that inefficiency and higher class coupling.
The higher the degree of coupling between classes, the higher the odds that any change you make will break something somewhere else. This tends to create fragile, brittle code.
Depending on your application, the development and maintenance costs of high class coupling may easily swamp run-time inefficiencies in most cases.
Command/Query Separation
Now back to to the ask vs. tell thing. To ask is a query, to tell is a command. I subscribe to the notion of maintaining these as separate methods. Why bother?
- It helps to maintain the “Tell, Don’t Ask” principle if you think in terms of commands that perform a very specific, well defined action.
- It helps you to think about class invariants if you class is primarily command based. (If you are just tossing data out, you probably aren’t thinking much in the way of invariants).
- If you can assume that evaluation of a query is free of any side effects, then you can:
[list]
- use queries from within a debugger without affecting the process under test.
- create built-in, automatic regression tests.
- evaluate class invariants, pre- and post-conditions.
[/list]
The last, of course, is why Eiffel requires only side-effect free methods to be called from within an Assertion. But even in C++ or Java, if you want to manually check the state of an object at some point in the code, you can do so with confidence if you know the queries you are calling will not cause anything else to change.
References
[SHARP] Sharp, A. “Smalltalk By Example” McGraw-Hill, 1997.
[APPLETON] Appleton, B.
Introducing Demeter and Its Laws
分享到:
相关推荐
2. 改为否定式:Don't ask him. 3. 改为否定式:Please don't wait for her. 4. 改为否定式:Don't read the book carelessly. 5. 改为否定式:Don't sit under the tree, please. 6. 改为否定式:Let's not go ...
在使用seek()函数时,有时候会报错为 “io.UnsupportedOperation: can’t do nonzero cur-relative seeks”,如下: with open('C:/Users/Desktop/xian.txt', 'r') as filename: print(filename.read()) print...
旋转验证码识别是验证码识别技术的一个分支,专门针对那些包含旋转元素的验证码进行破解。 在当前的网络安全环境中,验证码已经成为防止垃圾邮件、刷票、恶意注册等行为的重要工具。而随着人工智能和深度学习技术的...
直接引语变间接引语是英语语法中的一个重要概念,它涉及到句子结构的转换,通常用于转述别人的话。直接引语是直接引用某人的话语,而间接引语则是以自己的话来转述这些内容。在转换过程中,需要注意时态、人称、语序...
根据提供的文件信息,我们可以从《Don't Sweat the Small Stuff》这本书中提炼出一系列与心态调整相关的知识点。本书作者为Richard Carlson博士,旨在帮助人们通过简单的方法来避免生活中琐碎小事对情绪的影响。以下...
TellMe 是一个用于创建面向用户的报告的工具包。 客户端将条目写入报告后端(具有可选架构),并以多种格式生成输出。 条目是 Python 字典,如果提供,则符合模式。 可以从后端源以多种报告格式生成报告。 地位 ...
完整工程案例:图像描述---Show and Tell: A Neural Image Caption Generator,基于Inception V3与LSTM实现图像描述,运行环境(Tensorflow1.0及以上,Python3.6)
祈使句是英语语法中的一个重要概念,特别是在初中英语学习阶段。祈使句主要用于表达命令、请求、建议等,其特点是通常没有明确的主语,直接以动词原形开头,或者在强调时会加上助动词"Do"。下面将详细阐述祈使句的...
Steal This File Sharing Book: What They Won't Tell You About File Sharing 有问题,我的QQ:571669275
Steal This Computer Book 3: What They Won't Tell You About the Internet 有问题,我的QQ:571669275
Tellme - 文本到语音命令执行通知程序Tellme 运行命令并在过程完成时通过文本到语音通知用户。 例如: tellme sudo yum update最终会说“成功完成 yum 更新”。 是的,它足够聪明,可以去掉 sudo。 一些配置选项可用...
完整工程:图像描述---Show and Tell: A Neural Image Caption Generator,使用keras实现图像描述,运行环境(keras==2.0.3,tensorflow==1.1.0,pandas==0.19.1,numpy==1.12.1,h5py==2.7.0,matplotlib==2.1.0,...
这篇PPT课件主要讲解了祈使句的使用和如何将其转化为间接引语,这是英语语法中的一个重要知识点。祈使句通常用来表达命令、请求、劝告、警告或禁止等,其基本结构是动词原形加上其他成分,或者以"Please"或"Don't/...
保持句意不变:原句表示Sandy比班上任何其他女孩都年轻,等同于她比班上任何一个女孩都年轻。填入"the other",句子变为:Sandy is younger than any of the other girls in her class. A4. 对划线部分提问:提问...
本题主要涉及的是英语中的定语从句,这是英语语法中的一个重要部分,它用于修饰名词或代词,使表达更为精确。定语从句通常由关系词(如who, which, that等)引导,关系词在从句中担任一定的成分,如主语、宾语、定语...
否定祈使句是在动词原形前加"Don't",如"Don't eat in the classroom."(不要在教室吃东西)。 9. **时间状语的使用**: 如"on Monday"(在周一)和"on Sunday"(在周日),表示特定时间发生的动作。 10. **介词的...
Tellus公司,作为加拿大的主要通信服务提供商,选择了Sun Microsystems的Sun ONE架构来构建一个以Web为中心的解决方案,以满足服务提供商(SP)的需求。这个灵活的架构允许客户随时随地通过任何设备进行Web业务。 Sun...
- 第一个空缺:He told Li Ping not to forget to give the message to his wife. (将"Don’t forget to give the message to my wife"转换为间接引语) - 第四个空缺:Mother told me not to spend too much time ...