2018年8月26日日曜日

入力データの検証場所に関する議論

Webアプリケーションに限らず、ほぼ全てのアプリケーションでは外部からの入力値のバリデーション(検証)が必要です。
いわゆるMVCアーキテクチャでは、それをControllerでやるのか?Modelでやるのか?についての議論が度々俎上に載ります。

今回はそれについての私見を。

そもそもバリデーションとは

「バリデーション」と一言でいっても複数のステップがあって、それぞれの認識が違っていたりします。
  • パラメータの存在チェック
    • 必須パラメータが全て存在しているか?
    • 値が空文字やnullでも存在しているとみなす
    • 省略可能パラメータが省略されていた場合、デフォルト値を設定
  • 型のチェック
    • 例; 年齢が数値で入力されているか?
    • 型が一致しないが変換できる場合(文字列の"20"が渡された場合など)、必要に応じて変換
  • 定義域のチェック
    • 例; 取得数の上限limitは1以上100以下の整数か?
    • 定義域外の値を必要に応じて定義域に収める
これらのどこまでをバリデーションと考えているのかをまず合わせる必要があります。

validationの語義としてはチェックのみで値の変更(デフォルト値の設定や型変換等)は含まれないという意見もありますが、チェックはControllerで、変更はModelでやるということはまずないと思いますので議論の対象は変わりません。

Expressの例

公私ともに最近一番触れているのがNode.js/Expressなので、これを例にとって説明します。
ExpressではRouterがControllerに相当します(細かい違いはあるかもしれませんが、とりあえずそういうことにしておいてください)

こんなAPIを想定します。
  • 機能: 新規ユーザーの作成
  • リクエスト: POST /users
  • パラメータ: {name: "名前(文字列/必須)", age: 年齢(数値/任意/省略時はnullとみなす)}

1つ目は、Model内で行う方法。
app.post("/users", (req, res, next) = { // req.bodyに nameとageが入っている(はず)
    // createUser() に入力値をそのまま渡して、値のチェックは全てModel内で行う
    createUser(req.body);
});

// Model
function createUser(body) {
    // 値を取り出して検証
    let name = body.name;
    let age = body.age;
    if(typeof name !== "string") {...}
    if(age === undefined) { age = null; }
    else if(typeof age !== "number") {...}

    // 登録処理
    ...
}
2つ目は、Controller内で行う方法。
app.post("/users", (req, res, next) = { // req.bodyに nameとageが入っている(はず)
    let name = req.body.name;
    let age = req.body.age;
    if(typeof name !== "string") {...}
    if(age === undefined) { age = null; }
    else if(typeof age !== "number") {...}

    // createUser() には、検証済みの意味のある値を渡す
    createUser(name, age);
});

// Model
function createUser(name, age) {
    // そのまま登録処理を行う
    ...
}

どっちがいい?

どちらも一長一短ありますが、ここでは後者のControllerで検証する方法を推奨します。

一番大きな理由は、Modelに渡された時点で存在と型が保証されるという点です。
この保証により、TypeScriptやFlowなどの型チェック機構を有効活用でき、安全なコーディングができます。

とはいえ…

とはいっても、この方法だとController側に型チェックなどのロジックが入ってしまうという問題があります。
しかも存在チェックや型チェックは同じようなコードばかりで正直ロジックを書くのが面倒なんですよね。

そこで!

そこで登場するのが 、このブログでも何度か取り上げているnode-adjuster
上で挙げた程度の条件であれば、直感的に簡単に作成できます。
import adjuster from "adjuster";

const constraints = {
    name: adjuster.string(),                          // 文字列
    age: adjuster.number().default(null).minValue(0), // 数値 / 省略可 / 省略時はnull / 0以上
};

app.post("/users", (req, res, next) = { // req.bodyに nameとageが入っている(はず)
    try {
        const adjusted = adjuster.adjust(req.body, constraints);
        createUser(adjusted.name, adjusted.age);
    } catch(err) {
        // 値の検証エラー
    }
});

// Model
function createUser(name, age) {
    // そのまま登録処理を行う
    ...
}
Controllerの中に判定ロジックや制御構造は含まれず、戻り値をそのまま信用してModelに渡せます。 便利でしょう?

こんな便利なライブラリが、何と間もなく正式版としてリリースされます。お楽しみに!

0 件のコメント:

コメントを投稿