在PHP中实现注册表模式的可伸缩方式


Scalable way of implementing the Registry pattern in PHP?

我想知道是否有一种很好的方法在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]);
    } 
  }
 }

我知道,注册表可以是一个单例,所以你永远不会有两个注册表在同一时间(谁需要他们,有人可以认为,但谁知道)。总之,存储/访问实例的外部方式是这样的:

$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');

在内部,当调用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);
  }
}

显然间接引用注册表(第二种情况)对我来说似乎更具可扩展性,但是如果有一天我需要更改注册表并使用另一种实现,我需要更改对getInstance()方法内的registry::get()和registry::set()的调用,以适应更改或是否有更智能的方法?

你们中有人遇到这个问题,并找到一个简单的方法来交换不同的注册表取决于应用程序的类型和复杂性等?

配置类应该是解决方案吗?或者,如果可能的话,是否有更聪明的方法来实现可伸缩的注册中心模式?

感谢您的关注!希望能有所帮助!

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

作为一个经验法则,请记住:从一个类内部从任何存储访问全局对象将导致类和存储之间的紧密耦合。

让我们来看看Martin Fowler对这个话题是怎么说的:

关键的区别在于,使用服务定位器时,服务的每个用户都依赖于该定位器。定位器可以隐藏对其他实现的依赖,但您确实需要看到定位器。因此,选择定位器还是注入器取决于依赖关系是否存在问题。

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

这取决于你在构建什么。如果你有一个小的应用程序,有少量的依赖,去它的,继续使用注册表(但你绝对应该删除一个类的行为来存储自己或从注册表中抓取)。如果不是这样,你正在构建复杂的服务,想要一个干净直接的API,可以使用类型提示和构造函数注入显式地定义依赖项。

<?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);
配置类应该是解决方案吗?或者,如果可能的话,是否有更聪明的方法来实现可伸缩的注册中心模式?

这种方法经常遇到,您可能听说过一些关于DI-Container的事情。Fabien Potencier写道:

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

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

所以你看,没有最终的答案,这取决于你在构建什么。我可以建议深入挖掘这个主题,因为如何管理依赖关系是每个应用程序的核心关注点。

<标题>进一步阅读
  • 为什么注册表模式是反模式。还有什么替代方案呢?
  • 服务定位器是一个反模式
  • 你需要依赖注入容器吗?