如果单例不好,那么为啥服务容器是好的?

Posted

技术标签:

【中文标题】如果单例不好,那么为啥服务容器是好的?【英文标题】:If Singletons are bad then why is a Service Container good?如果单例不好,那么为什么服务容器是好的? 【发布时间】:2011-08-27 10:40:23 【问题描述】:

我们都知道糟糕的单例有多糟糕,因为它们隐藏了依赖关系并且对于other reasons。

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

为了解决这个问题,有人告诉我使用所谓的“对象管理器”(或 Service Container,如 symfony),它在内部存储对服务(记录器等)的每个引用。

但是为什么服务提供者不像纯单例那样糟糕呢?

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

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

添加

我要补充一点:现在单身人士并不那么邪恶,phpUnit 的创建者在这里解释了它:

http://sebastian-bergmann.de/archives/882-Testing-Code-That-Uses-Singletons.html

DI + Singleton 解决问题:

<?php
class Client 

    public function doSomething(Singleton $singleton = NULL)

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

        // ...
    

?>

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

除了 DI 和服务容器还有什么好的可接受的解决方案来访问这个帮助对象吗?

【问题讨论】:

@yes 您的编辑做出了错误的假设。 Sebastian 绝不表示代码 sn-p 正在减少使用 Singleons 的问题。这只是使无法测试的代码更具可测试性的一种方法。但这仍然是有问题的代码。事实上,他明确指出:“仅仅因为你可以,并不意味着你应该”。正确的解决方案仍然是根本不使用单例。 @yes 遵循 SOLID 原则。 我对单身不好的说法提出异议。它们可能被滥用,是的,但 any 工具也是如此。手术刀可用于挽救生命或结束生命。电锯可以清除森林以防止丛林大火,或者如果您不知道自己在做什么,它可以切断您手臂的相当大一部分。学会明智地使用你的工具,不要把建议当成福音——那是不加思索的头脑。 @paxdiablo 但他们 很糟糕。单例违反 SRP、OCP 和 DIP。它们将全局状态和紧密耦合引入您的应用程序,并使您的 API 对它的依赖项撒谎。所有这些都会对代码的可维护性、可读性和可测试性产生负面影响。在极少数情况下,这些缺点超过了一点点好处,但我认为在 99% 的情况下你不需要 Singleton。尤其是在 PHP 中,Singleton 只对 Request 是唯一的,而且从 Builder 中组装协作者图非常简单。 不,我不这么认为。工具是执行功能的一种手段,通常是通过某种方式使其变得更容易,尽管有些(emacs?)具有使其更难的罕见区别:-) 在这方面,单例与平衡树或编译器没有什么不同.如果您只需要确保一个对象的一个​​副本,那么单例会执行此操作。它是否做得好可以争论,但我不相信你可以争辩说它根本没有做到这一点。并且可能有更好的方法,例如电锯比手锯更快,或者钉枪与锤子。这不会使手锯/锤子不再是一种工具。 【参考方案1】:

Service Locator 可以说是两害相权取其轻。 “较小”归结为这四个差异(至少我现在想不出其他任何差异):

单一职责原则

Service Container 并没有像 Singleton 那样违反单一职责原则。单例混合了对象创建和业务逻辑,而服务容器严格负责管理应用程序的对象生命周期。在这方面,Service Container 更好。

耦合

由于静态方法调用,单例通常被硬编码到您的应用程序中,这会导致您的代码中出现tight coupled and hard to mock dependencies。另一方面,SL 只是一个类,它可以被注入。所以虽然你所有的类都依赖它,但至少它是一个松散耦合的依赖。因此,除非您将 ServiceLocator 实现为 Singleton 本身,否则会更好,也更容易测试。

但是,所有使用 ServiceLocator 的类现在都将依赖于 ServiceLocator,这也是一种耦合形式。这可以通过使用 ServiceLocator 的接口来缓解,因此您不必绑定到具体的 ServiceLocator 实现,但您的类将取决于某种定位器的存在,而根本不使用 ServiceLocator 会显着增加重用。

隐藏的依赖

虽然隐藏依赖的问题非常存在。当您只是将定位器注入您的消费类时,您不会知道任何依赖关系。但与 Singleton 相比,SL 通常会在后台实例化所有需要的依赖项。因此,当您获取服务时,您不会像 Misko Hevery in the CreditCard example 那样结束,例如您不必手动实例化所有依赖项。

从实例内部获取依赖项也违反了Law of Demeter,它指出您不应该深入研究协作者。一个实例应该只与它的直接合作者交谈。这是 Singleton 和 ServiceLocator 的问题。

全局状态

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

另请参阅 Service Locator vs Dependency Injection 上的 Fowler 进行更深入的讨论。


关于您的更新和Sebastian Bergmann on testing code that uses Singletons 的链接文章的注释:Sebastian 绝不表示建议的解决方法可以减少使用 Singleons 的问题。这只是使无法测试的代码更具可测试性的一种方法。但这仍然是有问题的代码。事实上,他明确指出:“仅仅因为你可以,并不意味着你应该”。

【讨论】:

特别是这里应该强制执行可测试性。您不能模拟静态方法调用。但是,您可以模拟通过构造函数或设置器注入的服务。【参考方案2】:

服务定位器模式是一种反模式。它并没有解决暴露依赖的问题(你不能通过查看一个类的定义来判断它的依赖是什么,因为它们没有被注入,而是被从服务定位器中拉出来)。

那么,您的问题是:为什么服务定位器很好?我的回答是:他们不是。

避免,避免,避免。

【讨论】:

看来你对接口一无所知。类只是在构造函数签名中描述了必要的接口——这就是他需要知道的。传递的服务定位器应该实现接口,仅此而已。如果 IDE 会检查接口的实现,那么很容易控制任何更改。 @yes123:人们说错了,他们错了,因为 SL 是一种反模式。你的问题是“为什么 SL 好?”我的回答是:他们不是。 我不会争论 SL 是否是反模式,但我要说的是,与单例和全局相比,它的危害要小得多。你不能测试一个依赖单例的类,但你绝对可以测试一个依赖于 SL 的类(尽管你可以把 SL 设计搞砸到它不起作用的地步)......所以这是值得的注意到... @Jason 您需要传递实现接口的对象 - 这只是您需要知道的。您仅通过定义类构造函数来限制自己,并且想在构造函数中编写所有类(而不是接口)-这是愚蠢的主意。您所需要的只是接口。您可以使用 mock 成功地测试这个类,您可以在不更改代码的情况下轻松更改行为,没有额外的依赖项和耦合 - 这就是我们希望在 Dependency Injection 中拥有的全部(通常)。 当然,我只是将数据库、记录器、磁盘、模板、缓存和用户放在一个“输入”对象中,当然更容易判断我的对象依赖于哪些依赖项如果我使用了容器。【参考方案3】:

对我来说,我尽量避免使用全局常量、单例,原因很简单,在某些情况下我可能需要运行 API。

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

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

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

我对单例的想法是——你不能在父对象中添加相同的对象两次。比如

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

会给你留下一个实例,两个变量都指向它。

最后,如果你想使用面向对象的开发,那就使用对象,而不是类。

【讨论】:

所以你的方法是在你的框架周围传递$api var?我没有完全明白你的意思。此外,如果调用 add('Logger') 返回相同的实例,基本上你有一个服务容器 是的,没错。我将它们称为“系统控制器”,它们旨在增强 API 的功能。以类似的方式将“可审计”控制器两次添加到模型中的工作方式完全相同 - 只创建一个实例和一组审计字段。【参考方案4】:

服务容器像单例模式一样隐藏依赖关系。您可能想建议使用依赖注入容器,因为它具有服务容器的所有优点,但没有(据我所知)服务容器的缺点。

据我了解,两者的唯一区别在于,在服务容器中,服务容器是被注入的对象(从而隐藏了依赖关系),当你使用DIC时,DIC会为你注入适当的依赖关系。由 DIC 管理的类完全忽略了它由 DIC 管理的事实,因此您具有更少的耦合、清晰的依赖关系和快乐的单元测试。

这是一个很好的问题,可以解释两者的区别:What's the difference between the Dependency Injection and Service Locator patterns?

【讨论】:

“DIC 为你注入了适当的依赖” 单例不也会出现这种情况吗? @yes123 - 如果您使用的是单例,则不会注入它,大多数时候您只需全局访问它(这就是单例的重点)。我想如果你说如果你 inject 单例,它不会隐藏依赖关系,但它有点违背单例模式的原始目的 - 你会问自己,如果我不需要这个要全局访问的类,为什么我需要将其设为 Singleton?【参考方案5】:

因为您可以通过轻松替换Service Container中的对象 1)继承(对象管理器类可以被继承,方法可以被覆盖) 2) 更改配置(以 Symfony 为例)

而且,Singleton 不好,不仅因为高耦合,还因为它们是 _Single_tons。对于几乎所有类型的对象来说,它都是错误的架构。

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

【讨论】:

以上是关于如果单例不好,那么为啥服务容器是好的?的主要内容,如果未能解决你的问题,请参考以下文章

为啥不需要在 Docker 容器中运行 sshd

为啥 Spring Boot 使用单例容器方法生成 2 个测试容器?

微服务架构之「 容器技术 」

微服务架构之「 容器技术 」

在 Laravel 容器中覆盖单例

为啥 azure 需要这么长时间才能创建 Windows 核心服务器容器实例?