單一職責
UserController
的 index
方法從資料庫中獲取全部使用者,並返回渲染後的檢視。
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 是如何自動解析這些依賴並例項化控制器的呢,大致過程如下:
- 服務容器中是否存在一個
UserController
的解析器?答案是否。 - 通過反射檢查下
UserController
的依賴。 - 檢測到
UserController
依賴於UserRepositoryInterface
,遞迴的對依賴進行處理,解析出UserRepositoryInterface
,其他依賴同理。 - 最後,使用
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;
}