通過facade(尤其是realtime facade)來使程式碼更優雅

pilishen發表於2019-02-16

本文來自pilishen.com—-原文連結; 歡迎作客我們的php&Laravel學習群:109256050

該篇翻譯整理自laravel創始人Taylor的文章:Expressive Code & Real Time Facades,屬於《Laravel底層核心技術實戰揭祕》這一課程《laravel底層核心概念解析》這一章的擴充套件閱讀。

laravel 5.4引入了realtime facade的功能,也即任何一個class都可以隨時拿來當facade用,只要在其namespace前面加上Facades字首即可。當然這個功能不可能隨處都用到,但是偶爾呢,用它可以實現更簡潔優雅、易於測試的程式碼方案。雖然下面的例子講的是laravel 5.4的realtime facade,但是呢,其實也完全可以用在之前的版本上,因為所謂的realtime facade,無非就是系統自動給你註冊成facade而已,鑑於這個功能又不可能到處用到,所以即使在老的版本里,如果你發現facade的這種程式碼實現方式更有吸引力,那麼自己手動註冊一個facade也完全可以的。

接下來的示例是關於Laravel Forge的,laravel Forge是laravel官方推出的laravel專案部署管理平臺。當使用Forge的時候,你得在Forge後臺將你伺服器提供商的賬號資訊填上,然後呢交由Forge來具體管理。那麼,這裡假設呢我們有一個Model叫Provider,也就是對應著不同的主機提供商,比如國外的DigitalOcean、國內的阿里雲等。

<?php
use App;
use IlluminateDatabaseEloquentModel;
class Provider extends Model
{
    //
}

這裡呢假設我們將所有處理外來API請求的class放在AppServices資料夾下,我們得對應每一個主機供應商都有一個“service”class,假設DigitalOcean這家供應商的service class是這樣的:

<?php
namespace AppServices;
use AppContractsServerProvider;
class DigitalOcean implements ServerProvider
{
    public function createServer($name, $size)
    {
        //
    }
}

接下來呢,我們得能夠解析這個服務類,基於我們model裡的type這一欄的資訊,我們可以使用工廠(factory)模式來實現:

<?php
namespace AppServices;
use InvalidArgumentException;

class ServerProviderFactory
{
    public function make($type)
    {
        switch ($type) {
            case `DigitalOcean`:
                return new DigitalOcean;
            case `Linode`:
                return new Linode;
            default:
                throw new InvalidArgumentException;
        }
    }
}

然後呢,我們就可以在需要的地方呼叫這個工廠,來相應地建立一個server 服務,比如假設在controller裡呼叫:

<?php
namespace AppHttpControllers;
use AppProvider;
use IlluminateHttpRequest;
use AppServicesServerProviderFactory;

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 AppHttpControllers;
use AppProvider;
use IlluminateHttpRequest;
class ServerController extends Controller
{
    public function store(Request $request, Provider $provider)
    {
        $repsonse = $provider->service()->createServer(
            $request->name, $request->size
        );
        //
    }
}

我們只想簡單地呼叫Provider這個例項上的service方法,然後就能獲取到其背後對應的供應商,然後就能直接地createServer。這樣來寫呢,可能更像是我們日常中最直接的思考過程,雖然可能背後具體怎麼實現你還沒搞懂。那麼怎麼來實現呢?假設不借助facade,我們或許可以這樣:

<?php
namespace App;
use AppServicesServerProviderFactory;
use IlluminateDatabaseEloquentModel;

class Provider extends Model
{
    public function service()
    {
        return (new ServerProviderFactory)->make($this->type);
    }
}

貌似可行。但是這樣呢,因為這個factory類是直接在service方法內部例項化的,這是不好的,後期我們無法用它來mock測試。那麼如果用realtime facade的方式會怎麼樣呢?

<?php
namespace App;
use IlluminateDatabaseEloquentModel;
use FacadesAppServicesServerProviderFactory;

class Provider extends Model
{
    public function service()
    {
        return ServerProviderFactory::make($this->type);
    }
}

現在,不僅看起來更簡潔優雅,而且也可以測試了,因為facade可以進行mock,比如說這樣:

<?php
namespace TestsFeature;
use Mockery;
use AppProvider;
use TestsTestCase;
use AppContractsServerProvider;
use FacadesAppServicesServerProviderFactory;
use IlluminateFoundationTestingRefreshDatabase;

class ExampleTest extends TestCase
{
    
    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);
    }
}

你會發現real-time facade最有用的地方就是構建簡潔、優雅的object APIs,同時呢又不會影響到程式碼的可測試性。希望這能給你的實際開發帶來一定啟發。

相關文章