关于“告诉,不要问”如何被认为是好的面向对象的解释


49

这篇博文已在Hacker News上发表,并有几篇推荐。来自C ++的大多数示例似乎与我所教的内容背道而驰。

例如示例2:

坏:

def check_for_overheating(system_monitor)
  if system_monitor.temperature > 100
    system_monitor.sound_alarms
  end
end

与好:

system_monitor.check_for_overheating

class SystemMonitor
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

C ++中的建议是,您应该更喜欢自由函数而不是成员函数,因为它们会增加封装性。两者在语义上是相同的,那么为什么更喜欢可以访问更多状态的选择呢?

范例4:

坏:

def street_name(user)
  if user.address
    user.address.street_name
  else
    'No street name on file'
  end
end

与好:

def street_name(user)
  user.address.street_name
end

class User
  def address
    @address || NullAddress.new
  end
end

class NullAddress
  def street_name
    'No street name on file'
  end
end

为什么User要格式化不相关的错误字符串?如果我'No street name on file'没有印刷品,但又没有街道,该怎么办?如果这条街的名字相同,该怎么办?


有人可以启发我“讲,不要问”的好处和理由吗?我不是在寻找更好的方法,而是试图了解作者的观点。


我不知道,代码示例可能是Ruby,而不是Python。
Pubby 2012年

2
我总是想知道第一个示例是否不是违反SRP?
stijn 2012年


红宝石。@例如是简写形式,Python用空格隐式结束其块。
Erik Reppen

3
“ C ++中的建议是,您应该更喜欢自由函数而不是成员函数,因为它们会增加封装性。” 我不知道是谁告诉你的,但这不是真的。可以使用自由函数来增加封装,但是它们不一定会增加封装。
罗伯K

Answers:


81

询问对象的状态,然后根据在对象外部做出的决定在该对象上调用方法,这意味着该对象现在是一个泄漏抽象。它的某些行为位于对象的外部,内部状态(可能不必要)暴露于外部世界。

您应该尽力告诉对象您要它们做什么。不要问他们关于他们的状态的问题,做出决定,然后告诉他们该怎么做。

问题在于,作为调用者,您不应基于所调用对象的状态来做出决定,而导致您随后更改对象的状态。您正在实现的逻辑可能是被调用对象的责任,而不是您的责任。您在对象之外进行决策会违反其封装。

当然,您可能会说,这很明显。我永远不会那样写代码。仍然很容易陷入沉迷于检查一些引用的对象,然后根据结果调用不同的方法的情况。但这可能不是最好的方法。告诉对象您想要什么。让它弄清楚如何做。声明式思考,而不是程序性思考!

如果您开始根据班级的职责设计班级,那么更容易摆脱陷阱。然后,您可以自然地前进到指定类可以执行的命令,而不是将对象状态通知给您的查询。

http://pragprog.com/articles/tell-dont-ask


4
示例文本禁止许多明显是好的做法。
DeadMG 2012年

13
@DeadMG只会对那些狂热地跟随它的人说您要说的话,他们盲目地忽略了站点名称和站点作者的关键思想中的“实用主义”,这些想法已在其关键书中明确指出:“没有这样的事情是最佳解决方案...”
gna

2
永远不要读这本书。我也不想。我只阅读示例文本,这是完全公平的。
DeadMG

3
@DeadMG不用担心。现在,你知道,在预期的范围内(“没有这样的事,作为一个最佳的解决方案......”)提出这个例子(和其它任何一个从pragprog为此事)关键点,这是确定不读的书
蚊蚋

1
我仍然不确定在没有上下文的情况下应该告诉您什么“告诉,不要问”,但这确实是很好的OOP建议。
Erik Reppen

16

通常,该文章建议,如果您可以自己对会员国进行推理,则不要让会员国对其进行推理。

但是,没有明确说明的是,当推理超出特定类的责任范围时,该法律将陷入非常明显的限制。例如,每个其工作是持有某种价值或提供某种价值的类(尤其是通用类),或者该类提供必须扩展的行为的类。

例如,如果系统提供temperature查询作为查询,那么明天客户就可以check_for_underheating不必进行更改SystemMonitor。当SystemMonitor工具check_for_overheating本身实现时,情况并非如此。因此,一个SystemMonitor班级的工作是在温度过高时发出警报,但该SystemMonitor班级的工作是允许另一段代码读取温度,以便它可以控制诸如TurboBoost之类的东西。 , 不应该。

还要注意,第二个示例毫无意义地使用了Null Object Anti-pattern。


19
“空对象”不是我所说的反模式,所以我想知道您这样做的原因是什么?
康拉德·鲁道夫2012年

4
相当确定没有人有指定为“什么都不做”的任何方法。这使得称他们为毫无意义。这意味着任何实现Null Object的对象都至少会破坏LSP,并将其描述为实际上没有实现的实现操作。用户期望返回一个值。他们的程序的正确性取决于此。假装它不是一个值,您只会带来更多问题。您是否曾经尝试调试无提示失败的方法?这是不可能的,没有人应该经历过那样的苦难。
DeadMG

4
我认为这完全取决于问题领域。
康拉德·鲁道夫2012年

5
@DeadMG我同意上面的例子中是空对象图案的不好的用法,但有一个优点,以使用它。几次我使用某个接口或其他接口的“无操作”实现来避免进行空检查或在系统中渗透真正的“空”。
2012年

6
不确定我check_for_underheating是否同意SystemMonitor“ 客户无需更改即可”。与SystemMonitor那时的客户有何不同?您现在不是在多个类中分散监视逻辑吗?我也看不到监视器类的问题,该监视器类向其他类提供传感器信息,同时保留其自身的警报功能。升压控制器应该控制升压,而不必担心如果温度过高会发出警报。
TMN 2012年

9

过热示例的真正问题在于,对于不同系统而言,符合过热标准的规则很难轻易改变。假设系统A是您所拥有的(温度> 100过热),但是系统B更精密(温度> 93过热)。您是否更改控制功能以检查系统的类型,然后应用正确的值?

if (system is a System_A and system_monitor.temp >100)
  system_monitor.sound_alarms
else if (system is a System_B and system_monitor.temp > 93)
  system_monitor.sound_alarms
end

还是您有每种类型的系统定义其加热能力?

编辑:

system.check_for_overheating

class SystemA : System
  def check_for_overheating
    if temperature > 100
      sound_alarms
    end
  end
end

class SystemB : System
  def check_for_overheating
    if temperature > 93
      sound_alarms
    end
  end
end

当您开始处理更多系统时,前一种方法会使您的控制功能变得难看。后者使控制功能随着时间的流逝而稳定。


1
为什么不让每个系统都在监视器上注册。在注册期间,它们可以指示何时发生过热。
马丁·约克

@LokiAstari-您可以,但是随后您可能会遇到对湿度或大气压也敏感的新系统。原理是抽象出变化的原因-在这种情况下,这是过热的敏感性
Matthew Flynn 2012年

1
这就是为什么您应该拥有一个告诉模型的原因。您告诉系统当前状态,并通知您是否超出正常工作条件。这样,您无需修改​​SystemMoniter。那就是您的封装。
马丁·约克

@LokiAstari-我认为我们在这里是出于交叉目的讨论的-我确实在寻找创建不同的系统,而不是创建不同的监视器。问题是,与某些外部控制器功能相反,系统应该知道何时处于发出警报的状态。SystemA应该有其标准,SystemB应该有其标准。控制器应该能够(定期)询问系统是否正常。
马修·弗林

6

首先,我认为我必须对您对示例的描述为“差”和“好”表示例外。本文使用“不太好”和“更好”这两个词,我认为选择这些词是有原因的:这些是​​指导原则,根据情况,“不太好”的方法可能是适当的,或者实际上是唯一的解决方案。

当有选择权时,您应该优先考虑在类包括仅依赖类而不是类外部的所有功能-原因是由于封装,以及随着时间的推移使类更容易演化的事实。与大量免费功能相比,该类在宣传其功能方面也做得更好。

有时您必须说出来,因为决策依赖于班级之外的事情,或者仅仅是您不希望班级的大多数用户去做。有时您想要说出来,因为该行为对于班级而言是直觉相反的,并且您不想混淆班级的大多数用户。

例如,您抱怨街道地址返回错误消息,不是,它正在提供默认值。但是有时默认值不合适。如果这是州或城市,则在将记录分配给业务员或调查接受者时可能需要一个默认值,以便所有未知数都归特定的人所有。另一方面,如果您要打印信封,则您可能更希望使用一种例外或防护措施,以免您在无法传递的信件上浪费纸张。

因此,在某些情况下,“不太好”是可行的方法,但总的来说,“更好”是更好的选择。


3

数据/对象反对称

正如其他人指出的那样,Tell-Dont-Ask专门用于在您询问后更改对象状态的情况(例如,参见本页其他位置发布的Pragprog文本)。情况并非总是如此,例如,在询问“ user”对象的user.address后未对其进行更改。因此,是否适用Tell-Dont-Ask是合适的情况,这是有争议的。

Tell-Dont-Ask关心的是责任,而不是将逻辑从合理的类中拉出来。但是,并非所有处理对象的逻辑都必然是那些对象的逻辑。这甚至暗示了更深层次的含义,甚至超越了Tell-Dont-Ask的要求,我想对此做一个简短的评论。

就建筑设计而言,您可能希望拥有实际上只是属性容器的对象,甚至可能是不可变的,然后在这些对象的集合上运行各种功能,评估,过滤或转换它们,而不是向其发送命令(更多的Tell-Dont-Ask域名)。

对您的问题更合适的决定取决于您是否希望拥有稳定的数据(声明性对象),但要在功能方面进行更改/添加。或者,如果您希望此类功能的集合稳定且有限,但是希望在对象级别获得更多的变化,例如通过添加新类型。在第一种情况下,您更喜欢自由函数,在第二种对象方法中。

鲍勃·马丁(Bob Martin)在他的“清洁代码”一书中将其称为“数据/对象反对称性”(第95ff页),其他社区也将其称为“ 表达问题 ”。


3

这种范例有时被称为“告诉,不要问”,意思是告诉对象要做什么,不要问它的状态。有时称为“问,不告诉”,意思是要求对象为您做一些事情,而不告诉它的状态。围绕最佳实践的两种方法都是相同的-对象执行操作的方式是对象的关注点,而不是调用对象的关注点。接口应避免暴露其状态(例如,通过访问器或公共属性),而应暴露实现不透明的“正在执行”的方法。其他人则通过实用程序程序员的链接对此进行了介绍。

该规则与避免使用“双点”或“双箭头”代码(通常被称为“仅与直交朋友交谈”)的规则有关,该规则指出foo->getBar()->doSomething()情况不好,而应使用foo->doSomething();围绕bar功能的包装调用,并且实施简单return bar->doSomething();—如果foo负责管理bar,那就去做吧!


1

除了关于“告诉,不要问”的其他好答案之外,对您的特定示例的一些评论可能会有所帮助:

C ++中的建议是,您应该更喜欢自由函数而不是成员函数,因为它们会增加封装性。两者在语义上是相同的,那么为什么更喜欢可以访问更多状态的选择呢?

该选择无法访问更多状态。他们都使用相同数量的状态来完成工作,但是“不好的”例子要求类状态必须公开才能完成其工作。此外,“不良”示例中该类的行为被扩展到自由函数,从而使其更难查找且更难重构。

为什么用户负责格式化不相关的错误字符串?如果没有街道,我除了打印“文件中没有街道名称”之外,还想做些什么呢?如果这条街的名字相同,该怎么办?

为什么要同时执行“获取街道名称”和“提供错误消息”的职责是“ street_name”?至少在“好”版本中,每个部分都有一个责任。不过,这不是一个很好的例子。


2
这不是真的。您假设检查过热是唯一与温度有关的理智的事情。例如,如果该类打算成为许多温度监视器之一,并且系统必须根据其许多结果采取不同的措施,该怎么办?如果可以将这种行为限制为单个实例的预定义行为,请确定。否则,显然不适用。
DeadMG

当然可以,或者恒温器和警报是否存在于不同的类别中(可能应该如此)。
Telastyn

1
@DeadMG:一般建议是将事物设为私有/受保护,直到您需要访问它们为止。虽然这个例子很具体,但这与标准做法没有争议。
古凡特

一篇文章中关于这种做法是“ meh” 的例子对此提出了质疑。如果这种做法具有很大的好处,因此它是标准做法,那么为什么要找一个合适的例子却很麻烦呢?
Stijn de Witt

1

这些答案非常好,但是这里只是强调另一个示例:请注意,通常这是避免重复的一种方法。例如,假设您有多个地点,其代码如下:

Product product = productMgr.get(productUuid)
if (product.userUuid != currentUser.uuid) {
    throw BlahException("This product doesn't belong to this user")
}

那意味着你最好有这样的东西:

Product product = productMgr.get(productUuid, currentUser)

因为该重复意味着界面的大多数客户端将使用新方法,而不是在此处和此处重复相同的逻辑。您可以将要完成的工作交给您的代表,而不是亲自询问所需的信息。


0

我相信在编写高级对象时更是如此,但在深入到更高级的类(例如类库)时,情况就不那么正确了,因为不可能编写每个方法来满足所有类消费者。

例如#2,我认为它过于简单。如果实际上要实现这一点,那么SystemMonitor最终将把用于底层硬件访问的代码和用于高层抽象的逻辑嵌入到同一类中。不幸的是,如果我们试图将其分为两个类,则会违反“告诉,不要问”本身。

#4示例大致相同-将UI逻辑嵌入数据层。现在,如果我们要修复在没有地址的情况下用户希望看到的内容,我们必须修复数据层中的对象,如果两个项目使用同一对象但需要为空地址使用不同的文本该怎么办?

我同意,如果我们能够对所有内容实施“告诉,不要问”,那将非常有用-如果我能在现实生活中说出而不是问自己(自己去做),我自己会很高兴!但是,与现实生活中一样,该解决方案的可行性非常受限于高级课程。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.