Hur undviker man att döda ett projekt med dåliga kodningsrutiner?
Bartosz Slysz
Software Engineer
Många programmerare som är i början av sin karriär anser att det inte är särskilt viktigt att namnge variabler, funktioner, filer och andra komponenter. Som ett resultat är deras designlogik ofta korrekt - algoritmer körs snabbt och ger önskad effekt, samtidigt som de knappt kan läsas. I den här artikeln kommer jag kortfattat att försöka beskriva vad vi bör vägledas av när vi namnger olika kodelement och hur vi inte går från en ytterlighet till en annan.
Varför kommer ett försummat namngivningsstadium att förlänga (i vissa fall - enormt) utvecklingen av ditt projekt?
Låt oss anta att du och din Team håller på att ta över kod från andra programmerare. De projekt you inherit utvecklades utan någon som helst kärlek - det fungerade alldeles utmärkt, men varje enskild del kunde ha skrivits på ett mycket bättre sätt.
När det gäller arkitekturen, i fallet med kodarv, utlöser det nästan alltid hat och ilska från de programmerare som fick det. Ibland beror detta på användning av döende (eller utdöda) tekniker, ibland på fel sätt att tänka på applikationen i början av utvecklingen, och ibland helt enkelt på grund av bristande kunskap hos den ansvariga programmeraren.
I vilket fall som helst, när projekttiden går, är det möjligt att nå en punkt där programmerare är rasande galna på arkitekturer och tekniker. När allt kommer omkring behöver varje applikation omskrivning av vissa delar eller bara ändringar i specifika delar efter en tid - det är naturligt. Men det problem som kommer att göra programmerarnas hår grått är svårigheten att läsa och förstå den kod de ärvt.
Särskilt i extrema fall när variabler namnges med enstaka, meningslösa bokstäver och funktioner är en plötslig våg av kreativitet, som inte på något sätt överensstämmer med resten av applikationen, kan dina programmerare gå bärsärkagång. I ett sådant fall kräver varje kodanalys som skulle kunna utföras snabbt och effektivt med korrekt namngivning ytterligare analys av de algoritmer som är ansvariga för att producera funktionsresultatet, till exempel. Och en sådan analys, även om den är oansenlig - slösar bort en enorm mängd tid.
Att implementera nya funktioner i olika delar av applikationen innebär att man går igenom mardrömmen att analysera den, efter en tid måste man gå tillbaka till koden och analysera den igen eftersom dess avsikter inte är tydliga, och den tidigare tid som spenderades på att försöka förstå dess funktion var bortkastad eftersom man inte längre kommer ihåg vad syftet var.
Och så sugs vi in i en tornado av oordning som regerar i applikationen och långsamt konsumerar alla deltagare i dess utveckling. Programmerare hatar projektet, projektledare hatar att förklara varför utvecklingstiden börjar öka konstant, och kunden tappar förtroendet och blir arg för att ingenting går enligt plan.
Hur undviker man det?
Låt oss inse det - vissa saker går inte att hoppa över. Om vi har valt vissa tekniker i början av projektet måste vi vara medvetna om att de med tiden antingen kommer att sluta stödjas eller att allt färre programmerare kommer att behärska tekniker från några år sedan som långsamt håller på att bli föråldrade. Vissa bibliotek kräver i sina uppdateringar mer eller mindre omfattande ändringar i koden, vilket ofta medför en virvel av beroenden där man kan fastna ännu mer.
Å andra sidan är det inte ett så svart scenario; naturligtvis - teknologierna blir äldre, men den faktor som definitivt saktar ner utvecklingstiden för projekt som involverar dem är till stor del ful kod. Och naturligtvis måste vi här nämna boken av Robert C. Martin - det här är en bibel för programmerare, där författaren presenterar många goda metoder och principer som bör följas för att skapa kod som strävar efter perfektion.
Det grundläggande när man namnger variabler är att tydligt och enkelt förmedla deras avsikt. Det låter ganska enkelt, men ibland försummas eller ignoreras det av många människor. Ett bra namn anger exakt vad variabeln ska lagra eller vad funktionen ska göra - det får inte vara för generiskt, men å andra sidan får det inte heller bli en lång snigel vars blotta läsning är en utmaning för hjärnan. Efter en tid med kod av god kvalitet upplever vi fördjupningseffekten, där vi undermedvetet kan ordna namngivning och överföring av data till funktionen på ett sådant sätt att det hela inte lämnar några illusioner om vilken avsikt som driver den och vad det förväntade resultatet av att anropa den är.
En annan sak som kan hittas i JavaScriptär bland annat ett försök att överoptimera koden, vilket i många fall gör den oläslig. Det är normalt att vissa algoritmer kräver särskild omsorg, vilket ofta återspeglar det faktum att avsikten med koden kan vara lite mer invecklad. Ändå är de fall där vi behöver överdrivna optimeringar extremt sällsynta, eller åtminstone de fall där vår kod är smutsig. Det är viktigt att komma ihåg att många språkrelaterade optimeringar sker på en något lägre abstraktionsnivå; till exempel kan V8-motorn, med tillräckligt många iterationer, göra looparna betydligt snabbare. Det som bör betonas är det faktum att vi lever på 2000-talet och att vi inte skriver program för Apollo 13-uppdraget. Vi har mycket större manöverutrymme när det gäller resurser - de är till för att användas (helst på ett rimligt sätt :>).
Ibland ger det verkligen mycket att bryta koden i delar. När operationerna bildar en kedja vars syfte är att utföra åtgärder som är ansvariga för en specifik modifiering av data - är det lätt att gå vilse. Därför kan du på ett enkelt sätt, istället för att göra allt i en sträng, bryta ner de enskilda delarna av koden som är ansvariga för en viss sak i enskilda element. Detta kommer inte bara att göra avsikten med enskilda operationer tydlig, utan det kommer också att göra det möjligt för dig att testa kodfragment som bara är ansvariga för en sak och som enkelt kan återanvändas.
Några praktiska exempel
Jag tror att den mest korrekta representationen av några av påståendena ovan är att visa hur de fungerar i praktiken - i det här stycket ska jag försöka beskriva några dåliga kodmetoder som mer eller mindre kan omvandlas till bra. Jag kommer att peka på vad som stör läsbarheten av koden i vissa ögonblick och hur man kan förhindra det.
Förbannelsen med variabler med en enda bokstav
En hemsk metod som tyvärr är ganska vanlig, till och med på universitet, är att namnge variabler med en enda bokstav. Det är svårt att inte hålla med om att det ibland är en ganska bekväm lösning - vi slipper onödigt funderande på hur vi ska avgöra syftet med en variabel och istället för att använda flera eller flera tecken för att namnge den använder vi bara en bokstav - t.ex. i, j, k.
Paradoxalt nog är vissa definitioner av dessa variabler försedda med en mycket längre kommentar, som avgör vad författaren hade i åtanke.
Ett bra exempel här skulle vara att representera iterationen över en tvådimensionell matris som innehåller motsvarande värden i skärningspunkten mellan kolumn och rad.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// ganska dåligt
for (let i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// här är innehållet, men varje gång i och j används måste jag gå tillbaka och analysera vad de används till
}
}
// fortfarande dåligt men roligt
låt i; // rad
låt j; // kolumn
for (i = 0; i < array[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// här är innehållet, men varje gång i och j används måste jag gå tillbaka och kolla kommentarer vad de används till
}
}
// mycket bättre
const rowCount = array.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const rad = array[radIndex];
const columnCount = rad.längd;
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const column = rad[columnIndex];
// är det någon som tvivlar på vad som är vad?
}
}
Smygande överoptimering
En vacker dag stötte jag på en mycket sofistikerad kod skriven av en Programvaruingenjör. Den här ingenjören hade kommit på att det gick att optimera sändningen av användarrättigheter som strängar med specifika åtgärder med hjälp av några trick på bitnivå.
Förmodligen skulle en sådan lösning vara OK om målet var Commodore 64, men syftet med den här koden var en enkel webbapplikation, skriven i JS. Tiden har kommit för att övervinna denna egenhet: Låt oss säga att en användare bara har fyra alternativ i hela systemet för att ändra innehåll: skapa, läsa, uppdatera, radera. Det är ganska naturligt att vi antingen skickar dessa behörigheter i JSON-form som nycklar till ett objekt med tillstånd eller som en array.
Men vår smarta ingenjör upptäckte att siffran fyra är ett magiskt värde i den binära presentationen och räknade ut det på följande sätt:
Hela tabellen med funktioner har 16 rader, jag har bara listat 4 för att förmedla idén om att skapa dessa behörigheter. Läsningen av behörigheterna går till på följande sätt:
Det du ser ovan är inte WebAssembly-kod. Jag vill inte bli missförstådd här - sådana optimeringar är en normal sak för system där vissa saker måste ta mycket lite tid eller minne (eller båda). Webbapplikationer, å andra sidan, är definitivt inte en plats där sådana överoptimeringar är helt meningsfulla. Jag vill inte generalisera, men i arbetet med frontend-utvecklare utförs sällan mer komplexa operationer som når nivån för bitabstraktion.
Den är helt enkelt inte läsbar, och en programmerare som kan analysera en sådan kod kommer säkert att undra vilka osynliga fördelar denna lösning har och vad som kan skadas när utvecklingsteam vill skriva om det till en mer rimlig lösning.
Dessutom - jag misstänker att om man skickar behörigheterna som ett vanligt objekt skulle en programmerare kunna läsa avsikten på 1-2 sekunder, medan det tar minst några minuter att analysera hela den här saken från början. Det kommer att finnas flera programmerare i projektet, var och en av dem kommer att behöva stöta på den här koden - de kommer att behöva analysera den flera gånger, för efter en tid kommer de att glömma vilken magi som händer där. Är det värt att spara dessa få byte? Enligt min åsikt, nej.
Söndra och härska
Webbutveckling växer snabbt och det finns inget som tyder på att något snart kommer att förändras i detta avseende. Vi måste erkänna att ansvaret för frontend-utvecklare nyligen har ökat avsevärt - de tog över den del av logiken som ansvarar för presentationen av data i användargränssnittet.
Ibland är denna logik enkel och de objekt som tillhandahålls av API:et har en enkel och läsbar struktur. Ibland kräver de dock olika typer av mappning, sortering och andra operationer för att anpassa dem till olika platser på sidan. Och det är här vi lätt kan falla ner i träsket.
Många gånger har jag kommit på mig själv med att göra data i de operationer jag utförde praktiskt taget oläsliga. Trots korrekt användning av array-metoder och korrekt namngivning av variabler, förlorade kedjorna av operationer vid vissa tillfällen nästan sammanhanget för vad jag ville uppnå. Dessutom behövde vissa av dessa operationer ibland användas på andra ställen, och ibland var de globala eller tillräckligt sofistikerade för att kräva att man skrev tester.
Jag vet, jag vet - det här är inte någon trivial kodbit som enkelt illustrerar vad jag vill förmedla. Och jag vet också att beräkningskomplexiteten i de två exemplen är något annorlunda, medan vi i 99% av fallen inte behöver oroa oss för det. Skillnaden mellan algoritmerna är enkel eftersom de båda förbereder en karta över platser och enhetsägare.
Den första skapar denna karta två gånger, medan den andra bara gör det en gång. Och det enklaste exemplet som visar oss att den andra algoritmen är mer portabel ligger i det faktum att vi måste ändra logiken för att skapa denna karta för den första och t.ex. göra uteslutning av vissa platser eller andra konstiga saker som kallas affärslogik. När det gäller den andra algoritmen ändrar vi bara sättet att hämta kartan, medan alla övriga datamodifieringar som sker i de efterföljande raderna förblir oförändrade. När det gäller den första algoritmen måste vi justera varje försök att förbereda kartan.
Och det här är bara ett exempel - i praktiken finns det många sådana fall när vi behöver omvandla eller omarbeta en viss datamodell runt hela applikationen.
Det bästa sättet att undvika att hålla jämna steg med olika affärsförändringar är att förbereda globala verktyg som gör det möjligt för oss att extrahera information av intresse på ett ganska generiskt sätt. Även på bekostnad av de 2-3 millisekunder som vi kanske förlorar på grund av avoptimering.
Sammanfattning
Att vara programmerare är ett yrke som alla andra - varje dag lär vi oss nya saker och gör ofta en hel del misstag. Det viktigaste är att lära sig av dessa misstag, bli bättre i sitt yrke och inte upprepa dessa fel i framtiden. Man kan inte tro på myten att det arbete vi utför alltid kommer att vara felfritt. Du kan dock, baserat på andras erfarenheter, minska bristerna i enlighet med detta.
Jag hoppas att du genom att läsa den här artikeln kan undvika åtminstone några av de dålig kodningspraxis som jag har upplevt i mitt arbete. Om du har några frågor om bästa kodpraxis kan du nå Codest-besättningen ut för att rådgöra med dina tvivel.