Wie hält man Code mit Fortsetzungen / Rückrufen lesbar?

10

Zusammenfassung: Gibt es einige bewährte Best-Practice-Muster, die ich befolgen kann, um meinen Code trotz der Verwendung von asynchronem Code und Rückrufen lesbar zu halten?


Ich verwende eine JavaScript-Bibliothek, die viele Dinge asynchron erledigt und stark von Rückrufen abhängt. Es scheint, dass das Schreiben einer einfachen Methode "Laden A, Laden B, ..." ziemlich kompliziert und mit diesem Muster schwer zu befolgen ist.

Lassen Sie mich ein (erfundenes) Beispiel geben. Angenommen, ich möchte eine Reihe von Bildern (asynchron) von einem Remote-Webserver laden. In C # / async würde ich so etwas schreiben:

disableStartButton();

foreach (myData in myRepository) {
    var result = await LoadImageAsync("http://my/server/GetImage?" + myData.Id);
    if (result.Success) {
        myData.Image = result.Data;
    } else {
        write("error loading Image " + myData.Id);
        return;
    }
}

write("success");
enableStartButton();

Das Codelayout folgt dem "Ablauf der Ereignisse": Zuerst wird die Startschaltfläche deaktiviert, dann werden die Bilder geladen ( awaitstellt sicher, dass die Benutzeroberfläche reagiert) und dann wird die Startschaltfläche wieder aktiviert.

In JavaScript habe ich mithilfe von Rückrufen Folgendes gefunden:

disableStartButton();

var count = myRepository.length;

function loadImage(i) {
    if (i >= count) {
        write("success");
        enableStartButton();
        return;
    }

    myData = myRepository[i];
    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                return;
            }
            loadImage(i+1); 
        }
    );
}

loadImage(0);

Ich denke, die Nachteile liegen auf der Hand: Ich musste die Schleife in einen rekursiven Aufruf umwandeln, der Code, der am Ende ausgeführt werden soll, befindet sich irgendwo in der Mitte der Funktion, der Code, der den Download ( loadImage(0)) startet, befindet sich ganz unten. und es ist im Allgemeinen viel schwieriger zu lesen und zu folgen. Es ist hässlich und ich mag es nicht.

Ich bin mir sicher, dass ich nicht der erste bin, der auf dieses Problem stößt. Meine Frage lautet daher: Gibt es einige bewährte Best-Practice-Muster, die ich befolgen kann, um meinen Code trotz der Verwendung von asynchronem Code und Rückrufen lesbar zu halten?

Heinzi
quelle
Gibt es einen bestimmten Grund, warum Ihre "asynchronen" Anrufe nacheinander ausgeführt werden müssen? Ist dies eine vereinfachte Version eines anderen Codes?
Izkata
@Izkata: Der Grund ist nur, dass ich nett zum Remote-Server sein wollte (= ihn nicht mit Hunderten von gleichzeitigen Anfragen bombardieren). Es ist keine in Stein gemeißelte Anforderung. Ja, es ist eine vereinfachte Version des Codes, LoadImageAsyncin der Tat ein Aufruf Ext.Ajax.requestvon Sencha Touch.
Heinzi
1
In den meisten Browsern können Sie den Server ohnehin nicht hämmern. Sie stellen lediglich die Anforderungen in die Warteschlange und starten den nächsten, wenn einer der vorherigen abgeschlossen ist.
Izkata
Gott! viele schlechte Ratschläge hier. Keine Menge von Designmustern wird Ihnen helfen. Schauen Sie in async.js , async.waterfallist Ihre Antwort.
Salman von Abbas

Antworten:

4

Es ist höchst unwahrscheinlich, dass Sie mit einfachen js das gleiche Maß an Prägnanz und Ausdruckskraft erreichen können, wenn Sie mit Rückrufen arbeiten, die C # 5 hat. Der Compiler erledigt die Arbeit, indem er das gesamte Boilerplate für Sie schreibt, und bis die js-Laufzeiten dies tun, müssen Sie hier und da noch einen gelegentlichen Rückruf durchführen.

Möglicherweise möchten Sie Rückrufe jedoch nicht immer auf das Niveau der Einfachheit von linearem Code reduzieren - das Herumwerfen von Funktionen muss nicht hässlich sein, es gibt eine ganze Welt, die mit dieser Art von Code arbeitet, und sie bleiben ohne asyncund vernünftig await.

Verwenden Sie zum Beispiel Funktionen höherer Ordnung (mein js kann etwas rostig sein):

// generic - this is a library function
function iterateAsync(iterator, action, onSuccess, onFailure) {
var item = iterator();
if(item == null) { // exit condition
    onSuccess();
    return;
}
action(item,
    function (success) {
        if(success)
            iterateAsync(iterator, action, onSuccess, onFailure);
        else
            onFailure();
    });
}


// calling code
var currentImage = 0;
var imageCount = 42;

// you know your library function expects an iterator with no params, 
// and an async action with the current item and its continuation as params
iterateAsync(
// this is your iterator
function () {   
    if(currentImage >= imageCount)
        return null;
    return "http://my/server/GetImage?" + (currentImage++);
},

// this is your action - coincidentally, no adaptor for the correct signature is necessary
LoadImageAsync,

// these are your outs
function () { console.log("All OK."); },
function () { console.log("FAILED!"); }
);
vski
quelle
2

Ich habe ein bisschen gebraucht, um zu entschlüsseln, warum du es so machst, aber ich denke, das könnte nahe an dem liegen, was du willst?

function loadImages() {
   var countRemainingToLoad = 0;
   var failures = 0;

   myRepository.each(function (myData) {
      countRemainingToLoad++;

      LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) {
            if (success) {
                myData.Image = data;
            } else {
                write("error loading image " + myData.Id);
                failures++;
            }
            countRemainingToLoad--;
            if (countRemainingToLoad == 0 && failures == 0) {
                enableStartButton();
            }
        }
    );
}

disableStartButton();
loadImages();

Zunächst werden so viele AJAX-Anforderungen wie möglich gleichzeitig ausgeführt, und es wird gewartet, bis alle abgeschlossen sind, bevor die Schaltfläche Start aktiviert wird. Dies ist schneller als ein sequentielles Warten und meiner Meinung nach viel einfacher zu befolgen.

BEARBEITEN : Beachten Sie, dass dies voraussetzt, dass Sie .each()verfügbar sind, und dass dies myRepositoryein Array ist. Achten Sie darauf, welche Schleifeniteration Sie hier anstelle dieser verwenden, falls diese nicht verfügbar ist. Diese nutzt die Schließungseigenschaften für den Rückruf. Ich bin mir jedoch nicht sicher, was Sie zur Verfügung haben, da LoadImageAsynces Teil einer spezialisierten Bibliothek zu sein scheint - ich sehe keine Ergebnisse in Google.

Izkata
quelle
+1, ich habe .each()verfügbar, und jetzt, da Sie es erwähnen, ist es nicht unbedingt erforderlich, das Laden nacheinander durchzuführen. Ich werde auf jeden Fall Ihre Lösung ausprobieren. (Obwohl ich die Antwort von vski akzeptieren werde, da sie näher an der ursprünglichen, allgemeineren Frage liegt.)
Heinzi
@Heinzi Einverstanden darüber, wie unterschiedlich es ist, aber (ich denke) dies ist auch ein anständiges Beispiel dafür, wie verschiedene Sprachen unterschiedliche Möglichkeiten haben, mit derselben Sache umzugehen. Wenn sich etwas beim Übersetzen in eine andere Sprache unangenehm anfühlt, gibt es wahrscheinlich einen einfacheren Weg, dies mit einem anderen Paradigma zu tun.
Izkata
1

Haftungsausschluss: Diese Antwort beantwortet nicht speziell Ihr Problem. Sie ist eine allgemeine Antwort auf die Frage: "Gibt es einige bewährte Best-Practice-Muster, die ich befolgen kann, um meinen Code trotz Verwendung von asynchronem Code und Rückrufen lesbar zu halten?"

Soweit ich weiß, gibt es kein "gut etabliertes" Muster, um damit umzugehen. Ich habe jedoch zwei Arten von Methoden gesehen, um die verschachtelten Rückruf-Albträume zu vermeiden.

1 / Benannte Funktionen anstelle von anonymen Rückrufen verwenden

    function start() {
        mongo.findById( id, handleDatas );
    }

    function handleDatas( datas ) {
        // Handle the datas returned.
    }

Auf diese Weise vermeiden Sie die Verschachtelung, indem Sie die Logik der anonymen Funktion in einer anderen Funktion senden.

2 / Verwenden einer Flow Management-Bibliothek. Ich benutze gerne Step , aber es ist nur eine Frage der Präferenz. Es ist übrigens das, was LinkedIn verwendet.

    Step( {
        function start() {
            // the "this" magically sends to the next function.
            mongo.findById( this );
        },

        function handleDatas( el ) {
            // Handle the datas.
            // Another way to use it is by returning a value,
            // the value will be sent to the next function.
            // However, this is specific to Step, so look at
            // the documentation of the library you choose.
            return value;
        },

        function nextFunction( value ) {
            // Use the returned value from the preceding function
        }
    } );

Ich verwende eine Flow-Management-Bibliothek, wenn ich viele verschachtelte Rückrufe verwende, da diese viel lesbarer ist, wenn viel Code verwendet wird.

Florian Margaine
quelle
0

Einfach ausgedrückt, JavaScript hat nicht den syntaktischen Zucker von await.
Das Verschieben des "End" -Teils in den unteren Bereich der Funktion ist jedoch einfach. und mit einer sofort ausgeführten anonymen Funktion können wir vermeiden, einen Verweis darauf zu deklarieren.

disableStartButton();

(function(i, count) {
    var loadImage = arguments.callee;
    myData = myRepository[i];

    LoadImageAsync("http://my/server/GetImage?" + myData.Id,
        function(success, data) { 
            if (!success) {
                write("error loading image " + myData.Id);

            } else {
                myData.Image = data;
                if (i < count) {
                    loadImage(i + 1, count);

                } else {
                    write("success");
                    enableStartButton();
                    return;

                }

            }

        }
    );
})(0, myRepository.length);

Sie können den Teil "end" auch als Erfolgsrückruf an die Funktion übergeben.

Zigeunerkönig
quelle