前言
好的設(shè)計(jì)會提高程序的可復(fù)用性和可維護(hù)性,也間接的提高了開發(fā)人員的生產(chǎn)力。今天,我們就來說一下在很多框架中都使用的依賴注入引有。
一些概念
要搞清楚什么是依賴注入如何依賴注入,首先我們要明確一些概念倦逐。
DIP (Dependence Inversion Principle) 依賴倒置原則:
程序要依賴于抽象接口譬正,不要依賴于具體實(shí)現(xiàn)。
IOC (Inversion of Control) 控制反轉(zhuǎn):
遵循依賴倒置原則的一種代碼設(shè)計(jì)方案檬姥,依賴的創(chuàng)建 (控制) 由主動變?yōu)楸粍?(反轉(zhuǎn))曾我。
DI (Dependency Injection) 依賴注入:
控制反轉(zhuǎn)的一種具體實(shí)現(xiàn)方法。通過參數(shù)的方式從外部傳入依賴健民,將依賴的創(chuàng)建由主動變?yōu)楸粍?(實(shí)現(xiàn)了控制反轉(zhuǎn))抒巢。
光說理論有點(diǎn)不好理解,我們用代碼舉個例子秉犹。
首先蛉谜,我們看依賴沒有倒置時的一段代碼:
class Controller
{
protected $service;
public function __construct()
{
// 主動創(chuàng)建依賴
$this->service = new Service(12, 13);
}
}
class Service
{
protected $model;
protected $count;
public function __construct($param1, $param2)
{
$this->count = $param1 + $param2;
// 主動創(chuàng)建依賴
$this->model = new Model('test_table');
}
}
class Model
{
protected $table;
public function __construct($table)
{
$this->table = $table;
}
}
$controller = new Controller;
上述代碼的依賴關(guān)系是 Controller 依賴 Service,Service 依賴 Model凤优。從控制的角度來看悦陋,Controller 主動創(chuàng)建依賴 Service蜈彼,Service 主動創(chuàng)建依賴 Model筑辨。依賴是由需求方內(nèi)部產(chǎn)生的,需求方需要關(guān)心依賴的具體實(shí)現(xiàn)幸逆。這樣的設(shè)計(jì)使代碼耦合性變高棍辕,每次底層發(fā)生改變(如參數(shù)變動),頂層就必須修改代碼还绘。
接下來楚昭,我們使用依賴注入實(shí)現(xiàn)控制反轉(zhuǎn),使依賴關(guān)系倒置:
class Controller
{
protected $service;
// 依賴被動傳入拍顷。申明要 Service 類的實(shí)例 (抽象接口)
public function __construct(Service $service)
{
$this->service = $service;
}
}
class Service
{
protected $model;
protected $count;
// 依賴被動傳入
public function __construct(Model $model, $param1, $param2)
{
$this->count = $param1 + $param2;
$this->model = $model;
}
}
class Model
{
protected $table;
public function __construct($table)
{
$this->table = $table;
}
}
$model = new Model('test_table');
$service = new Service($model, 12, 13);
$controller = new Controller($service);
將依賴通過參數(shù)的方式從外部傳入(即依賴注入)抚太,控制的角度上依賴的產(chǎn)生從主動創(chuàng)建變?yōu)楸粍幼⑷耄蕾囮P(guān)系變?yōu)榱艘蕾囉诔橄蠼涌诙灰蕾囉诰唧w實(shí)現(xiàn)昔案。此時的代碼得到了解耦尿贫,提高了可維護(hù)性。
從單元測試的角度看踏揣,依賴注入更方便 stub 和 mock 操作庆亡,方便了測試人員寫出質(zhì)量更高的測試代碼。
如何依賴注入捞稿,自動注入依賴
有了上面的一些理論基礎(chǔ)又谋,我們大致了解了依賴注入是什么拼缝,能干什么。
不過雖然上面的代碼可以進(jìn)行依賴注入了彰亥,但是依賴還是需要手動創(chuàng)建咧七。我們可不可以創(chuàng)建一個工廠類,用來幫我們進(jìn)行自動依賴注入呢剩愧?OK猪叙,我們需要一個 IOC 容器。
實(shí)現(xiàn)一個簡單的 IOC 容器
依賴注入是以構(gòu)造函數(shù)參數(shù)的形式傳入的仁卷,想要自動注入:
- 我們需要知道需求方需要哪些依賴穴翩,使用反射來獲得
- 只有類的實(shí)例會被注入,其它參數(shù)不受影響
如何自動進(jìn)行注入呢锦积?當(dāng)然是 PHP 自帶的反射功能芒帕!
注:關(guān)于反射是否影響性能,答案是肯定的丰介。但是相比數(shù)據(jù)庫連接背蟆、網(wǎng)絡(luò)請求的時延,反射帶來的性能問題在絕大多數(shù)情況下并不會成為應(yīng)用的性能瓶頸哮幢。
1.雛形
首先带膀,創(chuàng)建 Container 類,getInstance 方法:
class Container
{
public static function getInstance($class_name, $params = [])
{
// 獲取反射實(shí)例
$reflector = new ReflectionClass($class_name);
// 獲取反射實(shí)例的構(gòu)造方法
$constructor = $reflector->getConstructor();
// 獲取反射實(shí)例構(gòu)造方法的形參
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
if ($class) { // 如果參數(shù)是一個類橙垢,創(chuàng)建實(shí)例
$di_params[] = new $class->name;
}
}
}
$di_params = array_merge($di_params, $params);
// 創(chuàng)建實(shí)例
return $reflector->newInstanceArgs($di_params);
}
}
這里我們獲取構(gòu)造方法參數(shù)時用到了 ReflectionClass 類垛叨,大家可以到官方文檔了解一下該類包含的方法和用法,這里就不再贅述柜某。
ok嗽元,有了 getInstance 方法,我們可以試一下自動注入依賴了:
class A
{
public $count = 100;
}
class B
{
protected $count = 1;
public function __construct(A $a, $count)
{
$this->count = $a->count + $count;
}
public function getCount()
{
return $this->count;
}
}
$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 110
2.進(jìn)階
雖然上面的代碼可以進(jìn)行自動依賴注入了喂击,但是問題是只能構(gòu)注入一層剂癌。如果 A 類也有依賴怎么辦呢?
ok翰绊,我們需要修改一下代碼:
class Container
{
public static function getInstance($class_name, $params = [])
{
// 獲取反射實(shí)例
$reflector = new ReflectionClass($class_name);
// 獲取反射實(shí)例的構(gòu)造方法
$constructor = $reflector->getConstructor();
// 獲取反射實(shí)例構(gòu)造方法的形參
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
if ($class) { // 如果參數(shù)是一個類佩谷,創(chuàng)建實(shí)例,并對實(shí)例進(jìn)行依賴注入
$di_params[] = self::getInstance($class->name);
}
}
}
$di_params = array_merge($di_params, $params);
// 創(chuàng)建實(shí)例
return $reflector->newInstanceArgs($di_params);
}
}
測試一下:
class C
{
public $count = 20;
}
class A
{
public $count = 100;
public function __construct(C $c)
{
$this->count += $c->count;
}
}
class B
{
protected $count = 1;
public function __construct(A $a, $count)
{
$this->count = $a->count + $count;
}
public function getCount()
{
return $this->count;
}
}
$b = Container::getInstance(B::class, [10]);
var_dump($b->getCount()); // result is 130
上述代碼使用遞歸完成了多層依賴的注入關(guān)系监嗜,程序中依賴關(guān)系層級一般不會特別深谐檀,遞歸不會造成內(nèi)存遺漏問題。
3.單例
有些類會貫穿在程序生命周期中被頻繁使用秤茅,為了在依賴注入中避免不停的產(chǎn)生新的實(shí)例稚补,我們需要 IOC 容器支持單例模式,已經(jīng)是單例的依賴可以直接獲取框喳,節(jié)省資源课幕。
為 Container 增加單例相關(guān)方法:
class Container
{
protected static $_singleton = [];
// 添加一個實(shí)例到單例
public static function singleton($instance)
{
if ( ! is_object($instance)) {
throw new InvalidArgumentException("Object need!");
}
$class_name = get_class($instance);
// singleton not exist, create
if ( ! array_key_exists($class_name, self::$_singleton)) {
self::$_singleton[$class_name] = $instance;
}
}
// 獲取一個單例實(shí)例
public static function getSingleton($class_name)
{
return array_key_exists($class_name, self::$_singleton) ?
self::$_singleton[$class_name] : NULL;
}
// 銷毀一個單例實(shí)例
public static function unsetSingleton($class_name)
{
self::$_singleton[$class_name] = NULL;
}
}
改造 getInstance 方法:
public static function getInstance($class_name, $params = [])
{
// 獲取反射實(shí)例
$reflector = new ReflectionClass($class_name);
// 獲取反射實(shí)例的構(gòu)造方法
$constructor = $reflector->getConstructor();
// 獲取反射實(shí)例構(gòu)造方法的形參
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
if ($class) {
// 如果依賴是單例厦坛,則直接獲取
$singleton = self::getSingleton($class->name);
$di_params[] = $singleton ? $singleton : self::getInstance($class->name);
}
}
}
$di_params = array_merge($di_params, $params);
// 創(chuàng)建實(shí)例
return $reflector->newInstanceArgs($di_params);
}
4.以依賴注入的方式運(yùn)行方法
類之間的依賴注入解決了,我們還需要一個以依賴注入的方式運(yùn)行方法的功能乍惊,可以注入任意方法的依賴杜秸。這個功能在實(shí)現(xiàn)路由分發(fā)到控制器方法時很有用。
增加 run 方法
public static function run($class_name, $method, $params = [], $construct_params = [])
{
if ( ! class_exists($class_name)) {
throw new BadMethodCallException("Class $class_name is not found!");
}
if ( ! method_exists($class_name, $method)) {
throw new BadMethodCallException("undefined method $method in $class_name !");
}
// 獲取實(shí)例
$instance = self::getInstance($class_name, $construct_params);
// 獲取反射實(shí)例
$reflector = new ReflectionClass($class_name);
// 獲取方法
$reflectorMethod = $reflector->getMethod($method);
// 查找方法的參數(shù)
$di_params = [];
foreach ($reflectorMethod->getParameters() as $param) {
$class = $param->getClass();
if ($class) {
$singleton = self::getSingleton($class->name);
$di_params[] = $singleton ? $singleton : self::getInstance($class->name);
}
}
// 運(yùn)行方法
return call_user_func_array([$instance, $method], array_merge($di_params, $params));
}
測試:
class A
{
public $count = 10;
}
class B
{
public function getCount(A $a, $count)
{
return $a->count + $count;
}
}
$result = Container::run(B::class, 'getCount', [10]);
var_dump($result); // result is 20
ok润绎,一個簡單好用的 IOC 容器完成了撬碟,動手試試吧!
完整代碼
IOC Container 的完整代碼請見 wazsmwazsm/IOCContainer莉撇, 原先是在我的框架 wazsmwazsm/WorkerA 中使用呢蛤,現(xiàn)在已經(jīng)作為單獨(dú)的項(xiàng)目,有完善的單元測試棍郎,可以使用到生產(chǎn)環(huán)境其障。