http://www.oschina.net/question/28_32343
注:此文为 OSChina.NET 原创翻译,转载请保留链接
如果一个函数既返回函数的状态值又修改对象的状态,那么此方函数是介于查询函数和修改函数之间的。前者允许检索获取对象的部分状态(在此我们不讨论SQL);而后者能够改变对象的可视状态。
与查询函数相伴产生的问题不仅仅是关于它的返回值的问题,更重要的是它会有副作用。因为一个人可能在不知道状态正在被修改的情况下多次调用此函数或是用它来处理多项不同的事情。所以它的副作用通常非常隐蔽,难于被发现。
什么是可视性的?
对象的可视状态指的是此状态可从客户端代码进行访问,或者是在某些方面影响消息退出应用程序(被写入的文件或者是插入数据库的代码)。
可视状态并不包括延迟加载的属性:它并不是因延迟加载违背了实现方法,从外部你可以任意多次地调用此查询函数;同时你仍会得到同样的功能行为,只不过第一次调用时会反应慢一些。
这同样适用于所有的高速缓存:因为它们的内部状态是不可见的,所以一旦首次调用时就修改了它的状态,那么从客户端来看就发现不了任何问题。他们会很自然而然地对$this区域进行赋值并在同一函数中获得返回值。
确认违背CQS的首要原则就是要看此函数是否有以下两个行为。其一是修改状态来对$this赋值或是调用它来修改其它对象的状态;其二是有一个区别于void的@返回值类型。
然后你就要从外部来查询这些更改的可视性。
CQS原则
CQS (Command Query Separation 命令与查询分离)原则是由Bertrand Meyer提出的。该原则使你能够重复调用getter方法而不必有所顾虑,或是把调用转移到一个绝对安全的对象。
其它地方也反映出了该原则:HTTP协议把GET作为一个安全方法从可修改服务器状态的POST方法中分离出来。所以代理服务器可以缓存对于GET请求的响应并转存到POST;同时爬网程序也可以在避免产生大量副作用的情况下执行链接。
同样的优点在调用这些方法的客户端代码中显现出来。你不必要知道你的getXXX()方法是否会修改一些东西;尤其是在调试或是在写一个测试程序的时候,你假设你可以任意多次地调用它,并且在不影响故障出现的情况下检查它的状态。
关于CQS原则的一个非常有名的异常(被Greg Young在一篇有关CQRS的演讲稿中所引用)是关于队列及其dequeue()方法。不过它并不是规则,而仅仅是个异常。
步骤
1.新建一个查询函数,令它的返回值与原方法相同。
2.修改原函数:令它调用查询函数,并获得返回的结果。
3.检查、测试程序。
4.把调用原函数的代码改为独立地调用修改函数以及新的查询函数。
5.把返回值改为void,去除所有的return语句。
我发现由Fowler列出的这些步骤并不能完全地提取出我们所需要的命令。如果我们想要从查询函数中消除副作用的话,下面的方法可能会更加合适。
实例
下面的对象代表一个包含其全名及计算领域的User。发生的情况是__toString()方法在程序的某处被调用,而一旦删除了该调用,那么这个不完整的对象在调用getXXX()方法时打印出来的将会是是NULL而不是它的名字。
02 |
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase |
04 |
public function testTheDomainObjectProvidesACalculatedField() |
06 |
$user = new User('Giorgio', 'Sironi'); |
07 |
$this->assertEquals('User: Giorgio Sironi', $user->__toString()); |
29 |
public function __construct($first, $last) |
31 |
$this->firstName = $first; |
32 |
$this->lastName = $last; |
35 |
public function getFirstName() { return $this->firstName; } |
36 |
public function getLastName() { return $this->lastName; } |
37 |
public function getFullName() { return $this->fullName; } |
39 |
public function __toString() |
41 |
$this->fullName = $this->firstName . ' ' . $this->lastName; |
42 |
return 'User: ' . $this->fullName; |
让我们在尚未清楚值域是如何计算的情况下编写一个测试程序来暴露CQS的问题。
02 |
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase |
04 |
public function testTheDomainObjectProvidesACalculatedField() |
06 |
$user = new User('Giorgio', 'Sironi'); |
07 |
$this->assertEquals('User: Giorgio Sironi', $user->__toString()); |
10 |
public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself() |
12 |
$user = new User('Giorgio', 'Sironi'); |
13 |
$oldFullName = $user->getFullName(); |
15 |
$this->assertEquals($oldFullName, $user->getFullName()); |
我们提取了一个命令,它的返回值由查询函数来处理。这次我更乐意于去提取命令而不是提取查询,是因为我想保留__toString()来为它命名。
因此,现在我们还应该修改调用。
02 |
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase |
04 |
public function testTheDomainObjectProvidesACalculatedField() |
06 |
$user = new User('Giorgio', 'Sironi'); |
07 |
$user->completeFields(); |
08 |
$this->assertEquals('User: Giorgio Sironi', $user->__toString()); |
11 |
public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself() |
13 |
$user = new User('Giorgio', 'Sironi'); |
14 |
$user->completeFields(); |
15 |
$oldFullName = $user->getFullName(); |
17 |
$this->assertEquals($oldFullName, $user->getFullName()); |
39 |
public function __construct($first, $last) |
41 |
$this->firstName = $first; |
42 |
$this->lastName = $last; |
45 |
public function getFirstName() { return $this->firstName; } |
46 |
public function getLastName() { return $this->lastName; } |
47 |
public function getFullName() { return $this->fullName; } |
49 |
public function completeFields() |
51 |
$this->fullName = $this->firstName . ' ' . $this->lastName; |
52 |
return $this->__toString(); |
55 |
public function __toString() |
57 |
return 'User: ' . $this->fullName; |
现在我们已经将它们完全地分离开了,把命令转化成了一种方法,它不返回任何值。
02 |
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase |
04 |
public function testTheDomainObjectProvidesACalculatedField() |
06 |
$user = new User('Giorgio', 'Sironi'); |
07 |
$user->completeFields(); |
08 |
$this->assertEquals('User: Giorgio Sironi', $user->__toString()); |
11 |
public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself() |
13 |
$user = new User('Giorgio', 'Sironi'); |
14 |
$user->completeFields(); |
15 |
$oldFullName = $user->getFullName(); |
17 |
$this->assertEquals($oldFullName, $user->getFullName()); |
39 |
public function __construct($first, $last) |
41 |
$this->firstName = $first; |
42 |
$this->lastName = $last; |
45 |
public function getFirstName() { return $this->firstName; } |
46 |
public function getLastName() { return $this->lastName; } |
47 |
public function getFullName() { return $this->fullName; } |
49 |
public function completeFields() |
51 |
$this->fullName = $this->firstName . ' ' . $this->lastName; |
54 |
public function __toString() |
56 |
return 'User: ' . $this->fullName; |
最后,既然总是在结构之后调用命令,所以不可避免地要将调用移动到类之内了。
02 |
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase |
04 |
public function testTheDomainObjectProvidesACalculatedField() |
06 |
$user = new User('Giorgio', 'Sironi'); |
07 |
$this->assertEquals('User: Giorgio Sironi', $user->__toString()); |
10 |
public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself() |
12 |
$user = new User('Giorgio', 'Sironi'); |
13 |
$oldFullName = $user->getFullName(); |
15 |
$this->assertEquals($oldFullName, $user->getFullName()); |
37 |
public function __construct($first, $last) |
39 |
$this->firstName = $first; |
40 |
$this->lastName = $last; |
41 |
$this->completeFields(); |
44 |
public function getFirstName() { return $this->firstName; } |
45 |
public function getLastName() { return $this->lastName; } |
46 |
public function getFullName() { return $this->fullName; } |
48 |
private function completeFields() |
50 |
$this->fullName = $this->firstName . ' ' . $this->lastName; |
53 |
public function __toString() |
55 |
return 'User: ' . $this->fullName; |
英文原文链接:http://css.dzone.com/articles/practical-php-refactoring-30