Die sechs Gebote für guten Code: Schreiben Sie Code, der die Zeit überdauert
Veröffentlicht: 2022-03-11Menschen beschäftigen sich erst seit ungefähr einem halben Jahrhundert mit der Kunst und Wissenschaft der Computerprogrammierung. Im Vergleich zu den meisten Künsten und Wissenschaften ist die Informatik in vielerlei Hinsicht noch ein Kleinkind, das gegen Wände läuft, über seine eigenen Füße stolpert und gelegentlich Essen über den Tisch wirft.
Aufgrund seiner relativen Jugend glaube ich, dass wir noch keinen Konsens darüber haben, was eine angemessene Definition von „gutem Code“ ist, da sich diese Definition ständig weiterentwickelt. Einige werden sagen, dass „guter Code“ Code mit 100 % Testabdeckung ist. Andere werden sagen, dass es superschnell ist und eine Killerleistung hat und auf 10 Jahre alter Hardware akzeptabel läuft.
Obwohl dies alles lobenswerte Ziele für Softwareentwickler sind, wage ich es, ein weiteres Ziel in den Mix zu werfen: Wartbarkeit. Insbesondere ist „guter Code“ Code, der von einer Organisation (nicht nur von seinem Autor!) leicht und leicht zu warten ist und länger als nur den Sprint, in dem er geschrieben wurde, überdauert. Das Folgende sind einige Dinge, die ich in meinem entdeckt habe Karriere als Ingenieur bei großen und kleinen Unternehmen in den USA und im Ausland, die mit wartbarer, „guter“ Software zu korrelieren scheinen.
Gebot Nr. 1: Behandeln Sie Ihren Code so, wie Sie möchten, dass der Code anderer Sie behandelt
Ich bin bei weitem nicht die erste Person, die schreibt, dass die primäre Zielgruppe für Ihren Code nicht der Compiler/Computer ist, sondern wer als Nächstes den Code lesen, verstehen, warten und verbessern muss (was in sechs Monaten nicht unbedingt Sie sein werden ). Jeder Ingenieur, der seinen Lohn wert ist, kann Code produzieren, der „funktioniert“; Was einen hervorragenden Ingenieur auszeichnet, ist, dass er effizient wartbaren Code schreiben kann, der ein Unternehmen langfristig unterstützt, und die Fähigkeit besitzt, Probleme einfach, klar und wartbar zu lösen.
In jeder Programmiersprache ist es möglich, guten oder schlechten Code zu schreiben. Angenommen, wir beurteilen eine Programmiersprache danach, wie gut sie das Schreiben von gutem Code erleichtert (es sollte ohnehin eines der obersten Kriterien sein), kann jede Programmiersprache „gut“ oder „schlecht“ sein, je nachdem, wie sie verwendet (oder missbraucht) wird ).
Ein Beispiel für eine Sprache, die von vielen als „sauber“ und lesbar angesehen wird, ist Python. Die Sprache selbst erzwingt ein gewisses Maß an Whitespace-Disziplin und die eingebauten APIs sind zahlreich und ziemlich konsistent. Allerdings ist es möglich, unsägliche Monster zu erschaffen. Beispielsweise kann man eine Klasse definieren und jede einzelne Methode dieser Klasse während der Laufzeit definieren/neu definieren/definieren (oft als Affen-Patching bezeichnet). Diese Technik führt natürlich bestenfalls zu einer inkonsistenten API und schlimmstenfalls zu einem unmöglich zu debuggenden Monster. Man könnte naiv denken: „Klar, aber das tut doch keiner!“ Leider tun sie das, und es dauert nicht lange, pypi zu durchsuchen, bevor Sie auf umfangreiche (und beliebte!) Bibliotheken stoßen, die Monkey Patching als Kern ihrer APIs (missbrauchen) verwenden. Ich habe kürzlich eine Netzwerkbibliothek verwendet, deren gesamte API sich abhängig vom Netzwerkstatus eines Objekts ändert. Stellen Sie sich zum Beispiel vor, Sie rufen client.connect() und erhalten manchmal einen MethodDoesNotExist anstelle von HostNotFound oder NetworkUnavailable .
Gebot Nr. 2: Guter Code ist leicht zu lesen und zu verstehen, in Teilen und im Ganzen
Guter Code ist leicht zu lesen und zu verstehen, ganz oder teilweise, von anderen (und in Zukunft auch vom Autor, der versucht, das „Hab ich das wirklich geschrieben?“- Syndrom zu vermeiden).
Mit „teilweise“ meine ich, dass ich, wenn ich ein Modul oder eine Funktion im Code öffne, in der Lage sein sollte, zu verstehen, was es tut, ohne auch den gesamten Rest der Codebasis lesen zu müssen. Es sollte so intuitiv und selbstdokumentierend wie möglich sein.
Code, der ständig auf winzige Details verweist, die das Verhalten anderer (scheinbar irrelevanter) Teile der Codebasis beeinflussen, ist wie das Lesen eines Buches, in dem Sie am Ende jedes Satzes auf die Fußnoten oder einen Anhang verweisen müssen. Du würdest nie die erste Seite lesen!
Einige andere Gedanken zur „lokalen“ Lesbarkeit:
Gut gekapselter Code ist tendenziell besser lesbar und trennt Bedenken auf allen Ebenen.
Namen sind wichtig. Aktivieren Sie das System des schnellen und langsamen Denkens, mit dem das Gehirn Gedanken bildet, und setzen Sie einige tatsächliche, sorgfältige Überlegungen in Variablen- und Methodennamen ein. Die wenigen zusätzlichen Sekunden können sich erheblich auszahlen. Eine gut benannte Variable kann den Code viel intuitiver machen, während eine schlecht benannte Variable zu Headfakes und Verwirrung führen kann.
Klugheit ist der Feind. Wenn Sie ausgefallene Techniken, Paradigmen oder Operationen (wie List Comprehensions oder ternäre Operatoren) verwenden, achten Sie darauf, sie so zu verwenden, dass Ihr Code besser lesbar und nicht nur kürzer wird.
Konsistenz ist eine gute Sache. Konsistenz im Stil, sowohl in Bezug auf die Platzierung von geschweiften Klammern als auch in Bezug auf Operationen, verbessert die Lesbarkeit erheblich.
Trennung von Bedenken. Ein bestimmtes Projekt verwaltet eine unzählige Anzahl von lokal wichtigen Annahmen an verschiedenen Punkten in der Codebasis. Setzen Sie jeden Teil der Codebasis so wenigen dieser Bedenken wie möglich aus. Angenommen, Sie hatten ein Personenverwaltungssystem, in dem ein Personenobjekt manchmal einen leeren Nachnamen haben kann. Für jemanden, der Code auf einer Seite schreibt, die Personenobjekte anzeigt, könnte das wirklich umständlich sein! Und wenn Sie kein Handbuch mit „Peinlichen und nicht offensichtlichen Annahmen unserer Codebasis“ pflegen (ich weiß, dass ich das nicht weiß), wird Ihr Anzeigeseitenprogrammierer nicht wissen, dass Nachnamen null sein können, und wird wahrscheinlich Code mit einem Nullzeiger schreiben Ausnahme darin, wenn der Nachname als Nullfall auftaucht. Behandeln Sie diese Fälle stattdessen mit gut durchdachten APIs und Verträgen, die verschiedene Teile Ihrer Codebasis verwenden, um miteinander zu interagieren.
Gebot Nr. 3: Guter Code hat ein gut durchdachtes Layout und eine gut durchdachte Architektur, um die Verwaltung des Status offensichtlich zu machen
Der Staat ist der Feind. Warum? Weil es der komplexeste Teil jeder Bewerbung ist und sehr bewusst und durchdacht behandelt werden muss. Häufige Probleme sind Datenbankinkonsistenzen, partielle UI-Updates, bei denen neue Daten nicht überall wiedergegeben werden, außer Betrieb befindliche Operationen oder einfach lähmend komplexer Code mit if-Anweisungen und Verzweigungen überall, was zu schwer lesbarem und noch schwieriger zu wartendem Code führt. Wenn Sie den Zustand auf einen Sockel stellen, der mit großer Sorgfalt behandelt werden muss, und äußerst konsistent und bewusst in Bezug darauf zu sein, wie auf den Zustand zugegriffen und geändert wird, wird Ihre Codebasis dramatisch vereinfacht. Einige Sprachen (z. B. Haskell) erzwingen dies auf programmatischer und syntaktischer Ebene. Sie wären erstaunt, wie sehr sich die Übersichtlichkeit Ihrer Codebasis verbessern lässt, wenn Sie Bibliotheken mit reinen Funktionen haben, die auf keinen externen Zustand zugreifen, und dann eine kleine Oberfläche mit zustandsbehaftetem Code, der auf die äußere reine Funktionalität verweist.

Gebot Nr. 4: Guter Code erfindet das Rad nicht neu, er steht auf den Schultern von Giganten
Bevor Sie möglicherweise ein Rad neu erfinden, denken Sie darüber nach, wie häufig das Problem ist, das Sie lösen möchten, oder die Funktion, die Sie ausführen möchten. Vielleicht hat jemand bereits eine Lösung implementiert, die Sie nutzen können. Nehmen Sie sich die Zeit, über solche Optionen nachzudenken und sie zu recherchieren, sofern angemessen und verfügbar.
Ein völlig vernünftiges Gegenargument ist jedoch, dass Abhängigkeiten nicht ohne Nachteile „umsonst“ sind. Durch die Verwendung einer Drittanbieter- oder Open-Source-Bibliothek, die einige interessante Funktionen hinzufügt, verpflichten Sie sich zu dieser Bibliothek und werden von ihr abhängig. Das ist eine große Verpflichtung; Wenn es sich um eine riesige Bibliothek handelt und Sie nur ein wenig Funktionalität benötigen, wollen Sie wirklich die Last der Aktualisierung der gesamten Bibliothek, wenn Sie beispielsweise auf Python 3.x aktualisieren? Wenn Sie auf einen Fehler stoßen oder die Funktionalität verbessern möchten, sind Sie außerdem entweder auf den Autor (oder Anbieter) angewiesen, um die Korrektur oder Verbesserung bereitzustellen, oder, wenn es sich um Open Source handelt, befinden Sie sich in der Position, eine ( möglicherweise erheblichen) Codebasis, die Sie mit dem Versuch, eine obskure Funktionalität zu reparieren oder zu modifizieren, völlig ungewohnt sind.
Je ausgelasteter der Code ist, auf den Sie angewiesen sind, desto unwahrscheinlicher ist es, dass Sie selbst Zeit in die Wartung investieren müssen. Das Fazit ist, dass es sich für Sie lohnt, Ihre eigenen Nachforschungen anzustellen und Ihre eigene Einschätzung darüber abzugeben, ob Sie externe Technologie einbeziehen oder nicht und wie viel Wartung diese bestimmte Technologie Ihrem Stack hinzufügen wird.
Nachfolgend finden Sie einige der häufigeren Beispiele für Dinge, die Sie in der Moderne in Ihrem Projekt wahrscheinlich nicht neu erfinden sollten (es sei denn, dies SIND Ihre Projekte).
Datenbanken
Finden Sie heraus, welche CAP Sie für Ihr Projekt benötigen, und wählen Sie dann die Datenbank mit den richtigen Eigenschaften aus. Datenbank bedeutet nicht mehr nur MySQL, Sie können wählen aus:
- „Traditionelles“ schematisiertes SQL: Postgres / MySQL / MariaDB / MemSQL / Amazon RDS usw.
- Key Value Stores: Redis / Memcache / Riak
- NoSQL: MongoDB/Cassandra
- Gehostete DBs: AWS RDS / DynamoDB / AppEngine-Datenspeicher
- Schweres Heben: Amazon MR / Hadoop (Hive/Pig) / Cloudera / Google Big Query
- Verrücktes Zeug: Mnesia von Erlang, Core Data von iOS
Datenabstraktionsschichten
In den meisten Fällen sollten Sie keine rohen Abfragen an die Datenbank schreiben, die Sie zufällig verwenden möchten. Höchstwahrscheinlich gibt es eine Bibliothek, die zwischen der Datenbank und Ihrem Anwendungscode sitzt und die Bedenken hinsichtlich der Verwaltung gleichzeitiger Datenbanksitzungen und Details des Schemas von Ihrem Hauptcode trennt. Zumindest sollten Sie niemals rohe Abfragen oder SQL inline in der Mitte Ihres Anwendungscodes haben. Packen Sie es lieber in eine Funktion und zentralisieren Sie alle Funktionen in einer Datei, die etwas wirklich Offensichtliches heißt (z. B. „queries.py“). Eine Zeile wie beispielsweise users = load_users() ist unendlich viel einfacher zu lesen als users = db.query(“SELECT username, foo, bar from users LIMIT 10 ORDER BY ID”) . Diese Art der Zentralisierung macht es auch viel einfacher, einen konsistenten Stil in Ihren Abfragen zu haben, und begrenzt die Anzahl der Stellen, an denen Sie die Abfragen ändern können, wenn sich das Schema ändert.
Andere gängige Bibliotheken und Tools, deren Nutzung in Erwägung gezogen werden sollte
- Warteschlangen oder Pub/Sub-Dienste. Wählen Sie aus AMQP-Anbietern, ZeroMQ, RabbitMQ, Amazon SQS
- Lager. Amazon S3, Google Cloud-Speicher
- Überwachung: Graphite/Hosted Graphite, AWS Cloud Watch, New Relic
- Protokollsammlung/-aggregation. Loggly, Splunk
Automatische Skalierung
- Automatische Skalierung. Heroku, AWS Beanstalk, AppEngine, AWS Opsworks, Digital Ocean
Gebot Nr. 5: Überquere nicht die Bäche!
Es gibt viele gute Modelle für Programmierdesign, Pub/Sub, Schauspieler, MVC usw. Wählen Sie, was Ihnen am besten gefällt, und bleiben Sie dabei. Verschiedene Arten von Logik, die sich mit verschiedenen Arten von Daten befassen, sollten in der Codebasis physisch isoliert werden (auch diese Trennung betrifft das Konzept und die Verringerung der kognitiven Belastung des zukünftigen Lesers). Der Code, der Ihre Benutzeroberfläche aktualisiert, sollte sich beispielsweise physisch von dem Code unterscheiden, der berechnet, was in die Benutzeroberfläche aufgenommen wird.
Gebot Nr. 6: Wenn möglich, lass den Computer die Arbeit machen
Wenn der Compiler logische Fehler in Ihrem Code erkennen und entweder schlechtes Verhalten, Fehler oder völlige Abstürze verhindern kann, sollten wir das unbedingt ausnutzen. Natürlich haben einige Sprachen Compiler, die dies einfacher machen als andere. Haskell zum Beispiel hat einen bekanntermaßen strengen Compiler, der dazu führt, dass Programmierer den größten Teil ihrer Mühe darauf verwenden, Code zum Kompilieren zu bekommen. Sobald es jedoch kompiliert ist, „funktioniert es einfach“. Für diejenigen unter Ihnen, die noch nie in einer stark typisierten funktionalen Sprache geschrieben haben, mag dies lächerlich oder unmöglich erscheinen, aber nehmen Sie mich nicht beim Wort. Im Ernst, klicken Sie auf einige dieser Links, es ist absolut möglich, in einer Welt ohne Laufzeitfehler zu leben. Und es ist wirklich so magisch.
Zugegeben, nicht jede Sprache hat einen Compiler oder eine Syntax, die sich für viele (oder in manchen Fällen überhaupt keine!) Überprüfungen zur Kompilierzeit anbietet. Nehmen Sie sich für diejenigen, die dies nicht tun, ein paar Minuten Zeit, um zu recherchieren, welche optionalen Strengeprüfungen Sie in Ihrem Projekt aktivieren können, und bewerten Sie, ob sie für Sie sinnvoll sind. Eine kurze, nicht umfassende Liste einiger gebräuchlicher, die ich in letzter Zeit für Sprachen mit milden Laufzeiten verwendet habe, umfasst:
- Python: Pylint, Pyflakes, Warnungen, Warnungen in Emacs
- Ruby: Warnungen
- JavaScript: jslint
Fazit
Dies ist keineswegs eine erschöpfende oder perfekte Liste von Geboten für die Erstellung von „gutem“ (dh leicht wartbarem) Code. Das heißt, wenn jede Codebasis, die ich jemals aufgreifen musste, in Zukunft auch nur der Hälfte der Konzepte in dieser Liste folgen würde, werde ich viel weniger graue Haare haben und vielleicht sogar in der Lage sein, am Ende meines Lebens weitere fünf Jahre hinzuzufügen. Und ich werde die Arbeit sicherlich angenehmer und weniger stressig finden.
