Funzioni di callback: semantica e manutenibilità, quando non sono necessarie [duplicate]

2

Nel contesto di JavaScript / Node.JS; L'utilizzo delle funzioni Callback migliorerà la manutenibilità del codice sorgente, quando c'è no necessario per la programmazione async ?

Ad esempio il codice semplice suona semanticamente più corretto e sarà più facile da mantenere / estendere, piuttosto che secondo uno che inutilmente utilizza le funzioni di callback?

Plain

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" );
    };
}

Callback-ed

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 );
    });
}

Aggiornamento # 1

Non sto cercando un consiglio generale su come scrivere codice leggibile e gestibile. Come scritto nella prima riga, sto specificatamente cercando di vedere se usare Callbacks (nemmeno in alcun linguaggio di programmazione, ma specificamente in JavaScript) migliorerà la manutenibilità del codice? E se ciò rende semanticamente più corretto il codice sorgente? (invoca validateId e se il risultato era true imposta userId confronto per invocare validateId e assegna userId a success )

    
posta 53777A 22.11.2014 - 23:50
fonte

1 risposta

2

Entrambe le soluzioni possono avere senso. L'utilizzo di funzioni come parametri è utile in molti casi e in genere rende più semplice scrivere codice corretto perché sei obbligato a fornire callback per tutte le circostanze. Tuttavia, un'API che richiede i callback tende a creare indentazioni profonde inutilmente. Questo diventa più ovvio quando abbiamo più di una convalida, e tutti devono passare:

// 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) });
}

Con questa convalida basata sul callback, il flusso di controllo è incomprensibile e poiché il gestore di successo arriva prima del gestore degli errori, la gestione degli errori è visivamente separata dal problema che sta trattando. Questo è un ostacolo alla manutenzione. Il semplice schiaffo dei callback sulle funzioni non è scalabile.

Esistono schemi per risolvere questo problema. Ad esempio, potremmo scrivere un combinatore che si occupa di comporre correttamente le funzioni di convalida basate sul callback:

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");
  });
}

Bene, ora è molto più bello usare la convalida, ma l'helper multipleValidations è brutto (e non ovvio e difficile da mantenere). La stessa interfaccia sarebbe stata molto più facile da implementare se ogni convalida fosse solo un predicato che restituisce un valore booleano (eccetto che ora la convalida non può definire il messaggio di errore stesso e deve invece dipendere dal callback dell'errore):

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();
}

L'utilizzo di callback dove non sono richiesti non è solo una violazione KISS (mantenerla semplice e stupida). A volte interferiscono attivamente e offuscano il codice. Alcuni pattern richiedono callback e il loro utilizzo è OK.

Un altro motivo per cui dovremmo preferire la soluzione più semplice è che possiamo facilmente trasformare entrambe le rappresentazioni di convalida l'una nell'altra:

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;
    });
  };
}

Nel codice originale, la soluzione basata sul callback non restituisce un valore, quindi la trasformazione di una callback in un predicato diventa marginalmente più difficile:

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

Poiché è così semplice trasformare una rappresentazione dello stesso concetto nell'altra, dovremmo iniziare con la più semplice. Se per qualche ragione è necessario, la soluzione basata sul callback è solo una funzione chiamata via. Ma molto probabilmente, non ne avrai bisogno (YAGNI).

    
risposta data 23.11.2014 - 13:30
fonte

Leggi altre domande sui tag