从零开始理解 Laravel 的设计哲学

单一职责

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;
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 4年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 5
jaak

为什么我感觉反而复杂化了呢

4年前 评论
largezhou 4年前

举例应该选择贴切的应用场景,比如数据源场景这个如下更简单

return Cache::forever('user_key',function(){
     retun User::all();
})
4年前 评论
Complicated

@jaak 单纯的讲设计模式和思想的时候,都会觉得是“脱了裤子放屁”,但是等到项目做大了,你会发现,没有这些设计在里边,写代码的超级快,而且健壮性还好。如果是小项目,那当然是怎么方便怎么来

4年前 评论

Model本身已经算是一层Repository了,个人感觉没必要再封装一层。

有复杂的功能我会选择通过scope 或者 trait添加到Model。

4年前 评论

谢谢,讲的简单易懂,看了很长时间服务容器和服务提供者,感觉看了这篇文章豁然开朗。

4年前 评论

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