2015年09月23日

Node.jsのasync.waterfallの上手い使い方が分からない

最近、お仕事でNode.jsを使っている。Node.jsは、いろいろな処理が非同期処理を前提とした作りとなっており、「ある処理を行った後にする処理」を、「ある処理」を行う関数のコールバック関数として書く必要がある。例えば、ファイルに何か書く場合は、こう。

require('fs');  // ファイルのIOを行うfsモジュール読み込み。

fs.readFile('foo.txt', 'utf8', function(error, data) {
	// エラーの場合は、コールバックの第1引数にエラーを表すオブジェクトが入る
	if(error) {
		console.log("error!");
		return;
	}
	console.log(data);  // ファイルの内容
});

わお! ファイルを読むだけで1つコールバック関数が出てきた。

例に挙げたような単純な物ならば特に問題にはならないだろうが、大体のプログラムは、こんなに簡単ではない。ファイルを読んで、DBのAとBとCテーブル読んで、それらでゴニョゴニョした結果を別のファイルに書き込んで、そんな処理が2つも3つも在って……とやっていくと、出来上がるのは深い深ーいコールバック関数の「谷」である。人はコレを「コールバック地獄」と呼ぶとか呼ばないとか。

コレは拙いよね、どうにかしないとね。という訳で使われるのが、async.jsというライブラリ。というか、その中のwaterfallseriesか。

この内、waterfall関数の効果的な使い方が、イマイチよく分からない。

waterfallには、その第1引数に関数のリストとなる配列、第2引数に最終的な処理を行う関数を指定する。第1引数の関数を順に実行し、何れかの処理に「失敗」した場合、或いは全ての関数の処理に「成功」した場合に、第2引数の関数が呼び出される。

var async = require('async');   // async.jsのロード。

async.waterfall(
	[   // 関数の配列。ここに挙げられた関数が順に実行される。
		process1,
		process2,
		process3,
	],
	// waterfallの第1引数の配列の関数が全て成功する、
	// 或いは何れかが失敗すると実行される。
	final
);

function process1(next) { // 最後の引数は、次に呼ぶ関数を呼び出す関数。

	// 略。いろいろ処理する。

	// 成功の場合は、第1引数に「真偽判定した時に偽になる」値を指定
	// (大体nullが使われる模様)、
	// 第2引数以降に、次の処理に渡したいデータを指定する。
	next(null, 'result1-1', 'result1-2');
}

function process2(arg1, arg2, arg3, next) {
	// 最後の引数より前の引数には、1つ前の処理から渡されたデータになる。
	var value1 = arg1;  // ← 'result1-1'
	var value2 = arg2;  // ← 'result1-2'

	// 略

	// 失敗の場合は、第1引数にエラーを表す値を、
	// 第2引数移行には、最終的な処理(この場合final)に渡すデータを指定。
	next({ value: 123 }, 'result2-1');
}

function process3(next) {
	// (この例の場合は、このコードは実行されない。)

	// waterfallの第1引数にリストアップされた最後の関数のnextは、
	// 最終的な処理(この場合final)を呼び出す。
	next(null, 'result3-1');
}

function final(error, arg) {
	// 第1引数には、直前の処理のコールバック関数(process1~process3で
	// nextと名付けた引数)の第1引数が、
	// それ以降の引数には、直前の処理のコールバック関数の第2引数以降が
	// 渡される。
	var e = error
	var value = arg;    // ← 'result2-1'
}

ここで挙げたコードは、あくまで例なので、「process1」〜「process3」なんて意味を持たない名前になっている。しかし、それぞれの関数は実際には「何をするのか」がハッキリしている筈だ。例えば「(ファイルから)設定を読み込む」「(DBから)条件に合うデータを検索する」「結果を(ファイルに)書き込む」……

さて、waterfallを使ったコードを改修する事になった、とする。例えばprocess1process2の間にprocess1_5が入る事になった。或いはprocess2process3を逆にする必要が生じたのかもしれない。

ここで、waterfallの第1引数の配列にprocess1_5を追加したり、process2process3を並べ直したりすれば解決するかというと、恐らく、そうはならない。各関数の仮引数、及びコールバック関数(next関数)の実引数が、「直前の処理」「直後の処理」に依存するからだ。「process1process2の間にprocess1_5を差し挟む」場合、「process1が呼ぶコールバック関数の引数列」を変更する必要がある。改修とは直接関係ない筈のprocess1process2の中身にも注意を払う必要が出てきてしまう。これは嬉しくない。特にJavaScriptは動的型付け言語なので、ついウッカリ修正を忘れた場合、それと分かりづらいエラーが発生する事になる(「TypeError: object is not a function」とか「TypeError: undefined is not a function」とか。理想的なエラーメッセージは「(対象が)関数ではない」事ではなく「引数の型が想定外」の筈だ)。

どうすれば、こういう危険性を排除してwaterfallを使う事が出来るのだろう……?

posted by 天井冴太 at 22:40| Comment(0) | TrackBack(0) | Tool | 更新情報をチェックする
この記事へのコメント
コメントを書く
コチラをクリックしてください

この記事へのトラックバック