The Clean Architecture in PHP 讀書筆記(四)

Dependency Injection

上篇最重要的是介紹了去耦的工具之一設計原則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直接實例化出類來使用,帶來的問題有:

  1. It makes it hard to make changes later

    由于依賴于一個具體的實現,具體的東西一般都是易變的,根據SOLID中D(Dependency Inversion Principle)原則,這顯然會導致代碼不易重構

  2. It makes it hard to test

    由于內部生成使用的類,我們測試的時候無法去除易變量,保證不了測試的時候只有一個可變部分

  3. 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請求,這么做的好處是:

  1. 收斂了CustomerRepository的創建,一旦創建CustomerRepository需要改變什么,可以只要一處修改就可以了
  2. 方便測試,我們可以根據測試要求,返回我們需要的CustomerRepository,如下面代碼所示:
$serviceLocator->setFactory( 'CustomerRepository', function () {
    return new Path\To\MockCustomerRepository(
        [
            1001 => ( new Customer() )->setName( 'ACME Corp' ),
        ] );
} );

到這邊是不是已經是做合理的代碼了呢?我們能不能進一步優化呢?答案是:yes!

我們來看下現在的實現還存在的問題:

  1. 仍然需要自己在需要時候,像serviceLocator請求
  2. 為了測試,我們需要修改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個:

  1. When the dependency is used by more than one component

    當有多個地方都去new同一個類,并且構建需要一些額外動作的時候,我們就要考慮將構建這個動作封裝起來了,核心點是:代碼復用

  2. The dependency has different configurations in different contexts

    當創建的邏輯變得復雜的時候,我們需要將創建抽取出來,核心點是:單一職責,關注點分離

  3. When you need a different configuration to test a component

    如果為了測試需要依賴類返回不同的測試數據,這就要將依賴變為注入的,核心點是:可測性

  4. 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

依賴注入那些事兒

laravel 學習筆記 —— 神奇的服務容器

ioc

如果大家對第一篇英文文章感興趣,有時間我可以翻譯下,通俗的給講一下的。

這是The Clean Architecture in PHP的第四篇,你的鼓勵是我繼續寫下去的動力,期待我們共同進步。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容