Top 10 Letlands-baserede softwareudviklingsvirksomheder
Læs om Letlands bedste softwareudviklingsvirksomheder og deres innovative løsninger i vores seneste artikel. Find ud af, hvordan disse teknologiledere kan hjælpe med at løfte din virksomhed.
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.
Lad os antage, at du og din hold overtager koden 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.
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.
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.
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?
}
}
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:
const user = { tilladelser: 11 };
const canCreate = Boolean(user.permissions & 8); // true
const canRead = Boolean(user.permissions & 4); // false
const canUpdate = Boolean(user.permissions & 2); // true
const canDelete = Boolean(user.permissions & 1); // true
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 udviklingsteamet ønsker at omskrive den 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.
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.
const devices = [
{ id: '001', version: 1, owner: { location: 'Texas', age: 21 } },
{ id: '002', version: 2, owner: { location: 'Texas', age: 27 } },
{ id: '003', version: 3, owner: { location: 'Arizona', age: 27 } }, { id: '003', version: 3, owner: { location: 'Arizona', age: 27 } },
{ id: '004', version: 2, ejer: { location: 'Chicago', alder: 24 } }, { id: '004', version: 2, ejer: { location: 'Chicago', alder: 24 } } },
{ id: '005', version: 2, owner: { location: 'Arizona', age: 19 } },
{ id: '006', version: 3, owner: { location: 'Texas', age: 42 } }, { id: '006', version: 3, owner: { location: 'Texas', age: 42 } },
];
// temmelig kompliceret
(() => {
const locationAgeMap = enheder
.map(enhed => enhed.ejer)
.reduce((map, item) => {
if (!map[item.location]) {
map[item.location] = item.age;
} else {
map[item.location] += item.age;
}
return map;
}, {});
const locationLegalAgeOwnersMap = enheder
.map(enhed => enhed.ejer)
.filter(ejer => ejer.alder >= 21)
.reduce((map, item) => {
if (!map[item.location]) {
map[item.location] = [item];
} else {
map[item.location].push(item);
}
return map;
}, {});
console.log({ locationAgeMap, locationLegalAgeOwnersMap });
})();
// stadig kompliceret, men mere kompakt
(() => {
const locationOwnersMap = enheder
.map(enhed => enhed.ejer)
.reduce((map, item) => {
if (!map[item.location]) {
map[item.location] = [item];
} else {
map[item.location].push(item);
}
return map;
}, {});
const locationAgeMap = Object.fromEntries(
Object.entries(locationOwnersMap).map(([location, owners]) => {
const ownersAge = owners.map(owner => owner.age).reduce((a, b) => a + b);
return [location, ownersAge];
})
);
const locationLegalAgeOwnersMap = Object.fromEntries(
Object.entries(locationOwnersMap).map(([location, owners]) => {
const filteredOwners = owners.filter(owner => owner.age >= 21);
return [location, filteredOwners];
}).filter(([_lokation, ejere]) => {
return owners.length > 0;
})
);
console.log({ locationAgeMap, locationLegalAgeOwnersMap });
})();
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.
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.
Læs mere om det:
Strategier for at hente data i NextJS
Webapp-sikkerhed - XSS-sårbarhed
Sikkerhed i webapps. Target="_blank" sårbarhed
*Titelgrafikken kommer fra speednet.pl