搞懂依賴注入, 用 PHP 手寫簡易 IOC 容器

前言

好的設(shè)計(jì)會提高程序的可復(fù)用性和可維護(hù)性,也間接的提高了開發(fā)人員的生產(chǎn)力。今天,我們就來說一下在很多框架中都使用的依賴注入引有。

一些概念

要搞清楚什么是依賴注入如何依賴注入,首先我們要明確一些概念倦逐。

DIP (Dependence Inversion Principle) 依賴倒置原則:

程序要依賴于抽象接口譬正,不要依賴于具體實(shí)現(xiàn)。

IOC (Inversion of Control) 控制反轉(zhuǎn):

遵循依賴倒置原則的一種代碼設(shè)計(jì)方案檬姥,依賴的創(chuàng)建 (控制) 由主動變?yōu)楸粍?(反轉(zhuǎn))曾我。

DI (Dependency Injection) 依賴注入:

控制反轉(zhuǎn)的一種具體實(shí)現(xiàn)方法。通過參數(shù)的方式從外部傳入依賴健民,將依賴的創(chuàng)建由主動變?yōu)楸粍?(實(shí)現(xiàn)了控制反轉(zhuǎn))抒巢。

光說理論有點(diǎn)不好理解,我們用代碼舉個例子秉犹。

首先蛉谜,我們看依賴沒有倒置時的一段代碼:


class Controller
{
    protected $service;

    public function __construct()
    {
        // 主動創(chuàng)建依賴
        $this->service = new Service(12, 13); 
    }       
}

class Service
{
    protected $model;
    protected $count;

    public function __construct($param1, $param2)
    {
        $this->count = $param1 + $param2;
        // 主動創(chuàng)建依賴
        $this->model = new Model('test_table'); 
    }
}

class Model
{
    protected $table;

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

$controller = new Controller;

上述代碼的依賴關(guān)系是 Controller 依賴 Service,Service 依賴 Model凤优。從控制的角度來看悦陋,Controller 主動創(chuàng)建依賴 Service蜈彼,Service 主動創(chuàng)建依賴 Model筑辨。依賴是由需求方內(nèi)部產(chǎn)生的,需求方需要關(guān)心依賴的具體實(shí)現(xiàn)幸逆。這樣的設(shè)計(jì)使代碼耦合性變高棍辕,每次底層發(fā)生改變(如參數(shù)變動),頂層就必須修改代碼还绘。

接下來楚昭,我們使用依賴注入實(shí)現(xiàn)控制反轉(zhuǎn),使依賴關(guān)系倒置:

class Controller
{
    protected $service;
    // 依賴被動傳入拍顷。申明要 Service 類的實(shí)例 (抽象接口)
    public function __construct(Service $service)
    {
        $this->service = $service; 
    }       
}

class Service
{
    protected $model;
    protected $count;
    // 依賴被動傳入
    public function __construct(Model $model, $param1, $param2)
    {
        $this->count = $param1 + $param2;
        $this->model = $model; 
    }
}

class Model
{
    protected $table;

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

$model = new Model('test_table');
$service = new Service($model, 12, 13);
$controller = new Controller($service);

將依賴通過參數(shù)的方式從外部傳入(即依賴注入)抚太,控制的角度上依賴的產(chǎn)生從主動創(chuàng)建變?yōu)楸粍幼⑷耄蕾囮P(guān)系變?yōu)榱艘蕾囉诔橄蠼涌诙灰蕾囉诰唧w實(shí)現(xiàn)昔案。此時的代碼得到了解耦尿贫,提高了可維護(hù)性。

從單元測試的角度看踏揣,依賴注入更方便 stub 和 mock 操作庆亡,方便了測試人員寫出質(zhì)量更高的測試代碼。

如何依賴注入捞稿,自動注入依賴

有了上面的一些理論基礎(chǔ)又谋,我們大致了解了依賴注入是什么拼缝,能干什么。

不過雖然上面的代碼可以進(jìn)行依賴注入了彰亥,但是依賴還是需要手動創(chuàng)建咧七。我們可不可以創(chuàng)建一個工廠類,用來幫我們進(jìn)行自動依賴注入呢剩愧?OK猪叙,我們需要一個 IOC 容器。

實(shí)現(xiàn)一個簡單的 IOC 容器

依賴注入是以構(gòu)造函數(shù)參數(shù)的形式傳入的仁卷,想要自動注入:

  • 我們需要知道需求方需要哪些依賴穴翩,使用反射來獲得
  • 只有類的實(shí)例會被注入,其它參數(shù)不受影響

如何自動進(jìn)行注入呢锦积?當(dāng)然是 PHP 自帶的反射功能芒帕!

注:關(guān)于反射是否影響性能,答案是肯定的丰介。但是相比數(shù)據(jù)庫連接背蟆、網(wǎng)絡(luò)請求的時延,反射帶來的性能問題在絕大多數(shù)情況下并不會成為應(yīng)用的性能瓶頸哮幢。

1.雛形

首先带膀,創(chuàng)建 Container 類,getInstance 方法:

class Container
{
    public static function getInstance($class_name, $params = [])
    {
        // 獲取反射實(shí)例
        $reflector = new ReflectionClass($class_name);
        // 獲取反射實(shí)例的構(gòu)造方法
        $constructor = $reflector->getConstructor();
        // 獲取反射實(shí)例構(gòu)造方法的形參
        $di_params = [];
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $class = $param->getClass();
                if ($class) { // 如果參數(shù)是一個類橙垢,創(chuàng)建實(shí)例
                    $di_params[] = new $class->name;
                }
            }
        }
        
        $di_params = array_merge($di_params, $params);
        // 創(chuàng)建實(shí)例
        return $reflector->newInstanceArgs($di_params);
    }
}

這里我們獲取構(gòu)造方法參數(shù)時用到了 ReflectionClass 類垛叨,大家可以到官方文檔了解一下該類包含的方法和用法,這里就不再贅述柜某。

ok嗽元,有了 getInstance 方法,我們可以試一下自動注入依賴了:

class A
{
    public $count = 100;
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 110

2.進(jìn)階

雖然上面的代碼可以進(jìn)行自動依賴注入了喂击,但是問題是只能構(gòu)注入一層剂癌。如果 A 類也有依賴怎么辦呢?

ok翰绊,我們需要修改一下代碼:

class Container
{
    public static function getInstance($class_name, $params = [])
    {
        // 獲取反射實(shí)例
        $reflector = new ReflectionClass($class_name);
        // 獲取反射實(shí)例的構(gòu)造方法
        $constructor = $reflector->getConstructor();
        // 獲取反射實(shí)例構(gòu)造方法的形參
        $di_params = [];
        if ($constructor) {
            foreach ($constructor->getParameters() as $param) {
                $class = $param->getClass();
                if ($class) { // 如果參數(shù)是一個類佩谷,創(chuàng)建實(shí)例,并對實(shí)例進(jìn)行依賴注入
                    $di_params[] = self::getInstance($class->name);
                }
            }
        }
        
        $di_params = array_merge($di_params, $params);
        // 創(chuàng)建實(shí)例
        return $reflector->newInstanceArgs($di_params);
    }
}

測試一下:


class C 
{
    public $count = 20;
}
class A
{
    public $count = 100;

    public function __construct(C $c)
    {
        $this->count += $c->count;
    }
}

class B
{
    protected $count = 1;

    public function __construct(A $a, $count)
    {
        $this->count = $a->count + $count;
    }

    public function getCount()
    {
        return $this->count;
    }
}

$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 130

上述代碼使用遞歸完成了多層依賴的注入關(guān)系监嗜,程序中依賴關(guān)系層級一般不會特別深谐檀,遞歸不會造成內(nèi)存遺漏問題。

3.單例

有些類會貫穿在程序生命周期中被頻繁使用秤茅,為了在依賴注入中避免不停的產(chǎn)生新的實(shí)例稚补,我們需要 IOC 容器支持單例模式,已經(jīng)是單例的依賴可以直接獲取框喳,節(jié)省資源课幕。

為 Container 增加單例相關(guān)方法:

class Container
{
    protected static $_singleton = []; 

    // 添加一個實(shí)例到單例
    public static function singleton($instance)
    {
        if ( ! is_object($instance)) {
            throw new InvalidArgumentException("Object need!");
        }
        $class_name = get_class($instance);
        // singleton not exist, create
        if ( ! array_key_exists($class_name, self::$_singleton)) {
            self::$_singleton[$class_name] = $instance;
        }
    }
    // 獲取一個單例實(shí)例
    public static function getSingleton($class_name)
    {
        return array_key_exists($class_name, self::$_singleton) ?
                self::$_singleton[$class_name] : NULL;
    }
    // 銷毀一個單例實(shí)例
    public static function unsetSingleton($class_name)
    {
        self::$_singleton[$class_name] = NULL;
    }

}

改造 getInstance 方法:


public static function getInstance($class_name, $params = [])
{
    // 獲取反射實(shí)例
    $reflector = new ReflectionClass($class_name);
    // 獲取反射實(shí)例的構(gòu)造方法
    $constructor = $reflector->getConstructor();
    // 獲取反射實(shí)例構(gòu)造方法的形參
    $di_params = [];
    if ($constructor) {
        foreach ($constructor->getParameters() as $param) {
            $class = $param->getClass();
            if ($class) { 
                // 如果依賴是單例厦坛,則直接獲取
                $singleton = self::getSingleton($class->name);
                $di_params[] = $singleton ? $singleton : self::getInstance($class->name);
            }
        }
    }
    
    $di_params = array_merge($di_params, $params);
    // 創(chuàng)建實(shí)例
    return $reflector->newInstanceArgs($di_params);
}

4.以依賴注入的方式運(yùn)行方法

類之間的依賴注入解決了,我們還需要一個以依賴注入的方式運(yùn)行方法的功能乍惊,可以注入任意方法的依賴杜秸。這個功能在實(shí)現(xiàn)路由分發(fā)到控制器方法時很有用。

增加 run 方法

public static function run($class_name, $method, $params = [], $construct_params = [])
{
    if ( ! class_exists($class_name)) {
        throw new BadMethodCallException("Class $class_name is not found!");
    }

    if ( ! method_exists($class_name, $method)) {
        throw new BadMethodCallException("undefined method $method in $class_name !");
    }
    // 獲取實(shí)例
    $instance = self::getInstance($class_name, $construct_params);

    // 獲取反射實(shí)例
    $reflector = new ReflectionClass($class_name);
    // 獲取方法
    $reflectorMethod = $reflector->getMethod($method);
    // 查找方法的參數(shù)
    $di_params = [];
    foreach ($reflectorMethod->getParameters() as $param) {
        $class = $param->getClass();
        if ($class) { 
            $singleton = self::getSingleton($class->name);
            $di_params[] = $singleton ? $singleton : self::getInstance($class->name);
        }
    }

    // 運(yùn)行方法
    return call_user_func_array([$instance, $method], array_merge($di_params, $params));
}

測試:

class A
{
    public $count = 10;
}

class B
{
    public function getCount(A $a, $count)
    {
        return $a->count + $count;
    }
}

$result = Container::run(B::class, 'getCount', [10]);
var_dump($result); // result is 20

ok润绎,一個簡單好用的 IOC 容器完成了撬碟,動手試試吧!

完整代碼

IOC Container 的完整代碼請見 wazsmwazsm/IOCContainer莉撇, 原先是在我的框架 wazsmwazsm/WorkerA 中使用呢蛤,現(xiàn)在已經(jīng)作為單獨(dú)的項(xiàng)目,有完善的單元測試棍郎,可以使用到生產(chǎn)環(huán)境其障。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市涂佃,隨后出現(xiàn)的幾起案子励翼,更是在濱河造成了極大的恐慌,老刑警劉巖辜荠,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汽抚,死亡現(xiàn)場離奇詭異,居然都是意外死亡伯病,警方通過查閱死者的電腦和手機(jī)造烁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狱从,“玉大人膨蛮,你說我怎么就攤上這事叠纹〖狙校” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵誉察,是天一觀的道長与涡。 經(jīng)常有香客問我,道長持偏,這世上最難降的妖魔是什么驼卖? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮鸿秆,結(jié)果婚禮上酌畜,老公的妹妹穿的比我還像新娘。我一直安慰自己卿叽,他們只是感情好桥胞,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布恳守。 她就那樣靜靜地躺著,像睡著了一般贩虾。 火紅的嫁衣襯著肌膚如雪催烘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天缎罢,我揣著相機(jī)與錄音伊群,去河邊找鬼。 笑死策精,一個胖子當(dāng)著我的面吹牛舰始,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播咽袜,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼蔽午,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了酬蹋?” 一聲冷哼從身側(cè)響起及老,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎范抓,沒想到半個月后骄恶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡匕垫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年僧鲁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片象泵。...
    茶點(diǎn)故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡寞秃,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出偶惠,到底是詐尸還是另有隱情春寿,我是刑警寧澤,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布忽孽,位于F島的核電站绑改,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兄一。R本人自食惡果不足惜厘线,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望出革。 院中可真熱鬧造壮,春花似錦、人聲如沸骂束。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至楞抡,卻和暖如春伟众,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背召廷。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工凳厢, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人竞慢。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓先紫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筹煮。 傳聞我的和親對象是個殘疾皇子遮精,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,781評論 2 361