《PHP设计模式介绍》第五章 注册模式

我们通常认为避免使用全局变量是一种好的选择,因此,对象经常被作为参数从一段代码传递到另一段。但是传递实例的一个问题就是对象有时候不知道将要传递给谁——?经过一个函数后才被传递到真正需要这个对象的函数。

为了编写,阅读,修改代码的方便,最好能够减少不同对象的数量,并且能够将大量广泛使用的对象统一表示为一个单一,常用的对象。

问题:

你如何通过单一的全局的对象来获取对其它对象的引用?

解决方案:

“注册模式”就像“对象的电话簿”——储存并且能够取回对对象引用的登记簿。(注:PHP中的“联合数组”也起到了类似“电话簿”的功能。事实上,“注册模式”就是围绕PHP中强大的数组完成的。)“注册模式”的一些特性经常被包含在“单一模式”中(参见第四章),使得“注册模式”成为你整个应用信息的决定性来源。

注释:“注册模式”类主要参考了Martin Fowlerdescribes用java语言实现的Patterns of Enterprise Application Architecture(企业应用程序体系结构模型)。Marcus Baker谢了一篇详细的PHP中应用“注册模式”的文章。该文章可在PHPPatterns.com的站点获的( http://www.PHPpatterns.com/index.PHP/article/articleview/75/1/1/)。Baker也涉及了一些测试considerations,示范了测试驱动的开发方法。



样本代码:

正如Martin Flower在他的“注册模式”一文中提及的样本代码所示,你可以用各种方法,提供各种接口实现“注册模式”。让我们仔细探究这种想法,并建立PHP4中的“注册模式”的一些不同实现。

让我们以编写能储存并恢复对象实例并能对“注册模式”提供全局访问的代码开始。这个类的实例变量能够缓存对象,并且“注册模式”本身是一个“单一模式”。像以前一样,测试决定需求。我们的第一个测试要确定“注册模式”是一个“单件模式”类。

  1. // PHP4
  2. class RegistryPHP4TestCase extends UnitTestCase {
  3. function testRegistryIsSingleton() {
  4. $this->assertIsA($reg =& Registry::getInstance(), ‘Registry’);
  5. $this->assertReference($reg, Registry::getInstance());
  6. }
  7. }
复制代码

这里,要把你在以前几章“单件模式”中学到的知识用上,你应该能够很快写出能够通过该测试的类。以下是一个满足测试要求的“注册模式”类 (ignoring the code required to enforce no direct object creation):

  1. class Registry {
  2. function &getInstance() {
  3. static $instance = array();
  4. if (!$instance) $instance[0] =& new Registry;
  5. return $instance[0];
  6. }
  7. }
复制代码

一个简单的静态数组就足够记录这个单一实例了。

接下来,让我们转到“注册模式”独特的特性上面。一个“注册模式”应该提供get() 和set()方法来存储和取得对象(用一些属性key)而且也应该提供一个isValid()方法来确定一个给定的属性是否已经设置。

这三个方法的一个简单实现在接下来讨论。这里是两个isValid():方法的测试方法。

代码:

  1. class RegistryPHP4TestCase extends UnitTestCase
  2. {function testRegistryIsSingleton() { /*...*/ }
  3. function testEmptyRegistryKeyIsInvalid()
  4. {$reg =& Registry::getInstance();
  5. $this->assertFalse($reg->isValid('key'));
  6. }
  7. function testEmptyRegistryKeyReturnsNull()
  8. {$reg =& Registry::getInstance();
  9. $this->assertNull($reg->get('key'));
  10. }
  11. }
复制代码
作者注:assertFalse()
assertFalse()仅仅是assertTrue()的反面,如果第一个参数预期是PHP中的布尔值false,测试通过。



通过基于测试驱动的开发方式,你可以编写尽可能少的代码来符合你现阶段的测试需求,你也可以增加测试——如果你还未满足这个类的需求。

以下为满足前述测试要求的最简单的代码:

代码:

  1. class Registry
  2. {function isValid() {return false;}
  3. function get() {}
  4. function &getInstance()
  5. {static $instance = array();
  6. if (!$instance) $instance[0] =& new Registry;
  7. return $instance[0];
  8. }
  9. }
复制代码

确实,isValid() 和 get()方法的代码片断并不是非常好,但是所有的测试通过了!下面我们添加更丰富的测试用例。

代码:

  1. class RegistryPHP4TestCase extends UnitTestCase
  2. {function testRegistryIsSingleton() { /*...*/ }
  3. function testEmptyRegistryKeyIsInvalid() { /*...*/ }
  4. function testEmptyRegistryKeyReturnsNull() { /*...*/ }
  5. function testSetRegistryKeyBecomesValid()
  6. {$reg =& Registry::getInstance();
  7. $test_value = 'something';$reg->set('key', $test_value);
  8. $this->assertTrue($reg->isValid('key'));
  9. }
  10. }
复制代码

为了满足testSetRegistryKeyBecomesValid()方法,“注册模式”类必须要有追踪(tracking)的功能——如果特定的属性用set()方法设置了。很明显的一种实现方式是利用PHP4中的联合数组作为实例变量,并利用PHP的array_key_exists()函数来检测我们想要的索引是否被创建了。

下面是“注册模式类”更进一步的实现。

代码:

  1. class Registry {var $_store = array();
  2. function isValid($key)
  3. {return array_key_exists($key, $this->_store);}
  4. function set($key, $obj)
  5. {$this->_store[$key] = $obj;}
  6. function get() {}
  7. function &getInstance()
  8. {static $instance = array();
  9. if (!$instance) $instance[0] =& new Registry;
  10. return $instance[0];
  11. }
  12. }
复制代码

通过在声明时初始化$_store变量,就没有设置构造函数的必要了。(注:在PHP4中没有适当的访问控制标记,以下代码遵循私有变量以下划线作前缀的约定)

测试又通过了!现在我们想最终特性进发:给定一个属性key,注册模式类的get()方法将返回一个对特定对象的引用。一下为符合这一要求的测试用例。

代码:

  1. class RegistryPHP4TestCase extends UnitTestCase
  2. {function testRegistryIsSingleton() { /*...*/ }
  3. function testEmptyRegistryKeyIsInvalid() { /*...*/ }
  4. function testEmptyRegistryKeyReturnsNull() { /*...*/ }
  5. function testSetRegistryKeyBecomesValid() { /*...*/ }
  6. function testSetRegistryValueIsReference()
  7. {$reg =& Registry::getInstance();$test_value = 'something';
  8. $reg->set('key', $test_value);
  9. $this->assertReference($test_value, $reg->get('key'));
  10. //another way to test the reference
  11. $test_value .= ' else';
  12. $this->assertEquual('something else',$reg->get('key'));
  13. }
  14. }
复制代码

以下为注册模式类的完整实现代码。

代码:

  1. class Registry
  2. {var $_store = array();
  3. function isValid($key)
  4. {return array_key_exists($key, $this->_store);}
  5. function &get($key)
  6. {if (array_key_exists($key, $this->_store))
  7. return $this->_store[$key];}
  8. function set($key, &$obj)
  9. {$this->_store[$key] =& $obj;}
  10. function &getInstance()
  11. {static $instance = array();
  12. if (!$instance) $instance[0] =& new Registry;
  13. return $instance[0];
  14. }
  15. }
复制代码

“注册模式”的get()方法会返回一个对象引用。类似的,set()方法的$obj参数要求得到一个对象引用并被赋值$this->_store[$key].。get()和set()方法的联合恰当使用能够满足assertReference()测试。

作者注:
“注册模式”的get()Registry::get()方法的代码应该写成@$this->_store[$key;]的形式,但是最好避免使用错误抑制符,使用错误抑制符的代码会变的摸棱两可,你需要花费额外的时间去了解你是否会再次访问这段代码。 array_key_exists()方法指出了应该避免的错误。



PHP5中,对象句柄(引用)带来了革命性的变化——你可以从对象引用的困境中解脱出来。事实上PHP5中注册模式的实现变的简单多了。因为你再也不用担心因为没有通过引用传递对象而引起致命错误的情况下使用联合数组。在PHP5中,你甚至能在注册模式中混和使用对象和变量。

一个例子:

在实际应用中“注册模式”会是什么样子?在网络应用程序开发中,通常我们只拥有一个数据库连接。(因此,广泛使用“单一模式”管理数据连接)但是,比如,由于历史遗留原因:你的应用的客户数据库与你的在线订单数据库是分开的,你的DBA又把你的旧订单转移到一个存档数据库中,而且它与你的客户数据库及订单(现有,最近)数据库也是完全隔离的。那么,你怎么才能方便地管理三个数据库连接而不用创建三个单独的“单一模式”呢?答安就是使用“注册模式”。

代码:

  1. class DbConnections extends Registry {}
复制代码
注:当你在你的代码中引入设计模式时,你的类名应该仍能反映他的角色和功能而没有必要使用模式的名字。使用模式的名字注释代码对与你的项目以外的程序员交流非常有帮助。但是在你的项目内,类的名字应该适合项目本身而且能够被项目成员很好的理解。虽然本章范例中的类名反映了设计模式的名字以及特定的实现方式,但是这并不是必须的。这仅仅是为了例子的清晰明了而不是好的命名规范。



DbConnections类是一个单件模式类,又继承了注册模式——DbConnections综合了两者的优点。

以下的代码片断创建并在注册模式类中存储了对每一个数据库的连接。

代码:

  1. //initial setup, somewhere near the start of your script
  2. $dbc =& DbConnections::getInstance();
  3. $dbc->set(
  4. 'contacts',
  5. new MysqlConnection('user1', 'pass1', 'db1', 'host1'));
  6. $dbc->set(
  7. 'orders',
  8. new MysqlConnection('user2', 'pass2', 'db2', 'host2'));
  9. $dbc->set(
  10. 'archives',
  11. new MysqlConnection('user3', 'pass3', 'db3', 'host3'));
复制代码

在其他类中将注册模式类连同数据一起载入就可以使用不同的连接了。

代码:

  1. // domain model classes
  2. class Customer {
  3. var $db;
  4. function Customer() {
  5. $dbc =& DbConnections::getInstance();
  6. $this->db =& $dbc->get('contacts');
  7. }
  8. //...
  9. }
  10. class Orders {
  11. var $db_cur;
  12. var $db_hist;
  13. function Contact() {
  14. $dbc =& DbConnections::getInstance();
  15. $this->db_cur =& $dbc->get('orders');
  16. $this->db_hist =& $dbc->get('archive');
  17. }
  18. //...
  19. }
复制代码

一个类依据客户数据库建模,另一个类依据历史和现在的客户订单建模。取得正确的数据库链接需要两个步骤:找到注册模式类,从中找出与给定的属性(key)相匹配的对象。

将注册模式实现为单件模式:

如前所述,把注册模式实现为单件模式有很多实现方式。

第一步,将注册模式实现为单件对象,(作者注:我们在第四章——The Singleton Pattern末尾简单讨论过)。

按照这种设计,注册模式类的任何一个实例都将访问同一个数组。我们把这个新类叫做RegistryGlobal以区别于我们前面开发的类,并反映这种实现方式的特性。

以下为反映这种思想的测试用例(它应该看起来很熟悉)。

代码:

  1. class RegistryGlobalPHP4TestCase extends UnitTestCase {
  2. function testRegistryGlobal() {
  3. $reg =& new RegistryGlobal;
  4. $this->assertFalse($reg->isValid('key'));
  5. $this->assertNull($reg->get('key'));
  6. $test_value = 'something';
  7. $reg->set('key', $test_value);
  8. $this->assertReference($test_value, $reg->get('key'));
  9. }
  10. }
复制代码

实现代码如下所示:

  1. class RegistryGlobal {
  2. var $_store = array();
  3. function isValid($key) {
  4. return array_key_exists($key, $this->_store);
  5. }
  6. function &get($key) {
  7. if (array_key_exists($key, $this->_store))
  8. return $this->_store[$key];
  9. }
  10. function set($key, &$obj) {
  11. $this->_store[$key] =& $obj;
  12. }
  13. }
复制代码

isValid(), get(),和set()方法与我们前面开发的注册模式类完全相同。

下一步:我们来编写验证RegistryGlobal类是单件模式的测试用例。

代码:

  1. class RegistryGlobalPHP4TestCase extends UnitTestCase {
  2. function testRegistryGlobal() { /*...*/ }
  3. function testRegistryGlobalIsMonoState() {
  4. $reg =& new RegistryGlobal;
  5. $reg2 =& new RegistryGlobal;
  6. $this->assertCopy($reg, $reg2);
  7. $test_value = 'something';
  8. $reg->set('test', $test_value);
  9. $this->assertReference(
  10. $reg->get('test')
  11. ,$reg2->get('test'));
  12. }
  13. }
复制代码

这里测试用例创建了RegistryGlobal类的两个实例,并确认他们不是对同一对象的引用——在一个实例内设置一个对象的属性值(value),最后证实两个实例返回相同的对象。若测试通过RegistryGlobal类就拥有单态的行为。

代码:

  1. define('REGISTRY_GLOBAL_STORE', '__registry_global_store_key__');
  2. class RegistryGlobal
  3. {var $_store;
  4. function RegistryGlobal()
  5. {if
  6. (!array_key_exists(REGISTRY_GLOBAL_STORE, $GLOBALS)||!is_array($GLOBALS[REGISTRY_GLOBAL_STORE])) {$GLOBALS[REGISTRY_GLOBAL_STORE] = array();
  7. }
  8. $this->_store =& $GLOBALS[REGISTRY_GLOBAL_STORE];
  9. }
  10. function isValid($key)
  11. {return array_key_exists($key, $this->_store);}
  12. function &get($key)
  13. {if (array_key_exists($key, $this->_store)) return $this->_store[$key];}
  14. function set($key, &$obj) {
  15. $this->_store[$key] =& $obj;
  16. }
  17. }
复制代码

本方法中的神奇之处在于$this->_store =& $GLOBALS[REGISTRY_GLOBAL_STORE;] 这一行,引用操作符将全局数组绑定到实例变量$_store上。这是单件模式实现的关键所在:每次在对象中使用$this->_store变量时,作用反映到全局变量中。

但是并不推荐基于全局变量的解决方案。如果PHP4支持这一特性的话,静态类变量会是更好的解决方案。然而,我们可以在代码中通过引用实现静态类变量吗?

测试与 RegistryGlobal 类的测试相似。

代码:

  1. class RegistryMonoStatePHP4TestCase extends UnitTestCase {
  2. function testRegistryMonoState() {
  3. $this->assertCopy(
  4. $reg =& new RegistryMonoState;
  5. $reg2 =& new RegistryMonoState);
  6. $this->assertFalse($reg->isValid(‘key’));
  7. $this->assertNull($reg->get(‘key’));
  8. $test_value = ‘something’;
  9. $reg->set(‘key’, $test_value);
  10. $this->assertReference($reg->get(‘key’), $reg2->get(‘key’));
  11. }
  12. }
复制代码

要自己实现类静态变量,可以将一个对函数静态变量的引用绑定到类的实例变量上。

代码:

  1. class RegistryMonoState {var $_store;
  2. function &_initRegistry() { static $store = array(); return $store;
  3. }
  4. function RegistryMonoState() {
  5. $this->_store =& $this->_initRegistry();
  6. }
  7. function isValid($key) {
  8. return array_key_exists($key, $this->_store);
  9. }
  10. function &get($key) {
  11. if (array_key_exists($key, $this->_store))
  12. return $this->_store[$key];
  13. }
  14. function set($key, &$obj) {
  15. $this->_store[$key] =& $obj;
  16. }
  17. }
复制代码

initRegistry()方法包含一个初始化为数组的静态变量。这个静态变量通过引用返回。在构造函数中$_store实例变量被赋于通过initRegistry()函数返回的引用——即静态数组。好!一个PHP4的类静态变量产生了。

使用类静态变量的实现:

PHP5中,没有必要自己实现类静态变量,因为PHP5直接支持类静态变量。因此,PHP5简化了实现。而且,PHP5中引用、对象不再有PHP4中的意义,但是assertReference() 处理了这种差别,如果两个变量指向同一个对象句柄也可以通过测试。

以下是为PHP5改写的类似的Registry测试用例。

代码:

  1. // PHP5
  2. class RegistryMonoStatePHP5TestCase extends UnitTestCase {
  3. function testRegistryMonoState() {
  4. $this->assertCopy(
  5. $reg = new RegistryMonoState
  6. ,$reg2 = new RegistryMonoState);
  7. $this->assertFalse($reg->isValid(‘key’));
  8. $this->assertNull($reg->get(‘key’));
  9. $test_value = new TestObj;
  10. $reg->set(‘key’, $test_value);
  11. $this->assertReference($test_value, $reg2->get(‘key’));
  12. }
  13. }
复制代码

以下是PHP5版本的使用静态类变量的Registry类。

代码:

  1. class RegistryMonoState {
  2. protected static $store = array();
  3. function isValid($key) {
  4. return array_key_exists($key, RegistryMonoState::$store);
  5. }
  6. function get($key) {
  7. if (array_key_exists($key, RegistryMonoState::$store))
  8. return RegistryMonoState::$store[$key];
  9. }
  10. function set($key, $obj) {
  11. RegistryMonoState::$store[$key] = $obj;
  12. }
  13. }
复制代码

PHP5中用这种方式编码Registry类的一个有趣的效果是你可以用相同的代码使用实例或者静态方法。以下是证明仅仅使用静态方法的测试用例。

代码:

  1. class RegistryMonoStatePHP5TestCase extends UnitTestCase {
  2. function testRegistryMonoState() { /*...*/ }
  3. function testRegistryMonoStateStaticCalls() {
  4. $this->assertFalse(RegistryMonoState::isValid(‘key’));
  5. $this->assertNull(RegistryMonoState::get(‘key’));
  6. $test_value = new TestObj; RegistryMonoState::set(‘key’, $test_value);
  7. $this->assertIdentical($test_value, RegistryMonoState::get(‘key’));
  8. }
复制代码

现在你已经看到在PHP5中的静态调用接口,下面让我们在PHP4中实现相同的接口。在前面的PHP4“静态类变量”部分,实现需要使用“函数静态变量返回引用”来跟踪。PHP4版本的静态调用接口测试与PHP5版本的测试类似。

代码:

  1. // PHP4
  2. class RegistryStaticPHP4TestCase extends UnitTestCase {
  3. function testRegistryStatic() {
  4. $this->assertFalse(RegistryStatic::isValid(‘key’));
  5. $this->assertNull(RegistryStatic::get(‘key’));
  6. $test_value = ‘something’; RegistryStatic::set(‘key’, $test_value);
  7. $this->assertReference($test_value, RegistryStatic::get(‘key’));
  8. }
  9. }
复制代码

以下是符合测试要求的代码实现。

代码:

  1. class RegistryStatic {
  2. function &_getRegistry() { static $store = array(); return $store;
  3. }
  4. function isValid($key) {
  5. $store =& RegistryStatic::_getRegistry();
  6. return array_key_exists($key, $store);
  7. }
  8. function &get($key) {
  9. $store =& RegistryStatic::_getRegistry();
  10. if (array_key_exists($key, $store))
  11. return $store[$key];
  12. }
  13. function set($key, &$obj) {
  14. $store =& RegistryStatic::_getRegistry();
  15. $store[$key] =& $obj;
  16. }
  17. }
复制代码

这个实现方法的重点是getRegistry()方法返回一个对静态数组的引用。

$store =& RegistryStatic::_getRegistry();这一行,在随后的函数中把变量$store通过引用赋给静态数组,允许所有的函数可以静态访问数组,允许所有的方法可以被静态调用。

也可以不使用PHP4“静态类变量跟踪”达到相同的效果:将原先的基于单件模式的Registry类与一个包装类结合以达到允许静态调用。这个类与testRegistryStatic()有相同的测试代码,但是他的实现如下所示:

代码:

  1. class RegistryStatic {
  2. function isValid($key) {
  3. $reg =& Registry::getInstance();
  4. return $reg->isValid($key);
  5. }
  6. function &get($key) {
  7. $reg =& Registry::getInstance();
  8. return $reg->get($key);
  9. }
  10. function set($key, &$obj) {
  11. $reg =& Registry::getInstance();
  12. $reg->set($key, $obj);
  13. }
  14. }
复制代码

结论:

虽然注册模式简化了对大量对象的访问,但是仍然有许多问题——与全局变量联合。你需要确定要求的属性Key在访问之已经被初始化了,而且设置属性的方法可以全局访问,你的对象仍然可能在你的代码的其他部分出乎意料的被替换掉。显然,全局数据非常有好处,方便,但是你需要时刻记住任何全局数据都是有一些不安全的。

内嵌的Registry模式

除了单独使用注册模式——如本章所示,Registry模式与其他对象结合时功能也是非常强大。例如:当对象的创建代价非常昂贵(例如需要查询大量数据库来初始化对象)时,而且对象在这个应用中被使用一次或多次,如果这样,你能创建一个结合了工作模式 (见第三章) 和注册模式的“Finder”类以获得已经创建的对象的缓存而不用再次创建他们?

以下是一个Contact类,AddressBook类是工厂类。

代码:

  1. class AddressBook {
  2. function &findById($id) {
  3. return new Contact($id);
  4. }
  5. }
  6. class Contact {
  7. function Contact($id) {
  8. // expensive queries to create object using $id
  9. }
  10. // ... other methods
  11. }
复制代码

你可以在AddressBook类中插入Registry模式来提供缓存。代码可以如下所示:

  1. class AddressBook {
  2. var $registry;
  3. function AddressBook() {
  4. $this->registry =& Registry::getInstance();
  5. }
  6. function &findById($id) {
  7. if (!$this->registry->isValid($id)) {
  8. $this->registry->set($id, new Contact($id));
  9. }
  10. return $this->registry->get($id);
  11. }
  12. }
复制代码

AddressBook类的构造函数将registry绑定到一个实例变量。当创建了一个特定的ID并被findById()方法调用时,Registry被检查以确定对象是否已经被缓存。如果没有,将创建一个新的对象并存储在Registry中。被调用的对象将通过函数从 Registry中取出并被返回。

你可能感兴趣的:(设计模式)