Hoe help je een project niet om zeep met slechte codeerpraktijken?
Bartosz Slysz
Software Engineer
Veel programmeurs die aan het begin van hun carrière staan, beschouwen de naamgeving van variabelen, functies, bestanden en andere onderdelen als niet erg belangrijk. Als gevolg daarvan is hun ontwerplogica vaak correct - algoritmen draaien snel en produceren het gewenste effect, terwijl ze nauwelijks leesbaar zijn. In dit artikel zal ik kort proberen te beschrijven waar we ons door moeten laten leiden bij het benoemen van verschillende code-elementen en hoe we niet van het ene uiterste naar het andere moeten gaan.
Waarom het verwaarlozen van de naamgevingsfase de ontwikkeling van je project (in sommige gevallen - enorm) zal verlengen?
Laten we aannemen dat jij en je team nemen de code van andere programmeurs. De project je erft is ontwikkeld zonder enige liefde - het werkte prima, maar elk element ervan had veel beter geschreven kunnen worden.
Als het op architectuur aankomt, roept overerving van code bijna altijd haat en woede op bij de programmeurs die het hebben gekregen. Soms is dit te wijten aan het gebruik van uitstervende (of uitgestorven) technologieën, soms de verkeerde manier van denken over de applicatie aan het begin van de ontwikkeling, en soms gewoon door het gebrek aan kennis van de programmeur die verantwoordelijk is.
Hoe dan ook, naarmate het project vordert, is het mogelijk om een punt te bereiken waarop programmeurs razend worden op architecturen en technologieën. Immers, elke applicatie moet na verloop van tijd herschreven worden of specifieke onderdelen moeten aangepast worden - dat is normaal. Maar het probleem waar programmeurs grijze haren van krijgen is de moeilijkheid om de code die ze geërfd hebben te lezen en te begrijpen.
Vooral in extreme gevallen, wanneer variabelen worden benoemd met losse, betekenisloze letters en functies een plotselinge uitbarsting van creativiteit zijn die op geen enkele manier consistent is met de rest van de applicatie, kunnen je programmeurs door het lint gaan. In zo'n geval vereist elke code-analyse die snel en efficiënt zou kunnen worden uitgevoerd met correcte naamgeving een aanvullende analyse van de algoritmen die verantwoordelijk zijn voor het produceren van bijvoorbeeld het functieresultaat. En zo'n analyse, hoewel onopvallend, verspilt enorm veel tijd.
Het implementeren van nieuwe functionaliteiten in verschillende delen van de applicatie betekent dat je de nachtmerrie van het analyseren ervan moet doorstaan, na verloop van tijd moet je teruggaan naar de code en deze opnieuw analyseren omdat de bedoelingen ervan niet duidelijk zijn en de tijd die je eerder hebt besteed aan het proberen te begrijpen van de werking ervan een verspilling was omdat je je niet meer herinnert wat het doel ervan was.
En zo worden we meegezogen in een tornado van wanorde die de applicatie beheerst en langzaam elke deelnemer aan de ontwikkeling opslokt. Programmeurs haten het project, projectmanagers haten het om uit te leggen waarom de ontwikkelingstijd steeds langer wordt en de klant verliest het vertrouwen en wordt boos omdat niets volgens plan verloopt.
Hoe vermijd je het?
Laten we eerlijk zijn - sommige dingen kunnen niet worden overgeslagen. Als we aan het begin van het project voor bepaalde technologieën hebben gekozen, moeten we ons ervan bewust zijn dat deze na verloop van tijd niet meer worden ondersteund of dat steeds minder programmeurs vloeiend omgaan met technologieën van een paar jaar geleden die langzaam verouderd raken. Sommige bibliotheken vereisen in hun updates meer of minder ingrijpende wijzigingen in de code, die vaak een draaikolk van afhankelijkheden met zich meebrengen waarin je nog meer vast kunt komen te zitten.
Aan de andere kant is het niet zo'n zwart scenario; natuurlijk - de technologieën worden ouder, maar de factor die de ontwikkelingstijd van projecten waarbij ze betrokken zijn zeker vertraagt, is grotendeels lelijke code. En natuurlijk moeten we hier het boek van Robert C. Martin noemen - dit is een bijbel voor programmeurs, waarin de auteur veel goede praktijken en principes presenteert die moeten worden gevolgd om code te maken die perfectie nastreeft.
Het belangrijkste bij het benoemen van variabelen is om duidelijk en eenvoudig de bedoeling ervan over te brengen. Het klinkt heel eenvoudig, maar soms wordt het door veel mensen verwaarloosd of genegeerd. Een goede naam specificeert wat de variabele precies moet opslaan of wat de functie moet doen - het mag niet te algemeen genoemd worden, maar aan de andere kant mag het ook geen lange slak worden waarvan alleen al het lezen een behoorlijke uitdaging voor de hersenen is. Na enige tijd met code van goede kwaliteit te hebben gewerkt, ervaren we het immersie-effect, waarbij we in staat zijn om onbewust de naamgeving en het doorgeven van gegevens aan de functie zo te regelen dat het geheel geen illusies laat bestaan over de intentie die eraan ten grondslag ligt en wat het verwachte resultaat is van het aanroepen ervan.
Iets anders dat je kunt vinden in JavaScriptis onder andere een poging om de code te over-optimaliseren, waardoor deze in veel gevallen onleesbaar wordt. Het is normaal dat sommige algoritmen speciale zorg nodig hebben, wat vaak een weerspiegeling is van het feit dat de bedoeling van de code wat ingewikkelder kan zijn. Desalniettemin zijn de gevallen waarin we buitensporige optimalisaties nodig hebben extreem zeldzaam, of in ieder geval de gevallen waarin onze code vuil is. Het is belangrijk om te onthouden dat veel taalgerelateerde optimalisaties plaatsvinden op een iets lager abstractieniveau; de V8 engine kan bijvoorbeeld, met genoeg iteraties, de loops aanzienlijk versnellen. Wat benadrukt moet worden is het feit dat we in de 21e eeuw leven en geen programma's schrijven voor de Apollo 13 missie. We hebben veel meer manoeuvreerruimte op het gebied van bronnen - ze zijn er om gebruikt te worden (bij voorkeur op een redelijke manier :>).
Soms levert het opdelen van de code in onderdelen echt veel op. Wanneer de operaties een keten vormen die tot doel heeft acties uit te voeren die verantwoordelijk zijn voor een specifieke wijziging van gegevens - is het gemakkelijk om te verdwalen. Daarom kun je op een eenvoudige manier, in plaats van alles in één string te doen, de afzonderlijke delen van de code die verantwoordelijk zijn voor een bepaald ding opdelen in afzonderlijke elementen. Dit zal niet alleen de bedoeling van individuele operaties duidelijk maken, maar het zal je ook toelaten om codefragmenten te testen die verantwoordelijk zijn voor slechts één ding, en die gemakkelijk hergebruikt kunnen worden.
Enkele praktische voorbeelden
Ik denk dat de meest nauwkeurige weergave van sommige van de bovenstaande beweringen zal zijn om te laten zien hoe ze in de praktijk werken - in deze paragraaf zal ik proberen een aantal slechte codepraktijken te schetsen die min of meer kunnen worden omgezet in goede. Ik zal aangeven wat de leesbaarheid van de code op sommige momenten verstoort en hoe je dat kunt voorkomen.
De vloek van variabelen met één letter
Een vreselijke gewoonte die helaas veel voorkomt, zelfs op universiteiten, is het benoemen van variabelen met één letter. Het is moeilijk om het er niet mee eens te zijn dat dit soms een handige oplossing is - we hoeven niet onnodig na te denken over hoe we het doel van een variabele moeten bepalen en in plaats van meerdere of meer tekens te gebruiken om de variabele een naam te geven, gebruiken we gewoon één letter - bijvoorbeeld i, j, k.
Paradoxaal genoeg zijn sommige definities van deze variabelen voorzien van een veel langer commentaar, dat bepaalt wat de auteur in gedachten had.
Een goed voorbeeld hier zou zijn om de iteratie over een tweedimensionale matrix weer te geven die de overeenkomstige waarden bevat op het snijpunt van kolom en rij.
const array = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
// behoorlijk slecht
for (laat i = 0; i < array[i]; i++) {
for (let j = 0; j < array[i][j]; j++) {
// hier is de inhoud, maar elke keer als i en j worden gebruikt moet ik teruggaan en analyseren waar ze voor worden gebruikt
}
}
// nog steeds slecht, maar grappig
let i; // rij
laat j; // kolom
for (i = 0; i < array[i]; i++) {
for (j = 0; j < array[i][j]; j++) {
// hier is de inhoud, maar elke keer als i en j worden gebruikt moet ik teruggaan en opmerkingen controleren waar ze voor worden gebruikt
}
}
// veel beter
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 kolom = rij[kolomIndex];
// heeft iemand twijfels over wat wat is?
}
}
Stiekeme overoptimalisatie
Op een mooie dag kwam ik een zeer geavanceerde code tegen, geschreven door een softwareontwikkelaar. Deze ingenieur had ontdekt dat het verzenden van gebruikerspermissies als strings met specifieke acties sterk geoptimaliseerd kon worden met behulp van een paar trucjes op bitniveau.
Waarschijnlijk zou een dergelijke oplossing OK zijn als het doel Commodore 64 was, maar het doel van deze code was een eenvoudige webapplicatie, geschreven in JS. De tijd is gekomen om deze gril te overwinnen: Laten we zeggen dat een gebruiker in het hele systeem slechts vier opties heeft om inhoud te wijzigen: maken, lezen, bijwerken, verwijderen. Het is vrij logisch dat we deze rechten in een JSON-vorm sturen als sleutels van een object met toestanden of een array.
Onze slimme ingenieur merkte echter op dat het getal vier een magische waarde is in de binaire presentatie en rekende het als volgt uit:
De hele tabel met mogelijkheden heeft 16 rijen, ik heb er maar 4 opgesomd om het idee van het maken van deze permissies over te brengen. Het lezen van de permissies gaat als volgt:
Wat je hierboven ziet is niet WebAssembly code. Ik wil hier niet verkeerd begrepen worden - zulke optimalisaties zijn normaal voor systemen waar bepaalde dingen heel weinig tijd of geheugen (of beide) nodig hebben. Webapplicaties daarentegen zijn zeker geen plaats waar zulke over-optimalisaties totaal zinvol zijn. Ik wil niet generaliseren, maar in het werk van front-end ontwikkelaars worden complexere operaties die het niveau van bit-abstractie bereiken zelden uitgevoerd.
Het is eenvoudigweg niet leesbaar en een programmeur die een analyse kan maken van een dergelijke code zal zich zeker afvragen welke onzichtbare voordelen deze oplossing heeft en wat er beschadigd kan raken wanneer de ontwikkelingsteam het wil herschrijven naar een redelijkere oplossing.
Bovendien - ik vermoed dat het versturen van de rechten als een gewoon object een programmeur in staat zou stellen om de intentie in 1-2 seconden te lezen, terwijl het analyseren van dit hele ding vanaf het begin minstens een paar minuten zal duren. Er zullen meerdere programmeurs in het project zijn, elk van hen zal dit stuk code tegenkomen - ze zullen het meerdere keren moeten analyseren, omdat ze na verloop van tijd vergeten zullen zijn wat voor magie er aan de hand is. Is het de moeite waard om die paar bytes te bewaren? Naar mijn mening niet.
Verdeel en heers
Webontwikkeling groeit snel en niets wijst erop dat er in dit opzicht snel iets zal veranderen. We moeten toegeven dat de verantwoordelijkheid van front-end ontwikkelaars onlangs aanzienlijk is toegenomen - zij namen het deel van de logica over dat verantwoordelijk is voor de presentatie van gegevens in de gebruikersinterface.
Soms is deze logica eenvoudig en hebben de objecten die door de API worden geleverd een eenvoudige en leesbare structuur. Soms hebben ze echter verschillende soorten mapping, sortering en andere bewerkingen nodig om ze aan te passen aan verschillende plaatsen op de pagina. En dit is de plek waar we gemakkelijk in het moeras kunnen vallen.
Ik heb mezelf er vaak op betrapt dat ik de gegevens in de bewerkingen die ik uitvoerde vrijwel onleesbaar maakte. Ondanks het correcte gebruik van array-methodes en de juiste naamgeving van variabelen, verloren de ketens van bewerkingen op sommige punten bijna de context van wat ik wilde bereiken. Ook moesten sommige van deze operaties soms elders gebruikt worden en soms waren ze globaal of geavanceerd genoeg om tests te moeten schrijven.
Ik weet het, ik weet het - dit is geen triviaal stukje code dat gemakkelijk illustreert wat ik wil overbrengen. En ik weet ook dat de rekenkundige complexiteit van de twee voorbeelden iets verschilt, terwijl we ons daar in 99% van de gevallen geen zorgen over hoeven te maken. Het verschil tussen de algoritmes is eenvoudig, omdat ze allebei een kaart van locaties en apparaateigenaren maken.
Het eerste bereidt deze kaart twee keer voor, terwijl het tweede dit maar één keer doet. En het eenvoudigste voorbeeld dat ons laat zien dat het tweede algoritme draagbaarder is, ligt in het feit dat we de logica van het maken van deze kaart voor het eerste algoritme moeten veranderen en bijvoorbeeld bepaalde locaties moeten uitsluiten of andere rare dingen die bedrijfslogica worden genoemd. In het geval van het tweede algoritme wijzigen we alleen de manier waarop de kaart wordt verkregen, terwijl de rest van de gegevenswijzigingen in de volgende regels ongewijzigd blijven. In het geval van het eerste algoritme moeten we elke poging om de kaart te maken aanpassen.
En dit is slechts een voorbeeld - in de praktijk zijn er genoeg van zulke gevallen waarin we een bepaald gegevensmodel voor de hele applicatie moeten transformeren of refactoren.
De beste manier om te voorkomen dat we verschillende bedrijfsveranderingen moeten bijhouden, is om globale tools te maken waarmee we op een vrij generieke manier interessante informatie kunnen extraheren. Zelfs ten koste van die 2-3 milliseconden die we zouden kunnen verliezen ten koste van de-optimalisatie.
Samenvatting
Programmeur zijn is een beroep als elk ander - elke dag leren we nieuwe dingen, waarbij we vaak veel fouten maken. Het belangrijkste is om van deze fouten te leren, beter te worden in je vak en deze fouten in de toekomst niet te herhalen. Je kunt niet geloven in de mythe dat het werk dat we doen altijd foutloos zal zijn. Je kunt echter wel, op basis van de ervaringen van anderen, de fouten verminderen.
Ik hoop dat je door het lezen van dit artikel ten minste enkele van de slechte codeerpraktijken die ik in mijn werk heb ervaren. Als je vragen hebt over de beste codepraktijken, kun je terecht bij The Codest bemanning uit om je twijfels te raadplegen.