PHP 5.4对象去引用是否成功地缓解了这个DI容器中静态存储参数的缺点


Does PHP 5.4 object dereferencing successfully mitigate the drawbacks of static storage parameter in this DI container?

公共服务更新:

自从我最初提出这个问题以来,我学到了很多。如果你正在读这篇文章,请接受我的建议,完全避免static。只是不要。使用It.没有办法进行依赖项注入;依赖项注入是一种方式


我最近花了很多时间深入研究各种控制反转(IOC)概念。我完全同意那些认为服务定位器是反模式的人。我构建了一个可以修改的,它允许使用静态定位器方法在类中间导入"全局"实体,并可以隐藏对象的实际依赖关系,这让我感到震惊。

从服务定位器开始,我开始创建一个依赖注入(DI)容器,该容器为我提供了静态依赖访问的灵活性,而没有伴随的静态变量的缺点。

下面是这样一个实现的简单示例:

<?php
class Container
{
  protected static $params = [];
  public function store($key, $val)
  {
    static::$params[$key] = $val;
    return $this;
  }
  public function fetch($key)
  {
    if (isset(static::$params[$key])) {
      return static::$params[$key];
    }
    $msg = "No parameter match found in container: $key";
    throw new OutOfBoundsException($msg);
  }
}
$container = new Container;
$container->store('widgetDep', new WidgetDependency);
$container->store('kumquatDep', new KumquatDependency);
// and somewhere else in the application without access to the global namespace
// (i.e. the $container instance we just created) ...
$widget  = new Widget(new Container);
$kumquat = new Kumquat(new Container);

这似乎是朝着正确的方向迈出的一步,因为静态$params属性受到保护,并且不存在在"全局"静态范围内访问或操作它的静态方法:对象需要访问容器才能访问依赖项。

哦,等等

不幸的是,将依赖项存储在此容器中意味着现在每个注入依赖项的对象都对容器对象有一个伪依赖项,从而隐藏了其真实依赖项另一个负面的副作用是,每个对象都可以访问容器中的每个可用依赖项,显然,Widget对象不应该访问Kumacquat的依赖项。此外,使用这种方法的抽象工厂只会将伪依赖从WidgetKumacquat类中移到工厂中。

PHP 5.4替代方案

使用5.4的新对象构造去引用功能,我们可以在不需要访问全局命名空间中已经创建的$container实例的情况下执行以下操作:

$widget  = new Widget((new Container)->fetch('widgetDep'));
$kumquat = new Kumquat((new Container)->fetch('kumquatDep'));

使用这种方法,我们成功地:

  1. 消除了Widget和Kumacquat对象的容器依赖,允许它们的构造函数键入所需的特定依赖对象
  2. 阻止了下游Widget和Kumacquat对象访问他们不应该知道存在的依赖项
  3. 保留了静态依赖关系存储功能

现在,一个可能的缺点是,这种方法意味着开发人员必须足够自律,不能将完整的Container对象作为依赖项传递。这一点至关重要。

所以问题是

分为两部分:

  1. 你认为这种方法有哪些具体的缺点,以及
  2. 静态Container::$params是否有必要?它是否应该是一个标准的受保护属性,由全局命名空间中的顶级对象图工厂类/方法访问(不需要static

您根本不应该在这里使用static。只需创建一个容器:$container = new DIContainer();,并将该对象用作典型的依赖项。毕竟,在应用程序的核心中有极少数地方需要访问整个容器。

看看Symfony的依赖注入组件——这是一段非常好的代码。


编辑:

根据第一条评论。是的,你误解了我的意思。通常你只需要容器中的几个依赖项,所以你会写这样的东西:

$service = new Service($container->get('dep.a'), $container->get('dep.b'), 123);

我的观点是,不应该在容器中使用静态属性,因为它只不过是一个全局对象。之间没有区别

global $container;
$widget  = new Widget($container->fetch('widgetDep'));
$kumquat = new Kumquat($container->fetch('kumquatDep'));
$widget  = new Widget(Container::getInstance()->fetch('widgetDep'));
$kumquat = new Kumquat(Container::getInstance()->fetch('kumquatDep'));
// You're using new objects but they share the same, **global** array.
// Therefore, they are actually global themselves.
$widget  = new Widget((new Container())->fetch('widgetDep'));
$kumquat = new Kumquat((new Container())->fetch('kumquatDep'));

换句话说,Container本身应该是一个局部变量,如果您需要在其他地方访问它(某些对象可能需要访问整个容器),那么您应该将它作为依赖项显式传递给该对象。

正如我之前所说的,看看Symfony DIC和整个框架,看看如何制作一个好的、写得好的DIC。


简单容器:

class Container {
    private $services = array();
    public function get($service) {
        if (!array_key_exists($this->services, $service)) {
            throw ...;
        }
        return $this->services[$service];
    }
}
$containerA = new Container();
$containerB = new Container();
// $containerA and $containerB are completely different 
// objects and don't share anything

我不喜欢创建一个新容器并共享一个全局数组的想法。

创建facade对象的解决方案对我来说似乎更好:

class IoC
{
  private static $container;
  public static function Initialize ( IContainer $Container )
  {
    self::$container = $Container;
  }
  public static function Resolve( $type, array $parameters = array() )
  {
    return self::$container->Resolve( $type, $parameters );
  }
}

在引导程序中,IoC可以初始化:

$container = new Container();
$container->Register( 'Logger', function() { return new Logger('somefile.log'); } );
IoC::Initialize ( $container );

使用容器:

$log = IoC::Resolve( 'Logger' );

Imho是一个比交响乐"解决方案"更好的解决方案。容器可以很容易地被另一个实现替换,而无需更改任何其他代码。对于测试,只需使用一个新的"容器"实例,而不使用facade对象。

我使用了一个类似Laravel的类。它允许我做这样的事情:

// Create a PDO connection instance
IoC::register('pdo', function($config)
{
    return new PDO($config['dsn'], $config['user'], $config['pass'], $config['params']);
});
// Create a Database instance using the default PDO connection instance
IoC::register('database', function($config)
{
    return new Database(IoC::resolve('pdo', $config, $config['identifier'], $config['type']));
});
$db = IoC::resolve('database', array('user' => 'root', 'pass' => '', ...));

这意味着我可以将每个类(或一组类)注册为由IoC容器处理的小型可插入模块。这使我能够根据单元测试之类的事情的需要覆盖这些对象,因为在PHP代码的其余部分中,每个类都不会特别创建。