[JavaScript]以BDD手寫依賴注入(dependency injection)

程序編寫過程當中,經常面臨的困境是寫的時候行雲流水,運行的時候捶胸搗腿!javascript

那爲何會出現這種情況?編寫習慣、或者說編寫流程起了重要因素,譬如:沒有測試用例——這表示你在編寫時壓根兒沒想過期刻追蹤編寫內容的正確性、健壯性;也沒考慮過程序如何適應來自PM的花樣需求。java

今天不談理論,咱們就來經過BDD(行爲驅動開發)的模式來完成一個相似AngularJS依賴注入的簡單模塊。經過這種方式,咱們來感覺一下BDD對於開發過程的幫助。git

什麼是BDD

BDD(行爲驅動開發)是第二代的、由外及內的、基於拉(pull)的、多方利益相關者的(stakeholder)、多種可擴展的、高自動化的敏捷方法。它描述了一個交互循環,能夠具備帶有良好定義的輸出(即工做中交付的結果):已測試過的軟件。angularjs

好吧,確定有人會問「這他媽是人話麼」。github

那我換個說法,咱們須要這樣一種編程方式,她從需求出發,描述模塊使用的各個場景,並經過測試用例監督其行爲;即使一開始沒法完成全部預計功能,也能夠經過迭代等方式持續交付;並且在需求變動時,也能經過測試用例來確認模塊的健壯性。正則表達式

那麼「測試用例」就是其中的關鍵,常見的BDD測試框架有:mochajasmine...。BDD測試框架的代碼有極鮮明的聲明式風格,代碼是可「讀」的!shell

好了,廢話不敢太多,咱們這就打立刻路。本章所涉源碼在此:naive-dependency-injectionnpm

建立項目

mkdir simple-di; cd simple-di
npm init

npm init後,參考下圖做答。咱們這裏使用mocha做爲測試框架,因此test command必須爲mocha編程

圖片描述

接着來,建立一個源碼目錄(src);一個測試用例目錄(test):數組

mkdir src test

還有,既然在npm init時都聲明瞭使用mocha做爲測試命令,那不裝一個mocha彷佛也說不過去嘛:

npm install --save-dev mocha

到此,各工做目錄準備就緒,來試運行一下測試命令吧:

npm test

由於一個測試用例也沒寫,因此0 passing,完美,沒毛病!

圖片描述

整裝待發

準備測試用例:

touch test/ut.js

用你喜歡的工具打開test/ut.js,並寫入以下內容:

'use strict';
var assert = require('assert');
var DI = require('../src/DI');

describe('test dependency injection', function() {
    //TODO:這裏後面咱們會陸續加入各個cases
});

需求1:能夠注入字面量

咱們的第一個需求能夠總結出以下特徵:

  • 她得是一個類(可被實例化)

  • 她得有一個註冊方法,能夠接受兩個參數,分別是一個名字、一個字面量

  • 她還有一個運行方法,能夠像AngularJS那樣接受一個數組,數組的最後一項是即將被執行的函數,其他都是要被注入到執行函數裏的實例名字

從需求出發,咱們按照期待模塊的「行爲」,寫出測試用例:

it('injecting literal object', function() {
    //她是一個類(可被實例化)
    var app = new DI();
    //她有一個註冊方法,能夠接受兩個參數,分別是一個名字、一個字面量
    app.register('duck', {
        fly: function() {
            return 'flying';
        }
    });

    var msg;
    //她有一個運行方法,接受一個數組,數組的最後一項是即將被執行的函數,其他都是要被注入到執行函數裏的實例名字
    app.run(['duck', function(d) {
        msg = d.fly();
    }]);

    assert.equal(msg, 'flying', '字面量注入失敗');
});

將以上case放入以前準備好的test/ut.jsTODO位置

這時候,咱們若是運行測試用例(npm test),結果可想而知:

圖片描述

第一次解決問題的時候來了,根據錯誤描述,咱們得知「Cannot find module '../src/DI'」,解決方法,異常簡單,請看

touch src/DI.js

再次運行測試用例(npm test),獲得以下結論:

圖片描述

此次的問題是「DI is not a function」,由於剛纔咱們僅僅建立了src/DI.js文件,並無提供任何內容,因此固然not a function。那咱們如今給src/DI.js提供一個function做爲返回值吧:

'use strict';

var DI = function() {};

module.exports = DI;

繼續運行測試用例(npm test),看到以下結論:

圖片描述

此次是「app.register is not a function」,很好修復該問題,聲明一下register方法不就行了麼,順便咱們能夠聯想到app.run確定也得not a function,也一塊兒補了吧:

DI.prototype.register = function(name, inst) {};
DI.prototype.run = function(arr) {};

不驕不躁,咱們再npm test一次:

圖片描述

總算入了正題,斷言assert.equal(msg, 'flying', '字面量注入失敗');失敗了,由於咱們只是聲明瞭要用到的各個方法,並無寫任何實現,那麼先從構造函數改起吧:

var DI = function() {
    //咱們須要一個成員store來保存register方法註冊的名字和其對應的實例
    this.store = {};
};

二來修改register方法:

DI.prototype.register = function(name, inst) {
    //將名字和對應的實例存入store
    this.store[name] = inst;
};

三來補充run方法:

DI.prototype.run = function(arr) {
    //1. 數組arr末尾元素的下標
    var lastIndex = arr.length - 1;
    //2. 取出末尾元素,做爲待執行函數
    var cb = arr[lastIndex];
    //3. 取出除末尾元素的其它全部元素,爲待注入的實例名字列表
    var argsName = arr.slice(0, lastIndex);
    //4. 將上述實例名字列表依次從store中獲取對應的實例,組成真正待執行函數的參數列表
    var args = argsName.map(name => this.store[name]);
    //5. 執行第2步取出的函數,並傳入第4步的到的參數
    cb.apply(null, args);
};

來吧,是時候證實本身了:

圖片描述

需求2:能夠注入class

咱們的第二個需求是在前一個基礎上作增量:

  • 她得有一個註冊方法,能夠接受兩個參數,分別是一個名字、一個字面量或者class

有了新的需求,咱們也得爲新「行爲」,添加測試用例:

it('injecting class', function() {
    //她依舊是一個類(可被實例化)
    var app = new DI();

    var woman = function() {
        this.cry = function() {
            return 'crying';
        };
    };
    //她依舊有一個註冊方法,只不過接受的實例也能夠是class
    app.register('woman', woman);
    var msg;
    app.run(['woman', function(w) {
        msg = w.cry();
    }]);

    assert.equal(msg, 'crying', 'failed injecting class');
});

代碼不改,想來也是不能工做的:

圖片描述

由於咱們以前沒適配過class注入的狀況,因此這裏注入的僅僅是woman的類聲明,並不是instance,因此須要對以前的實現進行調整。這時測試用例的好處就體現出來了,修改完src/DI.js後,只要再次npm test,輕輕鬆鬆證實實現是否知足了新需求,又不破壞原來的功能。

咱們須要調整的地方,僅僅是register方法:

DI.prototype.register = function(name, inst) {
    this.store[name] = typeof inst === 'function' ? new inst() : inst;
};

運行測試用例(npm test)後

圖片描述

再次happy

需求3:能夠根據$inject屬性注入實例

咱們的第三個需求依舊是在前一個基礎上作增量:

  • 她有一個運行方法,能夠像AngularJS那樣接受一個待執行函數,而這個函數有一個$inject屬性,該屬性爲數組,指定了要注入給待執行函數的實例名字列表

根據新需求,爲新「行爲」添加測試用例:

it('injecting with $inject attr', function() {
    //她依舊是一個類(可被實例化)
    var app = new DI();

    var man = function() {
        this.fight = function() {
            return 'fighting';
        };
    };

    app.register('man', man);
    app.register('cat', {
        action: function() {
            return 'scratch';
        }
    });
    var msg1,
        msg2;

    var exec = function(w, c) {
        msg1 = w.fight();
        msg2 = c.action();
    };
    exec.$inject = ['man', 'cat'];
    //run方法接受一個函數,該函數的$inject屬性聲明瞭他的依賴
    app.run(exec);

    assert.equal(msg1, 'fighting', 'failed injecting with $inject attr');
    assert.equal(msg2, 'scratch', 'failed injecting with $inject attr');
});

運行測試用例(npm test)後,這固然是錯的:

圖片描述

由於此次咱們run的,壓根不是個數組,而是一個附有$inject屬性的函數,因此src/DI.jsrun方法源碼必須調整:

DI.prototype.run = function(arr) {
    var cb,
        argsName,
        args,
        lastIndex = arr.length - 1;
    //傳入數組時,一切照舊
    if (Array.isArray(arr)) {
        cb = arr[lastIndex];
        argsName = arr.slice(0, lastIndex);
    } else if (arr.$inject) {//傳入函數,並附有$inject屬性時,$inject就是argsName
        cb = arr;
        argsName = arr.$inject;
    }
    //剩下照舊
    args = argsName.map(name => this.store[name]);
    cb.apply(null, args);
};

又成功的邁進了一步,high不high?

圖片描述

最後的需求:根據函數反射注入實例

咱們的第四個需求也是終極需求,特徵以下:

  • 她有一個運行方法,能夠像AngularJS那樣接受一個待執行函數,並能僅根據函數自己注入正確的實例

沒別的,先補充測試用例:

it('injecting with reflection', function() {
    //她依舊是一個類(可被實例化)
    var app = new DI();

    app.register('woman', function() {
        this.cry = function() {
            return 'crying';
        };
    });
    app.register('duck', {
        fly: function() {
            return 'flying';
        }
    });
    var msg1,
        msg2;
    //run方法接受一個函數,該函數僅在參數列表裏以變量名錶達了依賴關係
    app.run(function(woman, duck) {
        msg1 = woman.cry();
        msg2 = duck.fly();
    });

    assert.equal(msg1, 'crying', 'failed injecting woman');
    assert.equal(msg2, 'flying', 'failed injecting duck');
});

不用懷疑,npm test以後,必定又錯了!

圖片描述

回顧咱們上一份實現,彷佛並無考慮run方法接受的「是一個函數,並且這個函數沒有$inject屬性」這種狀況,理論上,再補一個路徑選擇就行了:

DI.prototype.run = function(arr) {
    var cb,
        argsName,
        args,
        lastIndex = arr.length - 1;
    //傳入數組時,一切照舊
    if (Array.isArray(arr)) {
        cb = arr[lastIndex];
        argsName = arr.slice(0, lastIndex);
    } else if (arr.$inject) {//傳入函數,並附有$inject屬性時,$inject就是argsName
        cb = arr;
        argsName = arr.$inject;
    } else {//傳入函數,又沒有$inject屬性時,經過toString反射函數體,利用正則表達式截取依賴列表
        cb = arr;
        argsName = arr
            .toString()
            .match(/\(\s*([a-zA-Z,\s]*)\)/)[1]
            .split(',')
            .map(name => name.trim());
    }
    args = argsName.map(name => this.store[name]);
    cb.apply(null, args);
};

世界終於清淨下來了!!!

圖片描述

總結

本章內容就是利用BDD的開發模式,幫咱們一步步抽絲剝繭,在需求不斷變化的狀況下有條不紊的經過重構代碼來實現新的內容,而又不破壞以前已實現的邏輯。

能夠這麼說,有了測試用例,理清BDD思路,媽媽不再用擔憂你的代碼重構了!

相關文章
相關標籤/搜索