Schreiben Sie Code, der leicht zu entfernen und zu debuggen ist



Einfach zu debuggender Code ist Code, der Sie nicht täuscht. Es ist schwieriger, Code mit verstecktem Verhalten, schlechter Fehlerbehandlung, Unsicherheiten, unzureichender oder übermäßig strukturierter Struktur oder Änderungen zu debuggen. In ausreichend großen Projekten erhalten Sie Code, den Sie nicht verstehen können.

Wenn das Projekt relativ alt ist, stoßen Sie möglicherweise auf Code, den Sie überhaupt vergessen haben, und wenn das Commit-Protokoll nicht vorhanden wäre, würden Sie schwören, dass diese Zeilen nicht von Ihnen geschrieben wurden. Wenn das Projekt wächst, wird es schwieriger, sich daran zu erinnern, was verschiedene Codeteile tun. Und die Situation verschärft sich, wenn der Code nicht das tut, was er zu tun scheint. Und wenn Sie Code ändern müssen, den Sie nicht verstehen, müssen Sie es schwer herausfinden: Debuggen.

Die Fähigkeit, Code zu schreiben, der einfach zu debuggen ist, beginnt mit dem Verständnis, dass Sie sich an nichts erinnern, was zuvor geschrieben wurde.

Regel 0: Guter Code enthält offensichtliche Fehler.


Anbieter weit verbreiteter Technologien behaupten, dass "klaren Code schreiben" "sauberen Code schreiben" bedeutet. Das Problem ist, dass der Grad der „Reinheit“ sehr kontextsensitiv ist. Reiner Code kann im System fest codiert werden, und manchmal wird ein schmutziger Hack geschrieben, damit er leicht deaktiviert werden kann. Manchmal wird der Code als sauber angesehen, weil der gesamte Schmutz irgendwohin geschoben wird. Guter Code ist nicht unbedingt sauber.

Sauberkeit kennzeichnet den Grad des Stolzes (oder der Schande), den der Entwickler in Bezug auf diesen Code erlebt, und nicht die einfache Wartung oder Änderung. Es ist besser, uns einen langweiligen Code zu geben, als einen sauberen, dessen Änderungen offensichtlich sind: Ich habe festgestellt, dass die Leute eher bereit sind, die Codebasis zu ändern, wenn die Frucht niedrig genug hängt und leicht zu pflücken ist. Der beste Code ist möglicherweise der, den Sie gerade angesehen und sofort verstanden haben, wie er funktioniert.

  • Code, der nicht versucht, ein hässliches Problem zu erstellen, um gut auszusehen, oder ein langweiliges Problem, um interessant auszusehen.
  • Code, bei dem die Fehler offensichtlich und das Verhalten klar sind, im Gegensatz zu Code ohne offensichtliche Fehler und mit unklarem Verhalten.
  • Der Code, in dem es dokumentiert ist, in dem es nicht ideal ist, im Gegensatz zu dem Code, der nach Perfektion strebt.
  • Code mit einem so offensichtlichen Verhalten, dass jeder Entwickler unzählige verschiedene Möglichkeiten finden kann, diesen Code zu ändern.

Manchmal ist der Code so böse, dass jeder Versuch, ihn sauberer zu machen, die Situation nur verschärft. Das Schreiben von Code ohne Verständnis der Konsequenzen ihrer Handlungen kann auch als Ritual zum Aufrufen von bequem gepflegtem Code angesehen werden.

Ich möchte nicht sagen, dass sauberer Code schlecht ist, aber manchmal sieht der Wunsch nach Sauberkeit eher so aus, als würde man Müll unter einen Teppich kehren. Das Debuggen von Code ist nicht unbedingt sauber. und mit Überprüfungen oder Fehlerbehandlung überfüllter Code ist selten lesbar.

Regel 1: Es gibt immer Probleme im Computer


Der Computer hat Probleme und das Programm ist beim letzten Lauf abgestürzt.

Die Anwendung muss zuerst sicherstellen, dass sie von einem bekannten, guten und sicheren Zustand ausgeht, bevor sie versucht, etwas zu tun. Manchmal gibt es einfach keine Kopie des Status, weil der Benutzer ihn gelöscht oder den Computer aktualisiert hat. Das Programm stürzte beim letzten Lauf und paradoxerweise auch beim ersten Lauf ab.

Wenn Sie beispielsweise den Status einer Datei lesen oder schreiben, können die folgenden Probleme auftreten:

  • Die Datei fehlt.
  • Die Datei ist beschädigt.
  • Datei einer älteren oder neueren Version.
  • Die letzte Änderung an der Datei ist nicht abgeschlossen.
  • Das Dateisystem lügt dich an.

Diese Probleme sind nicht neu, Datenbanken sind seit der Antike (1970-01-01) auf sie gestoßen. Die Verwendung von SQLite hilft bei der Bewältigung vieler ähnlicher Probleme. Wenn das Programm jedoch bei der letzten Ausführung abstürzt, funktioniert der Code möglicherweise mit fehlerhaften Daten und / oder fehlerhaften Methoden.

Bei geplanten Programmen passiert beispielsweise etwas aus dieser Liste:

  • Aufgrund der Sommerzeit startet das Programm zweimal in einer Stunde.
  • Das Programm wird zweimal gestartet, da der Bediener vergessen hat, dass es bereits ausgeführt wird.
  • Das Programm wird spät gestartet, da nicht genügend Speicherplatz oder mysteriöse Cloud- oder Netzwerkprobleme vorhanden sind.
  • Das Programm läuft länger als eine Stunde, was zu einer Verzögerung bei nachfolgenden Programmaufrufen führen kann.
  • Das Programm startet zur falschen Tageszeit.
  • Das Programm wird unweigerlich kurz vor einer Grenzzeit ausgeführt, z. B. Mitternacht, dem Ende des Monats oder Jahres, und schlägt aufgrund von Rechenfehlern fehl.

Das Erstellen nachhaltiger Software beginnt mit dem Schreiben von Software, die denkt, dass sie beim letzten Mal gefallen ist, und stürzt ab, wenn Sie nicht wissen, was Sie tun sollen. Das Beste daran, eine Ausnahme auszulösen und einen Kommentar im Stil „Dies sollte nicht passieren“ zu hinterlassen, ist, dass Sie, wenn dies unvermeidlich passiert, einen Vorsprung beim Debuggen Ihres Codes haben.

Das Programm ist nicht einmal verpflichtet, sich von einem Fehler zu erholen, es reicht aus, um es aufzugeben und die Situation nicht zu verschlechtern. Kleine Überprüfungen, die Ausnahmen generieren, können Wochen beim Snooping sparen, und eine einfache Sperrdatei kann Stunden bei der Wiederherstellung nach Sicherungen sparen.

Code, der leicht zu debuggen ist, ist:

  • ein Code, der prüft, ob alles in Ordnung ist, bevor er das tut, was er verlangt;
  • Code, der es einfach macht, in einen bekannten Zustand zurückzukehren und es erneut zu versuchen;
  • sowie Code mit Sicherheitsstufen, die dazu führen, dass Fehler so früh wie möglich auftreten.

Regel 2: Ihr Programm kämpft mit sich selbst


Der größte DoS-Angriff in der Geschichte von Google kam von uns selbst (weil unsere Systeme sehr groß sind). Zwar versucht von Zeit zu Zeit jemand, uns auf Stärke zu testen, aber wir können uns dennoch mehr schaden als andere.

Dies gilt für alle unsere Systeme.

Astrid Atkinson, Ingenieurin für langes Spiel

Programme stürzen während des letzten Laufs immer ab, es ist immer nicht genügend Prozessor, Speicher oder Speicherplatz vorhanden. Alle Mitarbeiter hämmern in eine leere Warteschlange, alle versuchen, eine fehlgeschlagene und seit langem veraltete Anforderung zu wiederholen, und alle Server pausieren gleichzeitig während der Speicherbereinigung. Das System ist nicht nur kaputt, es versucht ständig, sich selbst zu kaputt zu machen.

Große Schwierigkeiten können sogar zu einer Überprüfung des Systems führen.

Das Implementieren einer Serverbetriebsprüfung kann einfach sein, jedoch nur, wenn keine Anforderungen verarbeitet werden. Wenn Sie die Dauer der kontinuierlichen Betriebszeit nicht überprüfen, liegt das Programm möglicherweise zwischen den Überprüfungen. Fehler können auch durch Integritätsprüfungen ausgelöst werden: Ich musste Prüfungen schreiben, die zum Absturz des Systems führten, das sie schützen mussten. Zweimal mit einer Differenz von drei Monaten.

Der Fehlerbehandlungscode führt zwangsläufig dazu, dass noch mehr Fehler entdeckt werden, die verarbeitet werden müssen. Viele davon ergeben sich aus der Fehlerbehandlung selbst. In ähnlicher Weise sind Leistungsoptimierungen häufig die Ursache für Engpässe im System. Eine Anwendung, die auf einer Registerkarte verwendet werden kann, wird zu einem Problem, da sie in 20 Kopien gestartet wird.

Ein weiteres Beispiel: Ein Worker in einer Pipeline läuft zu schnell und verbraucht verfügbaren Speicher, bevor der nächste Teil der Pipeline darauf zugreift. Dies kann mit Staus verglichen werden: Sie entstehen aufgrund einer Geschwindigkeitssteigerung, und infolgedessen wächst die Verkehrsüberlastung in die entgegengesetzte Richtung. Optimierungen können also Systeme erzeugen, die unter hohen oder schweren Lasten stehen, oft auf mysteriöse Weise.
Mit anderen Worten: Je schneller das System ist, desto stärker ist der Druck auf das System. Wenn Sie dem System nicht erlauben, ein wenig entgegenzuwirken, wundern Sie sich nicht, wenn es bricht.

Gegenmaßnahmen sind eine der Rückkopplungsformen des Systems. Das Programm, das einfach zu debuggen ist und den Benutzer in die Rückkopplungsschleife einbezieht, ermöglicht es Ihnen, alle Verhaltensweisen innerhalb des Systems zu sehen, zufällig, absichtlich, erwünscht und nicht erwünscht. Sie können diesen Code leicht überprüfen, die damit verbundenen Änderungen sehen und verstehen.

Regel 3: Wenn Sie jetzt etwas mehrdeutig lassen, müssen Sie es später debuggen


Mit anderen Worten, es sollte für Sie einfach sein, die Variablen im Programm zu verfolgen und zu verstehen, was passiert. Nehmen Sie alle Routinen mit Alptraum linearer Algebra, sollten Sie sich bemühen, den Status des Programms so offensichtlich wie möglich darzustellen. Dies bedeutet, dass Sie mitten in einem Programm den Zweck einer Variablen nicht ändern können, da die Verwendung einer Variablen für zwei verschiedene Zwecke eine Todsünde ist.

Dies bedeutet auch, dass Sie das Semi-Prädikat-Problem sorgfältig vermeiden müssen. Verwenden Sie niemals einen einzelnen Wert ( count ), um ein Wertepaar ( boolean , count ) darzustellen. Es ist zu vermeiden, eine positive Zahl für das Ergebnis zurückzugeben und gleichzeitig -1 wenn nichts übereinstimmt. Tatsache ist, dass Sie sich leicht in einer Situation befinden können, in der Sie etwas wie " 0, but true " benötigen (und genau das ist eine Funktion in Perl 5). oder wenn Sie Code erstellen, der sich nur schwer mit anderen Teilen des Systems kombinieren lässt ( -1 für den nächsten Teil des Programms ist möglicherweise kein Fehler, sondern ein gültiger Eingabewert).

Zusätzlich zur Verwendung einer Variablen für zwei Zwecke wird nicht empfohlen, zwei Variablen für denselben Zweck zu verwenden, insbesondere wenn es sich um eine Boolesche Variable handelt. Ich will damit nicht sagen, dass es schlecht ist, zwei Zahlen zum Speichern eines Bereichs zu verwenden, aber die Verwendung von Booleschen Zahlen, um den Status eines Programms anzuzeigen, ist oft eine maskierte Zustandsmaschine.

Wenn ein Zustand nicht von oben nach unten verläuft, dh im Fall eines episodischen Zyklus, ist es am besten, den Zustand mit einer eigenen Variablen zu versehen und die Logik zu löschen. Wenn das Objekt eine Reihe von Booleschen Werten enthält, ersetzen Sie diese durch eine Variable namens state und verwenden Sie enum (oder eine Zeichenfolge, falls erforderlich, irgendwo). if Ausdrücke so aussehen, als if state == name , nicht if bad_name && !alternate_option .

Selbst wenn Sie eine explizite Zustandsmaschine erstellen, besteht die Möglichkeit einer Verwirrung: Manchmal enthält der Code zwei versteckte Zustandsmaschinen. Einmal wurde ich gefoltert, um HTTP-Proxys zu schreiben, bis ich jeden Computer explizit machte, den Verbindungsstatus verfolgte und ihn separat analysierte. Wenn Sie zwei Zustandsautomaten zu einer kombinieren, kann es schwierig sein, einen neuen Zustand hinzuzufügen oder genau zu verstehen, welchen Zustand etwas haben sollte.

Es geht mehr darum, Code zu erstellen, der nicht debuggt werden muss, als um einfaches Debuggen. Wenn Sie eine Liste korrekter Zustände erstellen, ist es viel einfacher, falsche Zustände zu verwerfen, ohne versehentlich einen oder zwei zu verpassen.

Regel 4: Zufälliges Verhalten ist das erwartete Verhalten.


Wenn Sie nicht verstehen, was die Datenstruktur bewirkt, werden diese Wissenslücken von den Benutzern geschlossen: Jedes absichtliche oder zufällige Verhalten des Codes hängt letztendlich von etwas ab. Viele gängige Programmiersprachen unterstützen Hash-Tabellen, die iteriert werden können und in den meisten Fällen die Reihenfolge nach dem Einfügen beibehalten.

In einigen Sprachen entspricht das Verhalten der Hash-Tabelle den Erwartungen der meisten Benutzer und durchläuft die Schlüssel in der Reihenfolge, in der sie hinzugefügt wurden. In anderen Sprachen gibt die Hash-Tabelle bei jeder Iteration die Schlüssel in einer anderen Reihenfolge zurück. In diesem Fall beschweren sich einige Benutzer, dass das Verhalten nicht zufällig genug ist .

Leider wird jede Zufallsquelle in Ihrem Programm irgendwann für statistische Simulationen oder noch schlimmer für die Kryptographie verwendet. und jede Sortierquelle wird zum Sortieren verwendet.

In Datenbanken enthalten einige Bezeichner etwas mehr Informationen als andere. Durch Erstellen einer Tabelle kann der Entwickler zwischen verschiedenen Arten von Primärschlüsseln wählen. Die richtige Wahl ist die UUID oder etwas, das nicht von ihr zu unterscheiden ist. Der Nachteil anderer Optionen besteht darin, dass sie Bestell- und Identifikationsinformationen offenlegen können. Das heißt, nicht nur a == b , sondern a <= b und andere Optionen bedeuten automatische Inkrementierungsschlüssel.

Bei Verwendung eines Auto-Inkrement-Schlüssels weist die Datenbank jeder Zeile in der Tabelle eine Nummer zu und fügt beim Einfügen einer neuen Zeile 1 hinzu. Und das Sortieren ist vage: Die Leute wissen nicht, welcher Teil der Daten kanonisch ist. Mit anderen Worten, sortieren Sie nach Schlüssel oder Zeitstempel? Wie bei einer Hash-Tabelle wählen die Leute selbst die richtige Antwort. Ein weiteres Problem besteht darin, dass Benutzer benachbarte Datensätze mit anderen Schlüsseln leicht vorhersagen können.

Jeder Versuch, die UUID zu überlisten, schlägt jedoch fehl: Wir haben bereits versucht, Postleitzahlen, Telefonnummern und IP-Adressen zu verwenden, und jedes Mal ist dies kläglich gescheitert. Eine UUID erleichtert möglicherweise nicht das Debuggen Ihres Codes, aber weniger zufälliges Verhalten bedeutet weniger Probleme.

Aus den Schlüsseln können Sie nicht nur Informationen zur Bestellung extrahieren. Wenn Sie in der Datenbank Schlüssel basierend auf anderen Feldern erstellen, verwerfen die Benutzer die Daten und stellen sie aus dem Schlüssel wieder her. Und es treten zwei Probleme auf: Wenn der Status des Programms an mehreren Stellen gespeichert wird, können Kopien sehr leicht nicht miteinander übereinstimmen. und die Synchronisierung wird schwieriger, wenn Sie nicht sicher sind, welche geändert werden muss oder welche geändert wurde.

Was auch immer Sie Ihren Benutzern erlauben, sie werden es tun. Das Schreiben von Code, der leicht zu debuggen ist, bedeutet, über Möglichkeiten nachzudenken, ihn zu missbrauchen und wie Menschen im Allgemeinen damit interagieren können.

Regel 5: Debuggen ist eine soziale Aufgabe, vor allem eine technische.


Wenn ein Projekt in Komponenten und Systeme unterteilt ist, kann es viel schwieriger sein, Fehler zu finden. Wenn Sie wissen, wie das Problem auftritt, können Sie die Änderungen in verschiedenen Teilen koordinieren, um das Verhalten zu korrigieren. Um Fehler in großen Projekten zu beheben, müssen sie weniger gefunden als vielmehr von der Existenz dieser Fehler oder von der Möglichkeit der Existenz überzeugt werden.
Es gibt Fehler in der Software, weil niemand ganz sicher ist, wer für was verantwortlich ist. Das heißt, es ist schwieriger, den Code zu debuggen, wenn nichts geschrieben ist. Sie müssen nach allem in Slack fragen, und niemand antwortet, bis ein Experte kommt.

Dies kann mit Planung, Tools, Prozessen und Dokumentation behoben werden.

Planung ist ein Weg, um den Stress des Kontakts, die Incident-Management-Struktur, loszuwerden. Mit den Plänen können Sie Käufer informieren, Personen freigeben, die zu lange in Kontakt waren, Probleme verfolgen und Änderungen vornehmen, um zukünftige Risiken zu verringern. Tools - eine Möglichkeit, die Anforderungen für die Ausführung einiger Arbeiten zu reduzieren, damit andere Entwickler besser darauf zugreifen können. Ein Prozess ist eine Möglichkeit, Verwaltungsfunktionen von einzelnen Teilnehmern zu entfernen und an ein Team weiterzugeben.

Menschen und Interaktionsweisen werden sich ändern, aber Prozesse und Werkzeuge bleiben erhalten, wenn sich das Team verändert. Es ist nicht so, dass eines wichtiger ist als das andere, sondern dass eines darauf ausgelegt ist, Änderungen im anderen zu unterstützen. Der Prozess kann auch verwendet werden, um Kontrollfunktionen aus dem Team zu entfernen. Dies ist nicht immer gut oder schlecht, aber es gibt immer einen Prozess, auch wenn er nicht formuliert ist. Und die Dokumentation ist der erste Schritt, um andere Menschen diesen Prozess ändern zu lassen.

Dokumentation ist mehr als Textdateien. Auf diese Weise können Sie Verantwortung übertragen, wie Sie Mitarbeiter zur Arbeit bringen und wie Sie Änderungen an die von diesen Änderungen Betroffenen melden. Das Schreiben von Dokumentation erfordert mehr Einfühlungsvermögen als beim Schreiben von Code und mehr Fähigkeiten: Es gibt keine einfachen Compiler-Flags oder Typprüfungen, und Sie können problemlos viele Wörter schreiben, ohne etwas zu dokumentieren.

Ohne Dokumentation kann man nicht erwarten, dass andere fundierte Entscheidungen treffen oder sogar den Konsequenzen der Verwendung der Software zustimmen. Ohne Dokumentation, Tools oder Prozesse ist es unmöglich, die Wartungslast zu teilen oder zumindest die Personen zu ersetzen, die das Problem jetzt lösen.

Der Wunsch, das Debuggen zu vereinfachen, gilt nicht nur für den Code selbst, sondern auch für codebezogene Prozesse. Dies hilft zu verstehen, in welche Skin Sie sich begeben müssen, um den Code zu reparieren.

Einfach zu debuggender Code ist leicht zu erklären.


Es gibt eine Meinung, dass Sie es selbst verstehen, wenn Sie jemandem während des Debuggens ein Problem erklären. Dafür brauchen Sie nicht einmal eine andere Person. Die Hauptsache ist, sich zu zwingen, die Situation von Grund auf neu zu erklären und die Wiedergabereihenfolge zu erklären. Und oft reicht dies aus, um die richtige Entscheidung zu treffen.

Wenn. Wenn wir um Hilfe bitten, fragen wir manchmal nicht, was benötigt wird. Dies ist so häufig, dass es als The XY-Problem bezeichnet wird: „ Wie erhalte ich die letzten drei Buchstaben eines Dateinamens? Huh? Nein, ich meinte Expansion . “

Wir sprechen über ein Problem in Bezug auf eine Lösung, die wir verstehen, und wir sprechen über eine Lösung in Bezug auf die Konsequenzen, die wir fürchten. Das Debuggen ist ein schwieriges Verständnis unerwarteter Konsequenzen und alternativer Lösungen. Für einen Programmierer ist es am schwierigsten, zuzugeben, dass er etwas falsch verstanden hat.

Es stellte sich heraus, dass dies kein Compilerfehler war.

Source: https://habr.com/ru/post/de412693/


All Articles