命令查询分离的艺术

函数有副作用。有时候,他们会改变系统的状态,特别是当你最不希望这么做时,从而造成各种各样的意想不到的破坏。在面向对象的编程范式中很难去除所有的副作用。我们需要确保可变状态得到明确管理,才能保证在我们没有注意到时不会被状态拖累。

将副作用管理起来
管理副作用的一个好方法是对命令和查询分离,并在它们之间创建一个强大的分隔边界。在良好命令和查询分离的情况下,当一个命令改变系统状态时,它就会有副作用。而查询只是返回一个计算好或系统的观测状态的值,并不会有任何改变,因此没有任何副作用。

例如当你调用一个函数getAmount(),你希望它只是返回的金额,而不会同时改变系统内状态。同样,当你调用setAmount() 时,它会有副作用,你期望它只是改变系统的状态。但你不会期望setAmount()同时还返回修改后的金额。(单一职责)

命令查询分离CQS/CQRS的定义:改变状态的函数不应该返回值,函数返回值不应该改变状态。

这个术语是由Bertrand Meyer在他的“ 面向对象软件构造”一书中创造的。

优点
遵循此分离的优点之一是,您可以轻松地识别函数是否具有副作用。
int m(); // 查询(无副作用)
void n(); //命令(有副作用)

看看下面的代码,它是命令还是查询?显然,它改变了系统的状态。
User u = UserService.login(username, password);

这是一个login函数调用,实现登陆功能职责,为什么它在登陆时还返回用户对象?下面这样查询用户函数实现返回用户对象不是更简单吗?

User u = UserService.getUser();

当命令更改状态出错时,login()函数是返回错误代码还是抛出异常?最好是抛出异常,这样能够保持login()命令返回的还是约定void。

尽管CQS在大多数时候都工作良好,但也有例外:
Element e = Stack<Element>.pop(); // 有态查询

这里pop()是一个堆栈弹出函数,弹出不但改变了堆栈集合内部状态,而且同时返回了弹出的值,这是一个命令和查询混合在一起的案例,当然可以通过纯函数方式来处理这种集合处理。

最后总结一下:
1.命令是返回void,而查询是返回值。

2.使用异常替代函数直接返回错误状态。

正如马丁·福勒所说,如果语言本身支持这些概念将是会很好。该语言可以检测状态改变方法,或者至少允许程序员标记它们。但是我们还没有看到支持这种标记的语言或IDE。

OO Tricks: The Art of Command Query Separation – F