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