测试又通过了!现在我们想最终特性进发:给定一个属性key,注册模式类的get()方法将返回一个对特定对象的引用。一下为符合这一要求的测试用例。
代码:
class RegistryPHP4TestCase extends UnitTestCase {function testRegistryIsSingleton() { /*...*/ } function testEmptyRegistryKeyIsInvalid() { /*...*/ } function testEmptyRegistryKeyReturnsNull() { /*...*/ } function testSetRegistryKeyBecomesValid() { /*...*/ } function testSetRegistryValueIsReference() {$reg =& Registry::getInstance();$test_value = 'something'; $reg->set('key', $test_value); $this->assertReference($test_value, $reg->get('key')); //another way to test the reference $test_value .= ' else'; $this->assertEquual('something else',$reg->get('key')); } } |
以下为注册模式类的完整实现代码。
代码:
class Registry {var $_store = array(); function isValid($key) {return array_key_exists($key, $this->_store);} function &get($key) {if (array_key_exists($key, $this->_store)) return $this->_store[$key];} function set($key, &$obj) {$this->_store[$key] =& $obj;} function &getInstance() {static $instance = array(); if (!$instance) $instance[0] =& new Registry; return $instance[0]; } } |
“注册模式”的get()方法会返回一个对象引用。类似的,set()方法的$obj参数要求得到一个对象引用并被赋值$this->_store[$key].。get()和set()方法的联合恰当使用能够满足assertReference()测试。
作者注:
“注册模式”的get()Registry::get()方法的代码应该写成@$this->_store[$key;]的形式,但是最好避免使用错误抑制符,使用错误抑制符的代码会变的摸棱两可,你需要花费额外的时间去了解你是否会再次访问这段代码。array_key_exists()方法指出了应该避免的错误。
PHP5中,对象句柄(引用)带来了革命性的变化——你可以从对象引用的困境中解脱出来。事实上PHP5中注册模式的实现变的简单多了。因为你再也不用担心因为没有通过引用传递对象而引起致命错误的情况下使用联合数组。在PHP5中,你甚至能在注册模式中混和使用对象和变量。
一个例子:
在实际应用中“注册模式”会是什么样子?在网络应用程序开发中,通常我们只拥有一个数据库连接。(因此,广泛使用“单一模式”管理数据连接)但是,比如,由于历史遗留原因:你的应用的客户数据库与你的在线订单数据库是分开的,你的DBA又把你的旧订单转移到一个存档数据库中,而且它与你的客户数据库及订单(现有,最近)数据库也是完全隔离的。那么,你怎么才能方便地管理三个数据库连接而不用创建三个单独的“单一模式”呢?答安就是使用“注册模式”。
代码:
class DbConnections extends Registry {} |
注:当你在你的代码中引入设计模式时,你的类名应该仍能反映他的角色和功能而没有必要使用模式的名字。使用模式的名字注释代码对与你的项目以外的程序员交流非常有帮助。但是在你的项目内,类的名字应该适合项目本身而且能够被项目成员很好的理解。虽然本章范例中的类名反映了设计模式的名字以及特定的实现方式,但是这并不是必须的。这仅仅是为了例子的清晰明了而不是好的命名规范。
DbConnections类是一个单件模式类,又继承了注册模式——DbConnections综合了两者的优点。
以下的代码片断创建并在注册模式类中存储了对每一个数据库的连接。
代码:
//initial setup, somewhere near the start of your script $dbc =& DbConnections::getInstance(); $dbc->set( 'contacts', new MysqlConnection('user1', 'pass1', 'db1', 'host1')); $dbc->set( 'orders', new MysqlConnection('user2', 'pass2', 'db2', 'host2')); $dbc->set( 'archives', new MysqlConnection('user3', 'pass3', 'db3', 'host3')); |
在其他类中将注册模式类连同数据一起载入就可以使用不同的连接了。
代码:
// domain model classes class Customer { var $db; function Customer() { $dbc =& DbConnections::getInstance(); $this->db =& $dbc->get('contacts'); } //... } class Orders { var $db_cur; var $db_hist; function Contact() { $dbc =& DbConnections::getInstance(); $this->db_cur =& $dbc->get('orders'); $this->db_hist =& $dbc->get('archive'); } //... } |
一个类依据客户数据库建模,另一个类依据历史和现在的客户订单建模。取得正确的数据库链接需要两个步骤:找到注册模式类,从中找出与给定的属性(key)相匹配的对象。
将注册模式实现为单件模式:
如前所述,把注册模式实现为单件模式有很多实现方式。
第一步,将注册模式实现为单件对象,(作者注:我们在第四章——The Singleton Pattern末尾简单讨论过)。
按照这种设计,注册模式类的任何一个实例都将访问同一个数组。我们把这个新类叫做RegistryGlobal以区别于我们前面开发的类,并反映这种实现方式的特性。
以下为反映这种思想的测试用例(它应该看起来很熟悉)。
代码:
class RegistryGlobalPHP4TestCase extends UnitTestCase { function testRegistryGlobal() { $reg =& new RegistryGlobal; $this->assertFalse($reg->isValid('key')); $this->assertNull($reg->get('key')); $test_value = 'something'; $reg->set('key', $test_value); $this->assertReference($test_value, $reg->get('key')); } } |
实现代码如下所示:
class RegistryGlobal { var $_store = array(); function isValid($key) { return array_key_exists($key, $this->_store); } function &get($key) { if (array_key_exists($key, $this->_store)) return $this->_store[$key]; } function set($key, &$obj) { $this->_store[$key] =& $obj; } } |
isValid(), get(),和set()方法与我们前面开发的注册模式类完全相同。
下一步:我们来编写验证RegistryGlobal类是单件模式的测试用例。
代码:
class RegistryGlobalPHP4TestCase extends UnitTestCase { function testRegistryGlobal() { /*...*/ } function testRegistryGlobalIsMonoState() { $reg =& new RegistryGlobal; $reg2 =& new RegistryGlobal; $this->assertCopy($reg, $reg2); $test_value = 'something'; $reg->set('test', $test_value); $this->assertReference( $reg->get('test') ,$reg2->get('test')); } } |
这里测试用例创建了RegistryGlobal类的两个实例,并确认他们不是对同一对象的引用——在一个实例内设置一个对象的属性值(value),最后证实两个实例返回相同的对象。若测试通过RegistryGlobal类就拥有单态的行为。
代码:
define('REGISTRY_GLOBAL_STORE', '__registry_global_store_key__'); class RegistryGlobal {var $_store; function RegistryGlobal() {if (!array_key_exists(REGISTRY_GLOBAL_STORE, $GLOBALS)||!is_array($GLOBALS[REGISTRY_GLOBAL_STORE])) {$GLOBALS[REGISTRY_GLOBAL_STORE] = array(); } $this->_store =& $GLOBALS[REGISTRY_GLOBAL_STORE]; } function isValid($key) {return array_key_exists($key, $this->_store);} function &get($key) {if (array_key_exists($key, $this->_store)) return $this->_store[$key];} function set($key, &$obj) { $this->_store[$key] =& $obj; } } |
本方法中的神奇之处在于$this->_store =& $GLOBALS[REGISTRY_GLOBAL_STORE;] 这一行,引用操作符将全局数组绑定到实例变量$_store上。这是单件模式实现的关键所在:每次在对象中使用$this->_store变量时,作用反映到全局变量中。
但是并不推荐基于全局变量的解决方案。如果PHP4支持这一特性的话,静态类变量会是更好的解决方案。然而,我们可以在代码中通过引用实现静态类变量吗?
测试与 RegistryGlobal 类的测试相似。
class RegistryMonoStatePHP4TestCase extends UnitTestCase { function testRegistryMonoState() { $this->assertCopy( $reg =& new RegistryMonoState; $reg2 =& new RegistryMonoState); $this->assertFalse($reg->isValid(‘key’)); $this->assertNull($reg->get(‘key’)); $test_value = ‘something’; $reg->set(‘key’, $test_value); $this->assertReference($reg->get(‘key’), $reg2->get(‘key’)); } } |
要自己实现类静态变量,可以将一个对函数静态变量的引用绑定到类的实例变量上。
class RegistryMonoState {var $_store; function &_initRegistry() { static $store = array(); return $store; } function RegistryMonoState() { $this->_store =& $this->_initRegistry(); } function isValid($key) { return array_key_exists($key, $this->_store); } function &get($key) { if (array_key_exists($key, $this->_store)) return $this->_store[$key]; } function set($key, &$obj) { $this->_store[$key] =& $obj; } } |
延伸阅读:
从魔兽看PHP设计模式
《PHP设计模式介绍》导言
PHP设计模式介绍 第一章 编程惯用法
PHP设计模式介绍 第二章 值对象模式
PHP设计模式介绍 第三章 工厂模式
PHP设计模式介绍 第四章 单件模式