Contributed by
Nicolas Grekas
in #20973.
作为我们 功能体验计划 的一部分,在 Symfony 3.3 中我们添加了一个全新的功能,叫作 getter injection。它向依赖注入添加了不常见的架构,但并不改变任何内容。而是提供一种特别的方式来满足某些特定用例。
Getter注入允许DI容器利用类来提供“基于继承”(inheritance-based)的扩展特点(extension points),以匹配下列需求:不带参的私有或受保护方法且无副作用。
通过用grep检索Symfony及其vendor,找到了一些例子:
Kernel::getRootDir/CacheDir/LogDir()
,位于 HttpKernelSessionListener::getSession()
同样位于 HttpKernelAbstractBaseFactory::getGenerator()
位于 ProxyManager
在Symfony核心和其他地方的所有类中,它们是仅有的应用了这一 “open/closed原则” 特性的小子集。如例程所示,这既应用了对象注入(服务/service)又应用了值注入(参数/parameter)
Getter注入,是一种通过简单的DI配置,来把这些类给转换成DI候选者(candidates)的方式。在Yaml中,以 SessionListener::getSession() 为例的话,应该是下面这样:
1 2 3 4 | services:
SessionListener:
getters:
getSession: '@session' |
实践中,这会告诉Symfony的 Dependency Injection Container 去创建一个匿名的“继承代理”(inheritance-proxy )类,像下面这种:
1 2 3 4 5 6 7 8 9 10 11 12 13 | $sessionListener = new class ($container) extends SessionListener {
private $container;
function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getSession()
{
return $this->container->get('session');
}
}; |
超高难文档 Symfony身为PHP框架之王,其霸气侧漏再次得到证明,参考 匿名类。
Getter注入的优点
执意设计为getter注入的类,相对于其他IoC策略,有若干优势:
使用继承,令它们从任意框架/DIC(对比注入容器)的任意耦合中解脱出来;
它令被注入的依赖immutable(对比setter/property注入);
在内部总是需要使用getter来取出某些依赖,进而允许对提及的依赖进行lazy实例化(对比constructor/setter/property注入);
它不会污染构造器,这对那些“为追求扩展性”而设计的类来说是一大优势(对比setter/property注入 - 同时对比构造器注入);
它可以和 “基于traits” 的组合相处得很好,每个trait提供了一个新的open getters组合(有和setter/property注入相同的优点 - 对比构造器注入);
它令依赖显式化(对比注入容器)
它允许使用abstracdt getter来声明任意依赖;
它允许提供默认getters来拥有可选依赖,默认getter返回
null
或 一个默认实现(如, 一个NullLogger
);它允许拥有和报告丢失的依赖,这些依赖在使用“由基类(一般是控制器或命令)提供的某些功能子集”时,作为某种条件下的需求 - 出现这种情况时可以抛出有用的异常信息。
用来影响注入特性的代理(proxies),很容易编写(见示例)或生成;
当注入的依赖自身改变时,代理不需要改变(对比当需要懒加载时使用dcorators);
在使用被注入的依赖时,没有性能损失(对比当需要懒加载时使用dcorators)
在使用匿名或已生成的proxy classes时,它不创建任何新的类型提示(new type to hint for),因此会从继承地狱中解脱。
Getter注入的缺点
它需要编写一个继承代理(inheritance proxy),在对这些类进行测试或关联时,相对于其他的注入策略,这便增加了更多的公式化代码(boilerplate);
根据设计,它不能工作在final类中,也不可以和private方法一起工作;
由于PHP(它不像其他语言)不提供“实时创建代理”的任何方式,而是需要,要么手写代码以及一个剥离出来的容器,或是使用
eval()
来支持基于实(runtime-based)时的DICs;当懒加载(laziness)是一个目标时,它把“懒责任”给了依赖的调用一方(对比当使用decoration时,责任在被调用一方) - 这便令它只能在 “懒加载是[向getter注入开放的类] 的核心业务逻辑的一部分” 时才可以使用;
增加了间接性,这便令debugging变困难(为何这是类抛出来的?,“噢,这个懒依赖的初始化在那时失败了”);
当inheritance proxy提供了laziness时,它可能在PHP对象关系图中创建出“循环依赖”(circular dependencies),这会令“垃圾收集”在某些情况下变成刚需;
如果尚未遇到 “不能有副作用” 的需求,它会导致bugs - 当不使用abstract getters时,此一需求从技术角度来说很难执行(需要静态代码分析/static code analyzes)。
讨论
“getter注入” 在介绍中是矛盾的,它有正面和反面的反馈。我已尽量在上面列表中总结出全部合理参数和优缺点。
统计之后,我希望“令人惊喜的效果”会随着时间消失,并且仅对getter注入的技术价值进行评估。我个人对它的观点是,它通过自己独有的优缺点,填补了一些空白,并以此为资格,在Symfony容器的支持下,成为开发者工具链的一环。
在Symfony内核中使用之展望
本功能首先锁定的是用在特定框架的需求之中。现时点的目标是提供解耦的组合式基类trait(s)。参见 pull request 18193 以了解此主题的下一步是什么。
问答
使用常规DI时,如果需要,我可以手动关联依赖。使用这种class时我如何做同样的事?
例如在使用匿名类时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $myDep = new DepClass();
$myConsumer = new class ($myDep) extends ConsumerClass {
private $dep;
public function __construct(DepClassInterface $dep)
{
$this->dep = $dep;
}
protected function getDep(): DepClassInterface
{
return $this->dep;
}
}; |
但那样的话有太多公式化代码!
是的,这已经列在“缺点”里了。我们使用DICs来完成烦人的关联(wiring) - 这里也一样。注意由于(PHP)语言的帮助,(代码)也可减少为下面这样(在PHP内核层面,关于此点有相关讨论,请去支持它):
1 2 3 4 5 6 7 8 9 | $myDep = new DepClass();
$myConsumer = new class () use ($myDep) extends ConsumerClass {
private $dep = $myDep;
protected function getDep(): DepClassInterface
{
return $this->dep;
}
}; |
我该如何测试这样一个类呢?
使用诸如 anonymous classes (见上例) - 或是一个 mock framework,比如 PHPUnit的:
1 2 | $consumer = $this->createMock('ConsumerClass')->setMethods(['getDep'])->getMock();
$consumer->expects($this->any())->method('getDep')->will($this->returnValue($myDep)); |
让人们开启他们的类去迎合getter注入,难道不是在提倡坏实践吗?
在以不好的方式来做事时,人们往往更有创造力。getter injection也不例外。
由于这样会在底层注入容器,我们能否直接把容器注入?
当容器把 “你的类正在使用的依赖”给隐藏起来时,注入容器 (或是 "service locator" 设计模式) 是个坏实践: 在代码中 $container->get('foo')->doFoo()
这样写的话,会依赖几个执行乏力的假设(loosely-enforced assumptions),换句话说就是:
- 那个
$container
有一个get
方法,通常由你框架的DI组件提供(已经与容器耦合了); - 那个
get
方法有一个名为foo
的服务 (这已经与外部配置信息耦合了); - 同时那个
foo
服务有一个doFoo
方法 (这同样与外部配置信息耦合)。
如果上述任意假设不再成立,则这样的代码便很脆弱:代码实际运行之前,错误难以发现(重构的噩梦)。
Getter注入(对比 constructor/setter injection)让你的类从类似问题中解脱出来:这个扩展的特点是,原生就是显式,并具备返回类型的提示(需要PHP7)。通过使用兼容的DIC,基于你的代码中的那些显式声明,代码可以自动生成。
我还是觉得它太糟糕
没问题!这个功能是可选的,你的Symfony程序可以不使用它。