利用 Real Time Facade 讓程式碼富有表達力

茄子發表於2017-05-08

file

此文翻譯自 Laravel 之父 Taylor Otwell 的專欄文章,以下第一人稱為 Taylor 本人

最近,我寫了點程式碼來描述我通常在什麼場景下使用 Laravel 5.4 的 realtime facaderealtime 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!

Night gathers, and now my watch begins.

相關文章