4种服务容器的使用方法帮助我们管理依赖

本文翻译自 4 Ways The Laravel Service Container Helps Us Managing Our Dependencies

在Laravel的世界里,服务容器(Service Container)是一个很复杂的话题,我看到有许多人在尝试搞清楚它到底是怎样的一个原理,但是我们仍然不太懂。对我来说也一样,这是因为很多的文章在解释怎样去“使用”服务容器。在这篇文章中,我将给大家解释“什么”是服务容器以及“何时”服务容器能帮助我们处理我们的依赖。

首先我们举个栗子🌰,假设我们有一个导出数据的类。它能够导出指定用户的数据到CSV文件中。

1
2
3
4
5
6
7
8
class UserStatsCsvExporter implements UserStatsExporterContract
{
public function export(int $userId)
{
// Load user statistics...
// Export file...
}
}

在控制器中,我们会new一个类,然后调用里面的export方法。

1
2
3
4
5
6
7
8
9
class ExportController extends Controller
{
public function handle()
{
$userStatsExporter = new UserStatsCsvExporter();

return $userStatsExporter->export(12);
}
}

对于我们的控制器来说,这个导出类就是一个依赖。就像上面的例子,我们能够自己处理。那么为什么我们需要服务容器来管理我们的依赖呢?答案就是:控制器中的handle方法不应当有职责来创建导出类。它的职责应当只是调用export方法。这样的话我们也能服从反转控制原则。

自动解析

这就是在我们有依赖的时候想要使用依赖注入的原因。那么与其在handle方法中新建一个类,不如直接注入。我们可以在控制器的构造函数中进行注入,也可以在Laravel中的方法中进行注入。这叫做方法注入(method-injection)

1
2
3
4
public function handle(UserStatsCsvExporter $userStatsExporter)
{
return $userStatsExporter->export(12);
}

通过上面的注入我们能够直接调用export方法,而我们不需要告诉Laravel怎样初始化这个类。这个方法能成功,主要的原因是在Laravel框架底层已经使用了服务容器。更确切的说,我们使用了服务容器的自动解析(auto-resolving)功能。

通过PHP的反射API,Laravel能够找到我们的导出类并且为我们自动创建。这是一个非常棒的功能。

但是,如果我们的导出类自己也包含依赖呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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。

1
2
3
4
5
6
7
8
9
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属性下面,你会发现已经包含了我们的导出类。

bindings

绑定到接口

你已经看到了我们的CSV导出类继承了一个接口(interface)。这是因为我们还有一个类是用来处理导出成XML格式的。它同样也继承了接口。假设我们现在需要将控制器中的CSV导出类替换成XML导出类。

当然,我们可以在控制器中使用XML类然后修改服务提供者中的代码。

1
2
3
4
5
6
7
8
9
10
11
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导出类,不如我们直接使用接口。

1
2
3
4
public function handle(UserStatsExporterContract $userStatsExporter)
{
return $userStatsExporter->export(12);
}

要让上面代码工作,我们还需要改动服务提供者的代码。

1
2
3
4
5
6
public function register()
{
$this->app->bind(UserStatsExporterContract::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale')));
});
}

以后如果我们需要换回CSV导出类或者其他的导出类,我们只需要更改服务提供者的代码即可。

共享实例

在这篇文章里我想最后介绍一下关于服务容器的一个功能就是共享。当我们检查2个同样的导出类时,你会看到两个不同的ID。这表示我们创建了2个实例。

1
2
3
4
5
6
public function handle(UserStatsExporterContract $userStatsExporter)
{
dd(app(UserStatsExporterContract::class), app(UserStatsExporterContract::class));

return $userStatsExporter->export(12);
}

share

对于大多数情况来说,这可能就是我们所需要的,但是某些情况下我们需要返回同样的实例。要达成这样的目的,我们仅需要使用singleton来替换bind方法即可。

1
2
3
4
5
6
public function register()
{
$this->app->singleton(UserStatsExporterContract::class, function() {
return new UserStatsXmlExporter(new Translator(config('app.locale')));
});
}

share

你可以看到ID已经一致了。这样做的原因主要有2个:

  1. 保存状态

当你在实例中保存了一些信息,其他部分的程序访问的时候这些信息仍然在那里。

  1. 性能更好

有些时候创建实例并不是简单的新建一个类就可以。你可能需要处理很多的依赖,导入配置等等。在这种情况下,共享已经创建号的实例会比重新创建的性能要好一些。

一个很好的例子就是Laravel的数据库服务,当你使用的时候,它需要创建一个与你数据库的连接。那么在整个程序执行的过程中,保持这个连接是一个很好的实现。而不必每次调用数据库服务的时候再创建。

结论

这篇文章介绍了4种服务容器的使用方法。希望能够让你们明白“为什么”以及“何时”使用服务容器。