程序編寫過程當中,經常面臨的困境是寫的時候行雲流水,運行的時候捶胸搗腿!javascript
那爲何會出現這種情況?編寫習慣、或者說編寫流程起了重要因素,譬如:沒有測試用例——這表示你在編寫時壓根兒沒想過期刻追蹤編寫內容的正確性、健壯性;也沒考慮過程序如何適應來自PM的花樣需求。java
今天不談理論,咱們就來經過BDD(行爲驅動開發)的模式來完成一個相似AngularJS
裏依賴注入的簡單模塊。經過這種方式,咱們來感覺一下BDD
對於開發過程的幫助。git
BDD(行爲驅動開發)是第二代的、由外及內的、基於拉(pull)的、多方利益相關者的(stakeholder)、多種可擴展的、高自動化的敏捷方法。它描述了一個交互循環,能夠具備帶有良好定義的輸出(即工做中交付的結果):已測試過的軟件。angularjs
好吧,確定有人會問「這他媽是人話麼」。github
那我換個說法,咱們須要這樣一種編程方式,她從需求出發,描述模塊使用的各個場景,並經過測試用例監督其行爲;即使一開始沒法完成全部預計功能,也能夠經過迭代等方式持續交付;並且在需求變動時,也能經過測試用例來確認模塊的健壯性。正則表達式
那麼「測試用例」就是其中的關鍵,常見的BDD測試框架有:mocha,jasmine...。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 });
咱們的第一個需求能夠總結出以下特徵:
她得是一個類(可被實例化)
她得有一個註冊方法,能夠接受兩個參數,分別是一個名字、一個字面量
她還有一個運行方法,能夠像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.js
的TODO
位置
這時候,咱們若是運行測試用例(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); };
來吧,是時候證實本身了:
咱們的第二個需求是在前一個基礎上作增量:
她得有一個註冊方法,能夠接受兩個參數,分別是一個名字、一個字面量或者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
咱們的第三個需求依舊是在前一個基礎上作增量:
她有一個運行方法,能夠像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.js
的run
方法源碼必須調整:
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思路,媽媽不再用擔憂你的代碼重構了!