說這個話題之前先講一個比較高端的思想--'依賴倒置原則'
"依賴倒置是一種軟件設計思想,在傳統軟件中,上層代碼依賴於下層代碼,當下層代碼有所改動時,上層代碼也要相應進行改動,因此維護成本較高。而依賴倒置原則的思想是,上層不應該依賴下層,應依賴接口。意為上層代碼定義接口,下層代碼實現該接口,從而使得下層依賴於上層接口,降低耦合度,提高系統彈性"
上面的解釋有點虛,下面我們以實際代碼來解釋這個理論
比如有這麼條需求,用戶注冊完成後要發送一封郵件,然後你有如下代碼:
先有郵件類'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
鄙人才疏學淺,有不足之處,歡迎補足!