利用 Real Time Facade 让代码富有表达力
此文翻译自 Laravel 之父 Taylor Otwell 的专栏文章,以下第一人称为 Taylor 本人
最近,我写了点代码来描述我通常在什么场景下使用 Laravel 5.4 的 realtime facade
。realtime facade
通过在导入类时的命名空间前加上前缀 Facades
,可以像 Laravel 的「facade」一样使用你应用中的任何类(译注:对于 realtime facade
还不是很熟悉的同学,社区里有一篇 深入浅出的介绍 - oustn )。我没有在代码中滥用这个特性,不过我发现在某些情况下 realtime facade
能提供一个干净的、方便测试的方法来编写富有表达力的对象 API。
我使用 Laravel Forge 的术语来说明一个例子。使用 Forge 时,你可以将服务器提供商帐户链接到你的Forge帐户。 服务器提供商是 DigitalOcean,Linode 或 AWS 等基础设施提供商。 这些是托管由 Forge 管理的实际服务器的提供商。现在,假设我们的应用程序有一个名为 Provider
的 Eloquent 模型。Provider
模型有一个 type
字段,用来代表服务器提供商的类型( DigitalOcean、Linode 等):
<?php
use App;
use Illuminate\Database\Eloquent\Model;
class Provider extends Model
{
//
}
当然,Forge 也可以在这些提供器上创建服务。 我通常将所有外部 API 的调用封装在 App\Services
目录中的类里。 想象一下,我们每个提供器都有一个「服务」类。 例如,DigitalOcean 服务可能看起来像这样:
<?php
namespace App\Services;
use App\Contracts\ServerProvider;
class DigitalOcean implements ServerProvider
{
public function createServer($name, $size)
{
//
}
}
接下来,我们需要能够根据模型的 type
字段解析给定提供器的服务类。 我们可以用一个简单的工厂类从提供器中生成服务:
<?php
namespace App\Services;
use InvalidArgumentException;
class ServerProviderFactory
{
public function make($type)
{
switch ($type) {
case 'DigitalOcean':
return new DigitalOcean;
case 'Linode':
return new Linode;
default:
throw new InvalidArgumentException;
}
}
}
现在,我们有好几种方式将这些东西组合起来,创建一个服务。 假设我们要在控制器中使用上面这个代码,我们可以将工厂类注入控制器:
<?php
namespace App\Http\Controllers;
use App\Provider;
use Illuminate\Http\Request;
use App\Services\ServerProviderFactory;
class ServerController extends Controller
{
protected $factory;
public function __construct(ServerProviderFactory $factory)
{
$this->factory = $factory;
}
public function store(Request $request, Provider $provider)
{
$service = $this->factory->make($provider->type);
$response = $service->createServer($request->name, $request->size);
//
}
}
然而,我并不喜欢这个有些笨重的方法,因为它需要在每一个使用提供器服务的类中注入一个工厂实例。 理想情况下,我想使用如下语法:
<?php
namespace App\Http\Controllers;
use App\Provider;
use Illuminate\Http\Request;
class ServerController extends Controller
{
public function store(Request $request, Provider $provider)
{
$repsonse = $provider->service()->createServer(
$request->name, $request->size
);
//
}
}
在上面的示例中,我们只需在 Provider
实例上调用 service
方法即可访问该提供器的服务。 事实上,我发现这是当人们有一个新想法时思考代码的自然倾向。不过,人们不确定如此实现,是否可以维持代码的可测试性。 那么,我们有哪些实现方法呢? 如果没有使用 realtime facade
,我们可能会这样实现:
<?php
namespace App;
use App\Services\ServerProviderFactory;
use Illuminate\Database\Eloquent\Model;
class Provider extends Model
{
public function service()
{
return (new ServerProviderFactory)->make($this->type);
}
}
然而,这种方法的问题是,由于工厂在该方法中直接实例化,因此无法模拟外部服务以供调用。 由于我不想每次运行测试时都在 DigitalOcean 上创建服务器,所以我一定需要模拟这些调用。 那么接下来,让我们使用 realtime facade
来让它可测试:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Facades\App\Services\ServerProviderFactory;
class Provider extends Model
{
public function service()
{
return ServerProviderFactory::make($this->type);
}
}
现在,我们不仅有一个简单而富有表现力的方式访问 provider
的外部服务提供器,通过 Facade
的 内置访问 Mockery
,测试我们的代码也非常的容易:
<?php
namespace Tests\Feature;
use Mockery;
use App\Provider;
use Tests\TestCase;
use App\Contracts\ServerProvider;
use Facades\App\Services\ServerProviderFactory;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* @return void
*/
public function testBasicTest()
{
$provider = factory(Provider::class)->create([
'id' => 1,
'type' => 'DigitalOcean',
]);
$service = Mockery::mock(ServerProvider::class);
ServerProviderFactory::shouldReceive('make')
->with('DigitalOcean')
->andReturn($service);
$service->shouldReceive('createServer')
->once()
->with('web', '2GB')
->andReturn('server-id');
$response = $this->json('POST', '/api/providers/1/server', [
'name' => 'web',
'size' => '2GB',
]);
$response->assertStatus(201);
}
}
我发现 realtime facade
对于像这样,不牺牲可测试性,且构建出干净的对象 API 时,最为有用。 希望这能为你的应用程序提供一些新鲜的想法! Enjoy!
本作品采用《CC 协议》,转载必须注明作者和本文链接
请允许我抱紧你的大腿
@JokerLinly 任务完成,并且顺便打开了赞赏 :smirk: