利用 Real Time Facade 让代码富有表达力

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!

本作品采用《CC 协议》,转载必须注明作者和本文链接
Night gathers, and now my watch begins.
本帖由 Summer 于 6年前 加精
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2

请允许我抱紧你的大腿

6年前 评论

@JokerLinly 任务完成,并且顺便打开了赞赏 :smirk:

6年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!