使用 IoC 容器進行程式碼優化

閃龍發表於2020-03-04

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 協議》,轉載必須註明作者和本文連結

相關文章