web-dev-qa-db-ja.com

コールバック関数:不要な場合のセマンティクスと保守性

JavaScript /Node.JSのコンテキスト内。 noasyncプログラミングが必要な場合、Callback関数を使用するとソースコードの保守性が向上しますか?

たとえば、 プレーンコード は意味的に正確に聞こえ、 2番目のもの その不必要にコールバック関数を使用するよりも、保守/拡張が容易になりますか? ?

平野

var validateId = function ( id ) {

    if ( undefined !== id ) {
        return true;
    } else {
        return false;
    }
}

var setId = function ( id ) {

    if ( true === validateId(id) ) {
        userId = id;
        console.log( "success" );
    } else {
        console.log( "failed: ", "invalid id" );
    };
}

コールバック済み

var validateId = function ( id, success, fail ) {

    if ( undefined !== id ) {
        success( id );
    } else {
        fail( "invalid id" );
    }
}

var setId = function ( id ) {

    validateId( id, function success (validatedId) {
        userId = validatedId;
        console.log( "success" );
    }, function fail ( error ) {
        console.log( "failed: ", error );
    });
}

アップデート#1

読みやすく保守しやすいコードの書き方に関する一般的なアドバイスは探していません。最初の行に書かれているように、Callbacksを使用して(どのプログラミング言語でも使用されていませんが、具体的にはJavaScriptで)コードの保守性が向上するかどうかを確認しています。そして、それがソースコードを意味的により正確にするなら? (validateIdを呼び出し、結果がtrueの場合は、userIdを比較してvalidateIdを呼び出し、userIdsuccess

2
53777A

どちらの解決策も理にかなっています。多くの場合、関数をパラメーターとして使用すると便利です。これにより、あらゆる状況でコールバックを提供する必要があるため、通常、正しいコードを簡単に記述できます。ただし、コールバックを必要とするAPIは、不必要に深いインデントを作成する傾向があります。これは、複数の検証があり、すべてが合格する必要がある場合に、より明白になります。

// simple solution
function validateA(a) { return a !== undefined }
function validateB(b) { return b !== undefined }
function validateC(c) { return c !== undefined }

function frobnicate(a, b, c) {
  if (! validateA(a)) {
    console.log("failed: ", "invalid a");
    return;
  }
  if (! validateB(b)) {
    console.log("failed: ", "invalid b");
    return;
  }
  if (! validateC(c)) {
    console.log("failed: ", "invalid c");
    return;
  }
  console.log("success");
}
// naive callback solution
function validateA(a, onSuccess, onError) {
  return (a !== undefined) ? onSuccess(a) : onError("invalid a");
}
function validateB(b, onSuccess, onError) {
  return (b !== undefined) ? onSuccess(b) : onError("invalid b");
}
function validateC(c, onSuccess, onError) {
  return (c !== undefined) ? onSuccess(c) : onError("invalid c");
}

function frobnicate(a, b, c) {
  return validateA(a, function successA(a) {
    return validateB(b, function successB(b) {
      return validateC(c, function successC(c) {
        console.log("success");
      }, function failC(msg) { console.log("failed: ", msg) });
    }, function failB(msg) { console.log("failed: ", msg) });
  }, function failA(msg) { console.log("failed: ", msg) });
}

このコールバックベースの検証では、制御フローが理解できず、成功ハンドラーがエラーハンドラーの前に来るため、エラー処理は処理している問題から視覚的に分離されます。これはメンテナンスの妨げになります。関数のコールバックをスラップするだけではスケーリングしません。

これを解決するためのパターンは存在します。たとえば、コールバックベースの検証関数を正しく構成するコンビネータを作成できます。

function multipleValidations(validations, onSuccess) {
   function compose(i) {
     if (i >= validations.length) {
       return onSuccess;
     }
     var cont = compose(i + 1);
     var what       = validations[i][0];
     var validation = validations[i][1];
     var onError    = validations[i][2] || function(msg) {};
     return function(x) { return validation(what, cont, onError) };
  }
  return compose(0)();
}

function frobnicate(a, b, c) {
  return multipleValidations([
    [a, validateA, function failA(msg) { console.log("fail: ", msg) }],
    [b, validateB, function failB(msg) { console.log("fail: ", msg) }],
    [c, validateC, function failC(msg) { console.log("fail: ", msg) }]
  ], function success() {
    console.log("success");
  });
}

さて、検証を使用する方がはるかに優れていますが、multipleValidationsヘルパーは醜い(そして自明ではなく、維持するのが難しい)です。同じ検証は、各検証がブール値を返す単なる述語であれば、実装がはるかに簡単になります(ただし、検証でエラーメッセージ自体を定義できず、代わりにエラーコールバックに依存する必要がある場合を除きます)。

function multipleValidations(validations, onSuccess) {
   for (var i = 0; i < validations.length; i++) {
     var what     = validations[i][0];
     var validate = validations[i][1];
     var onError  = validations[i][2] || function fail() {};
     if (! validate(what)) {
       return onError();
     }
   }
   return onSuccess();
}

必要のない場所でコールバックを使用することは、KISS違反(単純で愚かである)だけではありません。コールバックを積極的に邪魔してコードを難読化することがあります。一部のパターンでは、コールバックが必要であり、それらを使用します。 OKがあります。

シンプルなソリューションを選ぶべきもう1つの理由は、検証の両方の表現を簡単に相互に変換できることです。

function predicateToCallback(predicate, errorMessage) {
  return function validate(x, onSuccess, onError) {
    return (predicate(x)) ? onSuccess(x) : onError(errorMessage);
  };
}

function callbackToPredicate(validate) {
  return function predicate(x) {
    return validate(x, function onSuccess() {
      return true;
    }, function onError() {
      return false;
    });
  };
}

元のコードでは、コールバックベースのソリューションは値を返さないため、コールバックを述語に変換することはわずかに困難になります。

function callbackToPredicate(validate) {
  return function predicate(x) {
    val result;
    validate(x, function onSuccess() {
      result = true;
    }, function onError() {
      result = false;
    });
    return result;
  };
}

同じ概念のある表現を別の表現に変換するのはとても簡単なので、より単純な表現から始めるべきです。何らかの理由で必要な場合、コールバックベースのソリューションは関数呼び出しだけです。しかし、おそらく、あなたはそれを必要としないでしょう(YAGNI)。

2
amon