上篇最重要的是介紹了去耦的工具之一設(shè)計(jì)原則SOLID,本篇將繼續(xù)介紹去耦工具:依賴注入损离。
本文為系列文章的第四篇搬葬,前3篇地址是
The Clean Architecture in PHP 讀書(shū)筆記(一)
The Clean Architecture in PHP 讀書(shū)筆記(二)
The Clean Architecture in PHP 讀書(shū)筆記(三)
The Clean Architecture in PHP 讀書(shū)筆記(四)
到目前為止息拜,我們?cè)诿嫦驅(qū)ο笾杏龅降淖顗牡腸ode是:直接在一個(gè)類中實(shí)例化出另一個(gè)類,然后使用的倦微。看代碼:
class CustomerController {
public function viewAction()
{
$repository = new CustomerRepository();
$customer = $repository->getById( 1001 );
return $customer;
}
}
此處CustomerController
類如果脫離了CustomerRepository
類正压,將無(wú)法正常執(zhí)行璃诀,對(duì)CustomerRepository
是強(qiáng)依賴。
這種通過(guò)new class直接實(shí)例化出類來(lái)使用蔑匣,帶來(lái)的問(wèn)題有:
-
It makes it hard to make changes later
由于依賴于一個(gè)具體的實(shí)現(xiàn)劣欢,具體的東西一般都是易變的,根據(jù)SOLID中D(Dependency Inversion Principle)原則裁良,這顯然會(huì)導(dǎo)致代碼不易重構(gòu)
-
It makes it hard to test
由于內(nèi)部生成使用的類凿将,我們測(cè)試的時(shí)候無(wú)法去除易變量,保證不了測(cè)試的時(shí)候只有一個(gè)可變部分
-
We have no control over dependencies
由于使用new來(lái)新建類价脾,我們無(wú)法根據(jù)條件牧抵,選擇性的使用依賴類
控制反轉(zhuǎn)
首先我們回答第一個(gè)問(wèn)題:控制反轉(zhuǎn)是什么?
原先我們?cè)陬愔兄苯觧ew出我們依賴的類侨把,如前面CustomerController
中直接創(chuàng)建了CustomerRepository
犀变;此時(shí)我們?yōu)榱双@得一定的靈活性,可能通過(guò)配置的方式來(lái)實(shí)例化出需要的Repository
來(lái)秋柄,簡(jiǎn)單的配置方式就是一些if,else
語(yǔ)句获枝,現(xiàn)在我們?cè)龠M(jìn)一步,將這些if,else
配置移出CustomerController
類骇笔,通過(guò)一些外部的手段來(lái)達(dá)到相同的目的省店。
而上面我們介紹的這一個(gè)過(guò)程從類內(nèi)部到類外部的過(guò)程就是所謂的:控制反轉(zhuǎn)嚣崭。上面提到的外部的手段主要有兩種:
- 服務(wù)定位模式(Service Locator Pattern)
- 依賴注入(dependency injection)
下面先介紹第一個(gè)手段:服務(wù)定位模式
服務(wù)定位模式
先上代碼,有個(gè)感性認(rèn)識(shí)
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' )
);
} );
此時(shí)我們不會(huì)在viewAction
內(nèi)部直接new出CustomerRepository
懦傍,而是向serviceLocator
請(qǐng)求雹舀,這么做的好處是:
- 收斂了
CustomerRepository
的創(chuàng)建,一旦創(chuàng)建CustomerRepository
需要改變什么粗俱,可以只要一處修改就可以了 - 方便測(cè)試说榆,我們可以根據(jù)測(cè)試要求,返回我們需要的
CustomerRepository
寸认,如下面代碼所示:
$serviceLocator->setFactory( 'CustomerRepository', function () {
return new Path\To\MockCustomerRepository(
[
1001 => ( new Customer() )->setName( 'ACME Corp' ),
] );
} );
到這邊是不是已經(jīng)是做合理的代碼了呢签财?我們能不能進(jìn)一步優(yōu)化呢?答案是:yes废麻!
我們來(lái)看下現(xiàn)在的實(shí)現(xiàn)還存在的問(wèn)題:
- 仍然需要自己在需要時(shí)候荠卷,像
serviceLocator
請(qǐng)求 - 為了測(cè)試,我們需要修改
setFactory
的方法烛愧,給出測(cè)試的實(shí)現(xiàn)
那有沒(méi)有辦法解決呢油宜,當(dāng)然有,那就是下面介紹的依賴注入
依賴注入
依賴注入是一個(gè)過(guò)程:將類它所依賴的外部類由它自己管理變?yōu)閺耐獠孔⑷搿?/p>
下面會(huì)介紹兩種注入方法:
- Setter injection
- Constructor injection
使用setter injection
此時(shí)前面的代碼會(huì)變?yōu)?
$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;
}
}
依賴是通過(guò)setter方法注入的怜姿。
此時(shí)測(cè)試的時(shí)候慎冤,我們只需要通過(guò)setter方法設(shè)置MockCustomerRepository
即可。
這種方法有一個(gè)缺點(diǎn):如果我們忘記了調(diào)用setter方法沧卢,那就完蛋了蚁堤。
$controller = new CustomerController();
$customer = $controller->viewAction();
上面這種代碼,只能等著fatal了但狭。
使用Constructor injection
此時(shí)的代碼長(zhǎng)這個(gè)樣子:
$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;
}
}
我們不會(huì)出現(xiàn)之前setter injection那種忘了設(shè)計(jì)的問(wèn)題了披诗,因?yàn)槲覀兺ㄟ^(guò)構(gòu)造函數(shù)聲明了我們的規(guī)則,必須遵守立磁。
講了這么多依賴注入了呈队,那我們什么時(shí)候使用呢?
什么時(shí)候使用依賴注入
依賴注入解決的是依賴問(wèn)題唱歧,目的是去耦宪摧,那自然有耦合的地方就會(huì)有依賴注入,主要的場(chǎng)景有下面4個(gè):
-
When the dependency is used by more than one component
當(dāng)有多個(gè)地方都去new同一個(gè)類颅崩,并且構(gòu)建需要一些額外動(dòng)作的時(shí)候几于,我們就要考慮將構(gòu)建這個(gè)動(dòng)作封裝起來(lái)了,核心點(diǎn)是:代碼復(fù)用
-
The dependency has different configurations in different contexts
當(dāng)創(chuàng)建的邏輯變得復(fù)雜的時(shí)候沿后,我們需要將創(chuàng)建抽取出來(lái)沿彭,核心點(diǎn)是:?jiǎn)我宦氊?zé),關(guān)注點(diǎn)分離
-
When you need a different configuration to test a component
如果為了測(cè)試需要依賴類返回不同的測(cè)試數(shù)據(jù)得运,這就要將依賴變?yōu)樽⑷氲南ヲ冢诵狞c(diǎn)是:可測(cè)性
-
When the dependency being injected is part of a third party library
當(dāng)我們使用的依賴是第三方庫(kù)的時(shí)候锅移,我們更應(yīng)該使用依賴注入熔掺,核心點(diǎn)是:依賴抽象饱搏,不變的
有那么多適合使用依賴注入的場(chǎng)景,那自然會(huì)有不適合的地方置逻,如果需要構(gòu)建的依賴足夠簡(jiǎn)單推沸,沒(méi)有配置,我們無(wú)需引入依賴注入券坞,依賴注入的引入是為了解決問(wèn)題鬓催,而不是為了增加代碼復(fù)雜性。
使用工廠來(lái)創(chuàng)建依賴
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;
}
}
上面的需求是:我們希望能夠根據(jù)參數(shù)的不同恨锚,創(chuàng)建不同的響應(yīng)宇驾,可能是Html的,也可能是Json或者XML的猴伶,此時(shí)我們傳入一個(gè)工廠课舍,讓工廠來(lái)負(fù)責(zé)根據(jù)不同的參數(shù)來(lái)產(chǎn)生不同的對(duì)象,核心點(diǎn)還是說(shuō)依賴抽象的他挎,不變的筝尾,易變的東西我們都不要。
處理很多依賴
依賴處理不好办桨,很容易出現(xiàn)下面的代碼:
public function __construct(
CustomerRepository $customerRepository,
ProductRepository $productRepository,
UserRepository $userRepository,
TaxService $taxService,
InvoiceFactory $invoiceFactory,
ResponseFactory $factory,
// ...
){
// ...
}
出現(xiàn)上面的原因是因?yàn)轭愡`反了單一職責(zé)原則筹淫。沒(méi)有一個(gè)硬性的指標(biāo)說(shuō)我們應(yīng)該依賴多少類,但是一般來(lái)說(shuō)是越少越好呢撞,意味著職責(zé)更專一损姜。
我們?nèi)匀获詈下铮?/h2>
我們上面介紹了這么多依賴注入,目的都是為了去耦殊霞,回過(guò)頭來(lái)看下摧阅,我們做了這么多是否去耦了呢?
最初我們的代碼是:
public function viewAction()
{
$repository = new CustomerRepository();
$customer = $repository->getById( 1001 );
return $customer;
}
重構(gòu)后是:
class CustomerController {
protected $repository;
public function __construct( CustomerRepository $repo )
{
$this->repository = $repo;
}
public function viewAction()
{
$customer = $this->repository->getById( 1001 );
return $customer;
}
}
此時(shí)CustomerController
仍然依賴于CustomerRepository
脓鹃,具體是實(shí)現(xiàn)那就是易變的逸尖,我們的原則是依賴不變的,抽象的瘸右,因此娇跟,我們需要將進(jìn)一步的去耦,這就是下面要介紹的:通過(guò)接口來(lái)定義契約太颤。
最后給出一些講依賴注入非常好的幾篇文章:
Dependency "Injection" Considered Harmful
laravel 學(xué)習(xí)筆記 —— 神奇的服務(wù)容器
如果大家對(duì)第一篇英文文章感興趣苞俘,有時(shí)間我可以翻譯下,通俗的給講一下的龄章。
這是The Clean Architecture in PHP的第四篇吃谣,你的鼓勵(lì)是我繼續(xù)寫下去的動(dòng)力乞封,期待我們共同進(jìn)步。