Hvordan unngår man å drepe et prosjekt med dårlig kodingspraksis?
Bartosz Slysz
Software Engineer
Mange programmerere i begynnelsen av karrieren anser navngivning av variabler, funksjoner, filer og andre komponenter som lite viktig. Som et resultat er designlogikken deres ofte riktig - algoritmer kjører raskt og gir ønsket effekt, samtidig som de knapt kan være lesbare. I denne artikkelen vil jeg kort forsøke å beskrive hva vi bør ta utgangspunkt i når vi navngir ulike kodeelementer, og hvordan vi unngår å gå fra den ene ytterligheten til den andre.
Hvorfor vil det å forsømme navngivningsfasen forlenge (i noen tilfeller - enormt) utviklingen av prosjektet ditt?
La oss anta at du og din team er i ferd med å ta over kode fra andre programmerere. De prosjekt you inherit ble utviklet uten noen som helst kjærlighet - det fungerte helt fint, men hvert eneste element kunne vært skrevet på en mye bedre måte.
Når det gjelder arkitekturen, utløser det nesten alltid hat og sinne fra programmererne som har fått det, når det gjelder kodearv. Noen ganger skyldes dette bruk av døende (eller utdødd) teknologi, noen ganger feil måte å tenke på applikasjonen på i begynnelsen av utviklingen, og noen ganger rett og slett på grunn av mangel på kunnskap hos den ansvarlige programmereren.
Etter hvert som prosjekttiden går, er det uansett mulig å komme til et punkt der programmererne blir rasende på arkitekturer og teknologier. Tross alt må alle applikasjoner omskrives i noen deler eller bare endres i spesifikke deler etter en tid - det er naturlig. Men problemet som gjør programmererne grå i håret, er vanskeligheten med å lese og forstå koden de har arvet.
Spesielt i ekstreme tilfeller der variabler er navngitt med enkle, meningsløse bokstaver og funksjoner er et plutselig utbrudd av kreativitet, som på ingen måte er konsistent med resten av programmet, kan programmererne gå berserk. I slike tilfeller krever enhver kodeanalyse som kunne kjøres raskt og effektivt med korrekt navngivning, en tilleggsanalyse av algoritmene som er ansvarlige for å produsere funksjonsresultatet, for eksempel. Og en slik analyse, selv om den ikke er iøynefallende, sløser bort enormt mye tid.
Når man implementerer nye funksjoner i ulike deler av applikasjonen, må man gå gjennom marerittet med å analysere den, og etter en stund må man gå tilbake til koden og analysere den på nytt fordi intensjonene er uklare, og den tidligere tiden man brukte på å forstå hvordan den fungerte, var bortkastet fordi man ikke lenger husker hva som var formålet med den.
Og dermed blir vi sugd inn i en tornado av uorden som hersker i applikasjonen og langsomt fortærer alle som deltar i utviklingen. Programmererne hater prosjektet, prosjektlederne hater å forklare hvorfor utviklingstiden stadig øker, og kunden mister tilliten og blir sint fordi ingenting går etter planen.
Hvordan unngå det?
La oss innse det - noen ting kan ikke hoppes over. Hvis vi har valgt visse teknologier i begynnelsen av prosjektet, må vi være klar over at de med tiden enten vil slutte å bli støttet, eller at færre og færre programmerere vil beherske teknologier fra noen år tilbake som sakte blir foreldet. Noen biblioteker krever mer eller mindre omfattende endringer i koden når de oppdateres, noe som ofte fører til en virvel av avhengigheter som man kan bli sittende enda mer fast i.
På den annen side er det ikke et så svart scenario; selvfølgelig - teknologiene blir eldre, men den faktoren som definitivt bremser utviklingstiden for prosjekter som involverer dem, er stort sett stygg kode. Og selvfølgelig må vi her nevne boken av Robert C. Martin - dette er en bibel for programmerere, der forfatteren presenterer mange gode fremgangsmåter og prinsipper som bør følges for å lage kode som streber etter perfeksjon.
Det grunnleggende når man navngir variabler, er å formidle hensikten med dem på en tydelig og enkel måte. Det høres ganske enkelt ut, men noen ganger er det mange som neglisjerer eller ignorerer det. Et godt navn vil spesifisere nøyaktig hva variabelen skal lagre eller hva funksjonen skal gjøre - den kan ikke navngis for generisk, men på den annen side kan den heller ikke bli en lang snegle som bare det å lese den er en utfordring for hjernen. Etter en stund med god kvalitetskode opplever vi fordypningseffekten, der vi ubevisst er i stand til å ordne navngivning og overføring av data til funksjonen på en slik måte at det hele ikke etterlater noen illusjoner om hvilken intensjon som driver den, og hva som er det forventede resultatet av å kalle den.
En annen ting som kan finnes i JavaScripter blant annet et forsøk på å overoptimalisere koden, noe som i mange tilfeller gjør den uleselig. Det er normalt at noen algoritmer krever spesiell forsiktighet, noe som ofte gjenspeiler det faktum at hensikten med koden kan være litt mer innviklet. Likevel er det ytterst sjelden at vi trenger overdreven optimalisering, i hvert fall i de tilfellene der koden vår er skitten. Det er viktig å huske at mange språkrelaterte optimaliseringer foregår på et litt lavere abstraksjonsnivå; for eksempel kan V8-motoren, med nok iterasjoner, øke hastigheten på løkkene betydelig. Det som bør understrekes, er det faktum at vi lever i det 21. århundre, og at vi ikke skriver programmer for Apollo 13-oppdraget. Vi har mye større handlingsrom når det gjelder ressurser - de er der for å bli brukt (helst på en fornuftig måte :>).
Noen ganger gir det veldig mye å bryte koden i deler. Når operasjonene danner en kjede hvis formål er å utføre handlinger som er ansvarlige for en bestemt endring av data - er det lett å gå seg vill. Derfor kan du på en enkel måte, i stedet for å gjøre alt i en streng, bryte ned de enkelte delene av koden som er ansvarlige for en bestemt ting i individuelle elementer. Dette vil ikke bare gjøre hensikten med individuelle operasjoner tydelig, men det vil også gjøre det mulig å teste kodefragmenter som bare er ansvarlige for én ting, og som enkelt kan gjenbrukes.
Noen praktiske eksempler
Jeg tror den mest nøyaktige fremstillingen av noen av utsagnene ovenfor vil være å vise hvordan de fungerer i praksis - i dette avsnittet vil jeg prøve å skissere noen dårlige kodepraksiser som mer eller mindre kan forvandles til gode. Jeg vil påpeke hva som forstyrrer lesbarheten til koden i noen øyeblikk, og hvordan man kan forhindre det.
Forbandet med variabler med én bokstav
En forferdelig praksis som dessverre er ganske vanlig, selv på universiteter, er å navngi variabler med en enkelt bokstav. Det er vanskelig å ikke være enig i at det noen ganger er en ganske praktisk løsning - vi slipper å tenke unødvendig på hvordan vi skal bestemme formålet med en variabel, og i stedet for å bruke flere eller flere tegn for å navngi den, bruker vi bare én bokstav - f.eks. i, j, k.
Paradoksalt nok er noen definisjoner av disse variablene utstyrt med en mye lengre kommentar, som avgjør hva forfatteren hadde i tankene.
Et godt eksempel her kan være å representere iterasjonen over en todimensjonal matrise som inneholder de tilsvarende verdiene i skjæringspunktet mellom kolonne og rad.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// ganske dårlig
for (let i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// her er innholdet, men hver gang i og j brukes, må jeg gå tilbake og analysere hva de brukes til
}
}
// fortsatt dårlig, men morsomt
la i; // rad
let j; // kolonne
for (i = 0; i < matrise[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// her er innholdet, men hver gang i og j brukes, må jeg gå tilbake og sjekke kommentarene for hva de brukes til
}
}
// mye bedre
const rowCount = array.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = array[rowIndex];
const columnCount = rad.lengde;
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const kolonne = rad[kolonneIndeks];
// er det noen som er i tvil om hva som er hva?
}
}
Snikende overoptimalisering
En vakker dag kom jeg over en svært sofistikert kode skrevet av en programvareingeniør. Denne ingeniøren hadde funnet ut at det å sende brukerrettigheter som strenger som spesifiserte spesifikke handlinger, kunne optimaliseres kraftig ved hjelp av noen få triks på bitnivå.
Sannsynligvis ville en slik løsning vært OK hvis målet var Commodore 64, men formålet med denne koden var en enkel webapplikasjon, skrevet i JS. Tiden er inne for å overmanne denne særegenheten: La oss si at en bruker bare har fire alternativer i hele systemet for å endre innhold: opprette, lese, oppdatere, slette. Det er ganske naturlig at vi enten sender disse tillatelsene i JSON-format som nøkler i et objekt med tilstander eller som en matrise.
Vår smarte ingeniør la imidlertid merke til at tallet fire er en magisk verdi i den binære presentasjonen, og regnet det ut på følgende måte:
Hele tabellen med funksjoner har 16 rader, jeg har bare listet opp 4 for å formidle ideen om å opprette disse tillatelsene. Lesing av tillatelsene går som følger:
Det du ser ovenfor er ikke WebAssembly-kode. Jeg vil ikke misforstås her - slike optimaliseringer er en normal ting for systemer der visse ting må ta veldig lite tid eller minne (eller begge deler). Webapplikasjoner, derimot, er definitivt ikke et sted der slike overoptimaliseringer gir full mening. Jeg vil ikke generalisere, men i arbeidet til frontend-utviklere utføres det sjelden mer komplekse operasjoner som når nivået av bitabstraksjon.
Den er rett og slett ikke lesbar, og en programmerer som kan analysere en slik kode, vil helt sikkert lure på hvilke usynlige fordeler denne løsningen har, og hva som kan bli ødelagt når utviklingsteamet ønsker å skrive den om til en mer fornuftig løsning.
Hva mer - jeg mistenker at det å sende tillatelsene som et vanlig objekt vil tillate en programmerer å lese intensjonen på 1-2 sekunder, mens det å analysere hele denne tingen fra begynnelsen vil ta minst noen minutter. Det vil være flere programmerere i prosjektet, hver av dem må komme over dette kodestykket - de må analysere det flere ganger, for etter en tid vil de glemme hvilken magi som foregår der. Er det verdt å lagre disse få byte? Etter min mening, nei.
Del og hersk
Webutvikling vokser raskt, og det er ingenting som tyder på at noe snart vil endre seg i denne forbindelse. Vi må innrømme at frontend-utviklernes ansvar har økt betraktelig den siste tiden - de har overtatt den delen av logikken som er ansvarlig for presentasjonen av data i brukergrensesnittet.
Noen ganger er denne logikken enkel, og objektene som leveres av API-et, har en enkel og lesbar struktur. Noen ganger krever de imidlertid ulike typer mapping, sortering og andre operasjoner for å tilpasse dem til ulike steder på siden. Og det er her vi lett kan havne i sumpen.
Mange ganger har jeg tatt meg selv i å gjøre dataene i operasjonene jeg utførte, nesten uleselige. Til tross for korrekt bruk av array-metoder og riktig navngivning av variabler, mistet kjeden av operasjoner på noen punkter nesten sammenhengen i det jeg ønsket å oppnå. Noen av disse operasjonene måtte også brukes andre steder, og noen ganger var de globale eller sofistikerte nok til at det var nødvendig å skrive tester.
Jeg vet, jeg vet - dette er ikke noe trivielt stykke kode som enkelt illustrerer det jeg ønsker å formidle. Og jeg vet også at beregningskompleksiteten i de to eksemplene er litt forskjellige, mens vi i 99% av tilfellene ikke trenger å bekymre oss for det. Forskjellen mellom algoritmene er enkel, ettersom begge utarbeider et kart over steder og enhetseiere.
Den første lager dette kartet to ganger, mens den andre lager det bare én gang. Og det enkleste eksemplet som viser oss at den andre algoritmen er mer portabel, er at vi må endre logikken for å lage dette kartet for den første, og f.eks. ekskludere visse steder eller andre rare ting som kalles forretningslogikk. I den andre algoritmen endrer vi bare måten kartet hentes på, mens resten av datamodifikasjonene som skjer i de påfølgende linjene, forblir uendret. I den første algoritmen må vi justere hvert eneste forsøk på å utarbeide kartet.
Og dette er bare et eksempel - i praksis er det mange slike tilfeller der vi trenger å transformere eller refaktorere en bestemt datamodell rundt hele applikasjonen.
Den beste måten å unngå å holde tritt med ulike endringer i virksomheten på, er å utarbeide globale verktøy som lar oss hente ut informasjon av interesse på en ganske generisk måte. Selv på bekostning av de 2-3 millisekundene som vi kanskje mister på grunn av de-optimalisering.
Sammendrag
Å være programmerer er et yrke som alle andre - hver dag lærer vi nye ting, og vi gjør ofte mange feil. Det viktigste er å lære av disse feilene, bli bedre i yrket og ikke gjenta feilene i fremtiden. Du kan ikke tro på myten om at arbeidet vi gjør, alltid vil være feilfritt. Du kan imidlertid, basert på andres erfaringer, redusere feilene tilsvarende.
Jeg håper at du ved å lese denne artikkelen vil unngå i det minste noen av de dårlig kodingspraksis som jeg har erfart i arbeidet mitt. Hvis du har spørsmål om beste kodepraksis, kan du kontakte Codest-mannskapet ut for å konsultere din tvil.