細數Javascript技術棧中的四種依賴注入

做爲面向對象編程中實現控制反轉(Inversion of Control,下文稱IoC)最多見的技術手段之一,依賴注入(Dependency Injection,下文稱DI)可謂在OOP編程中大行其道經久不衰。好比在J2EE中,就有大名鼎鼎的執牛耳者Spring。Javascript社區中天然也不乏一些積極的嘗試,廣爲人知的AngularJS很大程度上就是基於DI實現的。遺憾的是,做爲一款缺乏反射機制、不支持Annotation語法的動態語言,Javascript長期以來都沒有屬於本身的Spring框架。固然,伴隨着ECMAScript草案進入快速迭代期的春風,Javascript社區中的各類方言、框架可謂羣雄並起,方興未艾。能夠預見到,優秀的JavascriptDI框架的出現只是遲早的事。javascript

本文總結了Javascript中常見的依賴注入方式,並以inversify.js爲例,介紹了方言社區對於Javascript中DI框架的嘗試和初步成果。文章分爲四節:html

一. 基於Injector、Cache和函數參數名的依賴注入
二. AngularJS中基於雙Injector的依賴注入
三. TypeScript中基於裝飾器和反射的依賴注入
四. inversify.js——Javascript技術棧中的IoC容器前端

 

一. 基於Injector、Cache和函數參數名的依賴注入java

儘管Javascript中不原生支持反射(Reflection)語法,可是Function.prototype上的toString方法卻爲咱們另闢蹊徑,使得在運行時窺探某個函數的內部構形成爲可能:toString方法會以字符串的形式返回包含function關鍵字在內的整個函數定義。從這個完整的函數定義出發,咱們能夠利用正則表達式提取出該函數所須要的參數,從而在某種程度上得知該函數的運行依賴。
好比Student類上write方法的函數簽名write(notebook, pencil)就說明它的執行依賴於notebook和pencil對象。所以,咱們能夠首先把notebook和pencil對象存放到某個cache中,再經過injector(注入器、注射器)向write方法提供它所須要的依賴:git

var cache = {};
// 經過解析Function.prototype.toString()取得參數名
function getParamNames(func) {
    // 正則表達式出自http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript
    var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1];
    paramNames = paramNames.replace(/ /g, '');
    paramNames = paramNames.split(',');
    return paramNames;
}
var injector = {
    // 將func做用域中的this關鍵字綁定到bind對象上,bind對象能夠爲空
    resolve: function (func, bind) {
        // 取得參數名
        var paramNames = getParamNames(func);
        var params = [];
        for (var i = 0; i < paramNames.length; i++) {
            // 經過參數名在cache中取出相應的依賴
            params.push(cache[paramNames[i]]);
        }
        // 注入依賴並執行函數
        func.apply(bind, params);
    }
};
 
function Notebook() {}
Notebook.prototype.printName = function () {
    console.log('this is a notebook');
};
 
function Pencil() {}
Pencil.prototype.printName = function () {
    console.log('this is a pencil');
};
 
function Student() {}
Student.prototype.write = function (notebook, pencil) {
    if (!notebook || !pencil) {
        throw new Error('Dependencies not provided!');
    }
    console.log('writing...');
};
// 提供notebook依賴
cache['notebook'] = new Notebook();
// 提供pencil依賴
cache['pencil'] = new Pencil();
var student = new Student();
injector.resolve(student.write, student); // writing...

有時候爲了保證良好的封裝性,也不必定要把cache對象暴露給外界做用域,更多的時候是以閉包變量或者私有屬性的形式存在的:github

function Injector() {
    this._cache = {};
}

Injector.prototype.put = function (name, obj) {
    this._cache[name] = obj;
};

Injector.prototype.getParamNames = function (func) {
    // 正則表達式出自http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript
    var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1];
    paramNames = paramNames.replace(/ /g, '');
    paramNames = paramNames.split(',');
    return paramNames;
};

Injector.prototype.resolve = function (func, bind) {
    var self = this;
    var paramNames = self.getParamNames(func);
    var params = paramNames.map(function (name) {
        return self._cache[name];
    });
    func.apply(bind, params);
};

var injector = new Injector();

var student = new Student();
injector.put('notebook', new Notebook());
injector.put('pencil', new Pencil())
injector.resolve(student.write, student); // writing...

好比如今要執行Student類上的另外一個方法function draw(notebook, pencil, eraser),由於injector的cache中已經有了notebook和pencil對象,咱們只須要將額外的eraser也存放到cache中:正則表達式

function Eraser() {}
Eraser.prototype.printName = function () {
    console.log('this is an eraser');
};

// 爲Student增長draw方法
Student.prototype.draw = function (notebook, pencil, eraser) {
    if (!notebook || !pencil || !eraser) {
        throw new Error('Dependencies not provided!');
    }
    console.log('drawing...');
};

injector.put('eraser', new Eraser());
injector.resolve(student.draw, student);

經過依賴注入,函數的執行和其所依賴對象的建立邏輯就被解耦開來了。
固然,隨着grunt/gulp/fis等前端工程化工具的普及,愈來愈多的項目在上線以前都通過了代碼混淆(uglify),於是經過參數名去判斷依賴並不老是可靠,有時候也會經過爲function添加額外屬性的方式來明確地說明其依賴:編程

Student.prototype.write.depends = ['notebook', 'pencil'];
Student.prototype.draw.depends = ['notebook', 'pencil', 'eraser'];
Injector.prototype.resolve = function (func, bind) {
    var self = this;
    // 首先檢查func上是否有depends屬性,若是沒有,再用正則表達式解析
    func.depends = func.depends || self.getParamNames(func);
    var params = func.depends.map(function (name) {
        return self._cache[name];
    });
    func.apply(bind, params);
};
var student = new Student();
injector.resolve(student.write, student); // writing...
injector.resolve(student.draw, student); // draw...

 

二. AngularJS中基於雙Injector的依賴注入gulp

熟悉AngularJS的同窗很快就能聯想到,在injector注入以前,咱們在定義module時還能夠調用config方法來配置隨後會被注入的對象。典型的例子就是在使用路由時對$routeProvider的配置。也就是說,不一樣於上一小節中直接將現成對象(好比new Notebook())存入cache的作法,AngularJS中的依賴注入應該還有一個"實例化"或者"調用工廠方法"的過程。
這就是providerInjector、instanceInjector以及他們各自所擁有的providerCache和instanceCache的由來。
在AngularJS中,咱們可以經過依賴注入獲取到的injector一般是instanceInjector,而providerInjector則是以閉包中變量的形式存在的。每當咱們須要AngularJS提供依賴注入服務時,好比想要獲取notebook,instanceInjector會首先查詢instanceCache上是存在notebook屬性,若是存在,則直接注入;若是不存在,則將這個任務轉交給providerInjector;providerInjector會將"Provider"字符串拼接到"notebook"字符串的後面,組成一個新的鍵名"notebookProvider",再到providerCache中查詢是否有notebookProvider這個屬性,若有沒有,則拋出異常Unknown Provider異常:前端工程化

若是有,則將這個provider返回給instanceInjector;instanceInjector拿到notebookProvider後,會調用notebookProvider上的工廠方法$get,獲取返回值notebook對象,將該對象放到instanceCache中以備未來使用,同時也注入到一開始聲明這個依賴的函數中。過程描述起來比較複雜,能夠經過下面的圖示來講明:

 

須要注意的是,AngularJS中的依賴注入方式也是有缺陷的:利用一個instanceInjector單例服務全局的反作用就是沒法單獨跟蹤和控制某一條依賴鏈條,即便在沒有交叉依賴的狀況下,不一樣module中的同名provider也會產生覆蓋,這裏就不詳細展開了。

另外,對於習慣於Java和C#等語言中高級IoC容器的同窗來講,看到這裏可能以爲有些彆扭,畢竟在OOP中,咱們一般不會將依賴以參數的形式傳遞給方法,而是做爲屬性經過constructor或者setters傳遞給實例,以實現封裝。的確如此,1、二節中的依賴注入方式沒有體現出足夠的面向對象特性,畢竟這種方式在Javascript已經存在多年了,甚至都不須要ES5的語法支持。但願瞭解Javascript社區中最近一兩年關於依賴注入的研究和成果的同窗,能夠繼續往下閱讀。

 

 三. TypeScript中基於裝飾器和反射的依賴注入

博主自己對於Javascript的各類方言的學習並非特別熱情,尤爲是如今EMCAScript提案、草案更新很快,不少時候藉助於polyfill和babel的各類preset就能知足需求了。可是TypeScript是一個例外(固然如今Decorator也已是提案了,雖然階段還比較早,可是確實已經有polyfill可使用)。上文提到,Javascript社區中遲遲沒有出現一款優秀的IoC容器和自身的語言特性有關,那就依賴注入這個話題而言,TypeScript給咱們帶來了什麼不一樣呢?至少有下面這幾點:
* TypeScript增長了編譯時類型檢查,使Javascript具有了必定的靜態語言特性
* TypeScript支持裝飾器(Decorator)語法,和傳統的註解(Annotation)頗爲類似
* TypeScript支持元信息(Metadata)反射,再也不須要調用Function.prototype.toString方法
下面咱們就嘗試利用TypeScript帶來的新語法來規範和簡化依賴注入。此次咱們再也不向函數或方法中注入依賴了,而是向類的構造函數中注入。
TypeScript支持對類、方法、屬性和函數參數進行裝飾,這裏須要用到的是對類的裝飾。繼續上面小節中用到的例子,利用TypeScript對代碼進行一些重構:

class Pencil {
    public printName() {
        console.log('this is a pencil');
    }
}

class Eraser {
    public printName() {
        console.log('this is an eraser');
    }
}

class Notebook {
    public printName() {
        console.log('this is a notebook');
    }
}

class Student {
    pencil: Pencil;
    eraser: Eraser;
    notebook: Notebook;
    public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) {
        this.notebook = notebook;
        this.pencil = pencil;
        this.eraser = eraser;
    }
    public write() {
        if (!this.notebook || !this.pencil) {
            throw new Error('Dependencies not provided!');
        }
        console.log('writing...');
    }
    public draw() {
        if (!this.notebook || !this.pencil || !this.eraser) {
            throw new Error('Dependencies not provided!');
        }
        console.log('drawing...');
    }
}

下面是injector和裝飾器Inject的實現。injector的resolve方法在接收到傳入的構造函數時,會經過name屬性取出該構造函數的名字,好比class Student,它的name屬性就是字符串"Student"。再將Student做爲key,到dependenciesMap中去取出Student的依賴,至於dependenciesMap中是什麼時候存入的依賴關係,這是裝飾器Inject的邏輯,後面會談到。Student的依賴取出後,因爲這些依賴已是構造函數的引用而非簡單的字符串了(好比Notebook、Pencil的構造函數),所以直接使用new語句便可獲取這些對象。獲取到Student類所依賴的對象以後,如何把這些依賴做爲構造函數的參數傳入到Student中呢?最簡單的莫過於ES6的spread操做符。在不能使用ES6的環境下,咱們也能夠經過僞造一個構造函數來完成上述邏輯。注意爲了使instanceof操做符不失效,這個僞造的構造函數的prototype屬性應該指向原構造函數的prototype屬性。

var dependenciesMap = {};
var injector = {
    resolve: function (constructor) {
        var dependencies = dependenciesMap[constructor.name];
        dependencies = dependencies.map(function (dependency) {
            return new dependency();
        });
        // 若是可使用ES6的語法,下面的代碼能夠合併爲一行:
        // return new constructor(...dependencies);
        var mockConstructor: any = function () {
            constructor.apply(this, dependencies);
        };
        mockConstructor.prototype = constructor.prototype;
        return new mockConstructor();
    }
};
function Inject(...dependencies) {
    return function (constructor) {
        dependenciesMap[constructor.name] = dependencies;
        return constructor;
    };
}

injector和裝飾器Inject的邏輯完成後,就能夠用來裝飾class Student並享受依賴注入帶來的樂趣了:

// 裝飾器的使用很是簡單,只須要在類定義的上方添加一行代碼
// Inject是裝飾器的名字,後面是function Inject的參數
@Inject(Notebook, Pencil, Eraser)
class Student {
    pencil: Pencil;
    eraser: Eraser;
    notebook: Notebook;
    public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) {
        this.notebook = notebook;
        this.pencil = pencil;
        this.eraser = eraser;
    }
    public write() {
        if (!this.notebook || !this.pencil) {
            throw new Error('Dependencies not provided!');
        }
        console.log('writing...');
    }
    public draw() {
        if (!this.notebook || !this.pencil || !this.eraser) {
            throw new Error('Dependencies not provided!');
        }
        console.log('drawing...');
    }
}
var student = injector.resolve(Student);
console.log(student instanceof Student); // true
student.notebook.printName(); // this is a notebook
student.pencil.printName(); // this is a pencil
student.eraser.printName(); // this is an eraser
student.draw(); // drawing
student.write(); // writing

利用裝飾器,咱們還能夠實現一種比較激進的依賴注入,下文稱之爲RadicalInject。RadicalInject對原代碼的侵入性比較強,不必定適合具體的業務,這裏也一併介紹一下。要理解RadicalInject,須要對TypeScript裝飾器的原理和Array.prototype上的reduce方法理解比較到位。

function RadicalInject(...dependencies){
    var wrappedFunc:any = function (target: any) {
        dependencies = dependencies.map(function (dependency) {
            return new dependency();
        });
        // 使用mockConstructor的緣由和上例相同
        function mockConstructor() {
            target.apply(this, dependencies);
        }
        mockConstructor.prototype = target.prototype;

        // 爲何須要使用reservedConstructor呢?由於使用RadicalInject對Student方法裝飾以後,
        // Student指向的構造函數已經不是一開始咱們聲明的class Student了,而是這裏的返回值,
        // 即reservedConstructor。Student的指向變了並非一件不能接受的事,可是若是要
        // 保證student instanceof Student如咱們所指望的那樣工做,這裏就應該將
        // reservedConstructor的prototype屬性指向原Student的prototype
        function reservedConstructor() {
            return new mockConstructor();
        }
        reservedConstructor.prototype = target.prototype;
        return reservedConstructor;
    }
    return wrappedFunc;
}

使用RadicalInject,原構造函數實質上已經被一個新的函數代理了,使用上也更爲簡單,甚至都不須要再有injector的實現:

@RadicalInject(Notebook, Pencil, Eraser)
class Student {
    pencil: Pencil;
    eraser: Eraser;
    notebook: Notebook;
    public constructor() {}
    public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) {
        this.notebook = notebook;
        this.pencil = pencil;
        this.eraser = eraser;
    }
    public write() {
        if (!this.notebook || !this.pencil) {
            throw new Error('Dependencies not provided!');
        }
        console.log('writing...');
    }
    public draw() {
        if (!this.notebook || !this.pencil || !this.eraser) {
            throw new Error('Dependencies not provided!');
        }
        console.log('drawing...');
    }
}
// 再也不出現injector,直接調用構造函數
var student = new Student(); 
console.log(student instanceof Student); // true
student.notebook.printName(); // this is a notebook
student.pencil.printName(); // this is a pencil
student.eraser.printName(); // this is an eraser
student.draw(); // drawing
student.write(); // writing

因爲class Student的constructor方法須要接收三個參數,直接無參調用new Student()會形成TypeScript編譯器報錯。固然這裏只是分享一種思路,你們能夠暫時忽略這個錯誤。有興趣的同窗也可使用相似的思路嘗試代理一個工廠方法,而非直接代理構造函數,以免這類錯誤,這裏再也不展開。

AngularJS2團隊爲了得到更好的裝飾器和反射語法的支持,一度準備另起爐竈,基於AtScript(AtScript中的"A"指的就是Annotation)來進行新框架的開發。但最終卻選擇擁抱TypeScript,因而便有了微軟和谷歌的奇妙組合。

固然,須要說明的是,在缺乏相關標準和瀏覽器廠商支持的狀況下,TypeScript在運行時只是純粹的Javascript,下節中出現的例子會印證這一點。

 

四. inversify.js——Javascript技術棧中的IoC容器

其實從Javascript出現各類支持高級語言特性的方言就能夠預見到,IoC容器的出現只是遲早的事情。好比博主今天要介紹的基於TypeScript的inversify.js,就是其中的先行者之一。
inversity.js比上節中博主實現的例子還要進步不少,它最初設計的目的就是爲了前端工程師同窗們能在Javascript中寫出符合SOLID原則的代碼,立意可謂很是之高。表如今代碼中,就是到處有接口,將"Depend upon Abstractions. Do not depend upon concretions."(依賴於抽象,而非依賴於具體)表現地淋漓盡致。繼續使用上面的例子,可是因爲inversity.js是面向接口的,上面的代碼須要進一步重構:

interface NotebookInterface {
    printName(): void;
}
interface PencilInterface {
    printName(): void;
}
interface EraserInterface {
    printName(): void;
}
interface StudentInterface {
    notebook: NotebookInterface;
    pencil: PencilInterface;
    eraser: EraserInterface;
    write(): void;
    draw(): void;
}
class Notebook implements NotebookInterface {
    public printName() {
        console.log('this is a notebook');
    }
}
class Pencil implements PencilInterface {
    public printName() {
        console.log('this is a pencil');
    }
}
class Eraser implements EraserInterface {
    public printName() {
        console.log('this is an eraser');
    }
}

class Student implements StudentInterface {
    notebook: NotebookInterface;
    pencil: PencilInterface;
    eraser: EraserInterface;
    constructor(notebook: NotebookInterface, pencil: PencilInterface, eraser: EraserInterface) {
        this.notebook = notebook;
        this.pencil = pencil;
        this.eraser = eraser;
    }
    write() {
        console.log('writing...');
    }
    draw() {
        console.log('drawing...');
    }
}

因爲使用了inversity框架,此次咱們就不用本身實現injector和Inject裝飾器啦,只須要從inversify模塊中引用相關對象:

import { Inject } from "inversify";

@Inject("NotebookInterface", "PencilInterface", "EraserInterface")
class Student implements StudentInterface {
    notebook: NotebookInterface;
    pencil: PencilInterface;
    eraser: EraserInterface;
    constructor(notebook: NotebookInterface, pencil: PencilInterface, eraser: EraserInterface) {
        this.notebook = notebook;
        this.pencil = pencil;
        this.eraser = eraser;
    }
    write() {
        console.log('writing...');
    }
    draw() {
        console.log('drawing...');
    }
}

這樣就好了嗎?還記得上節中提到TypeScript中各類概念只是語法糖嗎?不一樣於上一節中直接將constructor引用傳遞給Inject的例子,因爲inversify.js是面向接口的,而諸如NotebookInterface、PencilInterface之類的接口只是由TypeScript提供的語法糖,在運行時並不存在,所以咱們在裝飾器中聲明依賴時只能使用字符串形式而非引用形式。不過不用擔憂,inversify.js爲咱們提供了bind機制,在接口的字符串形式和具體的構造函數之間搭建了橋樑:

import { TypeBinding, Kernel } from "inversify";

var kernel = new Kernel();
kernel.bind(new TypeBinding<NotebookInterface>("NotebookInterface", Notebook));
kernel.bind(new TypeBinding<PencilInterface>("PencilInterface", Pencil));
kernel.bind(new TypeBinding<EraserInterface>("EraserInterface", Eraser));
kernel.bind(new TypeBinding<StudentInterface>("StudentInterface", Student));

注意這步須要從inversify模塊中引入TypeBinding和Kernel,而且爲了保證返回值類型以及整個編譯時靜態類型檢查可以順利經過,泛型語法也被使用了起來。
說到這裏,要理解new TypeBinding<NotebookInterface>("NotebookInterface", Notebook)也就很天然了:爲依賴於"NotebookInterface"字符串的類提供Notebook類的實例,返回值向上溯型到NotebookInterface。
完成了這些步驟,使用起來也還算順手:

var student: StudentInterface = kernel.resolve<StudentInterface>("StudentInterface");
console.log(student instanceof Student); // true
student.notebook.printName(); // this is a notebook
student.pencil.printName(); // this is a pencil
student.eraser.printName(); // this is an eraser
student.draw(); // drawing
student.write(); // writing

 

最後,順帶提一下ECMAScript中相關提案的現狀和進展。Google的AtScript團隊曾經有過Annotation的提案,可是AtScript胎死腹中,這個提案天然不了了之了。目前比較有但願成爲es7標準的是一個關於裝飾器的提案:https://github.com/wycats/javascript-decorators。感興趣的同窗能夠到相關的github頁面跟蹤瞭解。儘管DI只是OOP編程衆多模式和特性中的一個,但卻能夠折射出Javascript在OOP上艱難前進的道路。但總得說來,也算得上是路途坦蕩,前途光明。回到依賴注入的話題上,一邊是翹首以盼的Javascript社區,一邊是姍姍來遲的IoC容器,這兩者最終能產生怎樣的化學反應,讓咱們拭目以待。

 

做者:ralph_zhu

時間:2016-02-23 08:00

原文:http://www.cnblogs.com/front-end-ralph/p/5208045.html

相關文章
相關標籤/搜索