如果Singletons不好,那么服务容器为什么好?


91

我们都知道单身人士是多么糟糕,因为它们隐藏了依赖以及其他原因

但是在一个框架中,可能有许多对象仅需要实例化一次,并可以从任何地方调用(记录器,数据库等)。

为了解决这个问题,我被告知要使用所谓的“对象管理器”(或类似symfony的服务容器)在内部存储对服务(记录器等)的所有引用。

但是,为什么服务提供者不像单纯的Singleton那样糟糕?

服务提供者也隐藏了依赖关系,他们只是包装了第一个实例的创建。因此,我真的很难理解为什么我们应该使用服务提供商而不是单例。

PS。我知道不隐藏依赖项应该使用DI(如Misko所述)

我会补充:现在,单例并没有那么邪恶,PHPUnit的创建者在这里解释了这一点:

DI + Singleton解决了以下问题:

<?php
class Client {

    public function doSomething(Singleton $singleton = NULL){

        if ($singleton === NULL) {
            $singleton = Singleton::getInstance();
        }

        // ...
    }
}
?>

即使这不能解决所有问题,这还是很聪明的。

除了DI和Service Container 之外,还有什么好的可接受的解决方案来访问此帮助对象?


2
@是,您的编辑做出了错误的假设。Sebastian丝毫没有暗示使用Singnons编写的代码片段将使问题减少。这只是使代码无法进行更可测试的一种方法。但这仍然是有问题的代码。实际上,他明确指出:“仅仅因为您可以,并不意味着您应该这样做”。正确的解决方案仍然是完全不使用Singletons。
戈登

3
@是遵循SOLID原则。
戈登

19
我对单身人士不好的说法表示怀疑。它们可能会被滥用,是的,但是任何工具都可能被滥用。手术刀可用于挽救生命或终结生命。如果您不知道自己在做什么,电锯可以清除森林以防止森林大火,也可以使胳膊的相当大的一部分掉下来。学会明智地使用工具,不要将建议视为福音-那就是无思想的头脑。
paxdiablo 2011年

4
@paxdiablo,但它们糟糕。单例违反SRP,OCP和DIP。它们将全局状态和紧密耦合引入到您的应用程序中,并使您的API依赖于它的依赖关系。所有这些都会对代码的可维护性,可读性和可测试性产生负面影响。在少数情况下,这些弊端会带来微不足道的好处,但我认为99%的人不需要辛格尔顿。特别是在PHP中,单例无论如何仅对于请求是唯一的,并且从Builder组装协作者图非常简单。
戈登

5
不,我不相信。工具是执行功能的一种手段,通常是通过某种方式使它更容易实现,尽管某些(emacs?)具有使难度更难的罕见区别:-)在这种情况下,单例与平衡树或编译器没有什么不同。如果您只需要确保一个对象的一个​​副本,则单例执行此操作。它是否做得好可以进行辩论,但我不相信您可以说它根本做不到。也许还有更好的方法,例如电锯比手锯快,或者用钉枪对付锤子。但这并不能使手锯/锤子成为一种工具。
paxdiablo 2011年

Answers:


76

可以这么说,服务定位器只是两个弊端中的较小者。归结为这四个差异的“较小”(至少我现在无法想到其他任何差异):

单一责任原则

服务容器不像Singleton那样违反Single Responsibility Principle。单例混合了对象创建和业务逻辑,而服务容器严格负责管理应用程序的对象生命周期。在这方面,服务容器更好。

耦合

由于静态方法调用,单例通常被硬编码到您的应用程序中,这导致代码中的紧密耦合和难以模拟的依赖关系。另一方面,SL只是一个类别,可以注入。因此,尽管您所有分类的内容都将依赖它,但至少它是一个松散耦合的依赖关系。因此,除非您将ServiceLocator本身实现为Singleton,否则它会更好并且也更容易测试。

但是,所有使用ServiceLocator的类现在都将依赖于ServiceLocator,这也是一种耦合形式。可以通过使用ServiceLocator的接口来减轻这种情况,因此您不必绑定到具体的ServiceLocator实现,但是您的类将取决于某种Locator的存在,而根本不使用ServiceLocator会大大提高重用性。

隐藏的依赖

但是,非常存在隐藏依赖项的问题。当您仅将定位符注入到使用的类中时,您将不知道任何依赖项。但是与Singleton相比,SL通常会实例化幕后所需的所有依赖关系。因此,当您获取服务时,最终不会像CreditCard示例中的Misko Hevery那样结束,例如,您不必手动实例化依赖项的所有依赖关系。

从实例内部获取依赖关系也违反了Demeter定律,该定律指出您不应该深入研究协作者。实例只应与其直接的协作者交谈。Singleton和ServiceLocator都存在此问题。

全球状态

全局状态的问题也有所缓解,因为在测试之间实例化新的服务定位器时,所有先前创建的实例也会被删除(除非您犯了错误,并将其保存在SL的静态属性中)。当然,这对于SL管理的类中的任何全局状态都不成立。

另请参阅关于服务定位器与依赖项注入的 Fowler,以进行更深入的讨论。


关于您的更新的注释以及Sebastian Bergmann关于使用Singletons的测试代码的链接文章:Sebastian丝毫没有暗示所建议的解决方法使使用Singleons的问题减少了。这只是使代码无法进行更可测试的一种方法。但这仍然是有问题的代码。实际上,他明确指出:“仅仅因为您可以,并不意味着您应该这样做”。


1
特别是在这里应加强可测试性。您不能模拟静态方法调用。但是,您可以模拟通过构造函数或setter注入的服务。
大卫,

44

服务定位器模式是反模式。它不能解决暴露依赖项的问题(从类的定义中看不出来它的依赖项是什么,因为它们没有被注入,而是被从服务定位器中抽出了)。

因此,您的问题是:服务定位符为什么很好?我的答案是:他们不是。

避免,避免,避免。


6
看起来您对接口一无所知。类只是在构造函数签名中描述了必要的接口,而这正是他需要知道的一切。通过的服务定位器应实现接口,仅此而已。而且,如果IDE将检查接口的实现,则控制任何更改都将非常容易。
OZ_

4
@ yes123:人们说错了,因为SL是反模式,所以他们是错误的。您的问题是“为什么SL好?” 我的回答是:不是。
杰森

5
我不会争辩SL是否是anit模式,但是我要说的是,与单例和全局变量相比,它的弊端要小得多。您不能测试依赖于单例的类,但是您绝对可以测试依赖于SL的类(可以将SL设计固定到无法使用的程度)...因此值得注意...
ircmaxell

3
@Jason,您需要传递实现Interface的对象-这只是您需要了解的内容。您仅通过定义类构造函数来限制自己,并且想在构造函数中编写所有类(而不是接口)-这是愚蠢的主意。您需要的只是接口。您可以使用模拟程序成功测试该类,可以轻松更改行为而无需更改代码,没有额外的依赖关系和耦合-这就是(一般而言)我们希望在Dependency Injection中拥有的一切。
OZ_ 2011年

2
当然,我将数据库,记录器,磁盘,模板,高速缓存和用户放到一个“输入”对象中,肯定会比使用容器更容易判断对象所依赖的依赖项。
Mahn 2012年

4

服务容器会像Singleton模式一样隐藏依赖项。您可能要建议改用依赖项注入容器,因为它具有服务容器的所有优点,而据我所知,它没有缺点(据我所知)。

据我了解,两者之间的唯一区别是在服务容器中,服务容器是被注入的对象(因此隐藏了依赖性),当您使用DIC时,DIC会为您注入适当的依赖性。由DIC管理的类完全忽略了由DIC管理的事实,因此您的耦合更少,相关性明确且单元测试愉快。

在SO上,这是一个很好的问题,解释了两者的区别:依赖注入和服务定位器模式之间有什么区别?


“ DIC为您注入了适当的依赖项” Singleton也会发生这种情况吗?
动态的

5
@ yes123-如果您使用的是Singleton,则在大多数情况下您都不会全局注入(这就是Singleton的意义)。我想如果您说如果您注入 Singleton,它不会隐藏依赖关系,但是有悖于Singleton模式的初衷-您会问自己,如果我不需要全局访问此类,为什么?我需要使它成为Singleton吗?
rickchristie 2011年

2

因为您可以轻松地通过以下方式替换服务容器中的对象:
1)继承(可以继承对象管理器类,并且可以重写方法)
2)更改配置(对于Symfony)

而且,单身是坏的不仅是高耦合的,而是因为他们是_ _tons。几乎所有对象的体系结构都是错误的。

使用“纯” DI(在构造函数中),您将付出很大的代价-所有对象都应在传递给构造函数之前创建。这将意味着更多的已用内存和更低的性能。此外,并非总是可以仅在对象中创建并传递对象-可以创建依赖关系链...我的英文不够好,无法完全讨论,请在Symfony文档中进行阅读。


0

对我来说,出于一个简单的原因,我尝试避免使用全局常量和单例,在某些情况下,我可能需要运行API。

例如,我有前端和管理员。在管理员内部,我希望他们能够以用户身份登录。考虑一下admin内部的代码。

$frontend = new Frontend();
$frontend->auth->login($_GET['user']);
$frontend->redirect('/');

这可以为前端初始化建立新的数据库连接,新的记录器等,并检查用户是否确实存在,有效等。它还将使用适当的单独cookie和位置服务。

我的单例想法是-您不能在父对象中两次添加同一对象。例如

$logger1=$api->add('Logger');
$logger2=$api->add('Logger');

将使您只有一个实例,并且两个变量都指向它。

最后,如果要使用面向对象的开发,请使用对象而不是类。


1
所以您的方法是$api 在框架周围传递var?我没有完全明白你的意思。同样,如果呼叫add('Logger')基本上返回了相同的实例,则您有一个服务容器
动态的

是的,没错。我将它们称为“系统控制器”,它们旨在增强API的功能。以类似的方式两次向模型添加“可审核”控制器将以完全相同的方式工作-仅创建一个实例,并且仅创建一组审核字段。
romaninsh 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.