一、概念:依賴注入(DI)、控制反轉(IOC)、IOC容器
依賴注入(DI)和控制反轉(IOC)基本是一個意思,因為說起來誰都離不開誰。簡單來說,類A依賴類B,但A不控制B的建立和銷燬,僅使用B,那麼B的控制權則交給A之外處理,這叫控制反轉(IOC)。由於A依賴於B,因此在A中必然要使用B的instance,我們可以通過A的建構函式將B的例項注入,比如:
class B {
}class A {
constructor(b: B) {
console.log(b);
}
}const b = new B();
// 將B的例項注入到a中const a = new A(b);
複製程式碼
這個過程叫依賴注入(DI)。那麼什麼是IOC Container(容器)?在剛剛的例子中,將B的例項注入到A的建構函式中的這個過程是我們手動操作的,比較麻煩,特別是當類的關係變多變複雜時,這種方式顯得很難維護。因此IOC容器就是為了解決這樣的問題,IOC容器負責管理物件的生命週期、依賴關係等,實現物件的依賴查詢以及依賴注入。比如Java的Spring以及前端@Angular框架的依賴注入器(DI)就是屬於IOC容器。
接下來我將通過程式碼的形式對比使用依賴注入相比非依賴注入的好處體現在哪。
二、非依賴注入程式碼
我們先來看一段傳統的實現程式碼(非DI)car.ts
// 引擎 export class Engine {
public cylinders = '引擎發動機1';
}// 輪胎export class Tires {
public make = '品牌';
}export class Car {
public engine: Engine;
public tires: Tires;
public description = 'No DI';
constructor() {
this.engine = new Engine();
this.tires = new Tires();
} // Method using the engine and tires drive() {
return `${this.description
} car with ` + `${this.engine.cylinders
} cylinders and ${this.tires.make
} tires.`;
}
}複製程式碼
在以上程式碼中,Car類沒有通過第三方容器而是親自建立了一個引擎(engine)和一些輪胎(tires),這樣的程式碼耦合度比較高,這樣會存在以下問題:
問題1:如果有一天對引擎進行升級,程式碼如下:
// 引擎 export class Engine {
public cylinders = '';
constructor(_cylinders:string) {
this.cylinders = _cylinders;
}
}複製程式碼
在建立引擎的時候需要傳入一個引數,那麼這時候就需要修改Car類裡的new Engine(parameter),這樣就導致Car類被破壞了,這裡請思考一個問題:要怎麼做才能使引擎升級的時候不需要修改Car類呢?(答案:DI)
問題2:如果想在Car上使用不同品牌的輪胎,程式碼如下:
// 輪胎export class Tires {
public make = '品牌';
}export class Tires1 extends Tires {
public make = '品牌1';
}export class Tires2 extends Tires {
public make = '品牌2';
}export class Car {
//。。。。。。其他程式碼省略。。。。。。。 public tires: Tires;
constructor() {
this.tires = new Tires1();
}
}複製程式碼
此時又得重新修改Car的程式碼,這裡請思考一個問題:要怎麼做才能使Car更換其他不同品牌的輪胎的時候不需要修改Car類呢?(答案:DI)
問題3:如何實現資料共享,比如說車聯網,建立了一個Service資料中心,不同的Car通過Service實現資料通訊以及資料共享,如果是通過在Car裡new Service的方式,是無法實現資料共享和通訊的,因為不同Car裡的Service不是同一個例項。
這裡請思考一個問題:如何實現不同Car的資料通訊和共享呢?
問題4:測試比較難,根本無法測試。在示例程式碼中,Car類依賴於Engine類和Tires類,而Engine和Tires又可能各自依賴於其他的類,而其他的類又可能有各自更多的依賴,在這樣層層的依賴關係中,由於不能控制Car背後的隱藏依賴,要進行測試是比較難的,或者應該說,這樣的程式碼是根本無法進行測試的。比如說想同時測試不同品牌的輪子的car的效能,因為car裡頭的new已經寫死了,因此無法做到。比如說想同時測試不同引數的引擎的car的效能,因為car裡頭的new已經寫死了,因此無法做到。除非是每次只測試一種情況,下面拿測試不同品牌的輪子來舉例:先測試品牌1的輪子:car.ts
export class Tires {
public make = '品牌';
}export class Tires1 extends Tires {
public make = '品牌1';
}export class Tires2 extends Tires {
public make = '品牌2';
}export class Car {
public tires: Tires;
public description = 'No DI';
constructor() {
// new 一個品牌1的輪子 this.tires = new Tires1();
} // Method using the engine and tires drive() {
return `${this.description
} car with ` + ` ${this.tires.make
} tires.`;
}
}複製程式碼
測試程式car.spec.ts
import {
Car
} from './car.ts';
describe('Car類單元測試', function () {
it('測試品牌1輪子的Car的駕駛效能', function () {
const car = new Car();
car.drive().should.equal('No DI car with 品牌1 tires.');
})
})複製程式碼
以上程式碼對輪子品牌1進行測試,輸出輪子品牌1的car的駕駛效能。接著對輪子品牌2進行測試:修改Car類,將this.tires = new Tires1();
修改為this.tires = new Tires2();
此時輸出輪子品牌2的car的駕駛效能。
這樣的測試效率是很低的,因為每次只能手動的測試一種情況,如果再加上引擎的測試,那多種混合情況就更多了,根本就不能做到自動測試,所謂的自動測試,是一次性將所有的情況都寫到一個單元測試裡,一次執行,所有情況都會被測試到,當測試通過了,那麼就說明程式碼達到了預期。
針對以上問題,我們來看看使用DI的好處。
三、使用依賴注入(DI)
接下來將演示使用DI來解決以上的4個問題。先看使用DI實現的car.ts程式碼:car.ts
export class Engine {
public cylinders = '引擎發動機1';
}export class Tires {
public make = '品牌';
}export class Tires1 extends Tires {
public make = '品牌1';
}export class Tires2 extends Tires {
public make = '品牌2';
}export class Car {
public description = 'DI';
// 通過建構函式注入Engine和Tires constructor(public engine: Engine, public tires: Tires) {
} // Method using the engine and tires drive() {
return `${this.description
} car with ` + `${this.engine.cylinders
} cylinders and ${this.tires.make
} tires.`;
}
}複製程式碼
在以上程式碼中,通過往建構函式中傳入engine和tires來建立Car,Car類不再親自建立engine和tires,而是消費它們,此時最大的好處就是engine和tires與Car解除了強耦的關係。在new Car的時候,可以傳入任何型別的Engine和Tires,即 let car = new Car(new Engine(),new Tires());
解決問題1:如果有一天對引擎進行升級,程式碼如下:
export class Engine {
public cylinders = '';
constructor(_cylinders:string) {
this.cylinders = _cylinders;
}
}複製程式碼
在建立引擎的時候需要傳入一個引數,這時候不需要修改Car類,只需要修改主程式即可:
主程式程式碼:
main(){
const car = new Car(new Engine('引擎啟動機2'), new Tires1());
car.drive();
}複製程式碼
解決問題2:如果想在Car上使用不同品牌的輪胎,程式碼如下:
export class Tires {
public make = '品牌';
}export class Tire1 extends Tires {
public make = '品牌1';
}export class Tire2 extends Tires {
public make = '品牌2';
}export class Car {
//。。。。。。其他程式碼省略。。。。。。。 constructor(public engine: Engine, public tires: Tires) {
}
}複製程式碼
此時不需要修改Car類,只需要修改主程式即可:主程式程式碼:
main(){
// 使用品牌2的輪胎 const car = new Car(new Engine('引擎啟動機2'), new Tires2());
car.drive();
}複製程式碼
解決問題3:如何實現資料共享,比如說車聯網,建立一個Service資料中心(就像angular的Service層,可以給多個component共享),不同的Car通過Service實現資料通訊以及資料共享。程式碼如下:Service.ts
export class Service {
public data = '';
// 向Service存資料 setData(_data: string) {
this.data = _data;
} // 從Service中取資料 getData() {
return this.data;
}
}複製程式碼
car.ts
export class Car {
constructor(public service: Service) {
} // 向Service存資料 setDataToService(_data: string) {
this.service.setData(_data);
} // 從Service中取資料 getDataFromService() {
return this.service.getData();
}
}複製程式碼
此時主程式如下:主程式程式碼:
main(){
// 建立一個共享服務中心Service const shareService = new Service();
const car1 = new Car(shareService);
const car2 = new Car(shareService);
// car1向服務中心存資料 car1.setDataToService('this data is from car1.');
// car2從服務中心取資料 car2.getDataFromService();
}複製程式碼
解決問題4:測試用例在示例程式碼中,Car類依賴於Engine類和Tires類,而Engine和Tires又可能各自依賴於其他的類,而其他的類又可能有各自的依賴,在這樣層層的依賴關係中,使用DI的程式碼測試是比較簡單的。測試程式如下:測試程式 car.spec.ts
import {
Car,Engine,Tires1, Tires2
} from './car.ts';
// 測試程式入口describe('Car類單元測試', function () {
const engine1 = new Engine('引擎發動機1');
const engine2 = new Engine('引擎發動機2');
const tires1 = new Tires1();
const tires2 = new Tires2();
it('測試引擎1 輪胎品牌1', function () {
const car = new Car(engine1, tires1);
car.drive().should.equal('DI car with 引擎發動機1 cylinders and 品牌1 tires.');
});
it('測試引擎1 輪胎品牌2', function () {
const car = new Car(engine1, tires2);
car.drive().should.equal('DI car with 引擎發動機1 cylinders and 品牌2 tires.');
});
it('測試引擎2 輪胎品牌1', function () {
const car = new Car(engine2, tires1);
car.drive().should.equal('DI car with 引擎發動機2 cylinders and 品牌1 tires.');
});
it('測試引擎2 輪胎品牌2', function () {
const car = new Car(engine2, tires2);
car.drive().should.equal('DI car with 引擎發動機2 cylinders and 品牌2 tires.');
});
})複製程式碼
此時覺得很棒有木有,自動測試的思想就是這樣的,將所有的情況的程式碼都配置好,一次執行,所有的情況都可以測試到。
至此,如果看懂以上的話,DI的思想以及為什麼要用DI就應該可以理解了。