1. 程式人生 > >laravel 服務容器例項——深入理解IoC模式

laravel 服務容器例項——深入理解IoC模式

剛剛接觸laravel,對於laravel的服務容器不是很理解。看了《Laravel框架關鍵技術解析》和網上的一些資料後對於服務容器有了一些自己的理解,在這裡分享給大家

1、依賴

IoC模式主要是用來解決系統元件之間相互依賴關係的一種模式。那麼什麼是依賴呢?下面給出依賴的例項

<?php

//設計公共介面
interface Go_To_School
{
    public function go();
}

//實現交通工具類
class Foot implements Go_To_School
{
    public function go()
    {
        echo 'walt to school';
    }
}

//設計學生類,實現去學校時要依賴的交通工具例項
class Student
{
    private $trafficTool;
    public function __construct()
    {
        //產生依賴
        $this->trafficTool = new Foot();
    }

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

$student = new Student();
$student->go_to_school();
這裡要實現的功能是學生去學校,當建立了一個學生例項的時候,同時也建立了一個交通工具的例項。可以看到學生和交通工具之間不可避免的產生了一個依賴。在程式中依賴可以理解為一個物件實現某個功能時需要其他物件相關功能的支援。

當然交通工具還有很多,接著完善程式碼

<?php

//設計公共介面
interface Go_To_School
{
    public function go();
}

//實現不同交通工具類
class Foot implements Go_To_School
{
    public function go()
    {
        echo 'walt to school';
    }
}

class Car implements Go_To_School
{
    public function go()
    {
        echo 'drive to school';
    }
}

class Bicycle implements Go_To_School
{
    public function go()
    {
        echo 'ride to school;';
    }
}

/*
 * 少量的依賴並不會有太過直觀的影響,我們隨著這個例子逐漸鋪開,讓大家慢慢意識到,當依賴達到一個量級時,是怎樣一番噩夢般的體驗
 */
class more implements Go_To_School
{
    public function go()
    {
        //...................
    }
}
/*
 *
 *
 *
 *
 * more and more
 *
 *
 *
 *
 *
 */

//設計學生類,實現去學校時要依賴的交通工具例項
class Student
{
    private $trafficTool;
    public function __construct()
    {
        //產生依賴
        $this->trafficTool = new Foot();
        //$this->trafficTool = new Car();
        //$this->trafficTool = new Bicycle();
        /*
         *
         *
         *
         *
         * more and more
         *
         *
         *
         *
         */
    }

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

$student = new Student();
$student->go_to_school();
是不是很恐怖!當用new關鍵字在一個元件內部例項化一個物件時就解決了一個依賴,但同時也引入了另一個嚴重的問題——耦合。在簡單情況下可能看不出耦合有多大問題,但是如果需求改變,如需要例項化的交通工具是自行車、汽車甚至是在設計中還不太清楚的工具時,就需要改變例項化的物件,如果又有很多地方用到了這段程式碼,而這個程式又不是你寫的,這時你面對的將是噩夢。所以,這裡我們不應該在學生內部固化’交通工具‘的初始化行為,而轉由外部負責

2、簡單工廠模式

原理:由一個工廠類根據傳入的引數(一般是字串引數),動態決定應該建立哪一個產品子類(這 些產品子類繼承自同一個父類或介面)的例項,並以父類形式返回。

適用情況:所有的產品子類都有同一個父類(或介面),屬於同一個產品系列   產品子類比較少的、建立操作比較簡單 。

在前面的例項中我們知道,交通工具的例項化過程是經常需要改變的,所以我們將這部分提取到外部來管理,這也就體現了面向物件設計的一個原則,及找出程式中會變化的方面然後將其和固定不變的方面相分離。一種簡單的實現方案是利用工廠模式,這裡我們用簡單工廠模式實現。例項如下:

class TrafficToolFactory
{
    public function createTrafficTool($name)
    {
        switch ($name){
            case 'Foot':
                return new Foot();
                break;
            case 'Car':
                return new Car();
                break;
            case 'Bicycle':
                return new Bicycle();
                break;
            default:
                exit('set trafficTool error!');
                break;
        }
    }
}

class Student
{
    private $trafficTool;
    public function __construct($trafficTool)
    {
        //通過工廠產生依賴的交通工具例項
        $factory = new TrafficToolFactory();
        $this->trafficTool = $factory->createTrafficTool($trafficTool);
    }
    public function go_to_school(){
        $this->trafficTool->go();
    }
}

$student = new Student('Car');
//$student2 = new Student('Foot');
//$student3 = new Student('Bicycle');
$student->go_to_school();
這裡我們添加了“交通工具”工廠,在學生例項化的過程中指定需要的交通工具,則工廠生產相應的交通工具例項。在一些簡單的情況下,簡單工廠模式可以解決這個問題。我們看到學生和交通工具之間的依賴關係沒有了,但是卻變成學生和交通工具工廠之間的依賴。當需求增加時,我們需要修改簡單工廠,如果依賴增多,工廠將十分龐大,依然不利於維護。下一步就是我們今天的主要配角 —— DI 
3、DI(Dependency Injection 依賴注入)

本文中提到的一系列依賴,只要不是由內部產生(比如初始化、建構函式 __construct 中通過工廠方法、自行手動 new 的),而是由外部以引數或其他形式注入的,都屬於依賴注入(DependencyInjection簡稱DI) 。例項如下:

class Student
{
    private $trafficTool;
    public function __construct(Go_To_School $trafficTool)
    {
        $this->trafficTool = $trafficTool;
    }
    public function go_to_school(){
        $this->trafficTool->go();
    }
}

$car = new Car();
$student = new Student($car);
$student->go_to_school();
現在初始化學生類時提供的引數必須是Go_To_School介面類的一個例項,即通過依賴注入的方式解決依賴問題,否則就會提示出錯。這裡要注意,依賴注入要以介面的形式進行限制,不能隨意開放

4、IoC容器(Inversion of Control 控制反轉)

IoC是將設計好的類交給系統去控制,而不是在類內部控制。這稱為控制反轉。上例中我們是通過手動注入依賴,而IoC容器實現了依賴的自動注入。下面給出一個簡化版的例子。裡面涉及到反射機制,網上有很多關於反射機制的講解,讀者可以自行查詢,這裡就不細說了。

class Container {

    //用於裝提供例項的回撥函式,真正的容器還會裝例項等其他內容
    //從而實現單例等高階功能
    protected $bindings = [];

    //繫結介面和生成相應例項的回撥函式
    public function bind($abstract, $concrete=null, $shared=false) {

        //如果提供的引數不是回撥函式,則產生預設的回撥函式
        if(!$concrete instanceof Closure) {
            $concrete = $this->getClosure($abstract, $concrete);
        }

        $this->bindings[$abstract] = compact('concrete', 'shared');
    }

    //預設生成例項的回撥函式
    protected function getClosure($abstract, $concrete) {

        return function($container) use ($abstract, $concrete) {
            $method = ($abstract == $concrete) ? 'build' : 'make';
            return $container->$method($concrete);
        };

    }
    //解決介面和要例項化類之間的依賴關係
    public function make($abstract) {

        $concrete = $this->getConcrete($abstract);

        if($this->isBuildable($concrete, $abstract)) {
            $object = $this->build($concrete);
        } else {
            $object = $this->make($concrete);
        }

        return $object;
    }

    protected function isBuildable($concrete, $abstract) {
        return $concrete === $abstract || $concrete instanceof Closure;
    }

    //獲取繫結的回撥函式
    protected function getConcrete($abstract) {
        if(!isset($this->bindings[$abstract])) {
            return $abstract;
        }

        return $this->bindings[$abstract]['concrete'];
    }

    //例項化物件
    public function build($concrete) {

        if($concrete instanceof Closure) {
            return $concrete($this);
        }

        $reflector = new ReflectionClass($concrete);

        if(!$reflector->isInstantiable()) {
            echo $message = "Target [$concrete] is not instantiable";
        }

        $constructor = $reflector->getConstructor();
        if(is_null($constructor)) {
            return new $concrete;
        }

        $dependencies = $constructor->getParameters();
        $instances = $this->getDependencies($dependencies);

        return $reflector->newInstanceArgs($instances);
    }

    //解決通過反射機制例項化物件時的依賴
    protected function getDependencies($parameters) {
        $dependencies = [];
        foreach($parameters as $parameter) {
            $dependency = $parameter->getClass();
            if(is_null($dependency)) {
                $dependencies[] = NULL;
            } else {
                $dependencies[] = $this->resolveClass($parameter);
            }
        }

        return (array)$dependencies;
    }

    protected function resolveClass(ReflectionParameter $parameter) {
        return $this->make($parameter->getClass()->name);
    }

}

這就是簡化版的IoC容器類,使用bind()函式進行服務繫結,使用make()函式來進行解析,最後在容器內由build()函式建立並返回例項。下面是具體如何使用

//例項化IOC容器
$ioc = new Container();

//填充容器
$ioc->bind('Go_To_School', 'Car');	//第一個引數'Go_To_School'是介面,第二個引數'Car'是交通工具類
$ioc->bind('student', 'Student');    //第一個引數'student'可以理解為服務別名,用make()例項化的時候直接使用別名即可,第二個引數'Student'是學生類

//通過容器實現依賴注入,完成類的例項化
$student = $ioc->make('student');
$student->go_to_school();
填充容器的時候也可以直接繫結自定義的回撥函式:
$ioc->bind('Go_To_School', function (){
    return new Car();
});

現在,我們不僅解除了學生類與交通工具類的依賴關係,而且容器類沒有和他們產生任何依賴。我們通過註冊、繫結(bind)的方式向容器中新增一段可以被執行的回撥(可以是匿名函式、非匿名函式、類的方法)作為建立一個類的例項的方法,只有在真正的建立(make)操作被呼叫執行時,才會觸發。這樣一種方式,使得我們更容易在建立一個例項的同時解決其依賴關係,並且更加靈活。當有新的需求,只需另外繫結一個回撥即可。例如現在我們想步行去學校,只要再繫結$ioc->bind('Go_To_School', 'Foot');就可以了。用何種方式去學校,我們可以自由的選擇。

通過上述例子可以看到IOC容器最核心的功能,解決依賴注入的根本問題。在實現過程中,沒有用new關鍵字來例項化物件,不需要人來關注元件之間的依賴關係,只要在容器填充過程中理順介面和實現類之間的關係及實現類與依賴介面之間的關係就可以流水線式的完成實現類的例項化過程。

好了,分享到此就進入尾聲了,如有錯誤,歡迎指正!