async.jsで有名なcaolanさんのNode.js: Style and structureを訳してみました。

async.jsで有名な、caolanさんのNode.js: Style and structureを日本語訳してみました。
( http://caolanmcmahon.com/posts/nodejs_style_and_structure/)

相変わらずのレベルの低さですが、おかしな部分がありましたら、指摘頂けますと幸いです。m(_ _)m
(OmegaTで訳したので、後で、bitbucketかgithubにあれば嬉しい人がいたり要望があれば、対応します)

出来る人には当たり前なのかもしれませんが、かなりイイ勉強になりました。

追記:コメント頂いた部分を修正しました。

Node.jsのスタイルと構造

私は新たなチームでNode.jsのプロジェクトを開始しようとしていました。これはNodeでの作業でいくつかのヒントをまとめる良い機会かもしれないと思ったのが始まりです。これは、通常の意味でのスタイルガイドではありません。あなたが既にJavaScriptのインデントおよび他のすべての宗教の立場で自分の意見を持っていると仮定し、私が本質的ないくつかのNodeの仕様をフォーカスするつもりです。

厄介な中かっこを配置する場所を含む、より完全なスタイルガイドの定義を探しているなら、次のいくつかを読むことをお勧めします

1. thisとnewを避ける

これはコーディングスタイルの上言い逃れのように思えるかもしれませんが、Node.jsで、関数が簡単に作成でき、再利用するための重要な手段です。

Node.jsは多くのコールバックを渡すと、制御フローを管理する為、高レベル関数を使用するため、関数が特定のコンテキストにバインドすることなく、移動が簡単に確認できなければなりません。クロージャ オブジェクトのメソッドを好むし、クロージャで明示的な引数を好みます。

// prototypeの 'this'を使用した不自然な例

function DB(url) {
    this.url = url;
}

DB.prototype.info = function (callback) {
    http.get(this.url + '/info', callback);
};

async.parallel([
    function (cb) {
        new DB('http://foo').info(cb);
    },
    function (cb) {
        new DB('http://bar').info(cb);
    }
], ...);
// クロージャを使った同様の例

function DB(url) {
    return { info: async.apply(http.get, url + '/info') };
}

async.parallel([
    DB('http://foo').info,
    DB('http://bar').info
], ...);


// すべての引数を明示的にすること

var dbInfo = function (url, callback) {
    http.get(url + '/info', callback);
};

async.map(['http://foo', 'http://bar'], dbInfo, ...);

すべての引数が明示された最終的なスタイルは、簡単にURLの任意のリストを渡し、それらすべてに同じオペレーションを実行することができます。関数的なスタイルを使用すると、このように関数を組み合わせることになり、とても簡単になります。

もちろん、全ての引数を明示的に作ることが望ましくないケースもあり、その場合、コンストラクタとプロトタイプを使用すると、より効率的です。私は単に前者より後者のスタイルを好むし、必要に応じてこれらの機能の使用を正当化することをお勧めします。

2. 小さな関数を多くする

asyncのような制御フローライブラリ探しをはじめる前に、単に小さなコンポーネントに関数を分割することによって「コールバック地獄」の多くを飼いならすことができます。

// 4つの非同期操作を含む深くネストされた関数

function convertJsonToCsv(filename, target, callback) {
    readFile(filename, function (err, content) {
        if (err) {
            return callback(err);
        }
        parseJson(content, function (err, data) {
            if (err) {
                return callback(err);
            }
            convertToCsv(data, function (err, csv) {
                if (err) {
                    return callback(err);
                }
                writeFile(target, csv, callback);
            });
        });
    });
}
// 同じ機能をより小さなパーツに分割

function convertJsonToCsv(filename, target, callback) {
    readJsonFile(filename, function (err, data) {
        if (err) {
            return callback(err);
        }
        writeCsvFile(target, data, callback);
    });
}

function readJsonFile(filename, callback) {
    readFile(filename, function (err, content) {
        if (err) {
            return callback(err);
        }
        parseJson(content, callback);
    });
}

function writeCsvFile(target, data, callback) {
    convertToCsv(data, function (err, csv) {
        if (err) {
            return callback(err);
        }
        writeFile(target, csv, callback);
    });
}

上記の例のように、一度に2つの非同期のオペレーションをする機能を抑える、またはリストを1度イテレーション(async.mapまたは類似の使用)することをお勧めします。より小さな関数を有することで後で容易に新しい方法で再結合することができます。

もちろん、さらに非同期制御フロー·ライブラリを使用してコードをクリーンアップすることができます。しかし、これは可読性をあげるための最初のステップです。

3. 一貫した非同期API

コードを予測可能にしておくために、関数は、常に非同期、または常に同期している必要があります。次の点を考慮します。

var CACHE = {};

function getRecord(id, callback) {
    if (CACHE[id]) {
        return CACHE[id];
    }
    http.get('http://foo/' + id, callback);
}

誰かがこの関数を使用する場合、完了しないコードがあり、キャッシュされたケースを非常に簡単に見落とします。

function getMyRecord(user, callback) {
    getRecord('record-' + user.id, callback);
}

getMyRecord を初めて呼び出す時は、正常に完了しますが、 2度目はコールバックが呼ばれれることはありません。(GetRecordは、キャッシュされた結果を直ちに返しているため)これは、リクエストの実行を停止することがあります。

もう一つの懸案が getRecord 関数の実行順です。

function getMyRecord(user, callback) {
    var r = getRecord('record-' + user.id, function (err, record) {
        if (record) {
            record.got = true;
        }
        return callback(err, record);
    });
    if (r) {
        r.got = true;
    }
    return r;
}

このようなコードの理由は信じられない程難しいです。 getRecord コールバック中、それ以後(同期である場合)に重複したコードを確実に実装する必要があります。 getMyRecord 関数を呼び出すたびに同じことを行う必要があります。これは、非常に早く、乱雑になり、混乱を招くことになります。

これに対処する正しい方法は、 process.nextTick を使用し、キャッシュされた結果を返すときに getRecord を非同期にします。

var CACHE = {};

function getRecord(id, callback) {
    if (CACHE[id]) {
        return process.nextTick(function () {
            return callback(null, CACHE[id]);
        });
    }
    http.get('http://foo/' + id, callback);
}

非同期の場合を考慮することで、コードは非常に予測しやすくなります。

4. 常にコールバックでエラーをチェックする

コールバック内でエラーチェックが不足していることが、デバッグで混乱する一般的な原因です。問題は、プログラムは実行が継続され、障害がコードの他の場所で発生し、別のエラーメッセージを与えることです。その後、元の場所に戻ってそれをトレースする必要があります。

明示的に、コールバック内のエラー チェックで失敗するバグを考慮しなければなりません。コードレビュープロセスにこのステップを構築することで、簡単に見落とさないようなるので、お勧めします。

// 誤っている!

function writeCsvFile(target, data, callback) {
    convertToCsv(data, function (err, csv) {
        writeFile(target, csv, callback);
    });
}
// 正しい

function writeCsvFile(target, data, callback) {
    convertToCsv(data, function (err, csv) {
        if (err) {
            return callback(err);
        }
        writeFile(target, csv, callback);
    });
}

余分なコードを削減したい場合は、if文1行でエラーかどうかをチェックすることができます。 if (err) return callback(err);. どちらのスタイルを選択しても、コードのこの部分は、それが不足していると容易に気付き、簡単に見つからなければならないので、一貫していることをお勧めします。

5. コールバックの戻り値

通常、関数内の最後にコールバックを呼び出します。 return と同じ意味で考えるかもしれませんがJavaScriptでは、callbackは関数の実行を停止しません。終了すると思っていても、誤ってコールバックを呼び出し、実行を継続させてしまうことは容易にあり得ます。実行の停止が期待どおりかを簡単に見つける方法として、コールバック関数の呼び出しをreturnすることをお勧めします。

// 誤っている!

function writeCsvFile(target, data, callback) {
    convertToCsv(data, function (err, csv) {
        if (err) {
            callback(err); // oops!no return
        }
        // この行が呼び出されると、theres an error
        writeFile(target, csv, callback);
    });
}
// 正しい

function writeCsvFile(target, data, callback) {
    convertToCsv(data, function (err, csv) {
        if (err) {
            return callback(err); // 実行はここで停止します。
        }
        writeFile(target, csv, callback);
    });
}

6. 同期関数だけをスローする

残念ながら、非同期コードに try/catch ブロックを使用することはできません。それは、投げた例外がキャッチされ、トップに浮上することがないことを意味します。uncaughtExceptionハンドラが設定されていない場合、全体のサーバプロセスをkillすることができます。この場合、エラーは、恐らく意味のある、コンテキストを有しておらず、例えば、httpリクエストに適切にレスポンスすることはできません。

常に、常に、常に非同期コールバック関数にエラーを戻します。"エラー コールバックを常にチェック" のルールに従っている限り、エラーは適切な場所で処理されます。

// 誤っている

function getRecord(id, callback) {
    http.get('http://foo/' + id, function (err, doc) {
        if (err) {
            return callback(err);
        }
        if (doc.deleted) {
            // getRecord関数の呼び出しで捕まらないでしょう
            throw new Error('Record has been deleted');
        }
        return callback(null, doc);
    });
}
// 正しい 

function getRecord(id, callback) {
    http.get('http://foo/' + id, function (err, doc) {
        if (err) {
            return callback(err);
        }
        if (doc.deleted) {
            return callback(new Error('Record has been deleted'));
        }
        return callback(null, doc);
    });
}

7. 同期呼び出しでエラーをキャッチ

これは本質的には以前のルールと同じです。例外をスローする可能性があり、同期関数を呼び出しているのであれば、非同期コードの内部には,非同期コードを呼び出すと、例外が発生する場合があります。

例えば、次のように

// 誤っている

function readJson(filename, callback) {
    fs.readFile(filename, function (err, content) {
        if (err) {
            return callback(err);
        }

        // おっと!コンテンツがある場合は、この行は、例外がスローされる可能性が
        // 有効なJSONではない
        var data = JSON.parse(content.toString());

        return callback(null, data);
    });
}
// 正しい

function readJson(filename, callback) {
    fs.readFile(filename, function (err, content) {
        if (err) {
            return callback(err);
        }
        try {
            var data = JSON.parse(content.toString());
        }
        catch (e) {
            return callback(e);
        }
        return callback(null, data);
    });
}

8. コールバック慣例に固執する

非同期コードを取り扱うための他の戦略の使用を促進する少数派の声が、Node.jsメーリングリストとどこか他の所にいます。中には、 "ブロッキングスタイル"のAPIを提供もしてくれます。興味深いものですし、チーム内でpromiseやfiberを使用する可能性があると思いますが、Nodeのコールバックスタイルにこだわった上で検討してください。コールバックは、非同期関数の最後の引数であり、コールバックへの最初の引数はエラーになります。(発生した場合)

大多数 の Node 開発者の大半は このスタイルとコンセンサスに従っています。チーム外部の Node 開発者 がモジュールを使用する場合、 慣例に固執しているくらいの方が楽になるでしょう。

結論

これのほとんどは、おそらく常識的ですが、私はこれらのルールが役に立つことを願っています。