天天看點

PHP 依賴注入,從此不再考慮加載順序

是否還在為依賴問題發愁,是否還在為引入檔案順序問題發愁,是否了解'依賴倒置原則'和'依賴注入',是否想要提高架構水準,請看這裡吧

說這個話題之前先講一個比較高端的思想--'依賴倒置原則'

"依賴倒置是一種軟體設計思想,在傳統軟體中,上層代碼依賴于下層代碼,當下層代碼有所改動時,上層代碼也要相應進行改動,是以維護成本較高。而依賴倒置原則的思想是,上層不應該依賴下層,應依賴接口。意為上層代碼定義接口,下層代碼實作該接口,進而使得下層依賴于上層接口,降低耦合度,提高系統彈性"

上面的解釋有點虛,下面我們以實際代碼來解釋這個理論

比如有這麼條需求,使用者注冊完成後要發送一封郵件,然後你有如下代碼:

先有郵件類'Email.class.php'

class Mail{
    public function send()
    {
        /*這裡是如何發送郵件的代碼*/
    }
}      

然後又注冊的類'Register.class.php'

class Register{
    private $_emailObj;

    public function doRegister()
    {
        /*這裡是如何注冊*/

        $this->_emailObj = new Mail();
        $this->_emailObj->send();//發送郵件
    }
}      

然後開始注冊

include 'Mail.class.php';
include 'Register.class.php';
$reg = new Register();
$reg->doRegister();      

看起來事情很簡單,你很快把這個功能上線了,看起來相安無事... xxx天過後,産品人員說發送郵件的不好,要使用發送短信的,然後你說這簡單我把'Mail'類改下...

又過了幾天,産品人員說發送短信費用太高,還是改用郵件的好...  此時心中一萬個草泥馬奔騰而過...

這種事情,常常在産品狗身上發生,無可奈何花落去...

以上場景的問題在于,你每次不得不對'Mail'類進行修改,代碼複用性很低,高層過度依賴于底層。那麼我們就考慮'依賴倒置原則',讓底層繼承高層制定的接口,高層依賴于接口。

interface Mail
{
    public function send();
}      
class Email implements Mail()
{
    public function send()
    {
        //發送Email
    }
}      
class SmsMail implements Mail()
{
    public function send()
    {
        //發送短信
    }
}      
class Register
{
    private $_mailObj;

    public function __construct(Mail $mailObj)
    {
        $this->_mailObj = $mailObj;
    }

    public function doRegister()
    {
        /*這裡是如何注冊*/
        $this->_mailObj->send();//發送資訊
    }
}      

 下面開始發送資訊

/* 此處省略若幹行 */
$reg = new Register();
$emailObj = new Email();
$smsObj = new SmsMail();

$reg->doRegister($emailObj);//使用email發送
$reg->doRegister($smsObj);//使用短信發送
/* 你甚至可以發完郵件再發短信 */      

上面的代碼解決了'Register'對資訊發送類的依賴,使用構造函數注入的方法,使得它隻依賴于發送短信的接口,隻要實作其接口中的'send'方法,不管你怎麼發送都可以。上例就使用了"注入"這個思想,就像注射器一樣将一個類的執行個體注入到另一個類的執行個體中去,需要用什麼就注入什麼。當然"依賴倒置原則"也始終貫徹在裡面。"注入"不僅可以通過構造函數注入,也可以通過屬性注入,上面你可以可以通過一個"setter"來動态為"mailObj"這個屬性指派。

上面看了很多,但是有心的讀者可能會發現标題中"從此不再考慮加載順序"這個字眼,你上面的不還是要考慮加載順序嗎? 不還是先得引入資訊發送類,然後在引入注冊類,然後再執行個體化嗎? 如果類一多,不照樣暈!

确實如此,現實中有許多這樣的案例,一開始類就那麼多,慢慢的功能越來越多,人員越來越多,編寫了很多類,要使用這個類必須先引入那個類,而且一定要確定順序正确。有這麼個例子, "a 依賴于b, b 依賴于c, c 依賴于 d, d 依賴于e", 要擷取'a'的執行個體,你必須依次引入 'e,d,c,b'然後依次進行執行個體化,老的員工知道這個坑,跳過去了。某天來了個新人,他想執行個體化'a' 可是一直報錯,他都不造咋回事,此時隻能看看看'a'的業務邏輯,然後知道要先擷取'b'的執行個體,然後在看'b'的業務邏輯,然後... 一天過去了,他還是沒有擷取到'a'的執行個體,然後上司來了...

那這個事情到底是新人的技術低下,還是當時架構人員的水準低下了?

現在切入話題,來實作如何不考慮加載順序,在實作前就要明白要是不考慮加載順序就意味着讓程式自動進行加載自動進行執行個體化。類要執行個體化,隻要保證完整的傳遞給'__construct'函數所必須的參數就OK了,在類中如果要引用其他類,也必須在構造函數中注入,否則調用時仍然會發生錯誤。那麼我們需要一個類,來儲存類執行個體化所需要的參數,依賴的其他類或者對象以及各個類執行個體化後的引用

該類命名為盒子 'Container.class.php', 其内容如下:

/**
*    依賴注入類
*/
class Container{
    /**
    *@var array 存儲各個類的定義  以類的名稱為鍵
    */
    private $_definitions = array();

    /**
    *@var array 存儲各個類執行個體化需要的參數 以類的名稱為鍵
    */
    private $_params = array();

    /**
    *@var array 存儲各個類執行個體化的引用
    */
    private $_reflections = array();

    /**
    * @var array 各個類依賴的類
    */
    private $_dependencies = array();

    /**
    * 設定依賴
    * @param string $class 類、方法 名稱
    * @param mixed $defination 類、方法的定義
    * @param array $params 類、方法初始化需要的參數
    */
    public function set($class, $defination = array(), $params = array())
    {
        $this->_params[$class] = $params;
        $this->_definitions[$class] = $this->initDefinition($class, $defination);
    }

    /**
    * 擷取執行個體
    * @param string $class 類、方法 名稱
    * @param array $params 執行個體化需要的參數
    * @param array $properties 為執行個體配置的屬性
    * @return mixed
    */
    public function get($class, $params = array(), $properties = array())
    {
        if(!isset($this->_definitions[$class]))
        {//如果重來沒有聲明過 則直接建立
            return $this->bulid($class, $params, $properties);
        }

        $defination = $this->_definitions[$class];

        if(is_callable($defination, true))
        {//如果聲明是函數
            $params = $this->parseDependencies($this->mergeParams($class, $params));
            $obj = call_user_func($defination, $this, $params, $properties);
        }
        elseif(is_array($defination))
        {
            $originalClass = $defination['class'];
            unset($definition['class']);

            //difinition中除了'class'元素外 其他的都當做執行個體的屬性處理
            $properties = array_merge((array)$definition, $properties);

            //合并該類、函數聲明時的參數
            $params = $this->mergeParams($class, $params);
            if($originalClass === $class)
            {//如果聲明中的class的名稱和關鍵字的名稱相同 則直接生成對象
                $obj = $this->bulid($class, $params, $properties);
            }
            else
            {//如果不同則有可能為别名 則從容器中擷取
                $obj = $this->get($originalClass, $params, $properties);
            }
        }
        elseif(is_object($defination))
        {//如果是個對象 直接傳回
            return $defination;
        }
        else
        {
            throw new Exception($class . ' 聲明錯誤!');
        }
        return $obj;
    }

    /**
    * 合并參數
    * @param string $class 類、函數 名稱
    * @param array $params 參數
    * @return array
    */
    protected function mergeParams($class, $params = array())
    {
        if(empty($this->_params[$class]))
        {
            return $params;
        }
        if(empty($params))
        {
            return $this->_params;
        }

        $result = $this->_params[$class];
        foreach($params as $key => $value) 
        {
            $result[$key] = $value;
        }
        return $result;
    }

    /**
    * 初始化聲明
    * @param string $class 類、函數 名稱
    * @param array $defination 類、函數的定義
    * @return mixed
    */
    protected function initDefinition($class, $defination)
    {
        if(empty($defination))
        {
            return array('class' => $class);
        }
        if(is_string($defination))
        {
            return array('class' => $defination);
        }
        if(is_callable($defination) || is_object($defination))
        {
            return $defination;
        }
        if(is_array($defination))
        {
            if(!isset($defination['class']))
            {
                $definition['class'] = $class;
            }
            return $defination;
        }
        throw new Exception($class. ' 聲明錯誤');
    }

    /**
    * 建立類執行個體、函數
    * @param string $class 類、函數 名稱
    * @param array $params 初始化時的參數
    * @param array $properties 屬性
    * @return mixed
    */
    protected function bulid($class, $params, $properties)
    {
        list($reflection, $dependencies) = $this->getDependencies($class);

        foreach ((array)$params as $index => $param) 
        {//依賴不僅有對象的依賴 還有普通參數的依賴
            $dependencies[$index] = $param;
        }

        $dependencies = $this->parseDependencies($dependencies, $reflection);

        $obj = $reflection->newInstanceArgs($dependencies);

        if(empty($properties))
        {
            return $obj;
        }

        foreach ((array)$properties as $name => $value) 
        {
            $obj->$name = $value;
        }

        return $obj;
    }

    /**
    * 擷取依賴
    * @param string $class 類、函數 名稱
    * @return array
    */
    protected function getDependencies($class)
    {
        if(isset($this->_reflections[$class]))
        {//如果已經執行個體化過 直接從緩存中擷取
            return array($this->_reflections[$class], $this->_dependencies[$class]);
        }

        $dependencies = array();
        $ref = new ReflectionClass($class);//擷取對象的執行個體
        $constructor = $ref->getConstructor();//擷取對象的構造方法
        if($constructor !== null)
        {//如果構造方法有參數
            foreach($constructor->getParameters() as $param) 
            {//擷取構造方法的參數
                if($param->isDefaultValueAvailable())
                {//如果是預設 直接取預設值
                    $dependencies[] = $param->getDefaultValue();
                }
                else
                {//将構造函數中的參數執行個體化
                    $temp = $param->getClass();
                    $temp = ($temp === null ? null : $temp->getName());
                    $temp = Instance::getInstance($temp);//這裡使用Instance 類标示需要執行個體化 并且存儲類的名字
                    $dependencies[] = $temp;
                }
            }
        }
        $this->_reflections[$class] = $ref;
        $this->_dependencies[$class] = $dependencies;
        return array($ref, $dependencies);
    }

    /**
    * 解析依賴
    * @param array $dependencies 依賴數組
    * @param array $reflection 執行個體
    * @return array $dependencies
    */
    protected function parseDependencies($dependencies, $reflection = null)
    {
        foreach ((array)$dependencies as $index => $dependency) 
        {
            if($dependency instanceof Instance)
            {
                if ($dependency->id !== null) 
                {
                    $dependencies[$index] = $this->get($dependency->id);
                } 
                elseif($reflection !== null) 
                {
                    $parameters = $reflection->getConstructor()->getParameters();
                    $name = $parameters[$index]->getName();
                    $class = $reflection->getName();
                    throw new Exception('執行個體化類 ' . $class . ' 時缺少必要參數:' . $name);
                }   
            }
        }
        return $dependencies;
    }
}      

下面是'Instance'類的内容,該類主要用于記錄類的名稱,标示是否需要擷取執行個體

class Instance{
    /**
     * @var 類唯一标示
     */
    public $id;

    /**
     * 構造函數
     * @param string $id 類唯一ID
     * @return void
     */
    public function __construct($id)
    {
        $this->id = $id;
    }

    /**
     * 擷取類的執行個體
     * @param string $id 類唯一ID
     * @return Object Instance
     */
    public static function getInstance($id)
    {
        return new self($id);
    }
}      

然後我們在'Container.class.php'中還是實作了為類的執行個體動态添加屬性的功能,若要動态添加屬性,需使用魔術方法'__set'來實作,是以所有使用依賴加載的類需要實作該方法,那麼我們先定義一個基礎類 'Base.class.php',内容如下

class Base{
    /**
    * 魔術方法
    * @param string $name
    * @param string $value
    * @return void
    */
    public function __set($name, $value)
    {
        $this->{$name} = $value;
    }
}      

然後我們來實作'A,B,C'類,A類的執行個體 依賴于 B類的執行個體,B類的執行個體依賴于C類的執行個體

'A.class.php'

class A extends Base{
    private $instanceB;

    public function __construct(B $instanceB)
    {
        $this->instanceB = $instanceB;
    }

    public function test()
    {
        $this->instanceB->test();
    }
}      

'B.class.php'

class B  extends Base{
    private $instanceC;

    public function __construct(C $instanceC)
    {
        $this->instanceC = $instanceC;
    }

    public function test()
    {
        return $this->instanceC->test();
    }
}      

'C.class.php'

class C  extends Base{
    public function test()
    {
        echo 'this is C!';
    }
}de      

然後我們在'index.php'中擷取'A'的執行個體,要實作自動加載,需要使用SPL類庫的'spl_autoload_register'方法,代碼如下

function autoload($className)
{
    include_once $className . '.class.php';
}
spl_autoload_register('autoload', true, true);
$container = new Container;

$a = $container->get('A');
$a->test();//輸出 'this is C!'      

上面的例子看起來是不是很爽,根本都不需要考慮'B','C' (當然,這裡B,C 除了要使用相應類的執行個體外,沒有其他參數,如果有其他參數,必須顯要調用'$container->set(xx)'方法進行注冊,為其制定執行個體化必要的參數)。有細心同學可能會思考,比如我在先擷取了'A'的執行個體,我在另外一個地方也要擷取'A'的執行個體,但是這個地方'A'的執行個體需要其中某個屬性不一樣,我怎麼做到?

你可以看到'Container' 類的 'get' 方法有其他兩個參數,'$params' 和 '$properties' , 這個'$properties' 即可實作剛剛的需求,這都依賴'__set'魔術方法,當然這裡你不僅可以注冊類,也可以注冊方法或者對象,隻是注冊方法時要使用回調函數,例如

$container->set('foo', function($container, $params, $config){
    print_r($params);
    print_r($config);
});

$container->get('foo', array('name' => 'foo'), array('key' => 'test'));      

還可以注冊一個對象的執行個體,例如

class Test
{
    public function mytest()
    {
        echo 'this is a test';
    }
}

$container->set('testObj', new Test());

$test = $container->get('testObj');
$test->mytest();      

 以上自動加載,依賴控制的大體思想就是将類所要引用的執行個體通過構造函數注入到其内部,在擷取類的執行個體的時候通過PHP内建的反射解析構造函數的參數對所需要的類進行加載,然後進行執行個體化,并進行緩存以便在下次擷取時直接從記憶體取得

以上代碼僅僅用于學習和實驗,未經嚴格測試,請不要用于生産環境,以免産生未知bug

鄙人才疏學淺,有不足之處,歡迎補足!