原文: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 行,Traveler
與 Train
兩個組件發生了耦合。之後想坐飛機出行,必須修改 __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')
便可。適用於需求簡單的狀況。
依舊沒有完全解決依賴:如今 Traveler
與 TrafficToolFactory
發生了依賴。當需求增多後,工廠的 switch...case
等代碼也不易維護。
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 容器
<?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 本身的服務容器是一個更加高級的 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.php
中 Container
類來梳理 Laravel 服務容器的工做流程。