如何在“不要说话”和“命令查询分隔”之间进行选择?


25

告诉不要问的原则说:

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

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

一个简单的“告诉,不要问”的例子

Widget w = ...;
if (w.getParent() != null) {
  Panel parent = w.getParent();
  parent.remove(w);
}

告诉版本是...

Widget w = ...;
w.removeFromParent();

但是,如果我需要知道removeFromParent方法的结果,该怎么办?我的第一个反应就是更改removeFromParent以返回一个布尔值,该布尔值指示是否删除了父对象。

但是后来我遇到了“ 命令查询分离模式”,它说“不要这样做”。

它指出,每个方法都应该是执行操作的命令,或者是将数据返回给调用方的查询,但不能同时是两者。换句话说,提出问题不应改变答案。更正式地讲,方法仅在引用透明且因此没有副作用的情况下才应返回值。

这两个真的是彼此矛盾的,如何在两者之间进行选择?我可以和Pragmatic Programmer或Bertrand Meyer一起使用吗?


1
您将如何处理布尔值?
大卫,

1
听起来您正在过度投入编码模式而又无法平衡可用性
Izkata

布尔值...这是一个易于执行的示例,但类似于下面的写操作,目标是使其成为操作的状态。
2011年

2
我的观点是,您不应该专注于“退货”,而应该专注于您要使用的东西。就像我在另一条评论中说的那样,如果您专注于失败,那么请使用异常,如果您想在操作完成后做一些事情,那么请使用回调或事件,如果您想记录发生了什么,那就是小部件责任...为什么我要问您的需求。如果找不到需要返回某些内容的具体示例,则可能意味着您不必在两者之间进行选择。
大卫,

1
命令查询分隔是一个原则,而不是一种模式。模式是可以用来解决问题的东西。遵循原则是不会造成问题的。
candied_orange

Answers:


25

实际上,您的示例问题已经说明缺乏分解。

让我们稍微改变一下:

Book b = ...;
if (b.getShelf() != null) 
    b.getShelf().remove(b);

这并没有真正的不同,但是使缺陷更加明显:为什么书会知道它的书架?简而言之,它不应该。它引入了书在书架上的依赖性(这没有意义),并创建了循环引用。都不好

同样,小部件也不必知道其父级。您会说:“好吧,但是小部件需要它的父级来正确地布局自身等。” 并且在内部,小部件了解其父级,并要求其提供其度量标准,以便根据它们来计算自己的度量标准。据告诉,不要问这是错误的。父母应该告诉所有的孩子进行渲染,并传递所有必要的信息作为参数。这样,一个人可以很容易地在两个父级中拥有相同的小部件(无论这实际上是否有意义)。

回到书籍示例-输入图书管理员:

Librarian l = ...;
Book b = ...;
l.findShelf(b).remove(b);

请您理解,我们不是要求不再在意义上来讲,不要问

在第一个版本中,书架是这本书的财产,您要了书架。在第二版中,我们对此书不做任何假设。我们所知道的是,图书馆员可以告诉我们一个装有书的架子。大概他是依靠某种查找表来执行此操作的,但是我们不确定(他也可以遍历每个书架中的所有书),实际上我们不想知道
我们不依赖于图书馆员的反应是否与其状态或它可能依赖的其他对象的状态相关联或如何耦合。我们告诉图书馆员找到书架。
在第一种情况下,我们直接将书籍和书架之间的关系编码为书籍状态的一部分,并直接检索此状态。这是非常脆弱的,因为我们还假设书返回的书架包含书(这是我们必须确保的约束,否则我们可能无法remove从书的实际书架中取出书)。

随着图书馆员的介绍,我们对这种关系进行了单独建模,从而实现了关注点分离。


我认为这是非常正确的观点,但根本无法回答问题。在您的版本中,问题是,Shelf类中的remove(Book b)方法是否应具有返回值?
围巾岭

1
@scarfridge:最重要的是,不要问是正确分离关注点的必然结果。如果您考虑一下,我将回答这个问题。至少我会这么认为;)
back2dos

1
@scarfridge可以传递一个在失败时被调用的函数/委托,而不是返回值。如果成功,就完成了,对吗?
Bless Yahu 2012年

2
@BlessYahu在我看来,这是过度设计的。我个人认为,命令查询分离在实际(多线程)世界中更理想。恕我直言,带有副作用的方法只要返回一个值即可,只要该方法的名称清楚地表明它会更改对象的状态即可。考虑堆栈的pop()方法。但是具有查询名称的方法不应具有副作用。
Scarridge

13

如果您需要知道结果,则可以;那是你的要求。

短语“方法仅在它们是参照透明的且因此没有副作用的情况下才应返回值”是遵循的良好准则(特别是如果您出于并行或其他原因而以函数式风格编写程序),但是不是绝对的。

例如,您可能需要知道文件写入操作的结果(对或错)。这是一个返回值但总是产生副作用的方法的示例没有办法解决。

要实现命令/查询分离,您将不得不使用一种方法执行文件操作,然后使用另一种方法检查其结果,这是一种不受欢迎的技术,因为它使结果与导致结果的方法分离。 如果在文件调用和状态检查之间对象的状态发生了变化该怎么办?

底线是:如果您使用方法调用的结果在程序中的其他位置进行决策,那么您就不会违反“告诉不要问”的原则。另一方面,如果您基于对对象的方法调用来为该对象制定决策,则应将这些决策移入该对象本身以保留封装。

这与命令查询分离一点也不矛盾。实际上,它会进一步执行它,因为不再需要出于状态目的而公开外部方法。


1
有人可能会争辩说,在您的示例中,如果“写”操作失败,则应引发异常,因此不需要“获取状态”方法。
大卫,

@David:然后回到OP的示例,如果实际发生更改,则返回true,否则返回false。我怀疑您是否想在这里抛出异常。
罗伯特·哈维

即使是OP的例子也不适合我。如果无法删除,您确实希望得到通知,在这种情况下,您将使用异常;或者,实际上要删除窗口小部件时,您希望得到通知,在这种情况下,您可以使用事件或回调机制。我只是没有看到一个真实的示例,在该示例中,您不想遵守“命令查询分离模式”
David

@David:我已经编辑了答案以弄清楚。
罗伯特·哈维

并不是说得太过分,但命令查询分离背后的全部思想是,发出命令来写入文件的人并不关心结果-至少在特定意义上(用户以后可能会提取所有最近文件操作的列表,但与初始命令没有任何关联)。如果您需要在命令完成后采取行动,无论您是否关心结果,正确的方法都是使用异步编程模型,该模型使用事件(可能)包含有关原始命令和/或其详细信息的事件。结果。
亚伦诺特,2011年

2

我认为您应该保持最初的直觉。有时设计应该是故意的危险,当您与对象通信时立即引入复杂性,以便您可以立即正确地正确处理它,而不是尝试在对象本身上处理它并隐藏所有刺眼的细节并使之难以清理改变基本假设。

如果对象为您提供了实现的等价openclose行为,您应该会下沉的感觉,并且当您看到布尔返回值(您认为某个简单的原子任务)时,您立即开始理解要处理的内容。

以后如何处理是您的事。您可以在上面创建一个抽象,一个带有的窗口小部件类型removeFromParent(),但是您应该始终具有一个低水平的后备,否则您可能会做出过早的假设。

不要试图使一切简单。当您依赖看起来优雅而天真的东西时,没有什么让人失望的,只是后来在最糟糕的时刻才意识到这是真正的恐怖。


2

您所描述的是命令查询分离原则的已知“例外”。

在本文中,马丁·福勒(Martin Fowler)解释了他将如何威胁这种例外。

Meyer绝对喜欢使用命令查询分离,但是也有例外。弹出堆栈是修改状态的查询的一个很好的例子。迈耶正确地说,您可以避免使用此方法,但这是一个有用的习惯用法。因此,我更愿意在可能的情况下遵循这一原则,但我准备打破这一原则以赢得关注。

在您的示例中,我将考虑相同的例外。


0

命令查询分离非常容易被误解。

如果我在系统中告诉您,则有一条命令也会从查询中返回一个值,并且您说“ Ha!您违反了!” 你在跳枪。

不,那不是禁止的。

禁止的是,当该命令是进行该查询的唯一方法时。不,我不必改变状态来提问。这并不意味着每次更改状态时我都必须闭上眼睛。

没关系,因为我还是要再次打开它们。这种过度反应的唯一好处是确保设计人员不会变得懒惰而忘记包括非状态更改查询。

真正的含义是如此的微妙,以至于懒惰的人更容易地说二传手应该返回虚无。这样可以更轻松地确保您没有违反实际规则,但这是一种过度反应,可能会变成真正的痛苦。

流利的接口和iDSL始终“消除”这种过度反应。如果您反应过度,将会忽略很多功能。

您可能会争辩说,命令应该只做一件事,而查询应该只做一件事。在很多情况下,这是一个好主意。谁说只有一个查询应该遵循一个命令?很好,但这不是命令查询分离。这就是单一责任原则。

以这种方式看,堆栈弹出并不是一个奇怪的异常。这是一个单一的责任。如果您忘记提供窥视,则Pop仅违反命令查询分隔。

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.