本文鏈接地址: 理解PHP 依賴注入|Laravel IoC容器
好吧,標題黨了,本文非原創啊!
看Laravel的IoC容器文檔只是介紹實例,但是沒有說原理,之前用MVC框架都沒有在意這個概念,無意中在phalcon的文檔中看到這個詳細的介紹,感覺豁然開朗,複製粘貼過來,主要是好久沒有寫東西了,現在確實很懶變得!
首先,我們假設,我們要開發一個組件命名爲SomeComponent。這個組件中現在將要注入一個數據庫連接。
在這個例子中,數據庫連接在component中被創建,這種方法是不切實際的,這樣做的話,我們將不能改變數據庫連接參數及數據庫類型等一些參數。
01 |
<?php |
02 |
03 |
class
SomeComponent |
04 |
{ |
05 |
06 |
/** |
07 |
*
The instantiation of the connection is hardcoded inside |
08 |
*
the component so is difficult to replace it externally |
09 |
*
or change its behavior |
10 |
*/ |
11 |
public
function
someDbTask() |
12 |
{ |
13 |
$connection
= new
Connection( array ( |
14 |
"host"
=> "localhost" , |
15 |
"username"
=> "root" , |
16 |
"password"
=> "secret" , |
17 |
"dbname"
=> "invo" |
18 |
)); |
19 |
20 |
//
... |
21 |
} |
22 |
23 |
} |
24 |
25 |
$some
= new
SomeComponent(); |
26 |
$some ->someDbTask(); |
爲了解決上面所說的問題,我們需要在使用前創建一個外部連接,並注入到容器中。就目前而言,這看起來是一個很好的解決方案:
01 |
<?php |
02 |
03 |
class
SomeComponent |
04 |
{ |
05 |
06 |
protected
$_connection ; |
07 |
08 |
/** |
09 |
*
Sets the connection externally |
10 |
*/ |
11 |
public
function
setConnection( $connection ) |
12 |
{ |
13 |
$this ->_connection
= $connection ; |
14 |
} |
15 |
16 |
public
function
someDbTask() |
17 |
{ |
18 |
$connection
= $this ->_connection; |
19 |
20 |
//
... |
21 |
} |
22 |
23 |
} |
24 |
25 |
$some
= new
SomeComponent(); |
26 |
27 |
//Create
the connection |
28 |
$connection
= new
Connection( array ( |
29 |
"host"
=> "localhost" , |
30 |
"username"
=> "root" , |
31 |
"password"
=> "secret" , |
32 |
"dbname"
=> "invo" |
33 |
)); |
34 |
35 |
//Inject
the connection in the component |
36 |
$some ->setConnection( $connection ); |
37 |
38 |
$some ->someDbTask(); |
現在我們來考慮一個問題,我們在應用程序中的不同地方使用此組件,將多次創建數據庫連接。使用一種類似全局註冊表的方式,從這獲得一個數據庫連接實例,而不是使用一次就創建一次。
01 |
<?php |
02 |
03 |
class
Registry |
04 |
{ |
05 |
06 |
/** |
07 |
*
Returns the connection |
08 |
*/ |
09 |
public
static
function
getConnection() |
10 |
{ |
11 |
return
new
Connection( array ( |
12 |
"host"
=> "localhost" , |
13 |
"username"
=> "root" , |
14 |
"password"
=> "secret" , |
15 |
"dbname"
=> "invo" |
16 |
)); |
17 |
} |
18 |
19 |
} |
20 |
21 |
class
SomeComponent |
22 |
{ |
23 |
24 |
protected
$_connection ; |
25 |
26 |
/** |
27 |
*
Sets the connection externally |
28 |
*/ |
29 |
public
function
setConnection( $connection ){ |
30 |
$this ->_connection
= $connection ; |
31 |
} |
32 |
33 |
public
function
someDbTask() |
34 |
{ |
35 |
$connection
= $this ->_connection; |
36 |
37 |
//
... |
38 |
} |
39 |
40 |
} |
41 |
42 |
$some
= new
SomeComponent(); |
43 |
44 |
//Pass
the connection defined in the registry |
45 |
$some ->setConnection(Registry::getConnection()); |
46 |
47 |
$some ->someDbTask(); |
現在,讓我們來想像一下,我們必須在組件中實現兩個方法,首先需要創建一個新的數據庫連接,第二個總是獲得一個共享連接:
01 |
<?php |
02 |
03 |
class
Registry |
04 |
{ |
05 |
06 |
protected
static
$_connection ; |
07 |
08 |
/** |
09 |
*
Creates a connection |
10 |
*/ |
11 |
protected
static
function
_createConnection() |
12 |
{ |
13 |
return
new
Connection( array ( |
14 |
"host"
=> "localhost" , |
15 |
"username"
=> "root" , |
16 |
"password"
=> "secret" , |
17 |
"dbname"
=> "invo" |
18 |
)); |
19 |
} |
20 |
21 |
/** |
22 |
*
Creates a connection only once and returns it |
23 |
*/ |
24 |
public
static
function
getSharedConnection() |
25 |
{ |
26 |
if
(self:: $_connection ===null){ |
27 |
$connection
= self::_createConnection(); |
28 |
self:: $_connection
= $connection ; |
29 |
} |
30 |
return
self:: $_connection ; |
31 |
} |
32 |
33 |
/** |
34 |
*
Always returns a new connection |
35 |
*/ |
36 |
public
static
function
getNewConnection() |
37 |
{ |
38 |
return
self::_createConnection(); |
39 |
} |
40 |
41 |
} |
42 |
43 |
class
SomeComponent |
44 |
{ |
45 |
46 |
protected
$_connection ; |
47 |
48 |
/** |
49 |
*
Sets the connection externally |
50 |
*/ |
51 |
public
function
setConnection( $connection ){ |
52 |
$this ->_connection
= $connection ; |
53 |
} |
54 |
55 |
/** |
56 |
*
This method always needs the shared connection |
57 |
*/ |
58 |
public
function
someDbTask() |
59 |
{ |
60 |
$connection
= $this ->_connection; |
61 |
62 |
//
... |
63 |
} |
64 |
65 |
/** |
66 |
*
This method always needs a new connection |
67 |
*/ |
68 |
public
function
someOtherDbTask( $connection ) |
69 |
{ |
70 |
71 |
} |
72 |
73 |
} |
74 |
75 |
$some
= new
SomeComponent(); |
76 |
77 |
//This
injects the shared connection |
78 |
$some ->setConnection(Registry::getSharedConnection()); |
79 |
80 |
$some ->someDbTask(); |
81 |
82 |
//Here,
we always pass a new connection as parameter |
83 |
$some ->someOtherDbTask(Registry::getConnection()); |
到此爲止,我們已經看到了如何使用依賴注入解決我們的問題。不是在代碼內部創建依賴關係,而是讓其作爲一個參數傳遞,這使得我們的程序更容易維護,降低程序代碼的耦合度,實現一種鬆耦合。但是從長遠來看,這種形式的依賴注入也有一些缺點。
例如,如果組件中有較多的依賴關係,我們需要創建多個setter方法傳遞,或創建構造函數進行傳遞。另外,每次使用組件時,都需要創建依賴組件,使代碼維護不太易,我們編寫的代碼可能像這樣:
01 |
<?php |
02 |
03 |
//Create
the dependencies or retrieve them from the registry |
04 |
$connection
= new
Connection(); |
05 |
$session
= new
Session(); |
06 |
$fileSystem
= new
FileSystem(); |
07 |
$filter
= new
Filter(); |
08 |
$selector
= new
Selector(); |
09 |
10 |
//Pass
them as constructor parameters |
11 |
$some
= new
SomeComponent( $connection ,
$session ,
$fileSystem ,
$filter ,
$selector ); |
12 |
13 |
//
... or using setters |
14 |
15 |
$some ->setConnection( $connection ); |
16 |
$some ->setSession( $session ); |
17 |
$some ->setFileSystem( $fileSystem ); |
18 |
$some ->setFilter( $filter ); |
19 |
$some ->setSelector( $selector ); |
我想,我們不得不在應用程序的許多地方創建這個對象。如果你不需要依賴的組件後,我們又要去代碼注入部分移除構造函數中的參數或者是setter方法。爲了解決這個問題,我們再次返回去使用一個全局註冊表來創建組件。但是,在創建對象之前,它增加了一個新的抽象層:
01 |
<?php |
02 |
03 |
class
SomeComponent |
04 |
{ |
05 |
06 |
//
... |
07 |
08 |
/** |
09 |
*
Define a factory method to create SomeComponent instances injecting its dependencies |
10 |
*/ |
11 |
public
static
function
factory() |
12 |
{ |
13 |
14 |
$connection
= new
Connection(); |
15 |
$session
= new
Session(); |
16 |
$fileSystem
= new
FileSystem(); |
17 |
$filter
= new
Filter(); |
18 |
$selector
= new
Selector(); |
19 |
20 |
return
new
self( $connection ,
$session ,
$fileSystem ,
$filter ,
$selector ); |
21 |
} |
22 |
23 |
} |
這一刻,我們好像回到了問題的開始,我們正在創建組件內部的依賴,我們每次都在修改以及找尋一種解決問題的辦法,但這都不是很好的做法。
一種實用和優雅的來解決這些問題,是使用容器的依賴注入,像我們在前面看到的,容器作爲全局註冊表,使用容器的依賴注入做爲一種橋樑來解決依賴可以使我們的代碼耦合度更低,很好的降低了組件的複雜性:
01 |
<?php |
02 |
03 |
class
SomeComponent |
04 |
{ |
05 |
06 |
protected
$_di ; |
07 |
08 |
public
function
__construct( $di ) |
09 |
{ |
10 |
$this ->_di
= $di ; |
11 |
} |
12 |
13 |
public
function
someDbTask() |
14 |
{ |
15 |
16 |
//
Get the connection service |
17 |
//
Always returns a new connection |
18 |
$connection
= $this ->_di->get( 'db' ); |
19 |
20 |
} |
21 |
22 |
public
function
someOtherDbTask() |
23 |
{ |
24 |
25 |
//
Get a shared connection service, |
26 |
//
this will return the same connection everytime |
27 |
$connection
= $this ->_di->getShared( 'db' ); |
28 |
29 |
//This
method also requires a input filtering service |
30 |
$filter
= $this ->_db->get( 'filter' ); |
31 |
32 |
} |
33 |
34 |
} |
35 |
36 |
$di
= new
Phalcon\DI(); |
37 |
38 |
//Register
a "db" service in the container |
39 |
$di ->set( 'db' ,
function (){ |
40 |
return
new
Connection( array ( |
41 |
"host"
=> "localhost" , |
42 |
"username"
=> "root" , |
43 |
"password"
=> "secret" , |
44 |
"dbname"
=> "invo" |
45 |
)); |
46 |
}); |
47 |
48 |
//Register
a "filter" service in the container |
49 |
$di ->set( 'filter' ,
function (){ |
50 |
return
new
Filter(); |
51 |
}); |
52 |
53 |
//Register
a "session" service in the container |
54 |
$di ->set( 'session' ,
function (){ |
55 |
return
new
Session(); |
56 |
}); |
57 |
58 |
//Pass
the service container as unique parameter |
59 |
$some
= new
SomeComponent( $di ); |
60 |
61 |
$some ->someTask(); |
現在,該組件只有訪問某種service的時候才需要它,如果它不需要,它甚至不初始化,以節約資源。該組件是高度解耦。他們的行爲,或者說他們的任何其他方面都不會影響到組件本身。
我們的實現辦法¶
Phalcon\DI 是一個實現了服務的依賴注入功能的組件,它本身也是一個容器。
由於Phalcon高度解耦,Phalcon\DI 是框架用來集成其他組件的必不可少的部分,開發人員也可以使用這個組件依賴注入和管理應用程序中不同類文件的實例。
基本上,這個組件實現了 Inversion of Control 模式。基於此,對象不再以構造函數接收參數或者使用setter的方式來實現注入,而是直接請求服務的依賴注入。這就大大降低了整體程序的複雜性,因爲只有一個方法用以獲得所需要的一個組件的依賴關係。
此外,這種模式增強了代碼的可測試性,從而使它不容易出錯。
在容器中註冊服務¶
框架本身或開發人員都可以註冊服務。當一個組件A要求調用組件B(或它的類的一個實例),可以從容器中請求調用組件B,而不是創建組件B的一個實例。
這種工作方式爲我們提供了許多優點:
我們可以更換一個組件,從他們本身或者第三方輕鬆創建。
在組件發佈之前,我們可以充分的控制對象的初始化,並對對象進行各種設置。
我們可以使用統一的方式從組件得到一個結構化的全局實例
服務可以通過以下幾種方式注入到容器:
01 |
<?php |
02 |
03 |
//Create
the Dependency Injector Container |
04 |
$di
= new
Phalcon\DI(); |
05 |
06 |
//By
its class name |
07 |
$di ->set( "request" ,
'Phalcon\Http\Request' ); |
08 |
09 |
//Using
an anonymous function, the instance will lazy loaded |
10 |
$di ->set( "request" ,
function (){ |
11 |
return
new
Phalcon\Http\Request(); |
12 |
}); |
13 |
14 |
//Registering
directly an instance |
15 |
$di ->set( "request" ,
new
Phalcon\Http\Request()); |
16 |
17 |
//Using
an array definition |
18 |
$di ->set( "request" ,
array ( |
19 |
"className"
=> 'Phalcon\Http\Request' |
20 |
)); |
在上面的例子中,當向框架請求訪問一個請求數據時,它將首先確定容器中是否存在這個”reqeust”名稱的服務。
容器會反回一個請求數據的實例,開發人員最終得到他們想要的組件。
在上面示例中的每一種方法都有優缺點,具體使用哪一種,由開發過程中的特定場景來決定的。
用一個字符串來設定一個服務非常簡單,但缺少靈活性。設置服務時,使用數組則提供了更多的靈活性,而且可以使用較複雜的代碼。lambda函數是兩者之間一個很好的平衡,但也可能導致更多的維護管理成本。
Phalcon\DI 提供服務的延遲加載。除非開發人員在注入服務的時候直接實例化一個對象,然後存存儲到容器中。在容器中,通過數組,字符串等方式存儲的服務都將被延遲加載,即只有在請求對象的時候才被初始化。
01 |
<?php |
02 |
03 |
//Register
a service "db" with a class name and its parameters |
04 |
$di ->set( "db" ,
array ( |
05 |
"className"
=> "Phalcon\Db\Adapter\Pdo\Mysql" , |
06 |
"parameters"
=> array ( |
07 |
"parameter"
=> array ( |
08 |
"host"
=> "localhost" , |
09 |
"username"
=> "root" , |
10 |
"password"
=> "secret" , |
11 |
"dbname"
=> "blog" |
12 |
) |
13 |
) |
14 |
)); |
15 |
16 |
//Using
an anonymous function |
17 |
$di ->set( "db" ,
function (){ |
18 |
return
new
Phalcon\Db\Adapter\Pdo\Mysql( array ( |
19 |
"host"
=> "localhost" , |
20 |
"username"
=> "root" , |
21 |
"password"
=> "secret" , |
22 |
"dbname"
=> "blog" |
23 |
)); |
24 |
}); |
以上這兩種服務的註冊方式產生相同的結果。然後,通過數組定義的,在後面需要的時候,你可以修改服務參數:
1 |
<?php |
2 |
3 |
$di ->setParameter( "db" ,
0, array ( |
4 |
"host"
=> "localhost" , |
5 |
"username"
=> "root" , |
6 |
"password"
=> "secret" |
7 |
)); |
從容器中獲得服務的最簡單方式就是使用”get”方法,它將從容器中返回一個新的實例:
1 |
<?php
$request
= $di ->get( "request" ); |
或者通過下面這種魔術方法的形式調用:
1 |
<?php |
2 |
3 |
$request
= $di ->getRequest(); |
4 |
5 |
Phalcon\DI
同時允許服務重用,爲了得到一個已經實例化過的服務,可以使用 getShared() 方法的形式來獲得服務。 |
具體的 Phalcon\Http\Request 請求示例:
1 |
<?php |
2 |
3 |
$request
= $di ->getShared( "request" ); |
參數還可以在請求的時候通過將一個數組參數傳遞給構造函數的方式:
1 |
<?php |
2 |
3 |
$component
= $di ->get( "MyComponent" ,
array ( "some-parameter" ,
"other" )) |