前言
好的設計會提高程序的可複用性和可維護性,也間接的提高了開發人員的生產力。今天,我們就來說一下在很多框架中都使用的依賴注入。
一些概念
要搞清楚什麼是依賴注入如何依賴注入,首先我們要明確一些概念。
DIP (Dependence Inversion Principle) 依賴倒置原則:
程序要依賴於抽象接口,不要依賴於具體實現。
IOC (Inversion of Control) 控制反轉:
遵循依賴倒置原則的一種代碼設計方案,依賴的創建 (控制) 由主動變爲被動 (反轉)。
DI (Dependency Injection) 依賴注入:
控制反轉的一種具體實現方法。通過參數的方式從外部傳入依賴,將依賴的創建由主動變爲被動 (實現了控制反轉)。
光說理論有點不好理解,我們用代碼舉個例子。
首先,我們看依賴沒有倒置時的一段代碼:
class Controller
{
protected $service;
public function __construct()
{
// 主動創建依賴
$this->service = new Service(12, 13);
}
}
class Service
{
protected $model;
protected $count;
public function __construct($param1, $param2)
{
$this->count = $param1 + $param2;
// 主動創建依賴
$this->model = new Model('test_table');
}
}
class Model
{
protected $table;
public function __construct($table)
{
$this->table = $table;
}
}
$controller = new Controller;
上述代碼的依賴關係是 Controller 依賴 Service,Service 依賴 Model。從控制的角度來看,Controller 主動創建依賴 Service,Service 主動創建依賴 Model。依賴是由需求方內部產生的,需求方需要關心依賴的具體實現。這樣的設計使代碼耦合性變高,每次底層發生改變(如參數變動),頂層就必須修改代碼。
接下來,我們使用依賴注入實現控制反轉,使依賴關係倒置:
class Controller
{
protected $service;
// 依賴被動傳入。申明要 Service 類的實例 (抽象接口)
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);
將依賴通過參數的方式從外部傳入(即依賴注入),控制的角度上依賴的產生從主動創建變爲被動注入,依賴關係變爲了依賴於抽象接口而不依賴於具體實現。此時的代碼得到了解耦,提高了可維護性。
從單元測試的角度看,依賴注入更方便 stub 和 mock 操作,方便了測試人員寫出質量更高的測試代碼。
如何依賴注入,自動注入依賴
有了上面的一些理論基礎,我們大致瞭解了依賴注入是什麼,能幹什麼。
不過雖然上面的代碼可以進行依賴注入了,但是依賴還是需要手動創建。我們可不可以創建一個工廠類,用來幫我們進行自動依賴注入呢?OK,我們需要一個 IOC 容器。
實現一個簡單的 IOC 容器
依賴注入是以構造函數參數的形式傳入的,想要自動注入:
- 我們需要知道需求方需要哪些依賴,使用反射來獲得
- 只有類的實例會被注入,其它參數不受影響
如何自動進行注入呢?當然是 PHP 自帶的反射功能!
注:關於反射是否影響性能,答案是肯定的。但是相比數據庫連接、網絡請求的時延,反射帶來的性能問題在絕大多數情況下並不會成爲應用的性能瓶頸。
1.雛形
首先,創建 Container 類,getInstance 方法:
class Container
{
public static function getInstance($class_name, $params = [])
{
// 獲取反射實例
$reflector = new ReflectionClass($class_name);
// 獲取反射實例的構造方法
$constructor = $reflector->getConstructor();
// 獲取反射實例構造方法的形參
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
if ($class) { // 如果參數是一個類,創建實例
$di_params[] = new $class->name;
}
}
}
$di_params = array_merge($di_params, $params);
// 創建實例
return $reflector->newInstanceArgs($di_params);
}
}
這裏我們獲取構造方法參數時用到了 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.進階
雖然上面的代碼可以進行自動依賴注入了,但是問題是隻能構注入一層。如果 A 類也有依賴怎麼辦呢?
ok,我們需要修改一下代碼:
class Container
{
public static function getInstance($class_name, $params = [])
{
// 獲取反射實例
$reflector = new ReflectionClass($class_name);
// 獲取反射實例的構造方法
$constructor = $reflector->getConstructor();
// 獲取反射實例構造方法的形參
$di_params = [];
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$class = $param->getClass();
if ($class) { // 如果參數是一個類,創建實例,並對實例進行依賴注入
$di_params[] = self::getInstance($class->name);
}
}
}
$di_params = array_merge($di_params, $params);
// 創建實例
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
上述代碼使用遞歸完成了多層依賴的注入關係,程序中依賴關係層級一般不會特別深,遞歸不會造成內存遺漏問題。
3.單例
有些類會貫穿在程序生命週期中被頻繁使用,爲了在依賴注入中避免不停的產生新的實例,我們需要 IOC 容器支持單例模式,已經是單例的依賴可以直接獲取,節省資源。
爲 Container 增加單例相關方法:
class Container
{
protected static $_singleton = [];
// 添加一個實例到單例
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;
}
}
// 獲取一個單例實例
public static function getSingleton($class_name)
{
return array_key_exists($class_name, self::$_singleton) ?
self::$_singleton[$class_name] : NULL;
}
// 銷燬一個單例實例
public static function unsetSingleton($class_name)
{
self::$_singleton[$class_name] = NULL;
}
}
改造 getInstance 方法:
public static function getInstance($class_name, $params = [])
{
// 獲取反射實例
$reflector = new ReflectionClass($class_name);
// 獲取反射實例的構造方法
$constructor = $reflector->getConstructor();
// 獲取反射實例構造方法的形參
$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);
// 創建實例
return $reflector->newInstanceArgs($di_params);
}
4.以依賴注入的方式運行方法
類之間的依賴注入解決了,我們還需要一個以依賴注入的方式運行方法的功能,可以注入任意方法的依賴。這個功能在實現路由分發到控制器方法時很有用。
增加 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 !");
}
// 獲取實例
$instance = self::getInstance($class_name, $construct_params);
// 獲取反射實例
$reflector = new ReflectionClass($class_name);
// 獲取方法
$reflectorMethod = $reflector->getMethod($method);
// 查找方法的參數
$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);
}
}
// 運行方法
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 中使用,現在已經作爲單獨的項目,有完善的單元測試,可以使用到生產環境。