Der Einstieg in ein neues Unternehmen – mit einer etablierten Kultur und Programmierpraktiken – kann eine entmutigende Erfahrung sein. Als ich dem Ansible-Team beitrat, beschloss ich, die Softwareentwicklungspraktiken und -prinzipien aufzuschreiben, die ich im Laufe der Jahre gelernt habe und nach denen ich mich bemühe, zu arbeiten. Dies ist eine nicht definitive, nicht erschöpfende Liste von Grundsätzen, die mit Weisheit und Flexibilität angewendet werden sollten.
Meine Leidenschaft gilt dem Testen, da ich glaube, dass gute Testverfahren sowohl einen Mindestqualitätsstandard gewährleisten (der leider in vielen Softwareprodukten fehlt) als auch die Entwicklung selbst lenken und gestalten können. Viele dieser Prinzipien beziehen sich auf Testpraktiken und Ideale. Einige dieser Prinzipien sind Python-spezifisch, aber die meisten sind es nicht. (Für Python-Entwickler sollte PEP 8 die erste Anlaufstelle für Programmierstil und Richtlinien sein.)
Im Allgemeinen sind wir Programmierer ein rechthaberischer Haufen, und starke Meinungen sind oft ein Zeichen großer Leidenschaft. In diesem Sinne, fühlen Sie sich frei, diesen Punkten nicht zuzustimmen, und wir können sie in den Kommentaren diskutieren und debattieren.
Entwicklung und Testen von Best Practices
1. YAGNI: „You Aint Gonna Need It“. Schreibe keinen Code, von dem du glaubst, dass du ihn in Zukunft brauchen könntest, den du aber noch nicht brauchst. Das ist Programmieren für imaginäre zukünftige Anwendungsfälle, und der Code wird unweigerlich zu totem Code oder muss umgeschrieben werden, weil der zukünftige Anwendungsfall immer etwas anders funktioniert, als Sie es sich vorgestellt haben.
Wenn Sie Code für einen zukünftigen Anwendungsfall schreiben, werde ich ihn bei einer Codeüberprüfung in Frage stellen. (Man kann und muss zum Beispiel APIs entwerfen, um zukünftige Anwendungsfälle zu ermöglichen, aber das ist ein anderes Thema.)
Das Gleiche gilt für das Auskommentieren von Code; wenn ein Block auskommentierten Codes in eine Version geht, sollte er nicht existieren. Wenn es sich um Code handelt, der wiederhergestellt werden kann, erstellen Sie ein Ticket und verweisen Sie auf den Commit-Hash für das Löschen des Codes. YAGNI ist ein Kernelement der agilen Programmierung. Die beste Referenz hierfür ist Extreme Programming Explained von Kent Beck.
2. Tests brauchen keine Tests. Infrastruktur, Frameworks und Bibliotheken für Tests brauchen Tests. Testen Sie den Browser oder externe Bibliotheken nur, wenn es wirklich nötig ist. Testen Sie den Code, den Sie schreiben, nicht den Code anderer Leute.
3. Das dritte Mal, dass Sie denselben Code schreiben, ist der richtige Zeitpunkt, ihn in eine allgemeine Hilfsfunktion zu extrahieren (und Tests dafür zu schreiben). Hilfsfunktionen innerhalb eines Tests müssen nicht getestet werden; wenn man sie herauslöst und wiederverwendet, brauchen sie Tests. Wenn Sie zum dritten Mal ähnlichen Code geschrieben haben, haben Sie in der Regel eine klare Vorstellung davon, wie das allgemeine Problem aussieht, das Sie lösen wollen.
4. Wenn es um das Design der API geht (External Facing und Object API): Einfache Dinge sollten einfach sein; komplexe Dinge sollten möglich sein. Entwerfen Sie zuerst für den einfachen Fall, möglichst ohne Konfiguration oder Parametrisierung, wenn das möglich ist. Fügen Sie Optionen oder zusätzliche API-Methoden für komplexere und flexiblere Anwendungsfälle hinzu (wenn sie benötigt werden).
5. Schnelles Fail. Prüfen Sie Eingaben und scheitern Sie bei unsinnigen Eingaben oder ungültigen Zuständen so früh wie möglich, vorzugsweise mit einer Ausnahme oder einer Fehlerantwort, die dem Aufrufer das genaue Problem verdeutlicht. Erlauben Sie jedoch „innovative“ Anwendungsfälle Ihres Codes (d.h. führen Sie keine Typüberprüfung für die Eingabevalidierung durch, es sei denn, es ist wirklich notwendig).
6. Unit-Tests testen die Einheit des Verhaltens, nicht die Einheit der Implementierung. Das Ziel ist es, die Implementierung zu ändern, ohne das Verhalten zu ändern oder einen Ihrer Tests ändern zu müssen, auch wenn dies nicht immer möglich ist. Behandeln Sie also Ihre Testobjekte nach Möglichkeit als Blackboxen, die über die öffentliche API testen, ohne private Methoden aufzurufen oder am Zustand herumzupfuschen.
Bei einigen komplexen Szenarien – wie dem Testen des Verhaltens bei einem bestimmten komplexen Zustand, um einen obskuren Fehler zu finden – ist das vielleicht nicht möglich. Das Schreiben von Tests als Erstes ist in diesem Fall sehr hilfreich, da es Sie dazu zwingt, über das Verhalten Ihres Codes nachzudenken und darüber, wie Sie ihn testen wollen, bevor Sie ihn schreiben. Wenn Sie zuerst testen, können Sie kleinere, modularere Codeeinheiten erstellen, was im Allgemeinen besseren Code bedeutet. Ein gutes Nachschlagewerk für den Einstieg in den „Test first“-Ansatz ist Test Driven Development by Example von Kent Beck.
7. Bei Unit-Tests (einschließlich Tests der Testinfrastruktur) sollten alle Codepfade getestet werden. Eine 100%ige Abdeckung ist ein guter Anfang. Man kann nicht alle möglichen Permutationen/Kombinationen von Zuständen abdecken (kombinatorische Explosion), daher ist dies zu berücksichtigen. Nur wenn es einen sehr guten Grund gibt, sollten Codepfade ungetestet bleiben. Zeitmangel ist kein guter Grund und kostet am Ende noch mehr Zeit. Mögliche gute Gründe sind: wirklich nicht testbar (in irgendeiner sinnvollen Weise), in der Praxis unmöglich zu treffen oder an anderer Stelle durch einen Test abgedeckt. Code ohne Tests ist eine Belastung. Die Messung der Testabdeckung und die Ablehnung von PRs, die den Prozentsatz der Testabdeckung verringern, ist eine Möglichkeit, um sicherzustellen, dass Sie allmählich Fortschritte in die richtige Richtung machen.
8. Code ist der Feind: Er kann schief gehen und muss gewartet werden. Schreiben Sie weniger Code. Löschen Sie Code. Schreiben Sie keinen Code, den Sie nicht brauchen.
9. Unvermeidlich werden Codekommentare mit der Zeit zu Lügen. In der Praxis aktualisieren nur wenige Leute Kommentare, wenn sich Dinge ändern. Bemühen Sie sich, Ihren Code durch gute Benennungspraktiken und einen bekannten Programmierstil lesbar und selbstdokumentierend zu machen.
Code, der nicht offensichtlich gemacht werden kann – um einen obskuren Fehler oder eine unwahrscheinliche Bedingung zu umgehen, oder eine notwendige Optimierung – muss kommentiert werden. Kommentieren Sie die Absicht des Codes, und warum er etwas tut, anstatt was er tut. (Dieser spezielle Punkt, dass Kommentare Lügen sind, ist übrigens umstritten. Ich denke immer noch, dass er richtig ist, und Kernighan und Pike, die Autoren von The Practice of Programming, stimmen mir zu.)
10. Schreibe defensiv. Denke immer darüber nach, was schiefgehen kann, was bei ungültiger Eingabe passiert und was schiefgehen könnte, was dir helfen wird, viele Fehler abzufangen, bevor sie passieren.
11. Logik ist einfach zu testen, wenn sie zustandslos und ohne Seiteneffekte ist. Teilen Sie die Logik in separate Funktionen auf, anstatt sie in zustandsabhängigen und mit Seiteneffekten versehenen Code zu mischen. Die Aufteilung von zustandsbehaftetem Code und Code mit Seiteneffekten in kleinere Funktionen erleichtert das Mockout und den Unit-Test ohne Seiteneffekte. (Weniger Overhead für Tests bedeutet schnellere Tests.) Seiteneffekte müssen getestet werden, aber sie einmal zu testen und sie überall sonst zu mocken ist generell ein gutes Muster.
12. Globale sind schlecht. Funktionen sind besser als Typen. Objekte sind wahrscheinlich besser als komplexe Datenstrukturen.
13. Die Verwendung der in Python eingebauten Typen – und ihrer Methoden – ist schneller als das Schreiben eigener Typen (es sei denn, Sie schreiben in C). Wenn Leistung eine Rolle spielt, versuchen Sie herauszufinden, wie Sie die standardmäßig eingebauten Typen verwenden können, anstatt eigene Objekte zu schreiben.
14. Dependency Injection ist ein nützliches Kodierungsmuster, um sich darüber klar zu werden, was Ihre Abhängigkeiten sind und woher sie kommen. (Lassen Sie Objekte, Methoden usw. ihre Abhängigkeiten als Parameter empfangen, anstatt selbst neue Objekte zu instanziieren.) Dadurch werden die API-Signaturen komplexer, so dass es einen Kompromiss gibt. Wenn Sie eine Methode haben, die 10 Parameter für alle ihre Abhängigkeiten benötigt, ist das ein gutes Zeichen dafür, dass Ihr Code ohnehin zu viel tut. Der maßgebliche Artikel über Dependency Injection ist „Inversion of Control Containers and the Dependency Injection Pattern,“ von Martin Fowler.
15. Je mehr Sie mocken müssen, um Ihren Code zu testen, desto schlechter ist Ihr Code. Je mehr Code Sie instanziieren und einbauen müssen, um ein bestimmtes Verhalten zu testen, desto schlechter ist Ihr Code. Das Ziel sind kleine testbare Einheiten, zusammen mit übergeordneten Integrations- und Funktionstests, um zu prüfen, ob die Einheiten korrekt zusammenarbeiten.
16. Bei APIs, die nach außen gerichtet sind, sind „Design im Voraus“ und Überlegungen zu künftigen Anwendungsfällen wirklich wichtig. Das Ändern von APIs ist für uns und unsere Benutzer schmerzhaft, und die Schaffung von Rückwärtsinkompatibilität ist schrecklich (auch wenn sie sich manchmal nicht vermeiden lässt). Entwerfen Sie nach außen gerichtete APIs sorgfältig und halten Sie sich dabei an den Grundsatz „Einfache Dinge sollten einfach sein“
17. Wenn eine Funktion oder Methode mehr als 30 Zeilen Code umfasst, sollten Sie sie aufteilen. Eine gute maximale Modulgröße sind etwa 500 Zeilen. Testdateien neigen dazu, länger zu sein als diese.
18. Arbeiten Sie nicht in Objektkonstruktoren, die schwer zu testen und überraschend sind. Füge keinen Code in __init__.py ein (außer Importe für Namespacing). __init__.py ist nicht der Ort, an dem Programmierer im Allgemeinen erwarten, Code zu finden, also ist es „überraschend“
19. DRY (Don’t Repeat Yourself) spielt bei Tests eine viel geringere Rolle als bei Produktionscode. Die Lesbarkeit einer einzelnen Testdatei ist wichtiger als die Wartbarkeit (Herausbrechen wiederverwendbarer Teile). Das liegt daran, dass Tests einzeln ausgeführt und gelesen werden und nicht selbst Teil eines größeren Systems sind. Natürlich bedeutet eine übermäßige Wiederholung, dass wiederverwendbare Komponenten aus Gründen der Bequemlichkeit erstellt werden können, aber das ist viel weniger ein Problem als bei der Produktion.
20. Refaktorieren Sie, wann immer Sie die Notwendigkeit sehen und die Möglichkeit dazu haben. Beim Programmieren geht es um Abstraktionen, und je näher Ihre Abstraktionen an der Problemdomäne liegen, desto einfacher ist Ihr Code zu verstehen und zu warten. Wenn Systeme organisch wachsen, müssen sie ihre Struktur an den wachsenden Anwendungsfall anpassen. Systeme wachsen aus ihren Abstraktionen und ihrer Struktur heraus, und wenn sie nicht geändert werden, entstehen technische Schulden, deren Beseitigung schmerzhafter (und langsamer und fehleranfälliger) ist. Berücksichtigen Sie die Kosten für die Beseitigung der technischen Schulden (Refactoring) bei den Schätzungen für die Arbeit an den Funktionen. Je länger Sie die Schulden liegen lassen, desto höher sind die Zinsen, die sie anhäufen. Ein hervorragendes Buch über Refactoring und Testen ist Working Effectively with Legacy Code von Michael Feathers.
21. Machen Sie den Code erst korrekt und dann schnell. Wenn Sie an Leistungsproblemen arbeiten, erstellen Sie immer zuerst ein Profil, bevor Sie Korrekturen vornehmen. Normalerweise liegt der Engpass nicht dort, wo man ihn vermutet. Obskuren Code zu schreiben, weil er schneller ist, lohnt sich nur, wenn Sie ein Profil erstellt und bewiesen haben, dass es sich tatsächlich lohnt. Wenn Sie einen Test schreiben, der den Code, den Sie profilieren, mit Timing umgibt, ist es einfacher zu wissen, wann Sie fertig sind, und kann in der Testsuite belassen werden, um Leistungsrückschritte zu verhindern. (Mit dem üblichen Hinweis, dass das Hinzufügen von Timing-Code immer die Leistungsmerkmale des Codes verändert, was die Arbeit an der Leistung zu einer der frustrierenderen Aufgaben macht)
22. Kleinere, engmaschigere Unit-Tests liefern mehr wertvolle Informationen, wenn sie fehlschlagen – sie sagen Ihnen genau, was falsch ist. Ein Test, der das halbe System aufruft, um das Verhalten zu testen, erfordert mehr Untersuchungen, um festzustellen, was falsch ist. Im Allgemeinen ist ein Test, der mehr als 0,1 Sekunden zur Ausführung benötigt, kein Unit-Test. So etwas wie einen langsamen Unit-Test gibt es nicht. Mit eng umrissenen Unit-Tests, die das Verhalten testen, fungieren Ihre Tests als De-facto-Spezifikation für Ihren Code. Wenn jemand Ihren Code verstehen will, sollte er im Idealfall die Testsuite als „Dokumentation“ für das Verhalten heranziehen können. Eine großartige Präsentation über Unit-Testing-Praktiken ist Fast Test, Slow Test, von Gary Bernhardt:
23. „Not Invented Here“ ist nicht so schlecht, wie die Leute sagen. Wenn wir den Code schreiben, dann wissen wir, was er tut, wir wissen, wie man ihn pflegt, und wir sind frei, ihn zu erweitern und zu verändern, wie wir es für richtig halten. Dies folgt dem YAGNI-Prinzip: Wir haben spezifischen Code für die Anwendungsfälle, die wir brauchen, und keinen Allzweckcode, der für Dinge, die wir nicht brauchen, zu komplex ist. Andererseits ist Code der Feind, und mehr Code als nötig zu besitzen ist schlecht. Bedenken Sie den Kompromiss, wenn Sie eine neue Abhängigkeit einführen.
24. Gemeinsamer Codebesitz ist das Ziel; isoliertes Wissen ist schlecht. Zumindest bedeutet dies, dass Designentscheidungen und wichtige Implementierungsentscheidungen diskutiert oder dokumentiert werden müssen. Die Codeüberprüfung ist der schlechteste Zeitpunkt, um mit der Diskussion von Entwurfsentscheidungen zu beginnen, da die Trägheit, weitreichende Änderungen vorzunehmen, nachdem der Code geschrieben wurde, schwer zu überwinden ist. (Natürlich ist es immer noch besser, Designfehler bei der Überprüfung aufzuzeigen und zu ändern als gar nicht.)
25. Generatoren sind toll! Sie sind im Allgemeinen kürzer und leichter zu verstehen als zustandsabhängige Objekte für Iteration oder wiederholte Ausführung. Eine gute Einführung in Generatoren ist „Generator Tricks for Systems Programmers,“ von David Beazley.
26. Lasst uns Ingenieure sein! Denken wir über Design nach und bauen wir robuste und gut implementierte Systeme, anstatt organische Monster zu züchten. Programmieren ist jedoch ein Balanceakt. Wir bauen nicht immer ein Raketenschiff. Übermäßiges Engineering (Zwiebelarchitektur) ist genauso schmerzhaft wie unterentwickelter Code. Fast alles von Robert Martin ist lesenswert, und Clean Architecture: A Craftsman’s Guide to Software Structure and Design ist eine gute Quelle zu diesem Thema. Design Patterns ist ein klassisches Programmierbuch, das jeder Ingenieur lesen sollte.
27. Regelmäßig fehlschlagende Tests untergraben den Wert Ihrer Testsuite, bis zu dem Punkt, an dem schließlich jeder die Ergebnisse von Testläufen ignoriert, weil immer etwas fehlschlägt. Das Reparieren oder Löschen von zeitweise fehlgeschlagenen Tests ist schmerzhaft, aber die Mühe wert.
28. Warten Sie generell, insbesondere bei Tests, auf eine bestimmte Änderung, anstatt eine beliebige Zeit lang zu schlafen. Voodoo-Schlafphasen sind schwer zu verstehen und verlangsamen Ihre Testsuite.
29. Lassen Sie Ihren Test immer mindestens einmal fehlschlagen. Bauen Sie einen absichtlichen Fehler ein und stellen Sie sicher, dass er fehlschlägt, oder führen Sie den Test aus, bevor das zu testende Verhalten abgeschlossen ist. Sonst wissen Sie nicht, dass Sie wirklich etwas testen. Es ist leicht, aus Versehen Tests zu schreiben, die eigentlich gar nichts testen oder die niemals fehlschlagen können.
30. Und zum Schluss noch ein Punkt für das Management: Ständiger Feature Grind ist eine schreckliche Art, Software zu entwickeln. Wenn man den Entwicklern nicht erlaubt, stolz auf ihre Arbeit zu sein, wird man nicht das Beste aus ihnen herausholen können. Wenn man sich nicht um die technischen Schulden kümmert, verlangsamt sich die Entwicklung und das Ergebnis ist ein schlechteres, fehlerhafteres Produkt.