在PHP中实现Registry模式的可扩展方式?

ton*_*nix 3 php registry design-patterns

我想知道是否有一种在PHP中实现注册表模式的好方法,让我更清楚:

我知道当你需要跟踪你实例化的对象以便重用它们而不是从脚本到脚本再次重新实例化时,就会使用注册表,例如我有一个我想要实例化一次的数据库类,然后用于我的所有脚本,我不想一次又一次地重新实例化它.另一个示例可以是表示当前登录用户的实例的User类.在这种情况下我无法使用Singleton,例如,当我想要检索当前登录用户的朋友等时,我需要另一个User实例.

所以我提出了这样的想法,即在这种情况下,注册表更适合这种需求.

我也知道有两种方法可以实现它,或者更好的两种方式来访问存储的实例:

  • 显式或外部,意味着每次需要恢复脚本中的实例或者需要在其中放置实例时,应该调用注册表;
  • 隐式或内部,意味着您使用getInstance()方法创建一个抽象类,该方法返回具有get_called_class()后期静态绑定功能的实例,将其添加到注册表,然后从注册表本身返回该实例,注意如果将$ label参数传递给getInstance()方法,则将返回注册表中的特定实例.这种方法对消费者来说是透明的,在我看来更清洁,更整洁(但我会展示两种实现方式).

我们来看一个基本的注册表(非常简单的实现,只是一本书的例子):

class Registry {

static private $_store = array();

static public function set($object, $name = null)
{
  // Use the class name if no name given, simulates singleton
  $name = (!is_null($name)) ? $name: get_class($object);
  $name = strtolower($name);
  $return = null;
  if (isset(self::$_store[$name])) {
    // Store the old object for returning
    $return = self::$_store[$name];
  }
    self::$_store[$name]= $object;
    return $return;
}

  static public function get($name)
  {
    if (!self::contains($name)) {
      throw new Exception("Object does not exist in registry");
    }
    return self::$_store[$name];
  }

  static public function contains($name)
  {
    if (!isset(self::$_store[$name])) {
      return false;
    }
    return true;
  }

  static public function remove($name)
  {
    if (self::contains($name)) {
      unset(self::$_store[$name]);
    } 
  }
 }
Run Code Online (Sandbox Code Playgroud)

我知道,注册表可能是一个单身人士,所以你从来没有同时拥有两个注册表(谁需要有人可以思考,但谁知道).无论如何,存储/访问实例的外部方式如下:

$read = new DBReadConnection;
Registry::set($read);
$write = new DBWriteConnection;
Registry::set($write);
// To get the instances, anywhere in the code:
$read = Registry::get('DbReadConnection');
$write = Registry::get('DbWriteConnection');
Run Code Online (Sandbox Code Playgroud)

在内部,在调用getInstance时在类中(取自本书):

abstract class DBConnection extends PDO {
  static public function getInstance($name = null)
  {
    // Get the late-static-binding version of __CLASS__
    $class = get_called_class();
    // Allow passing in a name to get multiple instances
    // If you do not pass a name, it functions as a singleton
    $name = (!is_null($name)) ?: $class;
    if (!Registry::contains($name)) {
      $instance = new $class();
      Registry::set($instance, $name);
    }
    return Registry::get($name);
  }
}
class DBWriteConnection extends DBConnection {
  public function __construct()
  {
parent::__construct(APP_DB_WRITE_DSN, APP_DB_WRITE_USER, APP_DB_WRITE_PASSWORD);
} }
class DBReadConnection extends DBConnection {
  public function __construct()
  {
parent::__construct(APP_DB_READ_DSN, APP_DB_READ_USER,APP_DB_READ_PASSWORD);
  }
}
Run Code Online (Sandbox Code Playgroud)

显然间接地指向注册表(第二种情况)对我来说似乎更具可扩展性,但是如果有一天我需要更改注册表并使用另一个实现,我需要将这些调用更改为Registry :: get()和Registry: :getInstance()方法中的set()是为了适应变化还是有更聪明的方法?

您是否有人遇到过这个问题,并根据复杂性等应用程序的类型找到了一种简单的方法来交换不同的注册表?

应该是配置类的解决方案吗?或者,如果有可能,是否有更智能的方法来实现可扩展的注册表模式?

感谢您的关注!希望得到一些帮助!

thp*_*hpl 6

首先.很高兴您自己发现了自己的方法问题.通过使用注册表,您可以将类紧密耦合到从中提取依赖项的注册表.不仅如此,如果你的类必须关心它们如何存储在注册表中并从中获取(在你的情况下每个类也会实现单例),你也违反了单一责任原则.

根据经验,请记住:从类中从任何存储中全局访问对象将导致类和存储之间的紧密耦合.

让我们看看Martin Fowler对此话题的看法:

关键的区别在于,使用服务定位器,服务的每个用户都对定位器具有依赖性.定位器可以隐藏其他实现的依赖关系,但您确实需要查看定位器.因此,定位器和注入器之间的决定取决于该依赖性是否是一个问题.

使用服务定位器,您必须在源代码中搜索定位器的调用.具有查找引用功能的现代IDE使这更容易,但它仍然不像查看构造函数或设置方法那么容易.

所以你看它取决于你正在建设什么.如果你有一个具有少量依赖关系的小应用程序,那么继续使用它,继续使用注册表(但你绝对应该删除一个类行为来存储自己或从注册表中获取).如果情况并非如此,并且您正在构建复杂的服务,并希望使用类型提示和构造函数注入来明确定义依赖关系.

<?php

class DbConsumer {

    protected $dbReadConnection;
    protected $dbWriteConnection;

    public function __construct(DBReadConnection $dbReadConnection, DBWriteConnection $dbWriteConnection)
    {
        $this->dbReadConnection  = $dbReadConnection;
        $this->dbWriteConnection = $dbWriteConnection;
    }

}

// You can still use service location for example to grab instances
// but you will not pollute your classes itself by making use of it
// directly. Instead we'll grab instances from it and pass them into
// the consuming class

// [...]

$read   = $registry->get('dbReadConnection'); 
$write  = $registry->get('dbWriteConnection'); 

$dbConsumer = new DbConsumer($read, $write);
Run Code Online (Sandbox Code Playgroud)

应该是配置类的解决方案吗?或者,如果有可能,是否有更智能的方法来实现可扩展的注册表模式?

这种方法经常遇到,你可能听说过有关DI-Container的事情.Fabien Potencier写道:

依赖注入容器是一个知道如何实例化和配置对象的对象.为了能够完成它的工作,它需要知道构造函数参数和对象之间的关系.

服务定位器和DI-Container之间的界限似乎相当模糊,但我喜欢这样的概念:服务定位器隐藏类的依赖关系,而DI-Container则不隐藏(与简单的单元测试的好处).

所以你看,没有最终答案,这取决于你正在建设什么.我可以建议进一步深入研究这个主题,因为如何管理依赖关系是每个应用程序的核心问题.

进一步阅读