使用 IoC 容器进行代码优化

PHP语言的灵活性

段誉奇道:“什么?只这么一会儿,便使了一十七种不同的武功?” 小说里面的慕容复博知天下武学,只一会儿功夫便使出了十七种不同的武功,php可以称之为编程界的慕容复,看看php一段Hello World。
<?php
/**
*php7.4 需要安装swoole扩展
*/
class Test{

    public static function main() : void{
        go(function() {
            $arr = ['H', 'e', 'l', 'l', 'o'];
            echo array_reduce($arr, fn($s, $e) => $s .= $e);
        });
    }
}

Test::main();

你是不是看到了Java、Golang、JavaScript的影子,php吸取了很多语言的特性,可函数式,可OOP,作为一门脚本语言当然也可以面向过程。因为PHP如此灵活再加上没有JavaEE这样企业级的标准导致代码风格”五花八门”。比如项目早期是这么创建对象的,每个方法前面一堆的new对象,文件头部一长串 use namespace。

<?php
namespace Model\Food;
use Psr\KLogg\Logger;
use Model\User\User;
use Model\Shop\Vip;
use Model\Pay\Pay;
use App\Libs\Http;
... 此处略去一万行
class Food{

    public function createOrder(){
        //日志类构造函数第一个参数日志目录,第二个参数调试
        $logger = new Logger('order', 'debug');
        $user = new User();
        $vip = new Vip();
        $pay = new Pay();
        $http = new Http();
        ... 此处略去一万行
}
  • 使用工厂模式优化

某一天项目上了分布式服务,原来的日志类不好用了,得换另外一个开源的日志类库,leader一声令下,一个个改,所有写日志的地方全得修改,程序猿痛苦不跌,而且每次写业务逻辑都得手动创建对象,既然是创建对象,那么就用对象创建型的工厂设计模式来生产对象。

<?php
class Model{

    public static $_model = [];
    /**
    *魔术静态方法可以减少很多工厂方法
    */
    public static function __callStatic($name, $argv) {
        if (isset(self::$_model[$name])) {
            return self::$_model[$name];
        }
        $name =  "\\App\\libs\\" . $name;
        self::$_model[$name] = new $name(...$argv);
        return self::$_model[$name];
    }

    public static function logger($path, $level){
        if(isset(self::$_model['logger'])){
            self::$_model['logger'] = new Psr\KLogg\Logger();
        }
        return self::$_model['logger']; 
    }

    public static function User(){
        if(isset(self::$_model['user'])){
            self::$_model['user'] = new \Model\User\User();
        }
        return self::$_model['user']; 
    }
    ......
}

好景不长,当项目不断迭代,工厂里的方法会越来越多,每次新增一个对象类型就得在工厂增加一个方法,违背了solid原则里的开闭原则,其实这个倒是影响不是很大,关键是这个工厂类会越来越膨胀,那么如何把对象的创建和工厂进行解耦呢?本文的主角IOC容器登场了,Laravel的核心就是一个IOC容器。

  • IOC容器

     laravel容器的作用是管理类的依赖和注入的工具, 原来类对外部的依赖需要在本类创建对象,使用了容器后,使得类把创建对象的工作反转到了容器。
  • laravel源码分析

  1. public/public/index.php入口

  2. bootstrap/app.php

  3. vendor/laravel/framework/src/Illuminate/Foundation/Application.php
    这一步会注册基础的服务提供者

  4. vendor/laravel/framework/src/Illuminate/Routing/RoutingServiceProvider.php

  5. 我们看到注册路由方法 执行了容器实例的singleton方法 vendor/laravel/framework/src/Illuminate/Foundation/Application.php


    bind方法重点看这一行,容器的属性bindings数组 以绑定的字符为key,闭包为值,闭包里有创建对象的方法。那这个闭包是什么时候会执行呢?
    由于控制器不是在我们的应用层,这里我们以Redis为例子来分析下。首先你当然得配置好redis连接配置。

  • 自动注入源码分析
  1. index.php会首先实例化kernel并执行handle方法, vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php

  2. vendor\laravel\framework\src\Illuminate\Foundation\Application.php

  3. bootstrppers数组里的服务提供者都会被实例化,并且执行bootstrap方法。
    vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterFacades.php
    vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php
    config/app.php里的provoders的创建方法会被自动注入到容器。
    config/app.php里的alials会执行class_alias
    vendor\laravel\framework\src\Illuminate\Foundation\AliasLoader.php

  • 实例真正被创建是在调用的时候
    我们写一个redis存取的例子,这里我把config/app.php里的Redis别名改成MyRedis

    <?php
    namespace App\Http\Controllers;
    class IndexController extends Controller
    {
        public function show()
        {
            /**@var $redis \Redis **/
            /**我们可以直接使用MyRedis别名,不需要引入任何**/
            $key = 'test_laravel';
            $redis = \MyRedis::connection();
            $redis->set($key, 2000000);
            echo $redis->get($key);
        }
    }

    那么redis实例到底是如何进行的呢?

  • 自动执行实例对象源码分析

  1. 这个类只有一个方法 getFaceeAccessor,我们调用的set方法其实是执行了Faced类的__callstaic方法


  2. static::getFacedAccessor()执行的就是 (使用了静态延迟绑定)

  3. 重点来了。。。

我们知道这个name的值是MyRedis。。。MyRedis是怎么实例化的呢? 谜底马上揭开了。。。

  1. containner类实现了ArrayAcess接口

原因就在于容器类实现了ArrayAceess接口,使得$staic::$app[$name],调用了offsetGet 然后执行了make方法,一切水落石出

知道了laravel的实现,我们就可以自己实现一个简陋的容器。

  • 使用IOC容器优化
  1. 容器类

    namespace sdk\container;
    class Container implements \ArrayAccess {
    
     /**
      * @var array 模型实例数组
      */
     private $instances = [];
    
     /**
      * @var array 绑定的闭包
      */
     private $bindings = [];
    
     public function isBinded($abstract){
         return isset($this->bindings[$abstract]);
     }
    
     public function bind($abstract, $concrete){
         if(!$this->isBinded($abstract)){
             $this->bindings[$abstract] = $concrete;
         }
         return $this;
     }
    
     public function make($abstract){
         if($this->bindings[$abstract] instanceof \Closure){
             return call_user_func($this->bindings[$abstract]);
         }else{
             return $this->bindings[$abstract];
         }
     }
    
     public function setInstances($alias, $instance){
         $this->instances[$alias] = $instance;
         return $instance;
     }
    
     public function getInstances(){
         return $this->instances;
     }
    
     /**
      * @inheritDoc
      */
     public function offsetExists($alias) {
         return isset($this->instances[$alias]);
     }
    
     /**
      * @inheritDoc
      */
     public function offsetGet($alias) {
         if(isset($this->instances[$alias])){
             return $this->instances[$alias];
         }
         return $this->make($alias);
     }
    
     /**
      * @inheritDoc
      */
     public function offsetSet($alias, $value) {
         $this->instances[$alias] = $value;
         return $this;
     }
    
     /**
      * @inheritDoc
      */
     public function offsetUnset($alias) {
         unset($this->instances[$alias]);
         return $this;
     }
    }
  2. 模型类

    namespace sdk\container;
    /**
    * @method static \sdk\cater\Order caterOrder()
    * @method static \sdk\cater\Waimai waimai()
    * @method static \Katzgrau\KLogger\Logger logger()
    */
    class Model {
    
     /**
      * @var array
      */
     private static $cfg = [
         'caterOrder'=>[
             \sdk\cater\Order::class,
         ],
         'waimai'=>[
             \sdk\cater\Waimai::class,
         ],
         'logger'=>[
             \Katzgrau\KLogger\Logger::class,
             [
                 SDK_PATH . '/../log/',
                 \Psr\Log\LogLevel::DEBUG,
             ]
         ],
     ];
    
     /**
      * 是否已经绑定过了
      * @var bool
      */
     private static $isBind = false;
    
     /**
      * @var \sdk\container\Container
      */
     private static $app;
    
     public static function bind(){
    
         foreach(static::$cfg as $alias=>$value){
             $class = $value[0] ?? '';
             if(empty($class)){
                 continue;
             }
             $args = $value[1] ?? [];
             static::$app->bind($alias, function() use($alias, $class, $args){
                 $ref = new \ReflectionClass($class);
                 return static::$app->setInstances($alias, $ref->newInstanceArgs($args));
             });
         }
     }
    
     public static function __callStatic($name, $args) {
    
         if(static::$app == null){
             static::$app = new Container();
         }
    
         if(static::$isBind == false){
             static::bind();
         }
    
         if(static::$app != null){
             return static::$app[$name];
         }
     }
    }

    以后加新的模型方法只需要增加配置以及使IDE具备提示功能的注释就可以了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

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