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


    单一职责

    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 AppRepositories;
    
    use AppUser;
    
    class DbUserRepository
    {
        public function all(): array
        {
            return User::all()->toArray();
        }
    }
    

    Redis 仓库

    <?php
    
    namespace AppRepositories;
    
    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 AppRepositories;
    
    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 AppServices;
    
    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("不存在该绑定");
       }
    
    }
    

    绑定服务

    AppServicesContainer::bind('UserRepository', function(){
        return new AppRepositoriesDbUserRepository;
    });
    

    解析服务

    $userRepository = AppServicesContainer::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 IlluminateContractsContainerBindingResolutionException
     */
    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;
    }
    
  • 相关阅读:
    Zend Framework 2.1.5 中根据服务器的环境配置调用数据库等的不同配置
    在基于 Eclipse 的 IDE 中安装和使用 Emmet(ZenCoding)
    【翻译】Emmet(Zen Coding)官方文档 之六 自定义 Emmet
    【翻译】Emmet(Zen Coding)官方文档 之二 缩写
    【翻译】Emmet(Zen Coding)官方文档 之七 一览表
    【翻译】Emmet(Zen Coding)官方文档 之三 CSS 缩写
    【翻译】Emmet(Zen Coding)官方文档 之四 动作
    【翻译】Emmet(Zen Coding)官方文档 之一 web 编程的必备工具
    Zend Framework 2 时区设置警告问题的解决
    【翻译】Emmet (Zen Coding) 元素类型
  • 原文地址:https://www.cnblogs.com/caibaotimes/p/13983097.html
Copyright © 2020-2023  润新知