注入的类型

3.4 版本
维护中的版本

把一个类的依赖进行显式地和必需地“注入”,是一个很好的实践,这可以令类更能复用、更易测试、相对于其他类的藕合度更低。

有若干方法能够把dependency进行注入。每种注入方式各有优缺点,使用服务容器时,它们各自的执行方式也有所不同。

构造器注入 

最常见的方式是构造器注入,通过类的constructor实现。你需要一个构造器参数来接收依赖:

1
2
3
4
5
6
7
8
9
10
11
class NewsletterManager
{
    protected $mailer;
 
    public function __construct(\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    // ...
}

你可以把想要注入的服务写在服务容器的配置里面:

1
2
3
4
5
6
services:
     my_mailer:
         # ...
     newsletter_manager:
         class:     NewsletterManager
         arguments: ['@my_mailer']
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="my_mailer">
            <!-- ... -->
        </service>
 
        <service id="newsletter_manager" class="NewsletterManager">
            <argument type="service" id="my_mailer"/>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->setDefinition('my_mailer', ...);
$container->setDefinition('newsletter_manager', new Definition(
    'NewsletterManager',
    array(new Reference('my_mailer'))
));

对注入的对象进行类型提示(type hinting),意味着你可以确保一个合适的依赖被注入。通过类型提示,如果不合适的依赖被注入,你会立即得到一个清晰的错误信息。如果在类型提示中使用接口而不是类的话,你能做出更有弹性的依赖选择。

使用构造器注入有如下优点:

  • 如果依赖是必需的、类不能无此依赖,那么注入通过构造器注入可以确保该依赖在类被使用时存在,因为类在没有依赖时根本不能被构造。

  • 当对象被实例化时构造器只被调用一次,所以你可以确保依赖在对象的生命周期中不发生改变。

以上优点也确实说明构造器注入不适合与“可选的依赖(非必须的依赖)”一起工作。当类存在继承关系时,这种注入也很困难:如果一个类使用构造器注入,那么继承它和覆写该构造器时,会变得充满不确定性。

Setter注入 

另一个可能的注入方式是添加一个setter方法,用来接收依赖:

1
2
3
4
5
6
7
8
9
10
11
class NewsletterManager
{
    protected $mailer;
 
    public function setMailer(\Mailer $mailer)
    {
        $this->mailer = $mailer;
    }
 
    // ...
}
1
2
3
4
5
6
7
services:
     my_mailer:
         # ...
     newsletter_manager:
         class:     NewsletterManager
         calls:
             - [setMailer, ['@my_mailer']]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="my_mailer">
            <!-- ... -->
        </service>
 
        <service id="newsletter_manager" class="NewsletterManager">
            <call method="setMailer">
                <argument type="service" id="my_mailer" />
            </call>
        </service>
    </services>
</container>
1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->setDefinition('my_mailer', ...);
$container->setDefinition('newsletter_manager', new Definition(
    'NewsletterManager'
))->addMethodCall('setMailer', array(new Reference('my_mailer')));

此时的优点有:

  • Setter注入可以很好地与可选依赖一起工作。如果你不需要这个依赖,你可以不调用setter方法。

  • 你可以多次调用setter。当该方法要将依赖添加到一个collection(数组)中时特别有用。然后你即拥有一个可变数量的依赖。

缺点在于:

  • 相较于服务在构造时注入,setter可以被调用多次,所以你不能确定依赖在类对象的生命周期之内“不被取代”。(除非在setter方法中显式地添加判断“setter是否被调用过”)

  • 你不能确定Setter是否被调用过,所以你需要添加校验,判断该类的“必要依赖”是否被注入。

属性注入 

另一个可能的方式是对类的public公共属性进行注入:

1
2
3
4
5
6
class NewsletterManager
{
    public $mailer;
 
    // ...
}
1
2
3
4
5
6
7
services:
     my_mailer:
         # ...
     newsletter_manager:
         class: NewsletterManager
         properties:
             mailer: '@my_mailer'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="my_mailer">
            <!-- ... -->
        </service>
 
        <service id="newsletter_manager" class="NewsletterManager">
            <property name="mailer" type="service" id="my_mailer" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
 
$container->setDefinition('my_mailer', ...);
$container->setDefinition('newsletter_manager', new Definition(
    'NewsletterManager'
))->setProperty('mailer', new Reference('my_mailer'));

属性注入基本只有缺点,它类似setter注入但是有以下严重问题:

  • 你完全不能控制何时依赖会被设置,它可以在类对象的生命周期内的任何时间点被改变。

  • 无法应用“类型提示”,所以你不能确定到底哪个依赖被注入,除非在类的代码中写入显式的判断,以在使用该依赖之前,对其实例化的对象进行测试。

但是,在服务容器中知道有这样一种注入方式也是有用的,特别是在当你要操作一些你难以控制的代码时,比如第三方的类库,它可能会使用public属性来接收dependency。

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)