上篇最重要的是介紹了去耦的工具之一設計原則SOLID,本篇將繼續介紹去耦工具:依賴注入。
本文為系列文章的第四篇,前3篇地址是
The Clean Architecture in PHP 讀書筆記(一)
The Clean Architecture in PHP 讀書筆記(二)
The Clean Architecture in PHP 讀書筆記(三)
The Clean Architecture in PHP 讀書筆記(四)
到目前為止,我們在面向對象中遇到的最壞的code是:直接在一個類中實例化出另一個類,然后使用的。看代碼:
class CustomerController {
public function viewAction()
{
$repository = new CustomerRepository();
$customer = $repository->getById( 1001 );
return $customer;
}
}
此處CustomerController
類如果脫離了CustomerRepository
類,將無法正常執行,對CustomerRepository
是強依賴。
這種通過new class直接實例化出類來使用,帶來的問題有:
-
It makes it hard to make changes later
由于依賴于一個具體的實現,具體的東西一般都是易變的,根據SOLID中D(Dependency Inversion Principle)原則,這顯然會導致代碼不易重構
-
It makes it hard to test
由于內部生成使用的類,我們測試的時候無法去除易變量,保證不了測試的時候只有一個可變部分
-
We have no control over dependencies
由于使用new來新建類,我們無法根據條件,選擇性的使用依賴類
控制反轉
首先我們回答第一個問題:控制反轉是什么?
原先我們在類中直接new出我們依賴的類,如前面CustomerController
中直接創建了CustomerRepository
;此時我們為了獲得一定的靈活性,可能通過配置的方式來實例化出需要的Repository
來,簡單的配置方式就是一些if,else
語句,現在我們再進一步,將這些if,else
配置移出CustomerController
類,通過一些外部的手段來達到相同的目的。
而上面我們介紹的這一個過程從類內部到類外部的過程就是所謂的:控制反轉。上面提到的外部的手段主要有兩種:
- 服務定位模式(Service Locator Pattern)
- 依賴注入(dependency injection)
下面先介紹第一個手段:服務定位模式
服務定位模式
先上代碼,有個感性認識
public function viewAction()
{
$repository = $this->serviceLocator->get( 'CustomerRepository' );
$customer = $repository->getById( 1001 );
return $customer;
}
$serviceLocator->setFactory( 'CustomerRepository', function ( $sl ) {
return new Path\To\CustomerRepository(
$sl->get( 'Connection' )
);
} );
此時我們不會在viewAction
內部直接new出CustomerRepository
,而是向serviceLocator
請求,這么做的好處是:
- 收斂了
CustomerRepository
的創建,一旦創建CustomerRepository
需要改變什么,可以只要一處修改就可以了 - 方便測試,我們可以根據測試要求,返回我們需要的
CustomerRepository
,如下面代碼所示:
$serviceLocator->setFactory( 'CustomerRepository', function () {
return new Path\To\MockCustomerRepository(
[
1001 => ( new Customer() )->setName( 'ACME Corp' ),
] );
} );
到這邊是不是已經是做合理的代碼了呢?我們能不能進一步優化呢?答案是:yes!
我們來看下現在的實現還存在的問題:
- 仍然需要自己在需要時候,像
serviceLocator
請求 - 為了測試,我們需要修改
setFactory
的方法,給出測試的實現
那有沒有辦法解決呢,當然有,那就是下面介紹的依賴注入
依賴注入
依賴注入是一個過程:將類它所依賴的外部類由它自己管理變為從外部注入。
下面會介紹兩種注入方法:
- Setter injection
- Constructor injection
使用setter injection
此時前面的代碼會變為:
$controller = new CustomerController();
$controller->setCustomerRepository( new CustomerRepository() );
$customer = $controller->viewAction();
class CustomerController {
protected $repository;
public function setCustomerRepository( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
依賴是通過setter方法注入的。
此時測試的時候,我們只需要通過setter方法設置MockCustomerRepository
即可。
這種方法有一個缺點:如果我們忘記了調用setter方法,那就完蛋了。
$controller = new CustomerController();
$customer = $controller->viewAction();
上面這種代碼,只能等著fatal了。
使用Constructor injection
此時的代碼長這個樣子:
$controller = new CustomerController( new CustomerRepository() );
$customer = $controller->viewAction();
class CustomerController {
protected $repository;
public function __construct( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
我們不會出現之前setter injection那種忘了設計的問題了,因為我們通過構造函數聲明了我們的規則,必須遵守。
講了這么多依賴注入了,那我們什么時候使用呢?
什么時候使用依賴注入
依賴注入解決的是依賴問題,目的是去耦,那自然有耦合的地方就會有依賴注入,主要的場景有下面4個:
-
When the dependency is used by more than one component
當有多個地方都去new同一個類,并且構建需要一些額外動作的時候,我們就要考慮將構建這個動作封裝起來了,核心點是:代碼復用
-
The dependency has different configurations in different contexts
當創建的邏輯變得復雜的時候,我們需要將創建抽取出來,核心點是:單一職責,關注點分離
-
When you need a different configuration to test a component
如果為了測試需要依賴類返回不同的測試數據,這就要將依賴變為注入的,核心點是:可測性
-
When the dependency being injected is part of a third party library
當我們使用的依賴是第三方庫的時候,我們更應該使用依賴注入,核心點是:依賴抽象,不變的
有那么多適合使用依賴注入的場景,那自然會有不適合的地方,如果需要構建的依賴足夠簡單,沒有配置,我們無需引入依賴注入,依賴注入的引入是為了解決問題,而不是為了增加代碼復雜性。
使用工廠來創建依賴
class CustomerController {
protected $repository;
protected $responseFactory;
public function __construct( CustomerRepository $repo, ResponseFactory $factory )
{
$this->repository = $repo;
$this->responseFactory = $factory;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
$response = $this->responseFactory->create( $this->params( 'context' ) );
$response->setData( 'customer', $customer );
return $response;
}
}
上面的需求是:我們希望能夠根據參數的不同,創建不同的響應,可能是Html的,也可能是Json或者XML的,此時我們傳入一個工廠,讓工廠來負責根據不同的參數來產生不同的對象,核心點還是說依賴抽象的,不變的,易變的東西我們都不要。
處理很多依賴
依賴處理不好,很容易出現下面的代碼:
public function __construct(
CustomerRepository $customerRepository,
ProductRepository $productRepository,
UserRepository $userRepository,
TaxService $taxService,
InvoiceFactory $invoiceFactory,
ResponseFactory $factory,
// ...
){
// ...
}
出現上面的原因是因為類違反了單一職責原則。沒有一個硬性的指標說我們應該依賴多少類,但是一般來說是越少越好,意味著職責更專一。
我們仍然耦合嘛?
我們上面介紹了這么多依賴注入,目的都是為了去耦,回過頭來看下,我們做了這么多是否去耦了呢?
最初我們的代碼是:
public function viewAction()
{
$repository = new CustomerRepository();
$customer = $repository->getById( 1001 );
return $customer;
}
重構后是:
class CustomerController {
protected $repository;
public function __construct( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
此時CustomerController
仍然依賴于CustomerRepository
,具體是實現那就是易變的,我們的原則是依賴不變的,抽象的,因此,我們需要將進一步的去耦,這就是下面要介紹的:通過接口來定義契約。
最后給出一些講依賴注入非常好的幾篇文章:
Dependency "Injection" Considered Harmful
如果大家對第一篇英文文章感興趣,有時間我可以翻譯下,通俗的給講一下的。
這是The Clean Architecture in PHP的第四篇,你的鼓勵是我繼續寫下去的動力,期待我們共同進步。