Bitte ohne App-Abstürze!

Veröffentlicht: 2020-02-12

Programmierer versuchen, Abstürze in ihrem Code zu vermeiden. Wenn jemand seine Anwendung verwendet, sollte sie nicht abbrechen oder unerwartet beendet werden. Dies ist eine der einfachsten Qualitätsmessungen – wenn eine App oft abstürzt, ist sie wahrscheinlich nicht gut gemacht.

App-Abstürze treten auf, wenn ein Programm im Begriff ist, etwas Undefiniertes oder Schlechtes zu tun, z . B. einen Wert durch Null zu dividieren oder auf eingeschränkte Ressourcen auf einem Computer zuzugreifen. Es kann auch ausdrücklich von dem Programmierer durchgeführt werden, der die Anwendung geschrieben hat. „Das wird nie passieren, also überspringe ich es“ – das ist durchaus gängige und nicht ganz unvernünftige Denkweise. Es gibt einige Fälle, die einfach nicht eintreten können, niemals, bis … es passiert.

Gebrochene Versprechungen

Einer der häufigsten Fälle, in denen wir wissen, dass etwas nicht passieren kann, sind APIs. Wir haben uns zwischen Backend und Frontend geeinigt – das ist die einzige Serverantwort, die Sie auf diese Anfrage erhalten können. Betreuer dieser Bibliothek haben dieses Funktionsverhalten dokumentiert. Etwas anderes kann die Funktion nicht. Beide Denkweisen sind richtig, aber beide können Probleme verursachen.

Wenn Sie eine Bibliothek verwenden, können Sie sich auf Sprachwerkzeuge verlassen, die Ihnen helfen, alle möglichen Fälle zu bewältigen. Wenn die von Ihnen verwendete Sprache keinerlei Typprüfung oder statische Analyse aufweist, müssen Sie sich selbst darum kümmern. Sie können dies jedoch vor dem Versand an die Produktionsumgebung überprüfen, sodass dies keine große Sache ist. Das kann schwierig sein, aber Sie lesen Änderungsprotokolle, bevor Sie Ihre Abhängigkeiten aktualisieren und Komponententests schreiben, richtig? Entweder Sie verwenden oder erstellen eine Bibliothek, je strenger Sie die Typisierung bereitstellen können, desto besser für Ihren Code und andere Programmierer.

Backend-Frontend-Kommunikation ist etwas schwieriger. Es ist oft lose gekoppelt, sodass Änderungen auf einer Seite leicht vorgenommen werden können, ohne sich bewusst zu sein, wie sich dies auf die andere Seite auswirkt. Änderungen am Backend können Ihre Annahmen am Frontend oft zunichte machen, und beide werden oft separat verteilt. Es muss böse enden. Wir sind nur Menschen und es kommt manchmal vor, dass wir die andere Seite nicht verstanden oder vergessen haben, ihnen von dieser kleinen Veränderung zu erzählen. Auch das ist keine große Sache mit der richtigen Netzwerkhandhabung – die Dekodierungsantwort wird fehlschlagen und wir wissen, wie man damit umgeht. Selbst der beste Dekodierungscode kann jedoch durch schlechtes Design beeinträchtigt werden …

Teilfunktionen. Schlechtes Design.

„Wir werden hier zwei boolesche Variablen haben: ‚isActive‘ und ‚canTransfer‘, natürlich können Sie nicht übertragen, wenn es nicht aktiv ist, aber das ist nur ein Detail.“ Hier beginnt es, unser schlechtes Design, das hart treffen kann. Jetzt wird jemand eine Funktion mit diesen beiden Argumenten erstellen und einige darauf basierende Daten verarbeiten. Die einfachste Lösung ist … einfach bei einem ungültigen Zustand abstürzen, das sollte nie passieren, also sollte es uns egal sein. Manchmal kümmern wir uns sogar darum und hinterlassen einen Kommentar, um das Problem später zu beheben oder zu fragen, was passieren soll, aber es kann schließlich versendet werden, ohne diese Aufgabe abzuschließen.

 // Pseudocode
function doTransfer(Bool isActive, Bool canTransfer) {
  Wenn ( isActive und canTransfer ) {
    // etwas für die Übertragung verfügbar machen
  } else if (nicht isActive und nicht canTransfer) {
    // etwas für die Übertragung tun nicht verfügbar
  } else if ( isActive und nicht canTransfer ) {
    // etwas für die Übertragung tun nicht verfügbar
  } else { // auch bekannt als ( nicht isActive und canTransfer )
    // es gibt vier mögliche Zustände
    // Dies sollte nicht passieren, die Übertragung sollte nicht verfügbar sein, wenn sie nicht aktiv ist
    Absturz()
  }
}

Dieses Beispiel mag albern aussehen, aber manchmal gerät man in eine Art Falle, die etwas schwieriger zu erkennen und zu lösen ist als diese. Am Ende erhalten Sie eine sogenannte Teilfunktion. Dies ist eine Funktion, die nur für einige ihrer möglichen Eingaben definiert ist, die andere ignorieren oder mit ihnen zusammenstoßen. Sie sollten partielle Funktionen immer vermeiden (bitte beachten Sie, dass in dynamisch typisierten Sprachen die meisten Funktionen als partiell behandelt werden können). Wenn Ihre Sprache kein korrektes Verhalten mit Typprüfung und statischer Analyse sicherstellen kann, kann es nach einiger Zeit auf unerwartete Weise zum Absturz kommen. Code entwickelt sich ständig weiter und Annahmen von gestern sind heute möglicherweise nicht mehr gültig.

Schnell scheitern. Oft scheitern.

Wie können Sie sich schützen? Die beste Verteidigung ist der Angriff! Es gibt diesen schönen Spruch: „Fail fast. Scheitere oft.“ Aber waren wir uns nicht gerade einig, dass wir App-Abstürze, Teilfunktionen und schlechtes Design vermeiden sollten? Erlang OTP gibt Programmierern den mythischen Vorteil, dass es sich nach unerwarteten Zuständen selbst heilt und während der Ausführung aktualisiert. Das können sie sich leisten, aber nicht jeder hat diesen Luxus. Warum sollten wir also schnell und oft scheitern?

Zunächst einmal, um diese unerwarteten Zustände und Verhaltensweisen zu finden. Wenn Sie nicht überprüfen, ob Ihr App-Status korrekt ist, kann dies zu noch schlimmeren Ergebnissen als zum Absturz führen!

Zweitens, um anderen Programmierern zu helfen, auf derselben Codebasis zusammenzuarbeiten . Wenn Sie gerade allein in einem Projekt sind, kommt vielleicht jemand anderes nach Ihnen. Sie könnten einige Annahmen und Anforderungen vergessen. Es ist ziemlich üblich, die bereitgestellte Dokumentation nicht zu lesen, bis alles funktioniert, oder interne Methoden und Typen überhaupt nicht zu dokumentieren. In diesem Zustand ruft jemand eine der verfügbaren Funktionen mit einem unerwarteten, aber gültigen Wert auf. Nehmen wir zum Beispiel an, wir haben eine Wartefunktion, die einen beliebigen ganzzahligen Wert akzeptiert und diese Anzahl von Sekunden wartet. Was ist, wenn jemand '-17' an ihn übergibt? Wenn es nicht sofort danach abstürzt, kann dies zu schwerwiegenden Fehlern und ungültigen Zuständen führen. Wartet es ewig oder gar nicht?

Der wichtigste Teil des absichtlichen Absturzes ist es , es elegant zu machen . Wenn Ihre Anwendung abstürzt, müssen Sie einige Informationen angeben, um eine Diagnose zu ermöglichen. Es ist ziemlich einfach, wenn Sie einen Debugger verwenden, aber Sie sollten eine Möglichkeit haben, App-Abstürze ohne ihn zu melden. Sie können Protokollierungssysteme verwenden, um diese Informationen zwischen Anwendungsstarts beizubehalten oder sie extern einzusehen.

Der zweitwichtigste Teil des absichtlichen Absturzes besteht darin, dies in der Produktionsumgebung zu vermeiden …

Nicht versagen. Je.

Sie werden Ihren Code schließlich versenden. Man kann es nicht perfekt machen, es ist oft zu teuer, auch nur daran zu denken, Korrektheitsgarantien zu geben. Sie sollten jedoch sicherstellen, dass es sich nicht verhält oder abstürzt. Wie können Sie das erreichen, da wir bereits beschlossen haben, schnell und oft zu stürzen?

Ein wichtiger Teil des absichtlichen Absturzes besteht darin, dies nur in Nicht-Produktionsumgebungen zu tun . Sie sollten Zusicherungen verwenden, die in Produktionsbuilds Ihrer Anwendung entfernt werden. Dies hilft bei der Entwicklung und ermöglicht das Erkennen von Problemen, ohne die Endbenutzer zu beeinträchtigen. Es ist jedoch immer noch besser, manchmal abzustürzen, um ungültige Anwendungszustände zu vermeiden. Wie können wir das erreichen, wenn wir bereits Teilfunktionen gemacht haben?

Undefinierte und ungültige Zustände unmöglich darstellbar machen und ansonsten auf gültige zurückgreifen. Das mag einfach klingen, erfordert aber viel Überlegung und Arbeit. Egal wie viel es ist, es ist immer weniger als nach Fehlern zu suchen, vorübergehende Korrekturen vorzunehmen und … Benutzer zu nerven. Es wird automatisch einige der Teilfunktionen weniger wahrscheinlich machen.

 // Pseudocode
Funktion doTransfer (Zustandszustand) {
  Schalter (Zustand) {
    Fall State.canTransfer {
      // etwas für die Übertragung verfügbar machen
    }
    Fall State.cannotTransfer {
      // etwas für die Übertragung tun nicht verfügbar
    }
    Fall State.notActive {
      // etwas für die Übertragung tun nicht verfügbar
    }
    // Es ist unmöglich, die Übertragung verfügbar darzustellen, ohne aktiv zu sein
    // Es gibt nur drei mögliche Zustände
  }
}

Wie können Sie ungültige Zustände unmöglich machen? Wählen wir zwei der vorherigen Beispiele aus. Im Fall unserer beiden booleschen Variablen 'isActive' und 'canTransfer' können wir diese beiden in eine einzelne Aufzählung umwandeln. Es wird alle möglichen und gültigen Zustände erschöpfend darstellen. Auch dann kann jemand undefinierte Variablen senden, aber das ist viel einfacher zu handhaben. Es wird ein ungültiger Wert sein, der nicht in unser Programm importiert wird, anstatt dass ein ungültiger Zustand übergeben wird, der alles schwieriger macht.

Unsere Wartefunktion kann auch in stark typisierten Sprachen verbessert werden. Wir können dafür sorgen, dass bei der Eingabe nur Ganzzahlen ohne Vorzeichen verwendet werden. Das allein wird alle unsere Probleme beheben, da ungültige Argumente vom Compiler entfernt werden. Aber was ist, wenn Ihre Sprache keine Typen hat? Wir haben einige mögliche Lösungen. Erstens – stürzen Sie einfach ab, diese Funktion ist für negative Zahlen undefiniert und wir werden keine ungültigen oder undefinierten Dinge tun. Wir müssen bei Tests eine unzulässige Verwendung finden. Unit-Tests (die wir sowieso machen sollten) werden hier wirklich wichtig sein. Zweitens – das kann riskant sein, aber je nach Kontext nützlich sein. Wir können auf gültige Werte zurückgreifen, die Assertion in Nicht-Produktions-Builds beibehalten, um ungültige Zustände nach Möglichkeit zu beheben. Es ist möglicherweise keine gute Lösung für Funktionen wie diese, aber wenn wir stattdessen den absoluten Wert einer ganzen Zahl verwenden, vermeiden wir App-Abstürze. Abhängig von der konkreten Sprache kann es auch eine gute Idee sein, stattdessen einen Fehler/eine Ausnahme zu werfen/auszulösen. Es kann sich lohnen, wenn möglich zurückzufallen, selbst wenn der Benutzer einen Fehler sieht, ist dies eine viel bessere Erfahrung als ein Absturz.

Nehmen wir hier noch ein Beispiel. Wenn der Status der Benutzerdaten in Ihrer Frontend-Anwendung in einigen Fällen ungültig wird, ist es möglicherweise besser, eine Abmeldung zu erzwingen und wieder gültige Daten vom Server zu erhalten, anstatt abzustürzen. Der Benutzer könnte dazu gezwungen werden oder in eine endlose Crash-Schleife geraten. Noch einmal – wir sollten in solchen Situationen in Nicht-Produktionsumgebungen behaupten und abstürzen, aber lassen Sie Ihre Benutzer nicht zu externen Testern werden.

Zusammenfassung

Niemand mag abstürzende und instabile Anwendungen. Wir mögen es nicht, sie herzustellen oder zu verwenden. Wenn Sie schnell mit Behauptungen scheitern, die während der Entwicklung und Tests nützliche Diagnosen liefern, werden viele Probleme frühzeitig erkannt. Fallback auf gültige Zustände in der Produktion macht Ihre App viel stabiler. Ungültige Zustände nicht darstellbar zu machen, wird eine ganze Klasse von Problemen beseitigen. Nehmen Sie sich etwas mehr Zeit, um vor der Entwicklung darüber nachzudenken, wie Sie ungültige Zustände entfernen und auf sie zurückgreifen können, und etwas mehr Zeit beim Schreiben, um einige Assertionen einzufügen. Sie können noch heute damit beginnen, Ihre Anwendungen zu verbessern!

Weiterlesen:

  • Vertragsgestaltung
  • Algebraischer Datentyp