TypeScript 中的依賴注入(譯)

我真正喜歡成熟框架的一點就是它們都實現了某種依賴注入轧膘。最近我一直在鉆研 TypeScript 這門技術灯节,以便更好地從表面上理解它是如何工作的猾警。


什么是依賴注入 (DI)妨蛹?

如果你還不了解什么是 依賴注入拆讯,我強烈推薦你了解一下脂男。既然這篇文章不是來介紹什么是 DI 的,更多地是講 DI 是如何實現的种呐,我們盡量用簡單點的話來描述它:

依賴注入是一種通過外部注入來表明對象的依賴關系的技術宰翅。

什么意思呢?原本我們通過各種參數來構造對象爽室,現在只需接收一個已實例化的對象汁讼。

想象以下代碼:

class Foo {
}

class Bar {
  foo: Foo;
  
  constructor() {
    this.foo = new Foo();
  }
}

class Foobar {
  foo: Foo;
  bar: Bar;
  
  constructor() {
    this.foo = new Foo();
    this.bar = new Bar();
  }
}

Bad.

這段代碼帶來很多弊端,由于類之間存在了直接和不可替換的依賴關系,會使代碼難以測試嘿架,可用性和復用性極差瓶珊。而通過依賴注入的方式,將依賴項注入到你的構造函數中耸彪,可以很好地解決這些弊端伞芹。

class Foo {
}

class Bar {
  constructor(foo: Foo) {
  }
}

class Foobar {
  constructor(foo: Foo, bar: Bar) {
  }
}

Better.

初始化 Foobar 實例你需要通過以下方式:

const foobar = new Foobar(new Foo(), new Bar(new Foo()));

Not cool.

通過 Injector 來創(chuàng)建對象,你可以簡單地這樣寫:

const foobar = Injector.resolve<Foobar>(Foobar); // returns an instance of Foobar, with all injected dependencies

Better.

為什么要使用依賴注入的原因有很多搜囱,包括可測試行丑瞧,可維護性,可讀性等等蜀肘。再說一遍绊汹,如果你還不了解它,那么很有必要去學習一下了扮宠。

TypeScript 中的依賴注入

這篇文章將介紹我們自己實現的(非澄鞴裕基礎的)Injector 。如果你只是想找一些現有 DI 解決方案來運用到你的項目中的話坛增,建議你看看 InversifyJS获雕,它是一個很好的控制反轉(IoC) 的 TypeScript 庫。

在本文中收捣,我們將會實現自己的 Injector 類届案,該類能夠通過注入所有必需的依賴項來解析實例。為此我們將實現一個 @Service 的裝飾器(如果你用過 Angular 的話可能知道 @Injectable)來定義我們的服務以及解析實際實例的 Injector罢艾。

在具體實現之前楣颠,你看你需要先了解有關 TypeScriptDI 的一些知識:

反射和裝飾

我們將使用 reflect-metadata 包來獲取運行時的反射功能 。通過這個包可以了解一個類是如何實現的 - 例如:

const Service = () : ClassDecorator => {
  return target => {
    console.log(Reflect.getMetadata('design:paramtypes', target));
  };
};

class Bar {}

@Service()
class Foo {
  constructor(bar: Bar, baz: string) {}
}

控制臺將輸出以下日志:

[ [Function: Bar], [Function: String] ]

因此我們確實知道需要注入的依賴項是什么咐蚯。如果你對這里的 Bar 是一個 Function 感到困惑:我將在下一個章節(jié)中介紹童漩。

重要提示:必須注意的是,沒有裝飾器的類是沒有任何 metadata 的春锋,這似乎是 reflect-metadata 的設計選擇矫膨,盡管我不確定其背后的原因

target 的類型

有個讓我非常困惑的地方是 Service 中的 target 的類型期奔。它很明顯是個 object 而非 Function 呀侧馅,但這是因為在 Javascript 中,類實際上就是一個特殊的函數

class Foo {
    constructor() {
        // the constructor
    }
    bar() {
        // a method
    }
}

轉變

var Foo = /** @class */ (function () {
    function Foo() {
        // the constructor
    }
    Foo.prototype.bar = function () {
        // a method
    };
    return Foo;
}());

編譯后代碼

Function 顯然不是我們想要的目標類型呐萌,因為它太泛了馁痴。由于我們現在還沒有真正使用一個實例,我們需要得到通過 new 創(chuàng)建的實例的確切類型:

interface Type<T> {
  new(...args: any[]): T;
}

Type<T> 能夠讓我們知道對象的原型是什么搁胆?或者換句話說:當我們使用 new 后拿到的是什么弥搞。回頭看看我們的 @Service 裝飾器渠旁,真正的目標類型應該是:

const Service = () : ClassDecorator => {
  return target => {
    // `target` in this case is `Type<Foo>`, not `Foo`
  };
};

讓我困擾的一個點是 ClassDecorator攀例,它看起來像這樣:

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;

不幸的是,因為我們現在的確知道對象的類型顾腊,要為類裝飾器獲得一個更靈活更通用的類型粤铭。

export type GenericClassDecorator<T> = (target: T) => void;

編譯后 interface 不見了

由于接口不是 Javascript 的一部分,所以 TypeScript 編譯后它們就消失了杂靶。這并不是什么新鮮事梆惯,但這意味著我們不能使用接口進行依賴注入。舉個例子:

interface LoggerInterface {
  write(message: string);
}

class Server {
  constructor(logger: LoggerInterface) {
    this.logger.write('Service called');
  }
}

由于接口在運行時不可用吗垮,所以我們的 Injector 將無法知道要注入什么垛吗。

這實在是個遺憾,因為這意味著我們總是必須通過類型提示來替代接口烁登。特別是在我們進行測試的時候非常不好怯屉。

還有一些解決方法,比如通過類而不是接口(感覺很奇怪饵沧,喪失了接口的意義)或者像這樣:

interface LoggerInterface {
  kind: 'logger';
}

class FileLogger implements LoggerInterface {
  kind: 'logger';
}

然而我真的不喜歡這種方法锨络,因為它又冗長又難看。

循環(huán)依賴帶來的麻煩

如果你試圖這樣做:

@Service()
class Bar {
  constructor(foo: Foo) {}
}

@Service()
class Foo {
  constructor(bar: Bar) {}
}

你將會得到一個 ReferenceError 狼牺,告訴你:

ReferenceError: Foo is not defined 

原因很明顯:TypeScript 在解釋 Bar 的時候羡儿, Foo 還不存在。

我不想在這里細究是钥,但有一個可能的解決方法是實現類似 AngularforwardRef 方法掠归。

實現我們自己的 Injector

好了,理論講得差不多了咏瑟。讓我們實現一個非撤鞯剑基礎的 Injector 類吧。

我們將使用以上學到的所有知識码泞,來開始我們的 @Service 裝飾器兄旬。

@Service 裝飾器

我們將會對所有服務進行裝飾處理,否則他們將無法返回元數據(就無法注入依賴項了)余寥。

// ServiceDecorator.ts

const Service = () : GenericClassDecorator<Type<object>> => {
  return (target: Type<object>) => {
    // do something with `target`, e.g. some kind of validation or passing it to the Injector and store them
  };
};

Injector

Injector 主要用于解析實例领铐。它可能也具備其他能力如存儲解析后的實例(我比較喜歡將它們稱為共享實例),但為了簡單起見宋舷,我們現在將盡可能簡單的實現绪撵。

// Injector.ts

export const Injector = new class {
  // Injector implementation
};

導出 const 而非 class(比如 export class Injector) 的原因在于我們的 Injector單例的。否則祝蝠,我們將永遠無法得到相同的 Injector 實例音诈,這意味著任何時候你引入 Injector 所得到的實例都是未注冊過服務的幻碱。(像所有單例一樣也存在一些缺點,尤其在測試方面)

下一步我們需要實現一個方法來解析實例:

// Injector.ts

export const Injector = new class {
  // resolving instances
  resolve<T>(target: Type<any>): T {
    // tokens are required dependencies, while injections are resolved tokens from the Injector
    let tokens = Reflect.getMetadata('design:paramtypes', target) || [],
        injections = tokens.map(token => Injector.resolve<any>(token));
    
    return new target(...injections);
  }
};

到此為止细溅,我們的 Injector 已經可以解析請求的實例了褥傍。讓我們回到開頭的例子(現在稍微擴展一下),通過 Injector 來解析它:

@Service()
class Foo {
  doFooStuff() {
    console.log('foo');
  }
}

@Service()
class Bar {
  constructor(public foo: Foo) {
  }

  doBarStuff() {
    console.log('bar');
  }
}

@Service()
class Foobar {
  constructor(public foo: Foo, public bar: Bar) {
  }
}

const foobar = Injector.resolve<Foobar>(Foobar);
foobar.bar.doBarStuff();
foobar.foo.doFooStuff();
foobar.bar.foo.doFooStuff();

控制臺輸出:

bar
foo
foo

這意味著我們的 Injector 成功注入了所有依賴項喇聊。哇嗚恍风!

總結

依賴注入絕對是你應該使用的強大工具。這篇文章是關于依賴注入是如何工作的誓篱,給你一些實現自己的 Injector 的靈感朋贬。

還有不少事情需要去做,大概舉幾個例子:

  • 錯誤處理
  • 解決循環(huán)依賴
  • 存儲已解析的實例
  • 注入比構造函數token更多的能力
  • 等等

但基本上窜骄,這就是Injector的工作原理锦募。

正如開頭所說的,我最近剛開始研究 DI 的實現邻遏。如果你對這篇文章或者Injector的實現感到任何困惑御滩,歡迎給我留言或者通過郵件聯(lián)系我。

跟往常一樣党远,所有的代碼(包含示例和測試)都可以在 Github上找到削解。

原文鏈接

https://nehalist.io/dependency-injection-in-typescript/

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沟娱,隨后出現的幾起案子氛驮,更是在濱河造成了極大的恐慌,老刑警劉巖济似,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矫废,死亡現場離奇詭異,居然都是意外死亡砰蠢,警方通過查閱死者的電腦和手機蓖扑,發(fā)現死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來台舱,“玉大人律杠,你說我怎么就攤上這事【和铮” “怎么了柜去?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拆宛。 經常有香客問我嗓奢,道長,這世上最難降的妖魔是什么浑厚? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任股耽,我火速辦了婚禮根盒,結果婚禮上,老公的妹妹穿的比我還像新娘物蝙。我一直安慰自己郑象,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布茬末。 她就那樣靜靜地躺著,像睡著了一般盖矫。 火紅的嫁衣襯著肌膚如雪丽惭。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天辈双,我揣著相機與錄音责掏,去河邊找鬼。 笑死湃望,一個胖子當著我的面吹牛换衬,可吹牛的內容都是我干的。 我是一名探鬼主播证芭,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼瞳浦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了废士?” 一聲冷哼從身側響起叫潦,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎官硝,沒想到半個月后矗蕊,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡氢架,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年傻咖,在試婚紗的時候發(fā)現自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岖研。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡卿操,死狀恐怖,靈堂內的尸體忽然破棺而出孙援,到底是詐尸還是另有隱情硬纤,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布赃磨,位于F島的核電站筝家,受9級特大地震影響,放射性物質發(fā)生泄漏邻辉。R本人自食惡果不足惜溪王,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一腮鞍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧莹菱,春花似錦移国、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蜜徽,卻和暖如春祝懂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背拘鞋。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工砚蓬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盆色。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓灰蛙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親隔躲。 傳聞我的和親對象是個殘疾皇子摩梧,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容