Hvordan dræber man ikke et projekt med dårlig kodningspraksis?
Bartosz Slysz
Software Engineer
Mange programmører i begyndelsen af deres karriere betragter emnet navngivning af variabler, funktioner, filer og andre komponenter som ikke særlig vigtigt. Resultatet er, at deres designlogik ofte er korrekt - algoritmerne kører hurtigt og giver den ønskede effekt, men kan være svært læselige. I denne artikel vil jeg kort forsøge at beskrive, hvad vi skal være opmærksomme på, når vi navngiver forskellige kodeelementer, og hvordan vi undgår at gå fra den ene yderlighed til den anden.
Hvorfor vil forsømmelse af navngivningsfasen forlænge (i nogle tilfælde - enormt) udviklingen af dit projekt?
Lad os antage, at du og din hold er ved at overtage Kode fra andre programmører. De projekt you inherit blev udviklet uden nogen form for kærlighed - det fungerede fint, men hvert eneste element kunne have været skrevet på en meget bedre måde.
Når det drejer sig om arkitektur, udløser kodearv næsten altid had og vrede fra de programmører, der har fået den. Nogle gange skyldes det brugen af døende (eller uddøde) teknologier, nogle gange den forkerte måde at tænke på applikationen på i begyndelsen af udviklingen, og nogle gange simpelthen på grund af den ansvarlige programmørs manglende viden.
Når projekttiden går, er det under alle omstændigheder muligt at nå et punkt, hvor programmørerne bliver rasende på arkitekturer og teknologier. Når alt kommer til alt, skal enhver applikation omskrives i nogle dele eller bare ændres i specifikke dele efter et stykke tid - det er naturligt. Men det problem, der vil gøre programmørerne gråhårede, er vanskelighederne med at læse og forstå den kode, de har arvet.
Især i ekstreme tilfælde, hvor variabler navngives med enkelte, meningsløse bogstaver, og funktioner er en pludselig bølge af kreativitet, som på ingen måde er i overensstemmelse med resten af programmet, kan dine programmører gå amok. I et sådant tilfælde kræver enhver kodeanalyse, der kunne køre hurtigt og effektivt med korrekt navngivning, yderligere analyse af de algoritmer, der er ansvarlige for at producere f.eks. funktionsresultatet. Og sådan en analyse - selv om den ikke er iøjnefaldende - spilder en enorm mængde tid.
At implementere nye funktioner i forskellige dele af applikationen betyder, at man skal igennem et mareridt med at analysere den, og efter noget tid skal man gå tilbage til koden og analysere den igen, fordi dens intentioner ikke er klare, og den tidligere tid, man brugte på at forstå, hvordan den fungerede, var spildt, fordi man ikke længere kan huske, hvad formålet var.
Og så bliver vi suget ind i en tornado af uorden, der hersker i applikationen og langsomt fortærer alle deltagere i dens udvikling. Programmørerne hader projektet, projektlederne hader at forklare, hvorfor udviklingstiden begynder at stige konstant, og kunden mister tilliden og bliver vred, fordi intet går efter planen.
Hvordan undgår man det?
Lad os se det i øjnene - nogle ting kan ikke springes over. Hvis vi har valgt bestemte teknologier i begyndelsen af projektet, skal vi være opmærksomme på, at de med tiden enten holder op med at blive understøttet, eller at færre og færre programmører behersker teknologier fra et par år siden, som langsomt er ved at blive forældede. Nogle biblioteker kræver i deres opdateringer mere eller mindre omfattende ændringer i koden, hvilket ofte medfører en hvirvel af afhængigheder, som man kan komme til at sidde endnu mere fast i.
På den anden side er det ikke så sort et scenarie; selvfølgelig bliver teknologierne ældre, men den faktor, der helt sikkert sænker udviklingstiden for projekter, der involverer dem, er stort set grim kode. Og selvfølgelig skal vi her nævne bogen af Robert C. Martin - det er en bibel for programmører, hvor forfatteren præsenterer en masse god praksis og principper, der bør følges for at skabe kode, der stræber efter perfektion.
Det grundlæggende, når man navngiver variabler, er at formidle deres hensigt klart og enkelt. Det lyder ret enkelt, men nogle gange bliver det overset eller ignoreret af mange mennesker. Et godt navn vil specificere, hvad variablen præcist skal gemme, eller hvad funktionen skal gøre - den kan ikke navngives for generisk, men på den anden side kan den heller ikke blive en lang snegl, hvis blotte læsning er en stor udfordring for hjernen. Efter nogen tid med kode af god kvalitet oplever vi fordybelseseffekten, hvor vi ubevidst er i stand til at arrangere navngivning og overførsel af data til funktionen på en sådan måde, at det hele ikke efterlader nogen illusioner om, hvilken hensigt der driver den, og hvad det forventede resultat af at kalde den er.
En anden ting, der kan findes i JavaScripter blandt andet et forsøg på at overoptimere koden, hvilket i mange tilfælde gør den ulæselig. Det er normalt, at nogle algoritmer kræver særlig omhu, hvilket ofte afspejler det faktum, at hensigten med koden kan være lidt mere indviklet. Ikke desto mindre er de tilfælde, hvor vi har brug for overdreven optimering, ekstremt sjældne, eller i hvert fald dem, hvor vores kode er beskidt. Det er vigtigt at huske, at mange sprogrelaterede optimeringer finder sted på et lidt lavere abstraktionsniveau; for eksempel kan V8-motoren med nok iterationer gøre løkkerne betydeligt hurtigere. Det, der skal understreges, er, at vi lever i det 21. århundrede, og vi skriver ikke programmer til Apollo 13-missionen. Vi har meget mere råderum, når det gælder ressourcer - de er der for at blive brugt (helst på en fornuftig måde :>).
Nogle gange giver det virkelig meget at bryde koden op i dele. Når operationerne danner en kæde, hvis formål er at udføre handlinger, der er ansvarlige for en bestemt ændring af data - er det let at fare vild. Derfor kan du på en enkel måde, i stedet for at gøre alt i én streng, opdele de enkelte dele af koden, der er ansvarlige for en bestemt ting, i individuelle elementer. Det vil ikke kun gøre hensigten med de enkelte operationer klar, men det vil også give dig mulighed for at teste kodefragmenter, der kun er ansvarlige for én ting, og som nemt kan genbruges.
Nogle praktiske eksempler
Jeg tror, at den mest præcise fremstilling af nogle af ovenstående udsagn vil være at vise, hvordan de fungerer i praksis - i dette afsnit vil jeg forsøge at skitsere nogle dårlige kodepraksisser, der mere eller mindre kan omdannes til gode. Jeg vil påpege, hvad der forstyrrer læsbarheden af koden i nogle øjeblikke, og hvordan man kan forhindre det.
Forbandelsen ved variabler med et enkelt bogstav
En forfærdelig praksis, som desværre er ret almindelig, selv på universiteterne, er at navngive variabler med et enkelt bogstav. Det er svært ikke at være enig i, at det nogle gange er en ret praktisk løsning - vi undgår unødvendige overvejelser om, hvordan vi skal finde ud af, hvad en variabel skal bruges til, og i stedet for at bruge flere tegn til at navngive den, bruger vi bare ét bogstav - f.eks. i, j, k.
Paradoksalt nok er nogle definitioner af disse variabler udstyret med en meget længere kommentar, som afgør, hvad forfatteren havde i tankerne.
Et godt eksempel her ville være at repræsentere iterationen over et todimensionalt array, der indeholder de tilsvarende værdier i skæringspunktet mellem kolonne og række.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// ret dårligt
for (let i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// her er indholdet, men hver gang i og j bruges, er jeg nødt til at gå tilbage og analysere, hvad de bruges til
}
}
// stadig dårligt, men sjovt
lad i; // række
let j; // kolonne
for (i = 0; i < array[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// her er indholdet, men hver gang i og j bruges, er jeg nødt til at gå tilbage og tjekke kommentarer, hvad de bruges til
}
}
// meget bedre
const rowCount = array.length;
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
const row = array[rowIndex];
const columnCount = row.length;
for (let columnIndex = 0; columnIndex < columnCount; columnIndex++) {
const column = row[columnIndex];
// er der nogen, der er i tvivl om, hvad der er hvad?
}
}
Snigende overoptimering
En skønne dag faldt jeg over en meget sofistikeret kode skrevet af en softwareingeniør. Denne ingeniør havde fundet ud af, at det at sende brugertilladelser som strenge, der specificerer specifikke handlinger, kunne optimeres kraftigt ved hjælp af et par tricks på bitniveau.
En sådan løsning ville sandsynligvis være OK, hvis målet var Commodore 64, men formålet med denne kode var en simpel webapplikation, skrevet i JS. Tiden er inde til at overvinde denne særhed: Lad os sige, at en bruger kun har fire muligheder i hele systemet for at ændre indhold: Opret, læs, opdater, slet. Det er ret naturligt, at vi enten sender disse tilladelser i en JSON-form som nøgler til et objekt med tilstande eller et array.
Men vores kloge ingeniør bemærkede, at tallet fire er en magisk værdi i den binære præsentation, og regnede det ud på følgende måde:
Hele tabellen med muligheder har 16 rækker, jeg har kun nævnt 4 for at formidle ideen om at oprette disse tilladelser. Læsning af tilladelserne foregår på følgende måde:
Det, du ser ovenfor, er ikke WebAssembly-kode. Jeg ønsker ikke at blive misforstået her - sådanne optimeringer er en normal ting for systemer, hvor visse ting skal tage meget lidt tid eller hukommelse (eller begge dele). På den anden side er webapplikationer bestemt ikke et sted, hvor sådanne overoptimeringer giver mening. Jeg vil ikke generalisere, men i frontend-udviklernes arbejde udføres der sjældent mere komplekse operationer, der når op på bit-abstraktionsniveau.
Det er simpelthen ikke læsbart, og en programmør, der kan analysere sådan en kode, vil helt sikkert undre sig over, hvilke usynlige fordele denne løsning har, og hvad der kan blive ødelagt, når udviklingsteam ønsker at omskrive det til en mere fornuftig løsning.
Hvad mere er - jeg formoder, at hvis man sender tilladelserne som et almindeligt objekt, vil en programmør kunne læse hensigten på 1-2 sekunder, mens det vil tage mindst et par minutter at analysere det hele fra begyndelsen. Der vil være flere programmører i projektet, og hver af dem vil støde på dette stykke kode - de vil være nødt til at analysere det flere gange, for efter noget tid vil de glemme, hvad der foregår af magi der. Er det værd at gemme de få bytes? Efter min mening ikke.
Del og hersk
Webudvikling vokser hurtigt, og der er ikke noget, der tyder på, at noget snart vil ændre sig i den henseende. Vi må indrømme, at frontend-udviklernes ansvar for nylig er steget betydeligt - de har overtaget den del af logikken, der er ansvarlig for præsentationen af data i brugergrænsefladen.
Nogle gange er denne logik enkel, og de objekter, der leveres af API'en, har en enkel og læsbar struktur. Men nogle gange kræver de forskellige typer af mapping, sortering og andre operationer for at tilpasse dem til forskellige steder på siden. Og det er her, vi nemt kan falde i sump.
Mange gange har jeg taget mig selv i at gøre dataene i de operationer, jeg udførte, næsten ulæselige. På trods af korrekt brug af array-metoder og korrekt navngivning af variabler mistede kæderne af operationer på nogle punkter næsten sammenhængen med det, jeg ønskede at opnå. Nogle af disse operationer skulle også bruges andre steder, og nogle gange var de globale eller sofistikerede nok til at kræve, at man skrev tests.
Jeg ved det godt - det er ikke et trivielt stykke kode, der nemt illustrerer det, jeg gerne vil formidle. Og jeg ved også, at beregningskompleksiteten i de to eksempler er lidt forskellig, men i 99% af tilfældene behøver vi ikke at bekymre os om det. Forskellen mellem algoritmerne er enkel, da de begge udarbejder et kort over placeringer og enhedsejere.
Den første forbereder dette kort to gange, mens den anden kun forbereder det én gang. Og det enkleste eksempel, der viser os, at den anden algoritme er mere bærbar, er, at vi er nødt til at ændre logikken i oprettelsen af dette kort for den første og f.eks. udelukke visse steder eller andre underlige ting, der kaldes forretningslogik. I den anden algoritme ændrer vi kun måden at hente kortet på, mens alle de øvrige datamodifikationer, der sker i de efterfølgende linjer, forbliver uændrede. I tilfældet med den første algoritme er vi nødt til at tilpasse hvert forsøg på at forberede kortet.
Og dette er kun et eksempel - i praksis er der masser af sådanne tilfælde, hvor vi har brug for at transformere eller refaktorere en bestemt datamodel omkring hele applikationen.
Den bedste måde at undgå at holde trit med forskellige forretningsændringer er at forberede globale værktøjer, der giver os mulighed for at udtrække oplysninger af interesse på en ret generisk måde. Selv på bekostning af de 2-3 millisekunder, som vi måske mister på grund af de-optimering.
Sammenfatning
At være programmør er et erhverv som alle andre - hver dag lærer vi nye ting og begår ofte en masse fejl. Det vigtigste er at lære af disse fejl, blive bedre til sit fag og ikke gentage fejlene i fremtiden. Man kan ikke tro på myten om, at det arbejde, vi udfører, altid vil være fejlfrit. Men du kan, baseret på andres erfaringer, reducere fejlene i overensstemmelse hermed.
Jeg håber, at du ved at læse denne artikel kan undgå i det mindste nogle af de dårlig kodningspraksis som jeg har oplevet i mit arbejde. Hvis du har spørgsmål om bedste kodepraksis, kan du kontakte Codest-besætningen ud for at konsultere din tvivl.