V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
pilishen
V2EX  ›  2018

通过 facade(尤其是 realtime facade)来使代码更优雅

  •  
  •   pilishen · 2018-02-02 21:55:01 +08:00 · 1196 次点击
    这是一个创建于 2490 天前的主题,其中的信息可能已经有所发展或是发生改变。

    本文来自 pilishen.com----原文链接

    该篇翻译整理自 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 Illuminate\Database\Eloquent\Model;
    class Provider extends Model
    {
        //
    }
    

    这里呢假设我们将所有处理外来 API 请求的 class 放在App\Services文件夹下,我们得对应每一个主机供应商都有一个“ service ” class,假设 DigitalOcean 这家供应商的 service class 是这样的:

    <?php
    namespace App\Services;
    use App\Contracts\ServerProvider;
    class DigitalOcean implements ServerProvider
    {
        public function createServer($name, $size)
        {
            //
        }
    }
    

    接下来呢,我们得能够解析这个服务类,基于我们 model 里的 type 这一栏的信息,我们可以使用工厂( factory )模式来实现:

    <?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;
            }
        }
    }
    

    然后呢,我们就可以在需要的地方调用这个工厂,来相应地创建一个 server 服务,比如假设在 controller 里调用:

    <?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方法,然后就能获取到其背后对应的供应商,然后就能直接地createServer。这样来写呢,可能更像是我们日常中最直接的思考过程,虽然可能背后具体怎么实现你还没搞懂。那么怎么来实现呢?假设不借助 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);
        }
    }
    

    貌似可行。但是这样呢,因为这个 factory 类是直接在service方法内部实例化的,这是不好的,后期我们无法用它来 mock 测试。那么如果用 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);
        }
    }
    

    现在,不仅看起来更简洁优雅,而且也可以测试了,因为 facade 可以进行 mock,比如说这样:

    <?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
    {
        
        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,同时呢又不会影响到代码的可测试性。希望这能给你的实际开发带来一定启发。

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3230 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 12:46 · PVG 20:46 · LAX 04:46 · JFK 07:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.