Det kan vara en skrämmande upplevelse att gå in i ett nytt företag med en etablerad kultur och etablerade programmeringsmetoder. När jag anslöt mig till Ansible-teamet bestämde jag mig för att skriva upp de metoder och principer för programvaruteknik som jag har lärt mig under åren och som jag strävar efter att arbeta efter. Detta är en icke-definitiv, icke uttömmande lista över principer som bör tillämpas med visdom och flexibilitet.
Jag brinner för testning, eftersom jag anser att goda testmetoder både kan garantera en minsta kvalitetsstandard (som tyvärr saknas i många mjukvaruprodukter) och kan vägleda och forma själva utvecklingen. Många av de här principerna rör testningsmetoder och ideal. Några av dessa principer är Python-specifika, men de flesta är det inte. (För Pythonutvecklare bör PEP 8 vara din första anhalt för programmeringsstil och riktlinjer.)
I allmänhet är vi programmerare en åsiktsstyrd skara, och starka åsikter är ofta ett tecken på stor passion. Med det i åtanke är du välkommen att inte hålla med om dessa punkter, så kan vi diskutera och debattera dem i kommentarerna.
Bästa metoder för utveckling och testning
1. YAGNI: ”You Aint Gonna Need It”. Skriv inte kod som du tror att du kan behöva i framtiden, men som du inte behöver ännu. Detta är kodning för imaginära framtida användningsfall, och oundvikligen kommer koden att bli död kod eller behöva skrivas om eftersom det framtida användningsfallet alltid visar sig fungera något annorlunda än hur du föreställde dig det.
Om du lägger in kod för ett framtida användningsfall kommer jag att ifrågasätta det i en kodgranskning. (Man kan, och måste, till exempel utforma API:er för att möjliggöra framtida användningsfall, men det är en annan fråga.)
Det samma gäller för att kommentera ut kod; om ett kommenterat kodblock ska ingå i en utgåva ska det inte finnas. Om det är kod som kan återställas, gör en biljett och referera till commit-hash för kodutplåningen. YAGNI är en central del av agil programmering. Den bästa referensen för detta är Extreme Programming Explained, av Kent Beck.
2. Tester behöver inte testas. Infrastruktur, ramverk och bibliotek för testning behöver tester. Testa inte webbläsaren eller externa bibliotek om du inte verkligen behöver det. Testa den kod du skriver, inte andras kod.
3. Tredje gången du skriver samma kodstycke är rätt tillfälle att extrahera det till en universalhjälpare (och skriva tester för den). Hjälpfunktioner inom ett test behöver inte testas; när du bryter ut dem och återanvänder dem behöver de tester. Vid tredje gången du skriver liknande kod tenderar du att ha en klar uppfattning om hur det generella problemet som du löser ser ut.
4. När det gäller API-design (externt vändande och objekts-API): Det enkla ska vara enkelt, det komplexa ska vara möjligt. Designa för det enkla fallet först, med helst noll konfiguration eller parametrisering, om det är möjligt. Lägg till alternativ eller ytterligare API-metoder för mer komplexa och flexibla användningsfall (när de behövs).
5. Fail fast. Kontrollera inmatning och misslyckas med nonsensinriktad inmatning eller ogiltigt tillstånd så tidigt som möjligt, helst med ett undantag eller ett felsvar som gör det exakta problemet tydligt för den som ringer. Tillåt dock ”innovativa” användningsfall av din kod (dvs. gör inte typkontroller för validering av inmatning om du inte verkligen behöver det).
6. Enhetstester testar beteendeenheten, inte implementeringsenheten. Målet är att ändra implementationen utan att ändra beteendet eller behöva ändra något av dina tester, även om det inte alltid är möjligt. Så där det är möjligt, behandla dina testobjekt som svarta lådor, testa genom det offentliga API:t utan att kalla privata metoder eller mixtra med tillstånd.
För vissa komplexa scenarier – till exempel att testa beteende på ett specifikt komplext tillstånd för att hitta ett obskyrt fel – är det kanske inte möjligt. Att skriva tester först hjälper verkligen till med detta eftersom det tvingar dig att tänka på kodens beteende och hur du ska testa den innan du skriver den. Att testa först uppmuntrar till mindre, mer modulära kodenheter, vilket i allmänhet innebär bättre kod. En bra referens för att komma igång med ”test först”-metoden är Test Driven Development by Example, av Kent Beck.
7. För enhetstester (inklusive testinfrastrukturtester) bör alla kodvägar testas. 100 % täckning är en bra början. Du kan inte täcka alla möjliga permutationer/kombinationer av tillstånd (kombinatorisk explosion), så det kräver övervägande. Endast om det finns mycket goda skäl bör kodvägar inte testas. Tidsbrist är inte ett bra skäl och slutar med att kosta mer tid. Möjliga goda skäl är bland annat följande: verkligen omöjliga att testa (på något meningsfullt sätt), omöjliga att träffa i praktiken eller täckta på annat håll i ett test. Kod utan tester är ett ansvar. Att mäta täckningen och avvisa PR som minskar täckningsgraden är ett sätt att se till att du gör gradvisa framsteg i rätt riktning.
8. Koden är fienden: Den kan gå fel och den behöver underhållas. Skriv mindre kod. Ta bort kod. Skriv inte kod som du inte behöver.
9. Kodkommentarer blir oundvikligen lögner med tiden. I praktiken är det få som uppdaterar kommentarerna när saker och ting förändras. Sträva efter att göra din kod läsbar och självdokumenterande genom god namngivningspraxis och känd programmeringsstil.
Kod som inte kan göras uppenbar – arbete för att kringgå ett obskyrt fel eller osannolikt tillstånd, eller en nödvändig optimering – behöver kommenteras. Kommentera kodens avsikt, och varför den gör något snarare än vad den gör. (Just denna punkt om att kommentarer är lögner är förresten kontroversiell. Jag anser fortfarande att den är korrekt, och Kernighan och Pike, författare till The Practice of Programming, håller med mig.)
10. Skriv defensivt. Tänk alltid på vad som kan gå fel, vad som kommer att hända vid ogiltig inmatning och vad som kan misslyckas, vilket kommer att hjälpa dig att fånga många fel innan de inträffar.
11. Logik är lätt att enhetstesta om den är stateless och sidoeffektfri. Dela upp logiken i separata funktioner, i stället för att blanda in logiken i tillståndslös och sidoeffektfylld kod. Att separera statsfylld kod och kod med sidoeffekter i mindre funktioner gör dem lättare att mocka ut och enhetstesta utan sidoeffekter. (Mindre overhead för tester innebär snabbare tester.) Sidoeffekter behöver testas, men att testa dem en gång och mocka ut dem överallt annars är i allmänhet ett bra mönster.
12. Globals är dåliga. Funktioner är bättre än typer. Objekt är sannolikt bättre än komplexa datastrukturer.
13. Att använda Pythons inbyggda typer – och deras metoder – kommer att vara snabbare än att skriva egna typer (om du inte skriver i C). Om prestandan är en faktor som spelar roll bör du försöka lista ut hur du kan använda de inbyggda standardtyperna i stället för egna objekt.
14. Beroendeinjektion är ett användbart kodningsmönster för att vara tydlig med vad dina beroenden är och var de kommer ifrån. (Låt objekt, metoder och så vidare ta emot sina beroenden som parametrar i stället för att själva instansiera nya objekt). Detta gör API-signaturer mer komplexa, så det är en kompromiss. Att sluta med en metod som behöver 10 parametrar för alla sina beroenden är ett bra tecken på att din kod ändå gör för mycket. Den definitiva artikeln om beroendeinjektion är ”Inversion of Control Containers and the Dependency Injection Pattern” av Martin Fowler.
15. Ju mer du måste mocka ut för att testa din kod, desto sämre är din kod. Ju mer kod du måste instantiera och sätta på plats för att kunna testa ett specifikt beteende, desto sämre är din kod. Målet är små testbara enheter, tillsammans med integrations- och funktionstester på högre nivå för att testa att enheterna samarbetar korrekt.
16. Externa API:er är områden där ”design up front” – och överväganden om framtida användningsfall – verkligen spelar roll. Att ändra API:er är jobbigt för oss och för våra användare, och att skapa bakåtkompatibilitet är hemskt (även om det ibland är omöjligt att undvika). Utforma externa API:er med omsorg och håll dig ändå till principen ”enkla saker ska vara enkla”.
17. Om en funktion eller metod överskrider 30 rader kod bör du överväga att bryta upp den. En bra maximal modulstorlek är cirka 500 rader. Testfiler tenderar att vara längre än så.
18. Gör inte arbete i objektkonstruktörer, som är svåra att testa och överraskande. Lägg inte in kod i __init__.py (förutom import för namespacing). __init__.py är inte där programmerare i allmänhet förväntar sig att hitta kod, så det är ”överraskande.”
19. DRY (Don’t Repeat Yourself) spelar mycket mindre roll i tester än i produktionskod. Läsbarheten i en enskild testfil är viktigare än underhållbarhet (att bryta ut återanvändbara delar). Det beror på att tester utförs och läses individuellt snarare än att de själva är en del av ett större system. Självklart innebär överdriven upprepning att återanvändbara komponenter kan skapas av bekvämlighetsskäl, men det är ett mycket mindre problem än vad det är för produktion.
20. Refaktorisera när du ser behovet och har chansen. Programmering handlar om abstraktioner, och ju närmare dina abstraktioner mappar till problemområdet, desto lättare är din kod att förstå och underhålla. När system växer organiskt behöver de ändra struktur för sina växande användningsområden. Systemen växer ur sina abstraktioner och sin struktur, och om de inte ändras blir det en teknisk skuld som det är mer smärtsamt (och långsammare och mer buggigt) att arbeta runt. Inkludera kostnaden för att rensa bort teknisk skuld (refaktorisering) i uppskattningarna för arbetet med funktionerna. Ju längre du låter skulden ligga kvar, desto högre blir räntan. En bra bok om refaktorisering och testning är Working Effectively with Legacy Code av Michael Feathers.
21. Gör koden korrekt i första hand och snabb i andra hand. När du arbetar med prestandaproblem ska du alltid profilera innan du gör korrigeringar. Vanligtvis är flaskhalsen inte riktigt där du trodde att den var. Att skriva obskyr kod för att den är snabbare är bara värt det om du har profilerat och bevisat att det faktiskt är värt det. Om du skriver ett test som tränar koden du profilerar med tidsplanering runtomkring gör det lättare att veta när du är klar, och det kan lämnas kvar i testsviten för att förhindra prestandaregressioner. (Med den vanliga notisen att lägga till timingkod alltid ändrar kodens prestandaegenskaper, vilket gör prestandaarbete till en av de mer frustrerande uppgifterna.)
22. Mindre, mer noggrant avgränsade enhetstester ger mer värdefull information när de misslyckas – de berättar specifikt vad som är fel. Ett test som står upp i halva systemet för att testa beteende kräver mer utredning för att avgöra vad som är fel. Generellt sett är ett test som tar mer än 0,1 sekunder att köra inte ett enhetstest. Det finns inget sådant som ett långsamt enhetstest. Med noggrant avgränsade enhetstester som testar beteende fungerar dina tester som en de facto-specifikation för din kod. Om någon vill förstå din kod ska de helst kunna vända sig till testsviten som ”dokumentation” för beteendet. En bra presentation om enhetstester är Fast Test, Slow Test av Gary Bernhardt:
23. ”Not Invented Here” är inte så illa som folk säger. Om vi skriver koden vet vi vad den gör, vi vet hur den ska underhållas och vi är fria att utöka och modifiera den som vi vill. Detta följer YAGNI-principen: Vi har specifik kod för de användningsfall vi behöver snarare än kod för allmänna ändamål som har komplexitet för saker vi inte behöver. Å andra sidan är kod fienden, och att äga mer kod än nödvändigt är dåligt. Tänk på kompromissen när du inför ett nytt beroende.
24. Delat kodägande är målet, siload kunskap är dåligt. Som ett minimum innebär detta att diskutera eller dokumentera designbeslut och viktiga implementeringsbeslut. Kodgranskning är den sämsta tidpunkten för att börja diskutera designbeslut eftersom trögheten att göra genomgripande ändringar efter att koden har skrivits är svår att övervinna. (Naturligtvis är det fortfarande bättre att påpeka och ändra designfel vid granskningstillfället än aldrig.)
25. Generatorer rockar! De är i allmänhet kortare och lättare att förstå än stateful-objekt för iteration eller upprepad exekvering. En bra introduktion till generatorer är ”Generator Tricks for Systems Programmers” av David Beazley.
26. Låt oss vara ingenjörer! Låt oss tänka på att designa och bygga robusta och välimplementerade system, snarare än att odla organiska monster. Programmering är dock en balansakt. Vi bygger inte alltid en raket. Överdriven ingenjörskonst (lökarkitektur) är lika smärtsamt att arbeta med som underdesignad kod. Nästan allt av Robert Martin är värt att läsa, och Clean Architecture: A Craftsman’s Guide to Software Structure and Design är en bra resurs i detta ämne. Design Patterns är en klassisk programmeringsbok som alla ingenjörer bör läsa.
27. Intermittent misslyckade tester urholkar värdet av din testföljd, till den punkt där så småningom alla ignorerar testkörningsresultaten eftersom det alltid är något som misslyckas. Att åtgärda eller radera intermittent misslyckade tester är smärtsamt, men värt besväret.
28. I allmänhet, särskilt i tester, vänta på en specifik förändring i stället för att sova under en godtycklig tid. Voodoo sleeps är svåra att förstå och gör testsviten långsammare.
29. Se alltid ditt test misslyckas minst en gång. Lägg in ett avsiktligt fel och se till att det misslyckas, eller kör testet innan beteendet som testas är färdigt. Annars vet du inte om du verkligen testar något. Att av misstag skriva tester som faktiskt inte testar något eller som aldrig kan misslyckas är lätt.
30. Och slutligen en punkt för ledningen: Ständig funktionsslipning är ett fruktansvärt sätt att utveckla mjukvara. Att inte låta utvecklare vara stolta över sitt arbete garanterar att du inte får ut det bästa av dem. Att inte ta itu med teknisk skuld gör utvecklingen långsammare och resulterar i en sämre och mer buggig produkt.