DI 是单例和/或静态对象的唯一解决方案


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

有人告诉我,单例很难测试。

  • http://misko.hevery.com/2008/08/17/singletons-are-pathological-liars/
  • http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/

有人告诉我,静态方法/对象也不好。

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

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

但。。。我真的不习惯DI,举这个例子:

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

使用 DI,我的代码将转向:

global $logger; //> consider i have been instanciated it at the start of my fw
$query = new PreparedQuery($logger);
$query->prepare() etc.

现在看来还不错。但是考虑一个需要许多查询的页面,我认为每次都必须在构造函数中编写$logger是非常多余的,特别是如果您考虑 PreparedQuery 是否需要构造函数中的许多其他依赖项。

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

因此,根据您除了 DI 之外的经验,使用什么模式是好的?

溶液:

PHPunit 的创建者解释了如何解决单例问题(以及如何解决 PHP 5.3 的静态方法测试问题)

  • (单例) http://sebastian-bergmann.de/archives/882-Testing-Code-That-Uses-Singletons.html
  • (静态)http://sebastian-bergmann.de/archives/883-Stubbing-and-Mocking-Static-Methods.html

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

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

好吧,在这种情况下,我会建立一个建筑商(或工厂)。所以你的工厂会为你注入依赖关系。 这样,您还可以避免全局变量:

class PreparedQueryFactory {
    protected $logger = null;
    public function __construct($loggger) {
        $this->logger = $logger;
    }
    public function create() {
        return new PreparedQuery($this->logger);
    }
}

这样,您只需执行一次:

$factory = new PreparedQueryFactory($logger);

然后,每当您需要新查询时,只需调用:

$query = $factory->create();

现在,这是一个非常简单的例子。 但是如果需要,您可以添加各种复杂的逻辑。 但关键是,通过避免代码中的new,也可以避免管理依赖项。 因此,您可以根据需要传递工厂。

好处是所有这些都

是 100% 可测试的,因为所有内容都在任何地方注入(而不是使用全局变量)。

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

日志记录通常是静态单例正常的示例。无论如何,您都不需要嘲笑您的日志记录,是吗?

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

一个简化的示例:

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 方面做得还不够

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

显然,该代码的目的是准备一个查询,而不是担心如何构建一个PreparedQuery,它应该只在类构造函数中请求一个。或者,如果它需要能够生成大量的 PreparedQuery,它应该要求一个原型(每当它需要一个新的原型时它就会克隆)或工厂对象。关键是,PreparedQuery有一个记录器这一事实无关紧要,应该在其他地方处理。

构造函数中"询问你需要什么"的原则原则上很容易理解,尽管我仍在尝试自己弄清楚它在各种情况下在实践中的含义,以及如何一直实现它到顶部("main 方法"或等效物)。但是,我认为这个原则说明了您遇到的一般问题。该new运算符不应该首先位于它的位置。