從零開始理解 Laravel 的設計哲學

心智極客發表於2019-10-17

單一職責

UserControllerindex 方法從資料庫中獲取全部使用者,並返回渲染後的檢視。

class UserController extends Controller
{
    public function index()
    {
        $users = User::all();

        return view('users.index', compact('users'));
    }
}

為了提高應用效率,使用者資料可能會儲存在 Redis 中

public function index()
{   
    // 因為資料獲取的不同而修改了程式碼
    $users = Redis::get('users')

    return view('users.index', compact('users'));
}

該例子違反了類的 單一職責。控制器應當作為 請求和響應的中介,不應當因為其他理由而修改程式碼。然而,我們卻因為資料獲取的不同(與控制器的職責無關)而修改了程式碼。

倉庫模式

對於控制器而言,並不需要知道資料是從 DB 還是從 Redis 中獲取,只需要知道如何獲取就行。資料的獲取交給專門的倉庫類處理即可。因此,分別定義一個 DB 倉庫和一個 Redis 倉庫來進一步劃分職責。

DB 倉庫

<?php

namespace App\Repositories;

use App\User;

class DbUserRepository
{
    public function all(): array
    {
        return User::all()->toArray();
    }
}

Redis 倉庫

<?php

namespace App\Repositories;

class RedisUserRepository
{
    public function all(): array
    {
        return Redis::get('users');
    }
}

控制器不再負責資料的處理

class UserController extends Controller
{
    private $users;

    public function __construct( )
    {
        $this->users = new DbUserRepository;
        // $this->users = new RedisRepository;
    }

    public function index()
    {   
        $users = $this->users->all();
        return $users;
    }
}

控制反轉

雖然獲取資料的職責委託給了倉庫類,但是該例子仍然存在問題。我們直接在控制器的建構函式中 主動宣告需要依賴的物件,這種在類中宣告依賴物件的行為,也可以稱為 依賴正轉

public function __construct( )
{
    $this->users = new DbUserRepository;
    // $this->users = new RedisRepository;
}

依賴正轉的不合理之處在哪裡呢?位於高層的控制器依賴於具體的底層資料獲取服務,當底層發生變動時,就需要對應的修改高層的內部結構。

我們對依賴關係進一步分析,可知控制器關注的並不是具體如何獲取資料,控制器關注的是「資料的可獲取性」這一抽象。因此,我們應當將依賴關係進行反轉,將對依賴的具體宣告職責轉移到外部,讓控制器僅依賴於抽象層(資料的可獲取性)。這種解決方式稱之為 控制反轉依賴倒置。通過控制反轉,高層不再依賴於具體的底層,僅僅是依賴於抽象層,高層和底層實現瞭解耦。

依賴注入

懂得控制反轉的含義後,就可以進一步實現控制反轉了。實現控制反轉的方式不止一種,其中最為常用的方式就是通過 依賴注入的方式。具體實現如下。

首先,用介面來表示「資料的可獲取性」這一抽象

<?php

namespace App\Repositories;

interface UserRepositoryInterface
{
    public function all(): array;
}

UserController 依賴的是「資料的可獲取性」,不依賴於具體的實現

class UserController extends Controller
{
    private $users;

    public function __construct(UserRepositoryInterface $users)
    {
        $this->users = $users;
    }
}

具體的實現交給對應的倉庫類即可

class DbUserRepository implements UserRepositoryInterface {}
class RedisRepository implements UserRepositoryInterface { }

根據自己的需要注入對應的服務,這樣就實現了依賴注入。

$userRepository = new DbUserRepository;
$userController = new UserController($userRepository)

總的來說,依賴注入由四部分構成

  • 被使用的服務 - DbUserRepository 或者 RedisRepository
  • 依賴某種服務的客戶端 - UserController
  • 宣告客戶端如何依賴服務的介面 - UserRepositoryInterface
  • 依賴注入器,用於決定注入哪項服務給客戶端

在上例中,我們的依賴注入器只是簡單的手工注入,對於 Laravel 而言,依賴注入器則是通過服務容器來進行。

服務容器

Laravel 的服務容器是一個用於管理類的依賴和執行依賴注入的強大工具,主要由「服務繫結」和「服務解析」兩部分構成,以下是一個簡單的服務容器的實現

namespace App\Services;

use Exception;

class Container 
{
    protected static $container = [];

    /**
     * 繫結服務
     * 
     * @param  服務名稱 $name 
     * @param  Callable $resolver
     * @return void
     */
    public static function bind($name, Callable $resolver)
    {   
        static::$container[$name] = $resolver;
    }

    /**
     * 解析服務
     * 
     * @param  服務名稱 $name
     * @return mix
     */
    public static function make($name)
    {
        if(isset(static::$container[$name])){
            $resolver = static::$container[$name];
            return $resolver();
        }

        throw new Exception("不存在該繫結");
   }

}

繫結服務

App\Services\Container::bind('UserRepository', function(){
    return new App\Repositories\DbUserRepository;
});

解析服務

$userRepository = App\Services\Container::make('UserRepository');
$userController = new UserController($userRepository)

Laravel 的服務容器的功能則更加的強大,比如,可以將介面與具體的實現進行繫結,通常在 服務提供者 中使用服務容器來進行繫結

public function register()
{
    $this->app->singleton(UserRepositoryInterface::class, function ($app) {
        return new UserRepository;
    });
}

這樣的話,我們就可以根據配置來進行靈活的切換,不需要手工的進行依賴注入。

自動解析依賴

Laravel 的服務容器最強大的地方在於可以通過反射來自動解析類的依賴,也就是說,大多數類可以自動解析,不需要在服務提供者中進行繫結。例如,我們在路由中只需要指定對應的控制器及方法,並不需要手動去例項化控制器

Route::get('users', 'UserController@index');

UserController 除了依賴 UserRepositoryInterface 外,可能還會依賴於 Request,Laravel 是如何自動解析這些依賴並例項化控制器的呢,大致過程如下:

  1. 服務容器中是否存在一個 UserController 的解析器?答案是否。
  2. 通過反射檢查下 UserController 的依賴。
  3. 檢測到 UserController 依賴於 UserRepositoryInterface,遞迴的對依賴進行處理,解析出 UserRepositoryInterface,其他依賴同理。
  4. 最後,使用 ReflectionClass->newInstanceArgs() 方法來例項化 UserController

對應的原始碼


/**
 * Resolve the given type from the container.
 *
 * @param  string  $abstract
 * @param  array  $parameters
 * @param  bool   $raiseEvents
 * @return mixed
 *
 * @throws \Illuminate\Contracts\Container\BindingResolutionException
 */
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
    $abstract = $this->getAlias($abstract);

    // 獲取該類的相關依賴繫結
    $needsContextualBuild = ! empty($parameters) || ! is_null(
        $this->getContextualConcrete($abstract)
    );

    // 單例模式直接返回,無需重新例項化
    if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
        return $this->instances[$abstract];
    }

    $this->with[] = $parameters;

    $concrete = $this->getConcrete($abstract);

    // 巢狀的解析依賴,構建服務
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete);
    } else {
        $object = $this->make($concrete);
    }

    // If we defined any extenders for this type, we'll need to spin through them
    // and apply them to the object being built. This allows for the extension
    // of services, such as changing configuration or decorating the object.
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    // If the requested type is registered as a singleton we'll want to cache off
    // the instances in "memory" so we can return it later without creating an
    // entirely new instance of an object on each subsequent request for it.
    if ($this->isShared($abstract) && ! $needsContextualBuild) {
        $this->instances[$abstract] = $object;
    }

    if ($raiseEvents) {
        $this->fireResolvingCallbacks($abstract, $object);
    }

    // Before returning, we will also set the resolved flag to "true" and pop off
    // the parameter overrides for this build. After those two things are done
    // we will be ready to return back the fully constructed class instance.
    $this->resolved[$abstract] = true;

    array_pop($this->with);

    return $object;
}

相關文章