此文翻譯自 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!