Wie kann man ein Projekt nicht durch schlechte Programmierpraktiken zerstören?
Bartosz Slysz
Software Engineer
Viele Programmierer, die am Anfang ihrer Karriere stehen, halten das Thema der Benennung von Variablen, Funktionen, Dateien und anderen Komponenten für nicht sehr wichtig. Infolgedessen ist ihre Entwurfslogik oft korrekt - Algorithmen laufen schnell und erzeugen den gewünschten Effekt, können aber kaum lesbar sein. In diesem Artikel werde ich kurz versuchen zu beschreiben, woran wir uns bei der Benennung verschiedener Code-Elemente orientieren sollten und wie wir nicht von einem Extrem ins andere fallen.
Warum die Vernachlässigung der Namensgebung die Entwicklung Ihres Projekts (in manchen Fällen) enorm verlängern wird.
Nehmen wir an, dass Sie und Ihr Team übernehmen die Code von anderen Programmierern. Die Projekt you inherit wurde ohne jegliche Liebe entwickelt - es hat gut funktioniert, aber jedes einzelne Element hätte viel besser geschrieben werden können.
Wenn es um die Architektur geht, löst die Codevererbung fast immer Hass und Wut bei den Programmierern aus, die sie übernommen haben. Manchmal ist dies auf die Verwendung aussterbender (oder ausgestorbener) Technologien zurückzuführen, manchmal auf die falsche Denkweise über die Anwendung zu Beginn der Entwicklung und manchmal einfach auf die mangelnden Kenntnisse des verantwortlichen Programmierers.
Auf jeden Fall kann man im Laufe der Projektzeit einen Punkt erreichen, an dem die Programmierer über Architekturen und Technologien wütend werden. Schließlich müssen bei jeder Anwendung nach einiger Zeit einige Teile umgeschrieben oder bestimmte Teile geändert werden - das ist ganz natürlich. Aber das Problem, das den Programmierern die Haare zu Berge stehen lässt, sind die Schwierigkeiten beim Lesen und Verstehen des von ihnen geerbten Codes.
Besonders im Extremfall, wenn Variablen mit einzelnen, bedeutungslosen Buchstaben benannt werden und Funktionen ein plötzlicher Kreativitätsschub sind, der in keiner Weise mit dem Rest der Anwendung übereinstimmt, können Ihre Programmierer durchdrehen. In einem solchen Fall erfordert jede Codeanalyse, die bei korrekter Benennung schnell und effizient durchgeführt werden könnte, eine zusätzliche Analyse der Algorithmen, die beispielsweise für die Erzeugung des Funktionsergebnisses verantwortlich sind. Und eine solche Analyse, auch wenn sie unauffällig ist, vergeudet viel Zeit.
Die Implementierung neuer Funktionen in verschiedenen Teilen der Anwendung bedeutet, dass man den Alptraum der Analyse durchmachen muss. Nach einiger Zeit muss man zum Code zurückkehren und ihn erneut analysieren, weil seine Absichten nicht klar sind und die Zeit, die man zuvor damit verbracht hat, seine Funktionsweise zu verstehen, umsonst war, weil man nicht mehr weiß, was sein Zweck war.
Und so werden wir in einen Tornado der Unordnung hineingezogen, der die Anwendung beherrscht und langsam alle an ihrer Entwicklung Beteiligten auffrisst. Die Programmierer hassen das Projekt, die Projektmanager hassen es zu erklären, warum die Entwicklungszeit immer länger wird, und der Kunde verliert das Vertrauen und wird wütend, weil nichts nach Plan läuft.
Wie kann man sie vermeiden?
Seien wir ehrlich - einige Dinge können nicht übersprungen werden. Wenn wir uns zu Beginn des Projekts für bestimmte Technologien entschieden haben, müssen wir uns darüber im Klaren sein, dass sie mit der Zeit entweder nicht mehr unterstützt werden oder immer weniger Programmierer mit Technologien von vor ein paar Jahren vertraut sind, die langsam veraltet sind. Einige Bibliotheken erfordern bei ihren Aktualisierungen mehr oder weniger umfangreiche Änderungen am Code, die oft einen Strudel von Abhängigkeiten nach sich ziehen, in dem man sich noch mehr verheddern kann.
Andererseits ist es kein so schwarzes Szenario; natürlich werden die Technologien älter, aber der Faktor, der die Entwicklungszeit von Projekten, die sie beinhalten, definitiv verlangsamt, ist weitgehend hässlicher Code. Und natürlich müssen wir hier das Buch von Robert C. Martin erwähnen - das ist eine Bibel für Programmierer, in der der Autor eine Menge guter Praktiken und Prinzipien vorstellt, die befolgt werden sollten, um Code zu erstellen, der nach Perfektion strebt.
Das Wichtigste bei der Benennung von Variablen ist, dass sie klar und einfach ihre Absicht vermitteln. Das hört sich recht einfach an, wird aber manchmal von vielen Leuten vernachlässigt oder ignoriert. Ein guter Name gibt an, was genau die Variable speichern oder die Funktion tun soll - sie darf nicht zu allgemein benannt werden, aber andererseits auch nicht zu einem langen Klotz werden, dessen bloßes Lesen eine ziemliche Herausforderung für das Gehirn darstellt. Nach einiger Zeit mit qualitativ hochwertigem Code erleben wir den Immersionseffekt, bei dem wir in der Lage sind, die Benennung und die Übergabe von Daten an die Funktion unbewusst so zu gestalten, dass das Ganze keine Illusionen darüber aufkommen lässt, welche Absicht dahinter steckt und was das erwartete Ergebnis des Funktionsaufrufs ist.
Eine weitere Sache, die man in JavaScriptist unter anderem ein Versuch, den Code übermäßig zu optimieren, was ihn in vielen Fällen unlesbar macht. Es ist normal, dass einige Algorithmen besondere Sorgfalt erfordern, was oft die Tatsache widerspiegelt, dass die Intention des Codes ein wenig verworrener sein kann. Dennoch sind die Fälle, in denen wir übermäßige Optimierungen benötigen, extrem selten, oder zumindest die, in denen unser Code schmutzig ist. Es ist wichtig, sich daran zu erinnern, dass viele sprachbezogene Optimierungen auf einer etwas niedrigeren Abstraktionsebene stattfinden; zum Beispiel kann die V8-Engine mit genügend Iterationen die Schleifen erheblich beschleunigen. Es sollte betont werden, dass wir im 21. Jahrhundert leben und keine Programme für die Apollo 13-Mission schreiben. Wir haben viel mehr Spielraum beim Thema Ressourcen - sie sind da, um genutzt zu werden (vorzugsweise auf vernünftige Weise :>).
Manchmal bringt die Aufteilung des Codes in Teile wirklich viel. Wenn die Operationen eine Kette bilden, deren Zweck es ist, Aktionen auszuführen, die für eine bestimmte Änderung von Daten verantwortlich sind, kann man sich leicht verirren. Anstatt alles in einem Strang zu machen, können Sie daher auf einfache Weise die einzelnen Teile des Codes, die für eine bestimmte Sache verantwortlich sind, in einzelne Elemente aufteilen. Dadurch wird nicht nur die Absicht der einzelnen Operationen deutlich, sondern Sie können auch Codefragmente testen, die nur für eine Sache zuständig sind und leicht wiederverwendet werden können.
Einige praktische Beispiele
Ich denke, die genaueste Darstellung einiger der obigen Aussagen wird sein, zu zeigen, wie sie in der Praxis funktionieren - in diesem Abschnitt werde ich versuchen, einige schlechte Code-Praktiken zu skizzieren, die mehr oder weniger in gute umgewandelt werden können. Ich werde aufzeigen, was die Lesbarkeit des Codes in manchen Momenten stört und wie man dies verhindern kann.
Der Fluch der Ein-Buchstaben-Variablen
Eine schreckliche Praxis, die leider auch an Universitäten recht verbreitet ist, ist die Benennung von Variablen mit einem einzigen Buchstaben. Es ist schwer, nicht zuzustimmen, dass dies manchmal eine recht bequeme Lösung ist - wir vermeiden unnötiges Nachdenken darüber, wie wir den Zweck einer Variablen bestimmen, und anstatt mehrere oder mehr Zeichen für die Benennung zu verwenden, benutzen wir nur einen Buchstaben - z. B. i, j, k.
Paradoxerweise sind einige Definitionen dieser Variablen mit einem viel längeren Kommentar versehen, aus dem hervorgeht, was der Autor im Sinn hatte.
Ein gutes Beispiel wäre hier die Iteration über ein zweidimensionales Array, das die entsprechenden Werte am Schnittpunkt von Spalte und Zeile enthält.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// ziemlich schlecht
for (let i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// hier ist der Inhalt, aber jedes Mal, wenn i und j verwendet werden, muss ich zurückgehen und analysieren, wofür sie verwendet werden
}
}
// immer noch schlecht, aber lustig
let i; // Zeile
let j; // Spalte
for (i = 0; i < array[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// hier ist der Inhalt, aber jedes Mal, wenn i und j verwendet werden, muss ich zurückgehen und in den Kommentaren nachsehen, wofür sie verwendet werden
}
}
// viel besser
const rowCount = array.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = array[rowIndex];
const columnCount = row.length;
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const column = row[columnIndex];
// hat jemand Zweifel, was was ist?
}
}
Schleichende Über-Optimierung
Eines schönen Tages stieß ich auf einen hochentwickelten Code, der von einem Software-Ingenieur. Dieser Ingenieur hatte herausgefunden, dass das Senden von Benutzerberechtigungen als Zeichenketten, die bestimmte Aktionen spezifizieren, durch einige Tricks auf Bit-Ebene erheblich optimiert werden kann.
Wahrscheinlich wäre eine solche Lösung in Ordnung, wenn das Ziel ein Commodore 64 wäre, aber der Zweck dieses Codes war eine einfache Webanwendung, geschrieben in JS. Es ist nun an der Zeit, diese Eigenart zu überwinden: Nehmen wir an, ein Benutzer hat im gesamten System nur vier Möglichkeiten zur Änderung von Inhalten: Erstellen, Lesen, Aktualisieren, Löschen. Es ist ganz natürlich, dass wir diese Berechtigungen entweder in einer JSON-Form als Schlüssel eines Objekts mit Zuständen oder als Array senden.
Unser schlauer Ingenieur hat jedoch bemerkt, dass die Zahl vier ein magischer Wert in der binären Darstellung ist, und hat es wie folgt herausgefunden:
Die gesamte Tabelle der Fähigkeiten hat 16 Zeilen, ich habe nur 4 aufgeführt, um die Idee der Erstellung dieser Berechtigungen zu vermitteln. Das Lesen der Berechtigungen geht wie folgt:
Was Sie oben sehen, ist nicht WebAssembly-Code. Ich möchte hier nicht missverstanden werden - solche Optimierungen sind eine normale Sache für Systeme, bei denen bestimmte Dinge sehr wenig Zeit oder Speicher (oder beides) benötigen. Webanwendungen hingegen sind definitiv kein Ort, an dem solche Überoptimierungen völlig sinnvoll sind. Ich möchte nicht verallgemeinern, aber bei der Arbeit von Frontend-Entwicklern werden komplexere Operationen, die die Ebene der Bit-Abstraktion erreichen, nur selten durchgeführt.
Er ist einfach nicht lesbar, und ein Programmierer, der einen solchen Code analysieren kann, wird sich sicherlich fragen, welche unsichtbaren Vorteile diese Lösung hat und was beschädigt werden kann, wenn die Entwicklungsteam will es zu einer vernünftigeren Lösung umschreiben.
Außerdem vermute ich, dass die Übermittlung der Berechtigungen als gewöhnliches Objekt es einem Programmierer ermöglichen würde, die Absicht in 1-2 Sekunden zu lesen, während die Analyse dieser ganzen Sache von Anfang an mindestens ein paar Minuten dauern wird. Es wird mehrere Programmierer in dem Projekt geben, jeder von ihnen wird auf dieses Stück Code stoßen müssen - sie werden es mehrmals analysieren müssen, weil sie nach einiger Zeit vergessen werden, welche Magie dort vor sich geht. Lohnt es sich, diese paar Bytes zu speichern? Meiner Meinung nach nicht.
Aufteilen und erobern
Web-Entwicklung wächst rasant, und es gibt keine Anzeichen dafür, dass sich daran bald etwas ändern wird. Wir müssen zugeben, dass die Verantwortung der Front-End-Entwickler in letzter Zeit erheblich zugenommen hat - sie haben den Teil der Logik übernommen, der für die Darstellung der Daten auf der Benutzeroberfläche verantwortlich ist.
Manchmal ist diese Logik einfach, und die von der API bereitgestellten Objekte haben eine einfache und lesbare Struktur. Manchmal erfordern sie jedoch verschiedene Arten von Zuordnungen, Sortierungen und anderen Operationen, um sie an verschiedene Stellen auf der Seite anzupassen. Und das ist der Punkt, an dem wir leicht in den Sumpf fallen können.
Oft habe ich mich dabei ertappt, dass ich die Daten in den Operationen, die ich durchführte, praktisch unlesbar gemacht habe. Trotz der korrekten Verwendung von Array-Methoden und der korrekten Benennung von Variablen ging in den Operationsketten an manchen Stellen fast der Kontext dessen verloren, was ich erreichen wollte. Außerdem mussten einige dieser Operationen manchmal an anderer Stelle verwendet werden, und manchmal waren sie global oder anspruchsvoll genug, um das Schreiben von Tests zu erfordern.
Ich weiß, ich weiß - dies ist kein triviales Stück Code, das leicht veranschaulicht, was ich vermitteln möchte. Und ich weiß auch, dass die Rechenkomplexität der beiden Beispiele leicht unterschiedlich ist, während wir uns in 99% der Fälle darüber keine Gedanken machen müssen. Der Unterschied zwischen den Algorithmen ist einfach, denn beide bereiten eine Karte der Standorte und Gerätebesitzer vor.
Der erste Algorithmus erstellt diese Karte zweimal, während der zweite Algorithmus sie nur einmal erstellt. Und das einfachste Beispiel, das uns zeigt, dass der zweite Algorithmus portabler ist, liegt in der Tatsache, dass wir die Logik der Erstellung dieser Karte für den ersten Algorithmus ändern müssen und z.B. den Ausschluss bestimmter Orte oder andere seltsame Dinge, die Geschäftslogik genannt werden, vornehmen. Im Falle des zweiten Algorithmus ändern wir nur die Art und Weise, wie wir die Karte erhalten, während alle anderen Datenänderungen in den nachfolgenden Zeilen unverändert bleiben. Im Falle des ersten Algorithmus müssen wir jeden Versuch, die Karte zu erstellen, optimieren.
Und dies ist nur ein Beispiel - in der Praxis gibt es viele solcher Fälle, in denen wir ein bestimmtes Datenmodell für die gesamte Anwendung umwandeln oder umgestalten müssen.
Der beste Weg, um zu vermeiden, dass wir mit den verschiedenen geschäftlichen Veränderungen Schritt halten müssen, besteht darin, globale Werkzeuge zu entwickeln, die es uns ermöglichen, die für uns interessanten Informationen auf eine ziemlich generische Weise zu extrahieren. Selbst auf Kosten der 2-3 Millisekunden, die wir durch eine De-Optimierung verlieren könnten.
Zusammenfassung
Der Beruf des Programmierers ist ein Beruf wie jeder andere - jeden Tag lernen wir neue Dinge und machen oft viele Fehler. Das Wichtigste ist, aus diesen Fehlern zu lernen, besser in seinem Beruf zu werden und diese Fehler in Zukunft nicht zu wiederholen. Sie dürfen nicht an den Mythos glauben, dass unsere Arbeit immer fehlerfrei sein wird. Sie können jedoch, basierend auf den Erfahrungen anderer, die Fehler entsprechend reduzieren.
Ich hoffe, dass die Lektüre dieses Artikels Ihnen helfen wird, zumindest einige der schlechte Kodierungspraktiken die ich bei meiner Arbeit erlebt habe. Falls Sie Fragen zu den besten Code-Praktiken haben, können Sie sich an The Codest-Besatzung um Ihre Zweifel zu äußern.