【问题标题】:In Symfony2, why is it a bad idea to inject the service container, rather than individual services?在 Symfony2 中,为什么注入服务容器而不是单个服务是一个坏主意?
【发布时间】:2014-05-29 10:34:15
【问题描述】:

我找不到这个问题的答案...

如果我注入服务容器,比如:

// config.yml
my_listener:
   class: MyListener
   arguments: [@service_container]

my_service:
   class: MyService

// MyListener.php
class MyListener
{
    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function myFunction()
    {
        $my_service = $this->container->get('my_service');
        $my_service->doSomething();
    }
}

然后它就像我一样工作:

// config.yml
my_listener:
   class: MyListener
   arguments: [@my_service]

my_service:
   class: MyService

// MyListener.php    
class MyListener
{
    protected $my_service;

    public function __construct(MyService $my_service)
    {
        $this->my_service = $my_service;
    }

    public function myFunction()
    {
        $this->my_service->doSomething();
    }
}

那么为什么我不应该只注入服务容器,并从我的类中获取服务呢?

【问题讨论】:

标签: symfony dependency-injection


【解决方案1】:

您应该更喜欢注入服务的原因列表:

  1. 您的类只依赖于它需要的服务,而不是服务容器。这意味着该服务可以在不使用 Symfony 服务容器的环境中使用。例如,你可以将你的服务变成一个可以在 Laravel、Phalcon 等中使用的库——你的类不知道依赖项是如何被注入的。

  2. 通过在配置级别定义依赖关系,您可以使用配置转储器了解哪些服务正在使用哪些其他服务。例如,通过注入@mailer,那么很容易从注入邮件的服务容器中计算出来。另一方面,如果您使用$container->get('mailer'),那么几乎唯一的方法就是使用find 来确定邮件的使用位置。

  3. 在编译容器时,而不是在运行时,您会收到有关缺少依赖项的通知。例如,假设您定义了一个服务,并将其注入到侦听器中。几个月后,您不小心删除了服务配置。如果您正在注入服务,您将在清除缓存后立即收到通知。如果你注入服务容器,你只会在侦听器失败时发现错误,因为容器无法获取服务。当然,如果您进行了彻底的集成测试,您可以选择它,但是……您已经进行了彻底的集成测试,不是吗? ;)

  4. 如果您注入了错误的服务,您会很快知道。例如,如果您有:

    public function __construct(MyService $my_service)
    {
       $this->my_service = $my_service;
    }
    

    但您已将侦听器定义为:

    my_listener:
        class: Whatever
        arguments: [@my_other_service]
    

    当监听器接收到MyOtherService 时,PHP 会抛出一个错误,告诉你它接收到了错误的类。如果您正在执行$container->get('my_service'),则您假设容器正在返回正确的类,并且可能需要很长时间才能确定它不是。

  5. 如果您使用的是 IDE,那么类型提示会增加一大堆额外的帮助。如果您使用的是$service = $container->get('service');,那么您的IDE 不知道$service 是什么。如果你注入

    public function __construct(MyService $my_service)
    {
       $this->my_service = $my_service;
    }
    

    那么您的 IDE 就知道 $this->my_serviceMyService 的一个实例,并且可以提供有关方法名称、参数、文档等方面的帮助。

  6. 您的代码更易于阅读。您的所有依赖项都在类的顶部定义。如果他们用$container->get('service') 分散在整个班级,那么就很难弄清楚了。

  7. 您的代码更容易进行单元测试。如果要注入服务容器,则必须模拟服务容器,并将模拟配置为返回相关服务的模拟。通过直接注入服务,您只需模拟服务并注入它们 - 您跳过了整个复杂层。

  8. 不要被“它允许延迟加载”的谬论所迷惑。您可以configure lazy loading at configuration level,只需将服务标记为lazy: true

就个人而言,唯一一次注入服务容器是最好的解决方案,那就是当我尝试将安全上下文注入到原则侦听器中时。这引发了循环引用异常,因为用户存储在数据库中。结果是原则和安全上下文在编译时相互依赖。通过注入服务容器,我能够绕过循环依赖。但是,这可能是代码异味,有办法解决(例如,using the event dispatcher),但我承认增加的复杂性可能超过好处。

【讨论】:

  • 嗨,你认为基于角色的注入是一个坏主意吗?例如,想要执行相同操作但略有不同的管理员?我有一种情况,动作的授权变得越来越复杂,因为取决于用户,并且取决于谁拥有实体,权限变得像一个矩阵(Role x ObjectOwner)。
【解决方案2】:

除了其他人解释的所有缺点(无法控制使用的服务、运行时编译、缺少依赖项等)

有一个主要原因破坏了使用 DIC 的主要优势 - 依赖替换

如果服务是在库中定义的,您将无法将其依赖项替换为满足您需求的本地依赖项。

只有这个理由足够强大,才不会注入整个 DIC。您只是打破了替换依赖项的整个想法,因为它们是HARDCODED!在服务中;)

顺便说一句。不要忘记在服务构造函数中尽可能多地要求 interfaces 而不是特定的类 - 再次很好地替换依赖项。

编辑:依赖替换示例

某些供应商的服务定义:

<service id='vendor_service' class="My\VendorBundle\SomeClass" />
    <argument type="service" id="vendor_dependency" />
</service>

在您的应用中替换:

<service id='vendor_service' class="My\VendorBundle\SomeClass" />
     <argument type="service" id="app_dependency" />
</service>

这允许您用自定义的逻辑替换供应商逻辑,但不要忘记实现所需的类接口。使用硬编码的依赖项,您无法在一处替换依赖项。

您也可以覆盖vendor_dependency 服务,但这将在所有地方替换它,而不仅仅是vendor_service

【讨论】:

  • 我不确定你所说的“用本地依赖替换”是什么意思。你能在答案中澄清一下吗?
  • 另外,你不应该注入业务对象,只注入横切关注点和外部组件依赖。外部我不是指第三方,尽管他们也很重要。
【解决方案3】:

这不是一个好主意,因为你让你的类依赖于 DI。当有一天你决定退出你的课程并将其用于一个完全不同的项目时会发生什么?现在我不是在谈论 Symfony 甚至 PHP,我在谈论一般性。因此,在这种情况下,您必须确保新项目使用相同类型的 DI 机制并支持相同的方法,否则会出现异常。如果项目根本不使用 DI,或者使用一些很酷的新 DI 实现会发生什么?您必须检查整个代码库并进行更改以支持新的 DI。在大型项目中,这可能会带来问题且成本高昂,尤其是当您拉动的课程不止一个时。

最好让您的课程尽可能独立。这意味着将 DI 排除在您通常的代码之外,就像第三个人决定什么去哪里,指出东西应该去哪里,但不会自己去那里做。我是这样理解的。

虽然,正如 tomazahlin 所说,我同意在 Symfony 项目中极少数情况下它有助于防止循环依赖。这是我使用它的唯一示例,我会确保这是唯一的选择。

【讨论】:

  • 我大致了解了,但如果我不希望将我的班级转移到其他地方怎么办?注入 DI 是否会对性能/安全产生任何影响?
  • 据我所知没有(任何人都可以纠正我)。只要你有一个实例(比如一个单例,另一个反模式),你应该没问题。我知道一些相当大且昂贵的项目非常广泛地使用这些反模式,并且它对它们非常有效。如果你看一下,同样的事情也会发生在 Symfony 基本控制器中。它注入了容器并且非常依赖它(在整个操作中使用get()has() 方法)。
  • 答案完全正确,但实际上您不太可能遇到这样的情况。所以这更像是一个理论问题。面试问题的好答案;)
  • “理论”与可能重写你的应用程序无关……这是一个非常简单的问题。测试。如果没有框架,您就无法对代码进行单元测试,以及与之相关的所有内容都在运行。如果您不关心单元测试,并且乐于专门编写集成测试,那么请不要担心。否则,您需要将所有业务代码与您使用的任何框架隔离开来。
【解决方案4】:

注入整个容器通常不是一个好主意。好吧,它可以工作,但是为什么要注入整个容器,而您只需要一些其他服务或参数。

有时您想注入整个容器以避免循环引用,因为如果您注入整个容器,您将获得所需服务的“延迟加载”。一个例子是教义实体监听器。

您可以从每个“容器感知”或有权访问内核的类中获取容器。

【讨论】:

    猜你喜欢
    • 2017-01-16
    • 2012-02-12
    • 1970-01-01
    • 2011-10-03
    • 1970-01-01
    • 2019-04-07
    • 2015-02-16
    • 2012-06-03
    • 2015-01-05
    相关资源
    最近更新 更多