Beheben Sie nicht den Fehler, sondern das System

Grace Hopper findet den ersten tatsächlichen Computerfehler, 1947

Bugs sind so ein Mist! Du bist nur rumhängen, versuchst ein cooles neues Ding zu schreiben, und dann kommt jemand und sagt: "Hey, erinnerst du dich an das, was du vorher geschrieben hast und denkst nicht mehr darüber nach? Es ist total kaputt und 100% unserer Kunden hassen dich jetzt, sogar Jennifer! "

Noch schlimmer ist es, wenn Sie einen Fehler beheben und dann sechs Monate später jemand sagt: "Hey, ich denke, das andere Ding ist auch kaputt." Das ist das Schlimmste!

Zu versuchen, Dinge zu haben, die nicht das Schlimmste sind, wenn ich einen Fehler finde, denke ich: „Was für eine Klasse von Fehlern ist das?“ Handelt es sich um einen logischen Fehler? Ein Problem mit dem Modul Foo unter der Annahme, dass etwas an der Modulleiste nicht stimmt? Ein Tippfehler? Parameter in der falschen Reihenfolge übergeben? Ein Missverständnis über Anforderungen?

Einige dieser Fehlerklassen (insbesondere die Anforderungen!) Lassen sich nur schwer systematisch beheben, es gibt jedoch viele vermeidbare Fehler.

Sehen wir uns dazu einige Techniken an!

Ändern des Systems, sodass der Fehler nicht möglich ist

Dies ist derjenige, über den kluge Leute immer kernige, nicht hilfreiche Aussagen machen!

"Es gibt zwei Möglichkeiten, ein Software-Design zu erstellen: Eine Möglichkeit, es so einfach zu gestalten, dass es offensichtlich keine Mängel gibt, und die andere Möglichkeit, es so kompliziert zu gestalten, dass es keine offensichtlichen Mängel gibt."
- Tony Hoare
"Wenn Sie effektivere Programmierer wollen, werden Sie feststellen, dass sie nicht ihre Zeit mit dem Debuggen verschwenden sollten. Sie sollten nicht die Fehler einführen, mit denen sie beginnen."
- Edgar Dijsktra

Dies ist immer das Ziel, aber ich komme bestimmt nicht immer dorthin und ich würde argumentieren, dass Sie nicht immer dorthin gelangen sollten! Angesichts der Wahl zwischen "Nehmen Sie diese 30-minütige Änderung vor, die genau das tut, was wir brauchen, aber unser System weniger elegant macht" und "verbringen Sie einen Monat damit, unser System so zu überarbeiten, dass es so aussieht, als wäre diese Funktion von Anfang an geplant", haben nur sehr wenige Leute die Luxus, letzteres zu pflücken. Der Versand ist wichtig und kann den Unterschied zwischen Konkurrenz und Zurückbleiben ausmachen.

Eine Vereinfachung sollte jedoch immer in Betracht gezogen werden, da auch das gegenteilige Problem auftritt: Sie bleiben zurück, weil Ihr System so kompliziert ist, dass das Vornehmen von Änderungen ein mühsamer und langwieriger Prozess ist.

Die Vorteile von "Code, bei dem ein Fehler nicht möglich ist" liegen auf der Hand. Die Schwierigkeit besteht darin, herauszufinden, wann dies möglich ist. Hier ist ein Beispiel, in dem ich denke, dass das Reparieren des Systems der richtige Aufruf war:

Wir haben einen Typ namens "Kontext", der Informationen zum Anforderungsbereich enthält (z. B., welcher Benutzer die Anforderung stellt, ob er authentifiziert ist usw.). Insbesondere hatte es diese beiden Dinge namens Betreff und Nachrichtentyp. MessageType war der Name des aufgerufenen Dienstes, und Subject war… kompliziert. In einer unserer Apps war es dasselbe wie MessageType, aber in einer anderen wurde es verwendet, um herauszufinden, ob die Nachricht, die ich gerade erhalten habe, eine direkte Antwort auf etwas ist, das ich gesendet habe, oder ob es sich um eine Sendung handelt Mechanismus, den wir haben.

Dies verursachte überall Fehler, da die Leute auf Context schauten und sich überlegten, „hmm, welches davon für mich das Richtige ist“, und manchmal Betreff auswählten, wenn sie MessageType meinten, und dann funktionierte ihr Code aber nur ein Teil der Zeit. Code, der funktioniert, aber nur ein Teil der Zeit ist die schlechteste Art von Code!

Also haben wir den Betreff entfernt und jetzt verfolgen wir, ob dies eine direkte Antwort auf etwas ist, das ich gesendet habe, indem wir beim Abonnieren einen Abschluss verwenden. Jetzt ist diese Art von "Verwendeter Betreff, als ich MessageType meinte" -Fehler nicht möglich, weil Betreff nichts mehr ist!

Die Fehlerklasse "Einige Funktionsargumente in der falschen Reihenfolge übergeben" ist auch ein guter Kandidat für die Behebung von Systemänderungen. Zum Beispiel hatten wir viele Stellen, an denen wir Paginierungsinformationen wie (Offset Int, Limit Int) übergaben, und es war schwer zu merken, in welche Reihenfolge sie gingen, was uns über die Jahre ein paar Fehler verursacht hatte, als die Leute diese Reihenfolge falsch verstanden hatten , so übergeben wir diese nun als verschiedene Typen und das Problem ist vollständig verschwunden, weil der Compiler uns anschreit, wenn wir es falsch machen!

Alle Versionen des Fehlers mit statischer Analyse abfangen

Statische Analysatoren sind Programme, die Ihr Programm untersuchen, ohne es auszuführen. Es stellt sich heraus, dass es viele Arten von Fehlern gibt, die auf diese Weise gefangen werden können!

Unser Backend ist in Go geschrieben, daher gibt es ein paar gute Optionen, die wir verwenden können. Go vet ist das am häufigsten verwendete statische Analysegerät auf Go-Basis, es gibt jedoch auch andere. Wir führen zum Beispiel Staticcheck durch, und das sollten Sie auch! Als wir es einführten, fanden wir eine Reihe von langjährigen Problemen in unserem Code, und als wir es zu unseren kontinuierlichen Integrationsprüfungen vor dem Zusammenführen hinzufügten, verhinderten wir, dass unzählige weitere Fehler überhaupt in den Code eindrangen. Staticcheck und Vet sind beide großartig, da ihre Ziele "Null falsch-positive" die Einführung sehr einfach machen, da Sie nur "den Code verbessern" und nicht "den Code in irgendeiner Weise ändern" müssen, um die statische Analyse zu beschwichtigen tools “oder„ eine Reihe von Filtern hinzufügen, um diese Fehlalarme zu ignorieren “.

Wir führen auch eine Rechtschreibprüfung für unsere Strings-Dateien, Goimports, eine separate Rechtschreibprüfung für unseren Quellcode, Ineffassign, Unconvert und Gosec durch. In Kombination verhindern diese Überprüfungen jede Woche Dutzende von Problemen - meistens pingelige Dinge, aber auch einige echte Fehler.

PRO TIPP: Die einfachste Zeit, einen neuen statischen Analysator einzuführen, ist jetzt! Das Hinzufügen zu neuen, kleinen Projekten ist einfach und beugt Fehlern vor. Je größer Ihre Codebasis wird, desto mehr Probleme müssen Sie lösen, wenn Sie versuchen, neue Überprüfungen einzuführen. Oculus CTO und der Programmier-Assistent John Carmack haben etwas darüber geschrieben, als er mit dem C ++ - Analysator PVS-Studio experimentierte, und das stimmt für uns mit Sicherheit:

„Mir ist aufgefallen, dass jedes Mal, wenn PVS-Studio aktualisiert wurde, etwas in unserer Codebasis mit den neuen Regeln gefunden wurde. Dies scheint darauf hinzudeuten, dass bei einer ausreichend großen Codebasis möglicherweise eine syntaktisch zulässige Fehlerklasse vorhanden ist. In einem großen Projekt ist die Codequalität genauso statistisch wie die physikalischen Materialeigenschaften - überall gibt es Fehler, und Sie können nur hoffen, die Auswirkungen auf Ihre Benutzer so gering wie möglich zu halten. “

Das bedeutet nicht, dass Sie, wenn Sie bereits über eine große Codebasis verfügen, keine neuen Analysetools hinzufügen können, sondern nur, dass es nur schwieriger wird, wenn Sie länger warten.

Schreiben Sie Ihre eigenen statischen Analysatoren

Die statischen Analysatoren anderer Leute sind großartig, weil sie bereits geschrieben wurden. Eine Option, die mehr Leute in Betracht ziehen sollten, ist das Schreiben von Analysatoren nur für Ihre Codebasis!

Lassen Sie mich ein Beispiel dafür zeigen, warum dies nützlich sein könnte - unser String-Lokalisierer unterstützt Vorlagenvariablen zum Ersetzen und sieht folgendermaßen aus:

str: = loc.T ("Hallo, {{.first_name}}, willkommen bei League!", ordne die [string] -Schnittstelle zu {} {"first_name": "Reilly"})

Das würde für einen französischen Benutzer zu "Bonjour, Reilly, Bienvenue à League!" Die Verwendung solcher Vorlagenvariablen ist nützlich, da die Übersetzer dadurch etwas mehr Kontext haben als nur% s, was sich möglicherweise auf die Übersetzung der Phrase auswirkt. Wenn Sie mehrere Parameter haben, müssen sie dies nicht tun erscheinen in der gleichen Reihenfolge in allen Sprachen.

Diese Schnittstelle ist jedoch fehleranfällig! Hier ist ein Fehler, den ich vor ein paar Wochen in unserem Code gefunden habe:

str: = loc.T ("Erinnerung: Sie haben Buchungen für {{.title}} am {{.date}} bestätigt.", Schnittstelle [string] zuordnen {} {"Uhrzeit": Titel, "Datum": tm })

Der Parameter timehere sollte title sein. Dies ist der Name des Termins, den der Benutzer gebucht hat.

Das Beheben dieses Fehlers ist recht einfach, und wir könnten einen Test hinzufügen, dass dieser eine Aufruf jetzt korrekt ist. Es ist jedoch auch einfach, diese Art von Fehler an einer anderen Stelle im Code einzuführen. Beheben wir diesen Fehler stattdessen überall und für immer!

Diese T () - Schnittstelle ist ziemlich praktisch, daher möchte ich sie nicht ändern, aber wir können einen einfachen statischen Analysator einführen, um nach solchen Fehlern zu suchen. Wenn Sie neugierig sind, sieht der vollständige Code so aus (so viel Einrückung!), Aber hier ist eine Zusammenfassung der Funktionen:

  1. Rufen wir eine Funktion mit dem Namen T () auf?
  2. Wenn ja, ist das erste Argument ein String-Literal?
  3. Wenn ja, handelt es sich um eine gültige Go-Vorlage? (Fehler raus wenn nicht)
  4. Wenn ja, sind nachfolgende Argumente zuzuordnen?
  5. Wenn ja, stimmen die Schlüssel für diese Maps mit den Vorlagenparametern in unserem ersten Argument überein? (Fehler raus wenn nicht)

Die 41 Codezeilen des Ganzen und 15 dieser Zeilen dienen nur dazu, aus all diesen verschachtelten Prüfungen herauszukommen. Nicht so schlecht!

Nun, ein Check wie dieser wird niemals den Weg in so etwas wie "go vet" finden, weil eine Funktion namens T () buchstäblich alles tun kann, was Turing-complete ist, und ich bin sicher, dass dieser Check auf wirklich seltsame Weise falsch positive Ergebnisse liefert Codebeispiele, an die ich nicht gedacht habe.

ABER!

Wir schreiben nicht go vet! Wir schreiben etwas, das auf genau einer Codebasis funktionieren muss: unserer. Auf diese Weise können Sie alle möglichen Annahmen treffen, die Sie mit einem Allzweckwerkzeug nicht treffen können! Dies ist übrigens auch der Grund, warum der Rest unseres Analysators nicht Open Source ist. Es ist für den Code eines anderen Benutzers nicht nützlich (ich habe https://github.com/reillywatson/enumcover veröffentlicht, was eine allgemein nützliche Überprüfung darstellt).

Sobald Sie den Dreh raus haben, können Sie diese Art von Check schnell schreiben. Das oben Genannte hat mich vielleicht eine Stunde vom Anfang bis zum Ende gekostet, und jetzt haben wir diese Art von Fehler nie wieder!

PRO TIPP: ast.Print () ist dein bester Freund hier! Ich kann mich nie an die verschiedenen Knoten und deren Namen erinnern, aber das muss ich nicht. Ich schreibe nur das kleinste Beispielprogramm, das mir einfällt, und starte es mit ast.Print (), um zu sehen, wie der Analysebaum aussieht. Dann schreibe ich meine Analysefunktion, damit sie Code auffängt, der dem Analysebaum des Beispielprogramms ähnelt .

Hier sind ein paar andere Arten von Bugs, die wir auf die gleiche Weise finden:

  • Aufruf von fmt.Printf () und Freunden im Produktionscode (wir haben ein internes Logger-Paket, das wir stattdessen verwenden möchten, es bietet mehr Kontext für das Debuggen und behandelt die Rotation der Log-Dateien)
  • Aufrufen bestimmter Funktionen, die eine Schnittstelle benötigen {}, aber das Argument muss unbedingt ein Zeiger sein (es gibt einen ähnlichen Check-in-Test für Unmarshal-Aufrufe, wir verwenden dieselbe Technik, aber mit einer Liste unserer eigenen Funktionen)

Die nächste Version von Go, 1.12, bietet Unterstützung für die Ausführung benutzerdefinierter Analysegeräte, ohne die Leistungskosten für das separate Parsen des AST für jeden von Ihnen erstellten Check (siehe hier für weitere Details). Wir verwenden es noch nicht (wir betreiben ein auf Staticcheck aufgebautes Gurtzeug), aber wir planen, kurz nach der Veröffentlichung von 1.12 darauf umzusteigen.

Fazit

Das Schreiben von Software in großem Maßstab ist schwierig, und die Geschwindigkeit, mit der sich die Dinge ändern, bedeutet, dass Fehler auftreten werden! Wenn Sie jeden Fehler mit der Einstellung "Wie ist das passiert und wie können wir sicherstellen, dass es nicht wieder passiert" behandeln, wird jeder Fehler zu einer Gelegenheit, alles zu verbessern, anstatt nur eine Sache weniger schlimm zu machen.

Wenn Sie gerne mit intelligenten Menschen zusammenarbeiten, um das Gute zu verbessern, ist dies eine Option. Wir stellen ein!