[译] 4 种服务容器(service container)的使用方法帮助我们管理依赖
本文翻译自 4 Ways The Laravel Service Container Helps Us Managing Our Dependencies
在Laravel的世界里,服务容器(Service Container)是一个很复杂的话题,我看到有许多人在尝试搞清楚它到底是怎样的一个原理,但是我们仍然不太懂。对我来说也一样,这是因为很多的文章在解释怎样去“使用”服务容器。在这篇文章中,我将给大家解释“什么”是服务容器以及“何时”服务容器能帮助我们处理我们的依赖。
首先我们举个栗子?,假设我们有一个导出数据的类。它能够导出指定用户的数据到CSV文件中。
class UserStatsCsvExporter implements UserStatsExporterContract
{
public function export(int $userId)
{
// Load user statistics...
// Export file...
}
}
在控制器中,我们会new
一个类,然后调用里面的export方法。
class ExportController extends Controller
{
public function handle()
{
$userStatsExporter = new UserStatsCsvExporter();
return $userStatsExporter->export(12);
}
}
对于我们的控制器来说,这个导出类就是一个依赖。就像上面的例子,我们能够自己处理。那么为什么我们需要服务容器来管理我们的依赖呢?答案就是:控制器中的handle
方法不应当有职责来创建导出类。它的职责应当只是调用export
方法。这样的话我们也能服从反转控制原则。
自动解析
这就是在我们有依赖的时候想要使用依赖注入的原因。那么与其在handle
方法中新建一个类,不如直接注入。我们可以在控制器的构造函数中进行注入,也可以在Laravel中的方法中进行注入。这叫做方法注入(method-injection)。
public function handle(UserStatsCsvExporter $userStatsExporter)
{
return $userStatsExporter->export(12);
}
通过上面的注入我们能够直接调用export
方法,而我们不需要告诉Laravel怎样初始化这个类。这个方法能成功,主要的原因是在Laravel框架底层已经使用了服务容器。更确切的说,我们使用了服务容器的自动解析(auto-resolving)功能。
通过PHP的反射API,Laravel能够找到我们的导出类并且为我们自动创建。这是一个非常棒的功能。
但是,如果我们的导出类自己也包含依赖呢?
class UserStatsCsvExporter implements UserStatsExporterContract
{
/** @var Translator */
private $translator;
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
public function export(int $userId)
{
// Load user statistics...
// Export file...
}
}
如上面的代码,我们在导出类的构造函数中加入了一个Translator
依赖。令人惊喜的是通过自动解析,代码仍然可以工作。所以,Laravel的自动解析功能十分聪明的为我们解决了相关的依赖问题。
只要我们的依赖是这种简单的注入,而不需要传值进去,上面的代码就能够一直正常工作。
绑定到容器
在Translator
类中,我加入了一个新的构造函数需要我们在其初始化的时候传入一个language
字符串。
class Translator
{
/** @var string */
private $language;
public function __construct(string $language)
{
$this->language = $language;
}
public function translate(string $word)
{
// Translate word...
}
}
现在由于Laravel不知道该传递什么值给Translator
类,因此自动解析方法已经无法使用了。这时我们需要告诉Laravel怎样创建导出实例以及需要怎样的依赖。那么最好的地方是在服务提供者(service provider)中进行处理。
下面我们新建一个 provider。
class UserStatsExporterProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(UserStatsCsvExporter::class, function() {
return new UserStatsCsvExporter(new Translator(config('app.locale')));
});
}
}
在每个服务提供者中,我们能够使用$this->app
来获得服务容器。我们新写入的language
字符串通过配置文件进行载入。我们需要新建导出实例的相关内容已经保存到了服务容器实例中。这样当我们需要导出类的时候,我们不必再写其他的代码来创建了。
如果你想的话,你可以使用dd(app()
查看一下现在的服务容器,在bindings
属性下面,你会发现已经包含了我们的导出类。
绑定到接口
你已经看到了我们的CSV导出类继承了一个接口(interface)。这是因为我们还有一个类是用来处理导出成XML格式的。它同样也继承了接口。假设我们现在需要将控制器中的CSV导出类替换成XML导出类。
当然,我们可以在控制器中使用XML类然后修改服务提供者中的代码。
public function handle(UserStatsXmlExporter $userStatsExporter)
{
return $userStatsExporter->export(12);
}
public function register()
{
$this->app->bind(UserStatsXmlExporter::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale'))
});
}
虽然上面的修改能够满足我们的需求,但是有一个更好的处理办法。由于我们已经定义了一个接口,与其使用CSV导出类或者XML导出类,不如我们直接使用接口。
public function handle(UserStatsExporterContract $userStatsExporter)
{
return $userStatsExporter->export(12);
}
要让上面代码工作,我们还需要改动服务提供者的代码。
public function register()
{
$this->app->bind(UserStatsExporterContract::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale')));
});
}
以后如果我们需要换回CSV导出类或者其他的导出类,我们只需要更改服务提供者的代码即可。
共享实例
在这篇文章里我想最后介绍一下关于服务容器的一个功能就是共享。当我们检查2个同样的导出类时,你会看到两个不同的ID。这表示我们创建了2个实例。
public function handle(UserStatsExporterContract $userStatsExporter)
{
dd(app(UserStatsExporterContract::class), app(UserStatsExporterContract::class));
return $userStatsExporter->export(12);
}
对于大多数情况来说,这可能就是我们所需要的,但是某些情况下我们需要返回同样的实例。要达成这样的目的,我们仅需要使用singleton
来替换bind
方法即可。
public function register()
{
$this->app->singleton(UserStatsExporterContract::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale')));
});
}
你可以看到ID已经一致了。这样做的原因主要有2个:
- 保存状态
当你在实例中保存了一些信息,其他部分的程序访问的时候这些信息仍然在那里。
- 性能更好
有些时候创建实例并不是简单的新建一个类就可以。你可能需要处理很多的依赖,导入配置等等。在这种情况下,共享已经创建号的实例会比重新创建的性能要好一些。
一个很好的例子就是Laravel的数据库服务,当你使用的时候,它需要创建一个与你数据库的连接。那么在整个程序执行的过程中,保持这个连接是一个很好的实现。而不必每次调用数据库服务的时候再创建。
结论
这篇文章介绍了4种服务容器的使用方法。希望能够让你们明白“为什么”以及“何时”使用服务容器。
本作品采用《CC 协议》,转载必须注明作者和本文链接
这篇写得太好了,整个服务容器的概念一下子清晰了。