細說 Angular 的依賴性注入

接灰的電子產品發表於2017-05-06

什麼是依賴性注入?

依賴性注入( Dependency Injection )其實不是 Angular 獨有的概念,這是一個已經存在很長時間的設計模式,也可以叫做控制反轉 ( Inverse of Control )。我們從下面這個簡單的程式碼片段入手來看看什麼是依賴性注入以及為什麼要使用依賴性注入。

class Person {
  constructor() {
    this.address = new Address('北京', '北京', '朝陽區', 'xx街xx號');
    this.id = Id.getInstance(ID_TYPES.IDCARD);
  }
}複製程式碼

上面的程式碼中,我們在 Person 這個類的建構函式中初始化了我們構建 Person 所需要的依賴類: AddressId ,其中 Address 是個人的地址物件,而 Id 是個人身份物件。這段程式碼的問題在於除了引入了內部所需的依賴之外, 它知道了這些依賴建立的細節 ,比如它知道 Address 的建構函式需要的引數(省、市、區和街道地址)和這些引數的順序,它還知道 Id 的工廠方法和其引數(取得身份證型別的 Id )。

但這樣做的問題究竟是什麼呢?首先這樣的程式碼是非常難以進行單元測試的,因為在測試的時候我們往往需要構造一些不同的測試場景(比如我們想傳入護照型別的 Id ),但這種寫法導致你沒辦法改變其行為。其次,我們在程式碼的可維護性和擴充套件性方面有了很大的障礙,設想一下如果我們改變了 Address 的建構函式或 Id 的工廠方法的話,我們不得不去更改 Person 類。一個類還好,但如果幾十個類都依賴 AddressPerson 的話,這會造成多大的麻煩?

那麼解決的方法呢?也很簡單,那就是我們把 Person 的構造改造一下:

class Person {
  constructor(address, id) {
    this.address = address;
    this.id = id;
  }
}複製程式碼

我們在構造中接受已經建立的 AddressId 物件,這樣在這段程式碼中就沒有任何關於它們的具體實現了。換句話說,我們把建立這些依賴性的職責向上一級傳遞了出去(噗~~推卸責任啊)。現在我們在生產程式碼中可以這樣構造 Person

const person = new Person(
  new Address('北京', '北京', '朝陽區', 'xx街xx號'),
  Id.getInstance(ID_TYPES.IDCARD)
);複製程式碼

而在測試時,可以方便的構造各種場景,比如我們將地區改為遼寧:

const person = new Person(
  new Address('遼寧', '瀋陽', '和平區', 'xx街xx號'),
  Id.getInstance(ID_TYPES.PASSPORT)
);複製程式碼

其實這就是依賴性注入了,這個概念是不是很簡單?但有的同學問了,那上一級要是單元測試不還是有問題嗎?是的,如果上一級需要測試,就得『推卸責任』到再上一級了。這樣一級一級的最後會推到最終的入口函式,但這也不是辦法啊,而且靠人工維護也很容易出錯,這時候就需要有一個依賴性注入的框架來解決了,這種框架一般叫做 DI 框架或者 IoC 框架。這種框架對於熟悉 Java 和 .Net 的同學不會陌生,鼎鼎大名的 Spring 最初就是一個這樣的框架,當然現在功能豐富多了,遠不止這個功能了。

Angular 中的依賴性注入框架

Angular 中的依賴性注入框架主要包含下面幾個角色:

  • Injector(注入者):使用 Injector 提供的 API 建立依賴的例項
  • Provider(提供者):Provider 告訴 Injector 怎樣 建立例項(比如我們上面提到的是通過某個建構函式還是工廠類建立等等)。Provider 接受一個令牌,然後把令牌對映到一個用於構建目標物件的工廠函式。
  • Dependency(依賴):依賴是一種 型別 ,這個型別就是我們要建立的物件的型別。

細說 Angular 的依賴性注入
Angular 中的依賴性注入框架

可能看到這裡還是有些雲裡霧裡,沒關係,我們還是用例子來說明:

import { ReflectiveInjector } from '@angular/core';
const injector = RelfectiveInjector.resolveAndCreate([
  // providers 陣列定義了多個提供者,provide 屬性定義令牌
  // useXXX 定義怎樣建立的方法
  { provide: Person, useClass: Person },
  { provide: Address, useFactory: () => {
        if(env.testing)
            return new Address('遼寧', '瀋陽', '和平區', 'xx街xx號');
        return new Address('北京', '北京', '朝陽區', 'xx街xx號');
    } 
  },
  { provide: Id, useFactory: (type) => {
        if(type === ID_TYPES.PASSPORT)
            return Id.getInstance(ID_TYPES.PASSPORT, someparam);
        if(type === ID_TYPES.IDCARD)
            return Id.getInstance(ID_TYPES.IDCARD);
        return Id.getDefaultInstance();
    } 
  }
]);

class Person {
  // 通過 @Inject 修飾器告訴 DI 這個引數需要什麼樣型別的物件
  // 請在 injector 中幫我找到並注入到對應引數中
  constructor(@Inject(Address) address, @Inject(Id) id) {
    // 省略
  }
}

// 通過 injector 得到物件
const person = injector.get(Person);複製程式碼

上述程式碼中,Angular 提供了 RelfectiveInjector 來解析和建立依賴的物件,你可以看到我們把這個應用中需要的 PersonIdAddress 都放在裡面了。誰需要這些物件就可以向 injector 請求,比如: injector.get(Person) ,當然也可以 injector.get(Address) 等等。可以把它理解成一個依賴性的池子,想要什麼就取就好了。

但是問題來了,首先 injector 怎麼知道如何建立你需要的物件呢?這個是靠 Provider 定義的,在剛剛的 RelfectiveInjector.resolveAndCreate() 中我們發現它是接受一個陣列作為引數,這個陣列就是一個 Provider 的陣列。Provider 最常見的屬性有兩個。第一個是 provide ,這個屬性其實定義的是令牌,令牌的作用是讓框架知道你要找的依賴是哪個然後就可以在 useXXX 這個屬性定義的構建方式中將你需要的物件構建出來了。

那麼 constructor(@Inject(Address) address, @Inject(Id) id) 這句怎麼理解呢?由於我們在 const person = injector.get(Person); 想取得 Person ,但 Person 又需要兩個依賴引數: address 和 id 。 @Inject(Address) address 是告訴框架我需要的是一個令牌為 Address 的物件,這樣框架就又到 injector 中尋找令牌為 Address 對應的工廠函式,通過工廠函式構造好物件後又把物件賦值到 address 。

由於這裡我們是用物件的型別來做令牌,上面的注入程式碼也可以寫成下面的樣子。利用 Typescript 的型別定義,框架看到有依賴的引數就會去 Injector 中尋找令牌為該型別的工廠函式。

class Person {
  constructor(address: Address, id: Id) {
    // 省略
  }
}複製程式碼

而對於令牌為型別的並且是 useClass 的這種形式,由於前後都一樣,對於這種 Provider 我們有一個語法糖:可以直接寫成 { Person } ,而不用完整的寫成 { provide: Person, useClass: Person } 這種形式。當然還要注意 Token 不一定非得是某個類的型別,也可以是字串, Angular 中還有 InjectionToken 用於建立一個可以避免重名的 Token。

那麼其實除了 useClassuseFactory ,我們還可以使用 useValue 來提供一些簡單資料結構,比如我們可能希望把系統的 API 基礎資訊配置通過這種形式讓所有想呼叫 API 的類都可以注入。如下面的例子中,基礎配置就是一個簡單的物件,裡面有多個屬性,這種情況用 useValue 即可。

{
  provide: 'BASE_CONFIG',
  useValue: {
    uri: 'https://dev.local/1.1',
    apiSecret: 'blablabla',
    apiKey: 'nahnahnah'
  }
}複製程式碼

依賴性注入進階

可能你注意到,上面提到的依賴性注入有一個特點,那就是需要注入的引數如果在 Injector 中找不到對應的依賴,那麼就會發生異常了。但確實有些時候我們是需要這樣的特性:該依賴是可選的,如果有我們就這麼做,如果沒有就那樣做。遇到這種情況怎麼辦呢?

Angular 提供了一個非常貼心的 @Optional 修飾器,這個修飾器用來告訴框架後面的引數需要一個可選的依賴。

constructor(@Optional(ThirdPartyLibrary) lib) {
    if (!lib) {
    // 如果該依賴不存在的情況
    }
}複製程式碼

需要注意的是,Angular 的 DI 框架建立的物件都是單件( Singleton )的,那麼如果我們需要每次都建立一個新物件怎麼破呢?我們有兩個選擇,第一種:在 Provider 中返回工廠而不是物件,像下面例子這樣:

  { 
    provide: Address, 
    useFactory: () => {
        // 注意:這裡返回的是工廠,而不是物件
        return () => {
            if(env.testing)
                return new Address('遼寧', '瀋陽', '和平區', 'xx街xx號');
            return new Address('北京', '北京', '朝陽區', 'xx街xx號');
        }
    } 
  }複製程式碼

第二種:我們建立一個 child injector (子注入者): Injector.resolveAndCreateChild()

const injector = ReflectiveInjector.resolveAndCreate([Person]);
const childInjector = injector.resolveAndCreateChild([Person]);
// 此時父 Injector 和子 Injector 得到的 Person 物件是不同的
injector.get(Person) !== childInjector.get(Person);複製程式碼

而且子 Injector 還有一個特性:如果在 childInjector 中找不到令牌對應的工廠,它會去父 Injector 中尋找。換句話說,這父子關係(多重的)是構成了一棵依賴樹,框架會從最下面的子 Injector 開始尋找,一直找到最上面的父 Injector。看到這裡相信你就知道為什麼父元件宣告的 providers 對於子元件是可見的,因為子元件中在自己 constructor 中如果發現有找不到的依賴就會到父元件中去找。

在實際的 Angular 應用中我們其實很少會直接顯式使用 Injector 去完成注入,而是在對應的模組、元件等的後設資料中提供 providers 即可,這是由於 Angular 框架幫我們完成了這部分程式碼,它們其實在後設資料配置後由框架放入 Injector 中了。

有問題的童鞋可以加入我的小密圈討論:t.xiaomiquan.com/jayRnaQ (該連結7天內(5月14日前)有效)

我的 《Angular 從零到一》紙書出版了,歡迎大家圍觀、訂購、提出寶貴意見。

下面是書籍的內容簡介:

本書系統介紹Angular的基礎知識與開發技巧,可幫助前端開發者快速入門。共有9章,第1章介紹Angular的基本概念,第2~7章從零開始搭建一個待辦事項應用,然後逐步增加功能,如增加登入驗證、將應用模組化、多使用者版本的實現、使用第三方樣式庫、動態效果製作等。第8章介紹響應式程式設計的概念和Rx在Angular中的應用。第9章介紹在React中非常流行的Redux狀態管理機制,這種機制的引入可以讓程式碼和邏輯隔離得更好,在團隊工作中強烈建議採用這種方案。本書不僅講解Angular的基本概念和最佳實踐,而且分享了作者解決問題的過程和邏輯,講解細膩,風趣幽默,適合有物件導向程式設計基礎的讀者閱讀。

京東連結:item.m.jd.com/product/120…

細說 Angular 的依賴性注入
Angular從零到一

相關文章