IOC 模式解耦與 Laravel 服務容器

原文:blog,轉載註明來源便可。
本文代碼:GitHubphp

前言

服務容器是 Laravel 框架實現模塊化解耦的核心。模塊化便是將系統拆成多個子模塊,子模塊間的耦合程度儘量的低,代碼中儘量的避免直接調用。這樣才能提升系統的代碼重用性、可維護性、擴展性。laravel

下邊出行例子有火車、飛機兩種出行方式,對應給出了 3 種耦合度愈來愈低的實現:高度耦合實現、工廠模式解耦、IOC 模式解耦。git

高度耦合實現

代碼實現

定義 TrafficTool 接口並用 Train、Plane 實現,最後在 Traveler 中實例化出行工具後說走就走。代碼十分簡潔:github

<?php

// 定義交通工具接口
interface TrafficTool
{
    public function go();
}

class Train implements TrafficTool
{
    public function go() {
        echo '[Travel By]: train', PHP_EOL;
    }
}

class Plane implements TrafficTool
{
    public function go() {
        echo '[Travel By]: plane', PHP_EOL;
    }
}


// 旅遊者類,使用火車出行
class Traveler
{
    protected $trafficTool;

    public function __construct() {
        // 直接 new 對象,在 Traveler 與 Train 兩個類之間產生了依賴
        // 若是程序內部要修改出行方式,必須修改 Traveler 的 __construct()
        // 代碼高度耦合,可維護性低
        $this->travelTool = new Train();
    }

    public function travel() {
        $this->travelTool->go();
    }
}


$me = new Traveler();
$me->travel();

運行:shell

$ php normal.php
[Travel By]: train

優勢

代碼十分簡潔:一個接口兩個類最後直接調用。數組

缺點

在第 32 行,TravelerTrain 兩個組件發生了耦合。之後想坐飛機出行,必須修改 __construct() 的內部實現:$this->travelTool = new Plane();框架

重用性和可維護性都不好:在實際的軟件開發中,代碼會根據業務需求的變化而不斷修改。若是組件之間直接相互調用,那組件的代碼就不能輕易修改,以避免調用它的地方出現錯誤。ide

工廠模式解耦

工廠模式

分離代碼中不變和變的部分,使得在不一樣條件下建立不一樣的對象。模塊化

代碼實現

...

class  TrafficToolFactory
{
    public function create($name) {
        switch ($name) {
            case 'train':
                return new Train();
            case 'plane':
                return new Plane();
            default:
                exit('[No Traffic Tool] :' . $name);
        }
    }
}


// 旅遊者類,使用火車出行
class Traveler
{
    protected $trafficTool;

    public function __construct($toolName) {
        // 使用工廠類實例化須要的交通工具
        $factory = new TrafficToolFactory();
        $this->travelTool = $factory->create($toolName);
    }

    public function travel() {
        $this->travelTool->go();
    }
}

// 傳入指定的方式
$me = new Traveler('train');
$me->travel();

運行:函數

$ php factory.php
[Travel By]: train

優勢

提取了代碼中變化的部分:更換交通工具,坐飛機出行直接修改 $me = new Traveler('plane') 便可。適用於需求簡單的狀況。

缺點

依舊沒有完全解決依賴:如今 TravelerTrafficToolFactory 發生了依賴。當需求增多後,工廠的 switch...case 等代碼也不易維護。

IOC 模式解耦

IOC 是 Inversion Of Controll 的縮寫,即控制反轉。這裏的「反轉」可理解爲將組件間依賴關係提到外部管理。

簡單的依賴注入

依賴注入是 IOC 的一種實現方式,是指組件間的依賴經過外部參數(interface)形式直接注入。好比對上邊的工廠模式進一步解耦:

<?php

interface TrafficTool
{
    public function go();
}

class Train implements TrafficTool
{
    public function go() {
        echo '[Travel By]: train', PHP_EOL;
    }
}

class Plane implements TrafficTool
{
    public function go() {
        echo '[Travel By]: plane', PHP_EOL;
    }
}


class Traveler
{
    protected $trafficTool;

    // 參數 $tool 就是控制反轉要反轉部分,將依賴的對象直接傳入便可
    // 之後再有 Car, GetWay ... 等新增工具也是實例化後傳參直接調用
    public function __construct(TrafficTool $tool) {
        $this->trafficTool = $tool;
    }

    public function travel() {
        $this->trafficTool->go();
    }
}

$train = new Train();
$me    = new Traveler($train);    // 將依賴直接以參數的形式注入
$me->travel();

運行:

$ php simple_ioc.php
[Travel By]: train

高級依賴注入

簡單注入的問題

若是三我的分別自駕遊、坐飛機、高鐵出去玩,那你的代碼多是這樣的:

$train = new Train();
$plane = new Plane();
$car   = new Car();

$a = new Traveler($car);
$b = new Traveler($plane);
$c = new Traveler($train);

$a->travel();
$b->travel();
$c->travel();

看起來就兩個字:藍瘦。上邊簡單的依賴注入相比工廠模式已經解耦挺多了,參考 Laravel 中服務容器的概念,還能繼續解耦。將會使用到 PHP 反射和匿名函數,參考:Laravel 框架中經常使用的 PHP 語法

IOC 容器

高級依賴注入 = 簡單依賴注入 + IOC 容器

<?php
# advanced_ioc.php    
...
    
class Container
{
    protected $binds = [];
    protected $instances = [];

    /**
     * 綁定:將回調函數綁定到字符指令上
     *
     * @param $abstract 字符指令,如 'train'
     * @param $concrete 用於實例化組件的回調函數,如 function() { return new Train(); }
     */
    public function bind($abstract, $concrete) {
        if ($concrete instanceof Closure) {
            // 向容器中添加能夠執行的回調函數
            $this->binds[$abstract] = $concrete;
        } else {
            $this->instances[$abstract] = $concrete;
        }
    }

    /**
     * 生產:執行回調函數
     *
     * @param $abstract     字符指令
     * @param array $params 回調函數所需參數
     * @return mixed        回調函數的返回值
     */
    public function make($abstract, $params = []) {
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        // 此時 $this 是有 2 個元素的數組
        // Array (
        //     [0] => Container Object (
        //                [binds] => Array ( ... )
        //                [instances] => Array()
        //            )
        //     [1] => "train"
        // )
        array_unshift($params, $this);

        // 將參數傳遞給回調函數
        return call_user_func_array($this->binds[$abstract], $params);
    }
}

$container = new Container();
$container->bind('traveler', function ($container, $trafficTool) {
    return new Traveler($container->make($trafficTool));
});

$container->bind('train', function ($container) {
    return new Train();
});

$container->bind('plane', function ($container) {
    return new Plane();
});

$me = $container->make('traveler', ['train']);
$me->travel();

運行:

$ php advanced_ioc.php
[Travel By]: train

簡化並解耦後的代碼

那三我的再出去玩,代碼將簡化爲:

$a = $container->make('traveler', ['car']);
$b = $container->make('traveler', ['train']);
$c = $container->make('traveler', ['plane']);

$a->travel();
$b->travel();
$c->travel();

更多參考:神奇的服務容器

Laravel 的服務容器

Laravel 本身的服務容器是一個更加高級的 IOC 容器,它的簡化代碼以下:

<?php
# laravel_ioc.php    
...
    

class Container
{
    // 綁定回調函數
    public $binds = [];

    // 綁定接口 $abstract 與回調函數
    public function bind($abstract, $concrete = null, $shared = false) {
        if (!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }
        $this->binds[$abstract] = compact('concrete', 'shared');
    }

    // 獲取回調函數
    public function getClosure($abstract, $concrete) {
        return function ($container) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';
            return $container->$method($concrete);
        };
    }

    protected function getConcrete($abstract) {
        if (!isset($this->binds[$abstract])) {
            return $abstract;
        }
        return $this->binds[$abstract]['concrete'];
    }


    // 生成實例對象
    public function make($abstract) {
        $concrete = $this->getConcrete($abstract);
        if ($this->isBuildable($abstract, $concrete)) {
            $obj = $this->build($concrete);
        } else {
            $obj = $this->make($concrete);
        }
        return $obj;
    }


    // 判斷是否要用反射來實例化
    protected function isBuildable($abstract, $concrete) {
        return $concrete == $abstract || $concrete instanceof Closure;
    }

    // 經過反射來實例化 $concrete 的對象
    public function build($concrete) {
        if ($concrete instanceof Closure) {
            return $concrete($this);
        }
        $reflector = new ReflectionClass($concrete);
        if (!$reflector->isInstantiable()) {
            echo "[can't instantiable]: " . $concrete;
        }

        $constructor = $reflector->getConstructor();
        // 使用默認的構造函數
        if (is_null($constructor)) {
            return new $concrete;
        }

        $refParams = $constructor->getParameters();
        $instances = $this->getDependencies($refParams);
        return $reflector->newInstanceArgs($instances);
    }


    // 獲取實例化對象時所需的參數
    public function getDependencies($refParams) {
        $deps = [];
        foreach ($refParams as $refParam) {
            $dep = $refParam->getClass();
            if (is_null($dep)) {
                $deps[] = null;
            } else {
                $deps[] = $this->resolveClass($refParam);
            }
        }
        return (array)$deps;
    }

    // 獲取參數的類型類名字
    public function resolveClass(ReflectionParameter $refParam) {
        return $this->make($refParam->getClass()->name);
    }
}


$container = new Container();

// 將 traveller 對接到 Train 
$container->bind('TrafficTool', 'Train');
$container->bind('traveller', 'Traveller');

// 建立 traveller 實例
$me = $container->make('traveller');
$me->travel();

運行:

$ php laravel_ioc.php     
[Travel By]: train

Train 類要能被實例化,須要先註冊到容器,這就涉及到 Laravel 中服務提供者(Service Provider)的概念了。至於服務提供者是怎麼註冊類、註冊以後如何實例化、實例化後如何調用的... 下節詳細分析。

總結

本文用一個旅遊出行的 demo,引出了高度耦合的直接實現、工廠模式解耦和 IOC 模式解耦共計三種實現方式,越日後代碼量越多還有些繞,但類(模塊)之間的耦合度愈來愈低,最後實現了簡化版的 Laravel 服務容器。

Laravel 的優美得益於開發的組件式解耦,這與服務容器和服務提供者的理念是離不開的,下篇將用 Laravel 框架 laravel/framework/src/Illuminate/Container.phpContainer 類來梳理 Laravel 服務容器的工做流程。

相關文章
相關標籤/搜索