关于php:DI是Singleton和/或静态对象的唯一解决方案吗?

Is DI the only solution to Singleton and/or static objects?

有人告诉我单件很难测试。

  • http://misko.hevery.com/2008/08/17/singletons-are-pathy-liars/
  • http://misko.hevery.com/code-reviewers-guide/crash-fr脆-global-state-singletons/

我听说静态方法/对象也不好。

  • http://misko.hevery.com/2008/12/15/static-methods-are-death-to-testability/

所以基本上唯一的解决方案就是依赖注入。

但是…我真的不习惯DI,举个例子:

在我的框架中,我有一个管理SQL的类。这个类(以及我的许多其他框架)使用一个单例日志记录器来记录消息(以及许多其他助手)。

有了DI,我的代码将变成:

1
2
3
4
global $logger; //> consider i have been instanciated it at the start of my fw

$query = new PreparedQuery($logger);
$query->prepare() etc.

现在看来还不错。但是考虑一个需要很多查询的页面,我认为每次在构造函数中编写EDOCX1[0]都是非常多余的,特别是考虑PreparedQuery是否需要构造函数中的许多其他依赖项时。

我找到的避免singleton的唯一解决方案是在主应用程序中使用一个方法(或只是一个简单的函数),它存储对这个助手对象(服务定位器/容器)的每个引用,但这并不能解决隐藏依赖项的问题。

所以在你的经验中,除了DI之外,什么是好的模式可以使用?

解决方案:

对于每个感兴趣的人,phpunit的创建者解释了如何解决单例问题(以及如何用php 5.3解决静态方法测试问题)。

  • (singleton)http://sebastian-bergmann.de/archives/882-testing-code-that-uses-singletons.html
  • (静态)http://sebastian-bergmann.de/archives/883-stubing-and-mocking-static-methods.html

如果你问我的话,读起来很有趣。


嗯,在这种情况下,我会建一个建筑商(或工厂)。所以你的工厂会为你注入依赖。这样你也可以避免你的全球范围:

1
2
3
4
5
6
7
8
9
class PreparedQueryFactory {
    protected $logger = null;
    public function __construct($loggger) {
        $this->logger = $logger;
    }
    public function create() {
        return new PreparedQuery($this->logger);
    }
}

这样,你只需要做一次:

1
$factory = new PreparedQueryFactory($logger);

当您需要新的查询时,只需调用:

1
$query = $factory->create();

现在,这是一个非常简单的例子。但如果需要的话,可以添加各种复杂的逻辑。但关键是,通过在代码中避免使用new,您还可以避免管理依赖项。因此,您可以根据需要将工厂移交给其他人。

好处是,所有这些都是100%可测试的,因为所有的东西都被注入到任何地方(而不是使用全局)。

您也可以使用注册表(也称为服务容器或DI容器),但请确保您正在将注册表注入。


请不要使用global。您需要在构造函数中传递$logger或传递服务容器(也称为对象管理器、服务定位器、资源管理器)。symfony框架的变体http://symfony.com/doc/current/book/service_container.html您可以创建自己的对象管理器,他的方法不应该是静态的。


上面的答案给了你一些想法。我将介绍另一个:实现插件架构。记录器变成一个插件,您可以随时启用/禁用/更改。

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Logger implements Observer {
    public function notify($tellMeWhatHappened) {
         // oh really? let me do xyz
    }
}

class Query implements Observable {
    private $observers = array();

    public function addObserver(Observer $observer) {
        $this->observers[] = $observer;
    }

    public function foo() {
        // great code
        foreach ($this->observers as $observer) { $observer->notify('did not work'); }
    }
}

这会将记录器从构造函数中移除。这是我喜欢的,如果它对物体的功能不是必需的。


日志记录通常是静态单例正常的例子。不管怎样,你不需要模仿你的日志记录,是吗?


在我对Misko Hevery关于DI和new运算符的讨论的理解中,问题是您在实现DI方面做得不够深入。

Hevery总是说,不应该将业务逻辑与对象构造混合在一起。但是,在示例的两行中,第一行($query = new PreparedQuery($logger);构造对象,第二行($query->prepare(/* ... */);构造业务逻辑。

显然,该代码的目标是准备一个查询,而不是担心如何构建一个PreparedQuery,它应该只在类构造函数中要求一个查询。或者,如果它需要能够生成大量准备好的查询,那么它应该请求一个原型(当它需要新的原型时,它将进行克隆)或者一个工厂对象。重点是,PreparedQuery有一个日志记录器这一事实并不重要,应该在其他地方加以注意。

在构造器中,"问你需要什么"的原则原则是很容易理解的,尽管我仍在努力为自己弄清楚它在各种情况下的实践中意味着什么,以及如何一路实现它(主方法或等效方法)。然而,我认为这个原则说明了你所面临的普遍问题。new运算符不应位于其最初所在的位置。