Was bedeutet es, wenn Code „leicht zu verstehen“ ist?

Sie haben den Ausdruck „leicht zu überlegen“ wahrscheinlich oft genug gehört, um Ihre Ohren bluten zu lassen.

Als ich diesen Ausdruck zum ersten Mal hörte, hatte ich keine Ahnung, was die Person damit meinte.

Bedeutet das Funktionen, die leicht zu verstehen sind?

Bedeutet das, dass Funktionen richtig funktionieren?

Bedeutet das Funktionen, die einfach zu analysieren sind?

Nach einer Weile hatte ich in so vielen Zusammenhängen „leicht zu überlegen“ gehört, dass ich dachte, es sei nur ein weiteres halb bedeutungsloses Entwickler-Schlagwort.

… Aber ist es wirklich bedeutungslos?

Die Wahrheit ist, dass der Ausdruck eine bedeutende Bedeutung hat. Es erfasst eine ziemlich komplexe Idee, was das Dekodieren etwas schwierig macht. Abgesehen von der Schwierigkeit hilft es uns, ein besseres Verständnis dafür zu haben, wie „leicht zu begründender“ Code aussieht, absolut bessere Programme zu schreiben.

Zu diesem Zweck widmet sich dieser Beitrag der Analyse des Ausdrucks „leicht zu begründen“ in Bezug auf die technischen Gespräche, die wir als Entwickler führen.

Das Verhalten Ihres Programms verstehen

Sobald Sie einen Code geschrieben haben, möchten Sie in der Regel auch das Verhalten des Programms, die Interaktion mit anderen Teilen des Programms und die Eigenschaften verstehen, die es aufweist.

Nehmen Sie zum Beispiel den folgenden Code. Dies sollte ein Array von Zahlen mit 3 multiplizieren.

Wie können wir testen, ob es wie beabsichtigt funktioniert? Eine logische Möglichkeit besteht darin, eine Reihe von Arrays als Eingabe zu übergeben und sicherzustellen, dass das Array immer mit jedem Element multipliziert mit 3 zurückgegeben wird.

Sieht soweit gut aus. Wir haben getestet, dass die Funktion das tut, was wir wollen.

Aber woher wissen wir, dass es nicht das tut, was wir nicht wollen? Bei sorgfältiger Prüfung können wir beispielsweise feststellen, dass die Funktion das ursprüngliche Array mutiert.

Ist es das, was wir beabsichtigt haben? Was ist, wenn wir Verweise sowohl auf das ursprüngliche Array als auch auf das resultierende Array benötigen? Schade, denke ich.

Als nächstes wollen wir sehen, was passiert, wenn wir dasselbe Array mehrmals hinter uns lassen. Gibt es für eine bestimmte Eingabe immer das gleiche Ergebnis zurück?

Oh oh. Es sieht so aus, als hätten wir das Array [1, 2, 3] beim ersten Mal an die Funktion übergeben. Es hat [3, 6, 9] zurückgegeben , aber später [49, 98, 147] . Das sind sehr unterschiedliche Ergebnisse.

Das ist , weil die multiplyByThree Funktion auf einem externen Variablen setzt Multiplikator . Also, wenn der äußere Zustand des Programms die Variable bewirkt Multiplikator auf Änderung zwischen den Aufrufen der Funktion multiplyByThree , das Verhalten der Funktion ändert sich auch dann , wenn wir die gleiche Anordnung in die Funktion übergeben.

Eeek. Sieht nicht mehr so ​​gut aus. Lassen Sie uns etwas tiefer graben.

Bisher haben wir perfekte Array-Eingänge getestet. Was wäre, wenn wir dies tun würden:

Was in aller Welt?!?

Das Programm sah an der Oberfläche großartig aus - als wir uns ein paar Minuten Zeit nahmen, um es zu bewerten, war es jedoch eine andere Geschichte.

Wir haben gesehen, dass es manchmal einen Fehler zurückgibt, manchmal dasselbe, was Sie an ihn übergeben haben, und nur gelegentlich das erwartete Ergebnis zurückgibt. Darüber hinaus hat es einige unbeabsichtigte Nebenwirkungen (Mutation des ursprünglichen Arrays) und scheint nicht konsistent zu sein, was es für eine bestimmte Eingabe zurückgibt (da es auf einem externen Status beruht).

Schauen wir uns nun eine etwas andere Funktion von multiplyByThree an :

Genau wie oben können wir die Richtigkeit prüfen.

Sieht soweit gut aus.

Lassen Sie uns auch testen, ob es das tut, was wir nicht wollen. Mutiert es das ursprüngliche Array?

Nee. Das ursprüngliche Array ist intakt!

Gibt es für eine bestimmte Eingabe dieselbe Ausgabe zurück?

Ja! Da die Multiplikatorvariable jetzt im Bereich der Funktion liegt, hat dies keine Auswirkungen auf das Ergebnis , selbst wenn wir eine doppelte Multiplikatorvariable im globalen Bereich deklarieren .

Gibt es dasselbe zurück, wenn wir eine Reihe verschiedener Arten von Argumenten übergeben?

Ja! Jetzt verhält sich die Funktion vorhersehbarer - sie gibt entweder einen Fehler oder ein neues resultierendes Array zurück.

Wie sicher sind wir zu diesem Zeitpunkt, dass diese Funktion genau das tut, was wir wollen? Haben wir alle Randfälle abgedeckt? Versuchen wir noch ein paar:

Verdammt. Sieht so aus, als ob unsere Funktion noch ein wenig Arbeit braucht. Wenn das Array selbst unerwartete Elemente wie undefinierte oder Zeichenfolgen enthält, sehen wir wieder ein seltsames Verhalten.

Versuchen wir, das Problem zu beheben, indem wir eine weitere Prüfung in unserer for-Schleifenprüfung auf ungültige Array-Elemente hinzufügen:

Versuchen Sie mit dieser neuen Funktion diese beiden Randfälle erneut:

Süss. Jetzt wird auch ein Fehler zurückgegeben, wenn eines der Elemente im Array keine Zahlen anstelle einer zufälligen funky Ausgabe sind.

Zum Schluss noch eine Definition

Durch Ausführen der obigen Schritte haben wir langsam eine Funktion aufgebaut, über die man leicht nachdenken kann, da sie folgende Schlüsselqualitäten aufweist:

  1. Hat keine unbeabsichtigten Nebenwirkungen
  2. Verlässt sich nicht auf den externen Zustand oder beeinflusst ihn nicht
  3. Bei gleichem Argument wird immer die gleiche entsprechende Ausgabe zurückgegeben (auch als "referentielle Transparenz" bezeichnet).

Möglichkeiten, wie wir diese Eigenschaften garantieren können

Es gibt viele verschiedene Möglichkeiten, wie wir garantieren können, dass unser Code leicht zu verstehen ist. Schauen wir uns einige an:

Unit-Tests

Erstens können wir Komponententests schreiben, um Codeteile zu isolieren und zu überprüfen, ob sie wie beabsichtigt funktionieren:

Unit-Tests wie diese helfen uns zu überprüfen, ob sich unser Code korrekt verhält, und geben uns eine lebendige Dokumentation darüber, wie kleine Teile des Gesamtsystems funktionieren. Die Einschränkung bei Unit-Tests ist, dass es unglaublich einfach ist, problematische Randfälle zu übersehen, wenn Sie nicht sehr nachdenklich und gründlich sind.

Zum Beispiel hätten wir nie herausgefunden, dass das ursprüngliche Array mutiert wird, wenn wir nicht irgendwie daran gedacht hätten, es zu testen. Unser Code ist also nur so robust wie unsere Tests.

Typen

Zusätzlich zu Tests können wir auch Typen verwenden, um das Nachdenken über Code zu vereinfachen. Wenn wir beispielsweise eine statische Typprüfung für JavaScript wie Flow verwenden, können wir sicherstellen, dass das Eingabearray immer ein Array von Zahlen ist:

Typen zwingen uns, explizit anzugeben, dass das Eingabearray ein Array von Zahlen ist. Sie helfen dabei, Einschränkungen für unseren Code zu schaffen, die viele Arten von Laufzeitfehlern verhindern, wie wir sie zuvor gesehen haben. In unserem Fall müssen wir nicht mehr darüber nachdenken, zu überprüfen, ob jedes Element im Array eine Nummer ist - dies ist eine Garantie, die uns bei Typen gegeben wird.

Unveränderlichkeit

Schließlich können wir auch unveränderliche Daten verwenden. Unveränderliche Daten bedeuten lediglich, dass die Daten nach ihrer Erstellung nicht mehr geändert werden können. Dies hilft, unbeabsichtigte Nebenwirkungen zu vermeiden.

In unserem früheren Beispiel hätte beispielsweise das Eingabearray, wenn es unveränderlich wäre, das unvorhersehbare Verhalten verhindert, bei dem das ursprüngliche Array mutiert ist. Und wenn der Multiplikator unveränderlich wäre, würde dies Situationen verhindern, in denen ein anderer Teil des Programms unseren Multiplikator mutieren kann.

Wir können die Vorteile der Unveränderlichkeit unter anderem durch die Verwendung einer funktionalen Programmiersprache nutzen, die von Natur aus die Unveränderlichkeit gewährleistet, oder durch die Verwendung einer externen Bibliothek wie Immutable.js, die die Unveränderlichkeit zusätzlich zu einer vorhandenen Sprache erzwingt.

Als lustige Erkundung werde ich Elm verwenden, eine typisierte funktionale Programmiersprache, um zu demonstrieren, wie Unveränderlichkeit uns hilft:

Dieses kleine Snippet macht dasselbe wie unsere JavaScript- Funktion multiplyByThree von früher, außer dass es jetzt in Elm ist. Da Elm eine typisierte Sprache ist, sehen Sie in Zeile 6, dass wir die Eingabe- und Ausgabetypen für die Funktion multiplyByThree als eine Liste von Zahlen definieren. Die Funktion selbst verwendet die grundlegende Kartenoperation , um das resultierende Array zu generieren.

Nachdem wir unsere Funktion in Elm definiert haben, führen wir eine letzte Runde der gleichen Tests durch, die wir für unsere frühere Funktion multiplyByThree durchgeführt haben :

Wie Sie sehen können, ist das Ergebnis das, was wir erwartet haben, und das OriginalArray wurde nicht mutiert.

Lassen Sie uns nun Elm für einen Trick werfen und versuchen, den Multiplikator zu mutieren:

Aha! Elm hindert dich daran. Es wirft einen sehr freundlichen Fehler.

Was wäre, wenn wir stattdessen eine Zeichenfolge als Argument übergeben würden?

Sieht so aus, als hätte Elm das auch erwischt. Da wir das Argument als Liste von Zahlen deklariert haben, können wir nur eine Liste von Zahlen übergeben, selbst wenn wir es versucht haben!

Wir haben in diesem Beispiel ein wenig geschummelt, indem wir eine funktionale Programmiersprache verwendet haben, die sowohl Typen als auch Unveränderlichkeit aufweist. Der Punkt, den ich beweisen wollte, ist, dass wir mit diesen beiden Funktionen nicht mehr daran denken müssen, manuell Überprüfungen für alle Randfälle hinzuzufügen, um die drei von uns diskutierten Eigenschaften zu erhalten. Typen und Unveränderlichkeit garantieren, dass wir für uns und im Gegenzug leichter über unseren Code nachdenken können?

Jetzt sind Sie an der Reihe, über Ihren Code nachzudenken

Ich fordere Sie auf, sich einen Moment Zeit zu nehmen, wenn Sie das nächste Mal jemanden sagen hören: "XYZ macht es einfach, über Code nachzudenken" oder "ABC macht es schwierig, über Code nachzudenken". Ersetzen Sie dieses ausgefallene Schlagwort durch die oben genannten Eigenschaften und versuchen Sie zu verstehen, was die Person bedeutet. Welche Eigenschaften hat der Code, über die man leicht nachdenken kann?

Persönlich hat mir diese Übung geholfen, kritisch über Code nachzudenken, und hat mich wiederum motiviert, darüber nachzudenken, wie man Programme schreibt, über die man leichter nachdenken kann. Ich hoffe, dass es auch für Sie so ist!

Ich würde gerne Ihre Gedanken zu anderen Eigenschaften hören, die ich möglicherweise übersehen habe und die Sie für wichtig halten. Bitte hinterlassen Sie Ihr Feedback in den Kommentaren!