2-3 callbacks

要介紹callback之前,要先提到Javascript的特色。Javascript是一種函數式語言(functional language),所有Javascript語言內的函數,都是高階函數(higher order function,這是數學名詞,計算機用語好像是first class function,意指函數使用沒有任何限制,與其他物件一樣)。也就是說,函數可以作為函數的參數傳給函數,也可以當作函數的返回值。這個特性,讓Javascript的函數,使用上非常有彈性,而且功能強大。

callback在形式上,其實就是把函數傳給函數,然後在適當的時機呼叫傳入的函數。Javascript使用的事件系統,通常就是使用這種形式。NodeJS中,有一個物件叫做EventEmitter,這是NodeJS事件處理的核心物件,所有會使用事件處理的函數,都會「繼承」這個物件。(這裡說的繼承,實作上應該像是mixin)他的使用很簡單:
  1. 可以使用 物件.on(事件名稱, callback函數) 或是 物件.addListener(事件名稱, callback函數) 把你想要處理事件的函數傳入
  2. 在 物件 中,可以使用 物件.emit(事件名稱, 參數...) 呼叫傳入的callback函數
這是Observer Pattern的簡單實作,而且跟在網頁中使用DOM的addEventListener使用上很類似,也很容易上手。不過NodeJS是大量使用非同步方式執行的應用,所以程式邏輯幾乎都是寫在callback函數中,當邏輯比較複雜時,大量的callback會讓程式看起來很複雜,也比較難單元測試。舉例來說:

var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory});
p_client.open(function(err, p_client) {
  p_client.dropDatabase(function(err, done) {
    p_client.createCollection('test_custom_key', function(err, collection) {
      collection.insert({'a':1}, function(err, docs) {
        collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) {
          cursor.toArray(function(err, items) {
            test.assertEquals(1, items.length);
            p_client.close();
          });
        });
      });
    });
  });
});

這是在網路上看到的一段操作mongodb的程式碼,為了循序操作,所以必須在一個callback裡面呼叫下一個動作要使用的函數,這個函數裡面還是會使用callback,最後就形成一個非常深的巢狀。

這樣的程式碼,會比較難進行單元測試。有一個簡單的解決方式,是盡量不要使用匿名函數來當作callback或是event handler。透過這樣的方式,就可以對各個handler做單元測試了。例如:

var http = require('http');
var tools = {
cookieParser: function(request, response) {
if(request.headers['Cookie']) {
//do parsing
}
}
};
var server = http.createServer(function(request, response) {
this.emit('init', request, response);
//...
});
server.on('init', tools.cookieParser);
server.listen(8080, '127.0.0.1');

更進一步,可以把tools改成外部module,例如叫做tools.js:

module.exports = {
cookieParser: function(request, response) {
if(request.headers['Cookie']) {
//do parsing
}
}
};

然後把程式改成:

var http = require('http');

var server = http.createServer(function(request, response) {
this.emit('init', request, response);
//...
});
server.on('init', require('./tools').cookieParser);
server.listen(8080, '127.0.0.1');

這樣就可以單元測試cookieParser了。例如使用nodeunit時,可以這樣寫:

var testCase = require('nodeunit').testCase;
module.exports = testCase({
    "setUp": function(cb) {
     this.request = {
     headers: {
     Cookie: 'name1:val1; name2:val2'
     }
     };
     this.response = {};
     this.result = {name1:'val1',name2:'val2'};
        cb();
    },
    "tearDown": function(cb) {
        cb();
    },
    "normal_case": function(test) {
     test.expect(1);
     var obj = require('./tools').cookieParser(this.request, this.response);
     test.deepEqual(obj, this.result);
     test.done();
    }
});

善於利用模組,可以讓程式更好維護與測試。

Comments