在编写面向对象的代码的时,有些时候你需要一个能够自己根据不同的条件来引入不同的操作对象实例。例如,一个菜单功能能够根据用户的“皮肤”首选项来决定是否采用水平的还是垂直的排列形式,或者一个计费系统可以自行根据用户的收货地址来决定税率。
一般来讲,一个控制菜单的对象实例包括了add(), delete(), 和 replace()等菜单元素;并通过set()进行配置,用render()来管理显示模式。无论你想生成什么样子的菜单,你都可以用同一个对象类来处理。不同菜单的对象实例只是一些方式函数的运算规则不同罢了,至少在刚才的例子里面render()函数是不同的。
但是如果你需要增加菜单的显示模式种类,或者你需要根据用户的国家、省份等信息来判断菜单排列的顺序的时候,该怎么做呢?而且如果有许多的方式函数都是经常变化的,那么简单的类封装将变得复杂、难易理解和升级的。
问题
怎么轻松地改变对象实例的执行过程,因而在代码执行的时候动态地改变执行过程?一旦实现了这个功能,如果去编写这样的类定义从而让维护和升级变得非常简单呢?
解决办法
当一个类封装了多个操作的时候,对象实例可以动态地选择这些操作来进行,可以用策略模式来把对象本身和运算规则区分开来。或者,更简单的处理是类里面定义的方式函数用case语句来进行控制。当然更简单的方法是使用策略模式。
策略模式功能非常强大,因为这个设计模式本身的核心思想就是面向对象编程的多形性的思想。
就在编程领域之外,有许多例子是关于策略模式的。如果我需要在清晨从家里去上班,我可以有几个策略可以考虑:我可以开车,乘坐公交车,走路,汽车或者甚至是搭乘直升飞机。每个策略都可以得到相同的结果,但是它们使用了不同的资源。选择策略的依据是费用,时间,使用工具还有每种方式的方便程度 。一个很好的策略也许在第二天就不能再被使用的,所以策略的选择是相对的。
你已经在前面的工厂模式章节看到了和策略模式相似的例子:因为不同特性的费用计算方式不同,所以Monopoly游戏的框架使用了许多相似的特性类,但是因为费用的计算不是从类本身获得,所以这个费用计算相对来说是一个TemplateMethod 设计模式。
例子
举例子说明,让我们做一个存储PHP参数的cache。这个cahce类需要把变量以PHP识别的方式写入到一个文件当中,所以你可以在以后加载该文件并使用它。这个类还应该可以让你为每个数据加个标识符和存储的方式。
数据缓存
注:缓存是为了在接下来的操作中继续使用而对资源进行缓存。你可以通过建立和使用缓存来节省直接从原数据库获取数据的时间。这方面的例子最常见的就是访问数据库或者解析大的XML文档,或者大的配置文件。
缓存也会出现一个问题:你的缓存可能会失去与原数据的同步。或者缓存需要使用太多内存。
最开始,我们开发一个缓存操作,并不使用策略模式。
因为你可能需要缓存的不止一个值,所以你需要使用标识符来标识出你需要指定的元素。在这个例子中,标识符就是’application_config’。下面试一个如果使用cache的例子。
- // PHP4
- $config_cache =& new VarCache(‘application_config’);
- if ($config_cache->isValid()) {
- $config = $config_cache->get();
- } else {
- $config = slow_expensive_function_to_get_config();
- $config_cache->set($config);
- }
复制代码
这个代码生成了一个新的VarCache对象存放在$config_cache变量里面。这个数据在缓存中的标识符是 ‘application_config’。如果在缓存里面有这个数据, isValid() 将返回真( true )并且获取缓存中的数据。反之,值被重新获取并写入缓存当中,以便下次使用。
按照一般的需求,让我们开始编写这段代码来进行测试。首先,如果缓存中没有该数据, isValid() 方式函数应该返回非值(false)。
- class VarCacheTestCase extends UnitTestCase {
- function TestUnsetValueIsInvalid() {
- $cache =& new VarCache(‘foo’);
- $this->assertFalse($cache->isValid());
- }
复制代码
因为VarCache现在没有代码,所以最简单的方式就是先构造一个方式函数。
- class VarCache {
- function isValid() {}
- }
复制代码
这样,我们就可以继续了。
- class VarCacheTestCase extends UnitTestCase {
- function TestUnsetValueIsInvalid() { /* ... */ }
- function TestIsValidTrueAfterSet() {
- $cache =& new VarCache(‘foo’);
- $cache->set(‘bar’);
- $this->assertTrue($cache->isValid());
- }
复制代码
上面的测试校验了缓存的数据是否是可用的。
开始编写cache类的主要部分。VarCache 引入一个标识符, 所以constructor了一个应该记录它的对象实例。这里面还有一个set()的方式函数,用来把数据存入缓存,或者当数据存在时,修改缓存当中的数据。
- class VarCache {
- var $_name;
- function VarCache($name) {
- $this->_name = ‘cache/’.$name;
- }
- function isValid() {
- return file_exists($this->_name.’.php’);
- }
- function set() {
- $file_handle = fopen($this->_name.’.php’, ‘w’);
- fclose($file_handle);
- }
- }
复制代码
对象实例的参数$_name 存放了缓存的标识符。在这个简单的操作中, $_name 被用来生成文件名(在实际的使用可能会数据库或者其它的数据源代替) set() 使用 fopen() 和 fclose() 来 “访问” 基于$_name的文件。当调用set()后, file_exists()在VarCache::isValid()里面调用返回真(true)。
运行这个测试来产生一个我们预期的结果;但是实际情况是报错!为什么呢?第一次运新的时候没有生成文件,所以第二次运行的时候找不到文件,显然我们不希望这种情况出现。我们期望的是每一次运行代码都是互不影响的。
幸运的是,把总体测试框架和特定功能的简单测试结合起来,我们就可以得到灵活的测试环境,并且在以后的测试中方便地使用。UnitTestCase::setUp()实现框架的初始化,而UnitTestCase::tearDown()实现具体的测试过程。
把以下这段代码加入到测试环境中以后,你可以确保每一个测试过程从开始的时候就各自独立地运行:
- class VarCacheTestCase extends UnitTestCase {
- function setup() {
- @unlink(‘cache/foo.php’);
- }
- // ...
- }
复制代码
现在缓存的文件在每一次测试执行之前都没删除掉了,这保证了每一次测试运行都是相互独立的。(测试驱动的开发更实用的方法是你可以写一个VarCache::clear()方式函数去处理一个缓存的参数的清除工作。)
当上一次代码执行出来的缓存结果被清除了,测试重新开始运行,这意味着你可以继续测试并编写新的代码。
- class VarCacheTestCase extends UnitTestCase {
- function setup() { /* ... */ }
- function TestUnsetValueIsInvalid() { /* ... */ } function TestIsValidTrueAfterSet() { /* ... */ } function TestCacheRetainsValue() {
- $test_val = ‘test’.rand(1,100);
- $cache =& new VarCache(‘foo’);
- $cache->set($test_val);
- $this->assertEqual($test_val, $cache->get());
- }
复制代码
上面的测试验证VarCache::get()返回的值是否与用VarCache::set()设置的相同。
- class VarCache {
- var $_name;
- function VarCache($name) { /* ... */ } function isValid() { /* ... */ } function get() {
- if ($this->isValid()) {
- return file_get_contents($this->_name.’.php’);
- }
- }
- function set($value) {
- $file_handle = fopen($this->_name.’.php’, ‘w’); fwrite($file_handle,$value); fclose($file_handle);
- }
- }
复制代码
通过黑体字部分的代码,VarCache::set() 方式函数把参数$value的内容写到文件中,并用VarCache::get() 方式函数通过file_get_content() 从文件中把内容读取出来.
从目前的执行情况来看,对于字符串和数字的操作是没有问题的,但是对于更复杂的参数比如数组和对象,执行起来就会出现问题了。我们用下面的代码进行处理:
- class VarCacheTestCase extends UnitTestCase {
- // ...
- function TestStringFailsForArray() {
- $test_val = array(‘one’,’two’);
- $cache =& new VarCache(‘foo’);
- $cache->set($test_val);
- $this->assertError(‘Array to string conversion’);
- $this->assertNotEqual($test_val, $cache->get());
- $this->assertEqual(‘array’,strtolower($cache->get()));
- }
复制代码
由于篇幅的关系,我们直接调到这个执行过程的结束部分,它随后也将实现策略式的判断。
这里就是增加一系列操作用来完善VarCache的地方。
- class VarCache {
- //...
- function get() {
- if ($this->isValid()) {
- include $this->_name.’.php’;
- return $cached_content;
- }
- //...
- }
复制代码
在这里关键性的改变是get() 方式函数(并且让PHP去验证有效性。
同时,get()返回参数$cached_content的值,所以无论set() 如果操作,它必须设置这个变量!
因此,对于数字来说,执行出来是什么样的结果呢?
- class VarCache {
- //...
- function set($value) {
- $file_handle = fopen($this->_name.’.php’, ‘w’);
- $template = ‘<?php $cached_content = %s;’;
- $content = sprintf($template
- The Strategy Pattern 129
- ,(float)$value);
- fwrite($file_handle, $content);
- fclose($file_handle);
- }
- }
复制代码
看起来对于一个数字,执行起来是没有问题的,那么对于字符串如何呢?对于字符串,缓存文件的数据编写方式就必须用= ‘%s’;结尾而不是= %s;。所以在这里我们需要引入一个“type” 参数:它用来指定缓存的数据类型是一个整型还是字符串。为了更容易地增加更多的数据类型,我们分别在set()和_getTemplate()函数增加一个case 判断。
- class VarCache {
- var $_name;
- var $_type;
- function VarCache($name, $type=’string’) {
- $this->_name = ‘cache/’.$name;
- $this->_type = $type;
- }
- // ...
- function _getTemplate() {
- $template = ‘<?php $cached_content = ‘;
- switch ($this->_type) {
- case ‘string’:
- $template .= “‘%s’;”;
- break;
- case ‘numeric’:
- $template .= ‘%s;’;
- break;
- default:
- trigger_error(‘invalid cache type’);
- }
- return $template;
- }
- function set($value) {
- $file_handle = fopen($this->_name.’.php’, ‘w’);
- switch ($this->_type) {
- case ‘string’:
- $content = sprintf($this->_getTemplate()
- ,str_replace(“‘“,”\\’”,$value));
- break;
- case ‘numeric’:
- $content = sprintf($this->_getTemplate()
- ,(float)$value);
- break;
- default:
- trigger_error(‘invalid cache type’);
- }
- fwrite($file_handle, $content);
- fclose($file_handle);
- }
- }
复制代码
现在,构造函数增加了第二个可选的参数用来确定第一个参数的数据类型是数字类型还是字符串。这个类的最终形式变为请看下面代码,包括了一个‘serialize’ 用来存储数据、对象等复杂数据的存储类型。
- class VarCache {
- var $_name;
- var $_type;
- function VarCache($name, $type=’serialize’) {
- $this->_name = ‘cache/’.$name;
- $this->_type = $type;
- }
- function isValid() {
- return file_exists($this->_name.’.php’);
- }
- function get() {
- if ($this->isValid()) {
- include $this->_name.’.php’;
- return $cached_content;
- }
- }
- function _getTemplate() {
- $template = ‘<?php $cached_content = ‘;
- switch ($this->_type) {
- case ‘string’:
- $template .= “‘%s’;”;
- break;
- case ‘serialize’:
- $template .= “unserialize(stripslashes(‘%s’));”;
- break;
- case ‘numeric’:
- $template .= ‘%s;’;
- break;
- default:
- trigger_error(‘invalid cache type’);
- }
- return $template;
- }
- function set($value) {
- $file_handle = fopen($this->_name.’.php’, ‘w’);
- switch ($this->_type) {
- case ‘string’:
- $content = sprintf($this->_getTemplate()
- ,str_replace(“‘“,”\\’”,$value));
- break;
- case ‘serialize’:
- $content = sprintf($this->_getTemplate()
- ,addslashes(serialize($value)));
- break;
- case ‘numeric’:
- $content = sprintf($this->_getTemplate()
- ,(float)$value);
- break;
- default:
- trigger_error(‘invalid cache type’);
- }
- fwrite($file_handle, $content);
- fclose($file_handle);
- }
- }
复制代码
请注意_getTemplate()和set() 函数中的case判断语句。它们都是基于同一个$_type 实例参数的。get() 函数中却没有受到$_type的影响,所以看起来因为存储的数据类型的变化只影响到数据的存储过程。同时,多重的case条件判断也是一个提示,这个地方如果使用了策略的设计模式会更好。
样本代码
从一个多重的switch 条件判断改变到策略模式是一个条件分解实例的经典例子。整个测试的环境没有变化;只是VarCache类的内部改变了。
首先我们把你想要封装在一个独立的类的各种情况分隔出来。就前面的例子来说,你有三种变化的情况需要进行考虑:‘string’, ‘numeric’, 和第三个‘serialize’。前面的例子中还在对象实例化的时候选择了数据输出的格式。基于这个运算法则,你需要创建一个API来封装它。
你可以用以下的代码开始:
- class CacheWriter {
- function store($file_handle, $var) {
- die(‘abstract class-implement in concrete CacheWriter’);
- }
- }
复制代码
这个就是PHP4版本的接口。(你可以从这个类进行继承来保证你使用的是子类,这样做的话只是增加了一些系统的负载。尤其在基类CacheWriter是在另外一个文件定义的时候。负载增加得稍微多一些。)
基类CacheWriter 调用了store() 方式函数来引入文件处理资源和参数来进行存储。每一个实际的类都从执行store()函数, 但是不同的实例在store()函数里面使用的运算法则是不一样的,以便不同的数据类型生成的$cached_content是不同的。每一个运算法则被当作一个单独的类来运行。
前面的例子中的代码被替换为:
- class VarCache {
- // ...
- function _getTemplate() {
- $template = ‘<?php $cached_content = ‘;
- switch ($this->_type) {
- case ‘string’:
- $template .= “‘%s’;”;
- break;
- }
- // ...
- }
- function set($value) {
- $file_handle = fopen($this->_name.’.php’, ‘w’);
- switch ($this->_type) {
- case ‘string’:
- $content = sprintf($this->_getTemplate()
- ,str_replace(“‘“,”\\’”,$value));
- break;
- // ...
- }
- fwrite($file_handle, $content);
- fclose($file_handle);
- }
- }
复制代码
针对每一个缓存的数据来型,你需要实例出相对应的_getTemplate() 和
set() 方式函数到相对应的类当中。这里是StringCacheWriter:
- class StringCacheWriter /* implements CacheWriter */ {
- function store($file_handle, $string) {
- $content = sprintf(
- “<?php\n\$cached_content = ‘%s’;”
- ,str_replace(“‘“,”\\’”,$string));
- fwrite($file_handle, $contents);
- }
- }
复制代码
(因为PHP 4不支持接口的使用,这里接口只是用注释来简单描述一下。)
这里我们得到另外一个运算法则存储“策略”。
- class NumericCacheWriter /* implements CacheWriter */ {
- function store($file_handle, $numeric) {
- $content = sprintf(“<?php\n\$cached_content = %s;”
- ,(double)$numeric);
- fwrite($file_handle, $content);
- }
- }
- class SerializingCacheWriter /* implements CacheWriter */ {
- function store($file_handle, $var) {
- $content = sprintf(
- “<?php\n\$cached_content = unserialize(stripslashes(‘%s’));”
- ,addslashes(serialize($var)));
- fwrite($file_handle, $content);
- }
- }
复制代码
通过把运算法则封装到交互的类中(同样的API,多形性),你现在可以回过头来通过策略设计模式重新执行VarCache()类。这个时候经过条件分解但是与原来非常类似的代码可以继续运行了。
- class VarCache {
- var $_name;
- var $_type;
- function VarCache($name, $type=’serialize’) {
- $this->_name = ‘cache/’.$name;
- switch (strtolower($type)) {
- case ‘string’: $strategy = ‘String’; break; case ‘numeric’: $strategy = ‘Numeric’; break; case ‘serialize’:
- default: $strategy = ‘Serializing’;
- }
- $strategy .= ‘CacheWriter’;
- $this->_type =& new $strategy;
- }
- function isValid() {
- return file_exists($this->_name.’.php’);
- }
- function get() {
- if ($this->isValid()) {
- include $this->_name.’.php’;
- return $cached_content;
- }
- }
- function set($value) {
- $file_handle = fopen($this->_name.’.php’, ‘w’);
- $this->_type->store($file_handle, $value);
- fclose($file_handle);
- }
- }
复制代码
通过创建实际的CacheWriter 类的实例并让它帮定实际的$_type变量,你可以使用 $this->_type->store($file_handle, $value) 语句来写入缓存数据。
缓存文件的时候,我们将不再关心初始化的时候是用什么运算法则来存储数据。
下面描述了定义策略设计模式的几个特性:一系列的运算法则,每个运算法则都是封装在独立的类中。但是,每一个对象都是绑定到一个公共的容器对象中。并且,通过一个公共的API使用同样的方式在进行引用。而这个公共的API的运行方式是与策略的选择无关的。
评论
策略设计模式的功能是非常强大的 。本书到现在为止所说的其它的设计模式提供的都是应用的基础模块功能,而 策略设计模式是目前第一个拥有设计模式和项目的迁移里面关键功能的设计模式。
它可以替换掉一个对象里面编写复杂的部分,改变整个对象的运行和性能,这点功能是非常强大的。另外,一个特定策略使用以后马上就被清空了,这个使得剩下的API非常容易执行。从根本上说,选用哪个运算法则对于其它的代码来说都是透明的。
互联网上有这么一个说法“本质上说,任何一个事情开始的时候都像在使用策略模式。”为什么呢?因为这个设计模式有效应用了多形性的特点,而这个也是面向对象编程最强大的几个方面之一。
相关的设计模式
策略模式和其它许多设计模式比较起来是非常类似的。策略模式和状态模式最大的区别就是策略模式只是的条件选择只执行一次,而状态模式是随着实例参数(对象实例的状态)的改变不停地更改执行模式。换句话说,策略模式只是在对象初始化的时候更改执行模式,而状态模式是根据对象实例的周期时间而动态地改变对象实例的执行模式。
注: 设计模式—状态
Design Pattern—State
状态设计模式允许一个对象实例因为一个内部的状态改变而改变其执行模式。 因此,对象实例自身可以有效地改变其类定义。
油漆工设计模式(见第十二章) 在概念上正好和策略模式是相反的。借用GoF的一个推论,策略模式改变的是一个对象实例的核心的复杂操作,而油漆工设计模式改变的是一个对象实例的皮肤。
最后一个相关的设计模式是访问者设计模式。在策略模式里面,你创建一个实际的选择的策略的对象实例然后把它绑定到一个实例参数中;在访问者模式里面,策略使用参数的方式进行传递的。你可以想象下访问者设计模式,它的设计思路和策略模式正好相反。