- Notifications
You must be signed in to change notification settings - Fork3
🛁 Clean Code concepts adapted for JavaScript
License
frappacchio/clean-code-javascript
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
- Lista dei contenuti
- Introduzione
- Variabili
- Utilizza nomi di variabili comprensibili e pronunciabili
- Usa la stessa semantica per lo stesso tipo di variabili
- Utilizza nomi che possano essere cercati
- Utilizza nomi di variabili esplicartivi
- Evita mappe mentali
- Non contestualizzare inutilmente
- Utilizza i valori di default (predefiniti), anzichè usare condizioni o valutazioni minime
- Funzioni
- Argomenti di una funzione (idealmente 2 o anche meno)
- Un metodo dovrebbe fare una sola cosa
- I nomi delle funzioni dovrebbero farti capire cosa fanno
- Le funzioni dovrebbero avere un solo livello di astrazione
- Rimuovi il codice duplicato
- Estendi un oggetto con Object.assign
- Non usare valori flag (true/false) come parametri di una funzione
- Evitare effetti inattesi (parte 1)
- Evitare effetti inattesi (parte 2)
- Non aggiungere funzioni globali
- Preferisci la programmazione funzionale a quella imperativa
- Incapsula le condizioni
- Evita di verificare condizioni in negativo
- Evita le condizioni
- Evita la validazione dei tipi (parte 1)
- Evita la validazione dei tipi (part 2)
- Non ottimizzare eccessivamente
- Rimuovi il codice inutilizzato
- Oggetti e strutture dati
- Classi
- SOLID
- Single Responsibility Principle (SRP) (Principio di singola responsabilità)
- Open/Closed Principle (OCP) (Principio aperto/chiuso)
- Liskov Substitution Principle (LSP) (Principio di sostituzione di Liskov)
- Interface Segregation Principle (ISP) (Principio di segregazione delle interfacce)
- Dependency Inversion Principle (DIP) (Principio di inversione delle dipendenze)
- Test
- Consequenzialità
- Gestione degli errori
- Formattazione
- Commenti
- Traduzioni
Principi di Ingegneria del Software, dal libro di Robert C. MartinClean Code,adattati a JavaScript.
Non si tratta di una guida stilistica, bensì di una guida per cercare di produrre softwareleggibile, riutilizzabile e rifattorizzabile in JavaScript.
Non tutti i principi di questa guida devono essere seguiti alla lettera, e solo alcuni sono universalmente condivisi. Sono linee guida e niente più, ma sono linee guida che derivano da anni di esperienza collettiva degli autori diClean code.
Il nostro lavoro come ingegneri del software esiste da soli 50 anni, e stiamo ancora imparando molto. Quando l'architettura del software godrà della stessa anzianità dell'architettura in sé, probabilmente avremo regole più rigide da seguire. Per ora, facciamo sì che queste linee guida servano come termine di paragone per valutare la qualità del software che producete tu ed il tuo team.
Un'ultima cosa: conoscere queste regole non ti trasformerà immediatamente in uno sviluppatore migliore, e lavorare tenendole presenti, anche per tanti anni, non ti impedirà di commettere errori.Ogni singola parte di codice nasce come una bozza, per poi prendere forma, proprio come una scultura di argilla.Solo alla fine perfezioneremo il nostro software, revisionando il codice con i nostri colleghi. Non abbatterti se il codice iniziale si deve migliorare. Piuttosto, vacci giù duro con il codice!
Da evitare
constyyyymmdstr=moment().format('YYYY/MM/DD');
Corretto
constcurrentDate=moment().format('YYYY/MM/DD');
Da evitare
getUserInfo();getClientData();getCustomerRecord();
Corretto
getUser();
Leggiamo molto più codice di quanto ne scriviamo. È importante che il codice che produciamo sia leggibile e consultabile. Se non assegnamo un nome a variabili importanti per capire il nostro software, infastidiamo chi lo legge.Fai in modo che i nomi delle tue variabili siano facili da cercare.Strumenti comebuddy.js eESLint possono essere utili per identificare costanti a cui dovrebbe essere assegnato un nome.
Da evitare
// Cosa caspita significa 86400000?setTimeout(blastOff,86400000);
Corretto
// Dichiara la costante assegnandole un nome e usando lettere maiuscole.constMILLISECONDI_IN_UN_GIORNO=86400000;//86400000;setTimeout(blastOff,MILLISECONDI_IN_UN_GIORNO);
Da evitare
constaddress='One Infinite Loop, Cupertino 95014';constcityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;saveCityZipCode(address.match(cityZipCodeRegex)[1],address.match(cityZipCodeRegex)[2]);
Corretto
constaddress='One Infinite Loop, Cupertino 95014';constcityZipCodeRegex=/^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;const[,city,zipCode]=address.match(cityZipCodeRegex)||[];saveCityZipCode(city,zipCode);
È meglio essere espliciti piuttosto che non esserlo.
Da evitare
constlocations=['Austin','New York','San Francisco'];locations.forEach((l)=>{doStuff();doSomeOtherStuff();// ...// ...// ...// Un momento, a cosa si riferiva `l`?dispatch(l);});
Corretto
constlocations=['Austin','New York','San Francisco'];locations.forEach((location)=>{doStuff();doSomeOtherStuff();// ...// ...// ...dispatch(location);});
Se il nome della tua classe o oggetto è già esplicativo, non ripeterlo nei nomi delle sue proprietà o funzioni.
Da evitare
constCar={carMake:'Honda',carModel:'Accord',carColor:'Blue'};functionpaintCar(car){car.carColor='Red';}
Corretto
constCar={make:'Honda',model:'Accord',color:'Blue'};functionpaintCar(car){car.color='Red';}
I parametri predefiniti, generalmente, sono più chiari di condizionali o short-circuitingvalutazioni minime. Ricorda che, se utilizzi parametri predefiniti, la tua funzione assegnerà il valore predefinito solamente ai parametri che non le verranno forniti (undefined).Gli altri valori assimilabili al booleano falso, ovvero'',"",false,null,0, eNaN, non saranno sostituiti dal valore predefinito specificato.
Da evitare
functioncreateMicrobrewery(name){constbreweryName=name||'Hipster Brew Co.';// ...}
Corretto
functioncreateMicrobrewery(name='Hipster Brew Co.'){// ...}
Limitare il numero di argomenti di una funzione è incredibilmente importante perchè ti permette di testarla più facilmente. Avere più di 3 argomenti può portare ad un'esplosione di combinazioni da testare, che produrranno una lunga serie di casi da verificare.
1 o 2 argomenti sono l'ideale e dovremmo evitarne un terzo se possibile. Generalmente se la tua funzione ha più di 2 argomenti, forse, sta facendo troppe operazioni. Nei casi in cui questo non sia del tutto vero, un oggetto può aiutare ad ovviare a questo problema.
Dal momento in cui JavaScript permette la creazione di oggetti al volo puoi usare un oggetto, se pensi che il tuo metodo richieda molti argomenti.
Per rendere evidente cosa la funzione si aspetta di ricevere, puoi utilizzare la sintassi destrutturata (destructuring syntax) di ES2015/ES6 che ha diversi vantaggi:
Quando qualcuno osserva la firma della tua funzione, è immediatamente chiaro che proprietà saranno utilizzate
Destrutturare, oltretutto, clona i valori primitivi passati alla funzione. Questo può prevenire effetti inattesi.Nota: oggetti ed array destrutturati nell'oggetto usato come argomento NON saranno clonati.
Un Linter può avvisarti che non stai utilizzando alcune delle proprietà del tuo oggetto, diversamente non sarebbe possibile.
Da evitare
functioncreateMenu(title,body,buttonText,cancellable){// ...}
Bene:
functioncreateMenu({ title, body, buttonText, cancellable}){// ...}createMenu({title:'Foo',body:'Bar',buttonText:'Baz',cancellable:true});
Questa è di sicuro la regola più importante nell'ingegneria del software. Quando un metodo si occupa di più di un solo aspetto sarà più difficile da testare, comporre e ragioraci sopra.Se è possibile far eseguire al metodo una sola azione sarà più facile il suo refactor e la leggibilità del tuo codice sarà maggiore e più chiara. Anche se non dovesse rimanerti in mente altro di questa guida, sarai comunque più avanti di molti sviluppatori.
Da evitare
functionemailClients(clients){clients.forEach((client)=>{constclientRecord=database.lookup(client);if(clientRecord.isActive()){email(client);}});}
Bene:
functionemailActiveClients(clients){clients.filter(isActiveClient).forEach(email);}functionisActiveClient(client){constclientRecord=database.lookup(client);returnclientRecord.isActive();}
Da evitare
functionaddToDate(date,month){// ...}constdate=newDate();// Difficile da dire esattamente cosa viene aggiunto tramite questa funzioneaddToDate(date,1);
Bene:
functionaddMonthToDate(month,date){// ...}constdate=newDate();addMonthToDate(1,date);
Quando hai più di un livello di astrazione, la tua funzione generalmente sta facendo troppe cose. Dividere in più funzioni aiuta a riutilizzarle e testare più facilmente.
Da evitare
functionparseBetterJSAlternative(code){constREGEXES=[// ...];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((REGEX)=>{statements.forEach((statement)=>{// ...});});constast=[];tokens.forEach((token)=>{// lex...});ast.forEach((node)=>{// parse...});}
Bene:
functionparseBetterJSAlternative(code){consttokens=tokenize(code);constast=lexer(tokens);ast.forEach((node)=>{// parse...});}functiontokenize(code){constREGEXES=[// ...];conststatements=code.split(' ');consttokens=[];REGEXES.forEach((REGEX)=>{statements.forEach((statement)=>{tokens.push(/* ... */);});});returntokens;}functionlexer(tokens){constast=[];tokens.forEach((token)=>{ast.push(/* ... */);});returnast;}
Fai del tuo meglio per evitare codice duplicato. Duplicare il codice è un male, perchè vuol dire che c'è più di un punto da modificare nel caso in cui dovessi cambiare alcune logiche.
Immagina di avere un ristorante e di dover tener traccia del tuo magazzino: la riserva di pomodori, cipolle, aglio, spezie, etc. Se hai più di una lista in cui tieni traccia di queste quantità dovrai aggiornarle tutte ogni volta che servirai, per esempio, un piatto con dei pomodori. Al contrario, se dovessi avere una sola lista, avrai un solo posto un cui dovrai tenere traccia delle modifiche sulle quantità in magazzino.
Generalmente si duplica il codice perchè ci sono due o tre piccole differenze tra una parte e l'altra del software. Questo permette di condividere le parti comuni del codice, ma allo stesso tempo avrai dei duplicati di parti che fanno la stessa cosa.Rimuovere questi duplicati, significa creare un'astrazione che permette di gestire queste differenze attraverso un unico metodo/modulo/classe.
Ottenere la sufficiente astrazione può essere complicato. Per questo dovresti seguire i principi SOLID, approfonditi nella sezioneClassi.Un'astrazione non ottimale potrebbe anche essere peggio del codice duplicato, per cui fai attenzione! Non ripeterti, altrimenti dovrai aggiornare tutte le occorrenze della stessa logica ogni volta che vorrai cambiare qualcosa.
Da evitare
functionshowDeveloperList(developers){developers.forEach((developer)=>{constexpectedSalary=developer.calculateExpectedSalary();constexperience=developer.getExperience();constgithubLink=developer.getGithubLink();constdata={ expectedSalary, experience, githubLink};render(data);});}functionshowManagerList(managers){managers.forEach((manager)=>{constexpectedSalary=manager.calculateExpectedSalary();constexperience=manager.getExperience();constportfolio=manager.getMBAProjects();constdata={ expectedSalary, experience, portfolio};render(data);});}
Bene:
functionshowEmployeeList(employees){employees.forEach((employee)=>{constexpectedSalary=employee.calculateExpectedSalary();constexperience=employee.getExperience();constdata={ expectedSalary, experience};switch(employee.type){case'manager':data.portfolio=employee.getMBAProjects();break;case'developer':data.githubLink=employee.getGithubLink();break;}render(data);});}
Da evitare
constmenuConfig={title:null,body:'Bar',buttonText:null,cancellable:true};functioncreateMenu(config){config.title=config.title||'Foo';config.body=config.body||'Bar';config.buttonText=config.buttonText||'Baz';config.cancellable=config.cancellable!==undefined ?config.cancellable :true;}createMenu(menuConfig);
Bene:
constmenuConfig={title:'Order',// User did not include 'body' keybuttonText:'Send',cancellable:true};functioncreateMenu(config){config=Object.assign({title:'Foo',body:'Bar',buttonText:'Baz',cancellable:true},config);// config adesso sarà: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}// ...}createMenu(menuConfig);
Un valore di tipo flag indica che la tua funzione può eseguire più di una sola operazione. Una funzione dovrebbe eseguire una sola operazione. Separa la tua funzione se deve eseguire più di una operazione in base al parametro flag che riceve in input.
Da evitare
functioncreateFile(name,temp){if(temp){fs.create(`./temp/${name}`);}else{fs.create(name);}}
Bene:
functioncreateFile(name){fs.create(name);}functioncreateTempFile(name){createFile(`./temp/${name}`);}
Una funzione può generare un effetto collaterale se fa altro oltre a ricevere un valore e restituirne uno o più. L'effetto collaterale potrebbe essere scrivere su un file, modificare una variabile globale o accidentalmente girare tutti i tuoi soldi ad uno sconosciuto.
Probabilmente, e occasionalmente, avrai bisogno di generare un effetto collaterale: come nell'esempio precedente magari proprio scrivere su un file.Quello che dovrai fare è centralizzare il punto in cui lo fai. Non avere più funzioni e classi che fanno la stessa cosa, ma averne un servizio ed uno soltanto che se ne occupa.
La questione più importante è evitare le insidie che stanno dietro ad errate manipolazioni di oggetti senza alcuna struttura, utilizzando strutture che possono essere modificate da qualunque parte.Se riuscirai ad evitare che questo accada...sarai ben più felice della maggior parte degli altri programmatori.
Da evitare
// Variable globale utilizzata dalla funzione seguente.// Nel caso in cui dovessimo utilizzarla in un'altra funzione a questo punto si tratterebbe di un array e potrebbe generare un errore.letname='Ryan McDermott';functionsplitIntoFirstAndLastName(){name=name.split(' ');}splitIntoFirstAndLastName();console.log(name);// ['Ryan', 'McDermott'];
Bene:
functionsplitIntoFirstAndLastName(name){returnname.split(' ');}constname='Ryan McDermott';constnewName=splitIntoFirstAndLastName(name);console.log(name);// 'Ryan McDermott';console.log(newName);// ['Ryan', 'McDermott'];
In Javascript i valori primitivi sono passati come valori, mentre oggetti ed array vengono passati come reference. Nel caso di oggetti ed array, se la tua funzione modifica l'array contenente un carrello della spesa, per esempio, aggiungendo o rimuovendo un oggetto da acquistare, tutte le altre funzioni che utilizzeranno l'arraycarrello saranno condizionati da questa modifica. Questo potrebbe essere un bene ed un male allo stesso tempo. Immaginiamo una situazione in cui questo è un male:
l'utente clicca sul tasto "Acquista", che richiamerà una funzioneacquista che effettua una richiesta ed invia l'arraycarrello al server. Per via di una pessima connessione, il metodoacquista riproverà ad effettuare la richiesta al server. Cosa succede se nello stesso momento accidentalmemte l'utente clicca su "Aggiungi al carrello" su di un oggetto che non ha intenzione di acquistare prima che venga eseguita nuovamente la funzione?Verrà inviata la richiesta con il nuovo oggetto accidentalmente aggiunto al carrello utilizzando la funzioneaggiungiOggettoAlCarrello.
Un'ottima soluzione è quella di di clonare sempre l'arraycarrello, modificarlo e restituire il clone.Questo ci assicurerà che che nessun'altra funzione che gestisce il carrello subirà cambiamenti non voluti.
Due precisazioni vanno fatte su questo approccio:
Potrebbe essere che tu voglia realmente modificare l'oggetto in input, ma vedrai che utilizzando questo approccio ti accorgerai che questi casi sono veramente rari. La maggior parte delle volte dovrai utilizzare questo approccio per non generare effetti inattesi
Clonare oggetti molto grandi potrebbe essere davvero dispendioso in termini di risorse. Fortunatamente questo non è un problema perchè esistonoottime librerie che permettono di utilizzare questo approccio senza dispendio di memoria e più velocemente rispetto al dovelo fare manualmente.
Da evitare
constaddItemToCart=(cart,item)=>{cart.push({ item,date:Date.now()});};
Bene:
constaddItemToCart=(cart,item)=>{return[...cart,{ item,date:Date.now()}];};
Contaminare o aggiungere delle variabili globali è una pratica sconsigliata, in quanto potresti entrare in conflitto con altre librerie e chi utiliza le tue API potrebbe non accorgersene fintanto che non si trova in produzione, generando un'eccezione.Facciamo un esempio pratico: supponiamo che tu voglia estendere il costrutto Array nativo di JavaScript aggiungendo il metododiff che mostra le differenze tra due array. Come puoi fare?Potresti scrivere il metodo utilizzandoArray.prototype, che però potrebbe entrare in conflitto con con un'altra libreria che fa la stessa cosa. Cosa succederebbe se anche l'altra libreria utilizzassediff per trovare le differenze tra due array? Ecco perchè è molto meglio utilizzare le classi ES2015/ES6 e semplicemente estendereArray.
Da evitare
Array.prototype.diff=functiondiff(comparisonArray){consthash=newSet(comparisonArray);returnthis.filter(elem=>!hash.has(elem));};
Bene:
classSuperArrayextendsArray{diff(comparisonArray){consthash=newSet(comparisonArray);returnthis.filter(elem=>!hash.has(elem));}}
Programmazione funzionale -Programmazione imperativa
Javascript non è un linguaggio funzionale alla stregua diHaskell, ma entrambi hanno qualcosa che li accomuna.I linguaggi funzionali generalmente sono più puliti e facili da testare.Preferisci questo stile se possibile.
Da evitare
constprogrammerOutput=[{name:'Uncle Bobby',linesOfCode:500},{name:'Suzie Q',linesOfCode:1500},{name:'Jimmy Gosling',linesOfCode:150},{name:'Gracie Hopper',linesOfCode:1000}];lettotalOutput=0;for(leti=0;i<programmerOutput.length;i++){totalOutput+=programmerOutput[i].linesOfCode;}
Bene:
constprogrammerOutput=[{name:'Uncle Bobby',linesOfCode:500},{name:'Suzie Q',linesOfCode:1500},{name:'Jimmy Gosling',linesOfCode:150},{name:'Gracie Hopper',linesOfCode:1000}];consttotalOutput=programmerOutput.map(output=>output.linesOfCode).reduce((totalLines,lines)=>totalLines+lines);
Da evitare
if(fsm.state==='fetching'&&isEmpty(listNode)){// ...}
Bene:
functionshouldShowSpinner(fsm,listNode){returnfsm.state==='fetching'&&isEmpty(listNode);}if(shouldShowSpinner(fsmInstance,listNodeInstance)){// ...}
Da evitare
functionisDOMNodeNotPresent(node){// ...}if(!isDOMNodeNotPresent(node)){// ...}
Bene:
functionisDOMNodePresent(node){// ...}if(isDOMNodePresent(node)){// ...}
Sembrerebbe un task impossibile. Ad un rapido sguardo molti sviluppatori potrebbero pensare "come posso pensare di far funzionare qualcosa senza utilizzare unif?"La risposta è che puoi utilizzare il polimorfismo per ottenere lo stesso risultato in molti casi.La seconda domanda generalmente è "Ottimo! Ma perchè dovrei farlo?".La risposta è data in uno dei concetti precedentemente descritti: una funzione dovrebbe eseguire una sola operazione.Quando hai una Classe con delle funzioni che utilizzano lo statoif stai dicendo all'utente che la tua funzione può fare più di una operazione.
Da evitare
classAirplane{// ...getCruisingAltitude(){switch(this.type){case'777':returnthis.getMaxAltitude()-this.getPassengerCount();case'Air Force One':returnthis.getMaxAltitude();case'Cessna':returnthis.getMaxAltitude()-this.getFuelExpenditure();}}}
Bene:
classAirplane{// ...}classBoeing777extendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getPassengerCount();}}classAirForceOneextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude();}}classCessnaextendsAirplane{// ...getCruisingAltitude(){returnthis.getMaxAltitude()-this.getFuelExpenditure();}}
JavaScript è un linguaggio non tipizzato, il che significa che le tue funzioni possono accettare qualunque tipo di argomento.Qualche volta potresti essere tentato da tutta questa libertà e potresti essere altrettanto tentato di veririfcare il tipo di dato ricevuto nella tua funzione.Ci sono molti modi per evitare di dover fare questo tipo di controllo.Come prima cosa cerca scrivere API consistenti.
Da evitare
functiontravelToTexas(vehicle){if(vehicleinstanceofBicycle){vehicle.pedal(this.currentLocation,newLocation('texas'));}elseif(vehicleinstanceofCar){vehicle.drive(this.currentLocation,newLocation('texas'));}}
Bene:
functiontravelToTexas(vehicle){vehicle.move(this.currentLocation,newLocation('texas'));}
Se stai lavorando con tipi di dati primitivi come stringhe o interi e non puoi utilizzare il paradigma del polimorfismo, ma senti ancora l'esigenza di validare il tipo di dato considera l'utilizzo diTypeScript.È una vlidissima alternativa al normale JavaScript che fornicsce una validazione di tipi statica utilizzando la sintassi JavaScript.Il problema con la validazione manuale dei tipi utilizzando lo standard JavaScript è che richiede tanto codice extra che non compensa la scarsa leggibilità del codice ottenuto.Mantieni pulito il tuo codice, scrivi dei test validi e cerca di fare delle buone revisioni del coidice. Altrimenti utilizza TypeScript (che come detto in precedenza è una validissima alternativa!)
Da evitare
functioncombine(val1,val2){if(typeofval1==='number'&&typeofval2==='number'||typeofval1==='string'&&typeofval2==='string'){returnval1+val2;}thrownewError('Must be of type String or Number');}
Bene:
functioncombine(val1,val2){returnval1+val2;}
I browser moderni eseguono un sacco di ottimizzazione "sottobanco". Molte volte, quando cerchi di ottimizzare il tuo codice, stai perdendo tempo prezioso.Ci sono tante guide che aiutano a capire quali tipi di ottimizzazione sono superflui e quali no. Utilizza queste guide nel frattempo, fintanto che queste lacune non verranno colmate.
Da evitare
// Nei vecchi browser ogni volta che in un ciclo verifichi `list.length` potrebbe// essere dispendioso per via del suo ricalcolo. Nei browser moderni// questa operazione è stata ottimizzatafor(leti=0,len=list.length;i<len;i++){// ...}
Bene:
for(leti=0;i<list.length;i++){// ...}
Il codice inutilizzato è dannoso quanto il codice duplicato. Non c'è motivo per cui tenerlo nel tuo codebase. Se realmente non viene utilizzato rimuovilo!Sarà comunque presente nella storia del tuo file se utilizzi un sistema di versioning nel caso in cui dovesse servirti.
Da evitare
functionoldRequestModule(url){// ...}functionnewRequestModule(url){// ...}constreq=newRequestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');
Bene:
functionnewRequestModule(url){// ...}constreq=newRequestModule;inventoryTracker('apples',req,'www.inventory-awesome.io');
Utilizzare getter e setter per acceedere ai dati di un oggetto può essere meglio che accedere direttamente alle sue proprietà. Ti starai sicuramente chiedendo il motivo.Eccoti qualche motivo per cui utilizzare getter e setter:
- Quando hai bisogno di eseguire un'operazione sul dato recuperato, non devi andare a modificarlo ogni volta che accedi al dato nel tuo codice.
- Valida il dato nel momento in cui lo setti con
set - Incapsula la rappresentazione interna
- È più facile loggare e gestire gli errori quando imposti o recuperi un dato
- Puoi caricare i dati del tuo oggetto in modalitàlazy, per esempio caricandoli dal server.
Da evitare
functionmakeBankAccount(){// ...return{balance:0,// ...};}constaccount=makeBankAccount();account.balance=100;
Bene:
functionmakeBankAccount(){// questa proprietà è privataletbalance=0;// un "getter", la rende pubblica restituendola in questo modofunctiongetBalance(){returnbalance;}// un "setter", la rende pubblica in questo modofunctionsetBalance(amount){// ... e la valida prima di impostarlabalance=amount;}return{// ... getBalance, setBalance,};}constaccount=makeBankAccount();account.setBalance(100);
Può essere fatto attraverso leclosure (per ES5 e versioni precedenti).
Da evitare
constEmployee=function(name){this.name=name;};Employee.prototype.getName=functiongetName(){returnthis.name;};constemployee=newEmployee('John Doe');console.log(`Employee name:${employee.getName()}`);// Employee name: John Doedeleteemployee.name;console.log(`Employee name:${employee.getName()}`);// Employee name: undefined
Bene:
functionmakeEmployee(name){return{getName(){returnname;},};}constemployee=makeEmployee('John Doe');console.log(`Employee name:${employee.getName()}`);// Employee name: John Doedeleteemployee.name;console.log(`Employee name:${employee.getName()}`);// Employee name: John Doe
È molto difficile ottenere leggibilità sull'ereditarietà, costrutti e metodi in definizioni di oggetti in ES5. Se hai bisogno di ereditarietà (e bada bene, non è detto che tu ne abbia bisogno), utilizza il costrutto Class di ES2015/ES6. Altrimenti utilizza piccole funzioni fintanto che non avrai bisogno di gestire oggetti più complessi.
Da evitare
constAnimal=function(age){if(!(thisinstanceofAnimal)){thrownewError('Instantiate Animal with `new`');}this.age=age;};Animal.prototype.move=functionmove(){};constMammal=function(age,furColor){if(!(thisinstanceofMammal)){thrownewError('Instantiate Mammal with `new`');}Animal.call(this,age);this.furColor=furColor;};Mammal.prototype=Object.create(Animal.prototype);Mammal.prototype.constructor=Mammal;Mammal.prototype.liveBirth=functionliveBirth(){};constHuman=function(age,furColor,languageSpoken){if(!(thisinstanceofHuman)){thrownewError('Instantiate Human with `new`');}Mammal.call(this,age,furColor);this.languageSpoken=languageSpoken;};Human.prototype=Object.create(Mammal.prototype);Human.prototype.constructor=Human;Human.prototype.speak=functionspeak(){};
Bene:
classAnimal{constructor(age){this.age=age;}move(){/* ... */}}classMammalextendsAnimal{constructor(age,furColor){super(age);this.furColor=furColor;}liveBirth(){/* ... */}}classHumanextendsMammal{constructor(age,furColor,languageSpoken){super(age,furColor);this.languageSpoken=languageSpoken;}speak(){/* ... */}}
Questo pattern è molto utilizzato in JavaScript e puoi trovarne applicazione in molte liberie comejQuery eLodash. Permette al tuo codice di essere maggiormente espressivo e meno verboso.Proprio per questo motivo, insisto, utilizza la concatenazione dei metodi e guarda come può essere più pulito il tuo codice. Nei metodi della tua classe, semplicemente restituisci il riferimentothis alla fine di ogni metodo, in modo da poter concatenare altri metodi.
Da evitare
classCar{constructor(make,model,color){this.make=make;this.model=model;this.color=color;}setMake(make){this.make=make;}setModel(model){this.model=model;}setColor(color){this.color=color;}save(){console.log(this.make,this.model,this.color);}}constcar=newCar('Ford','F-150','red');car.setColor('pink');car.save();
Bene:
classCar{constructor(make,model,color){this.make=make;this.model=model;this.color=color;}setMake(make){this.make=make;// NOTA: restituisci this per poter concatenare altri metodi.returnthis;}setModel(model){this.model=model;// NOTA: restituisci this per poter concatenare altri metodi.returnthis;}setColor(color){this.color=color;// NOTA: restituisci this per poter concatenare altri metodi.returnthis;}save(){console.log(this.make,this.model,this.color);// NOTA: restituisci this per poter concatenare altri metodi.returnthis;}}constcar=newCar('Ford','F-150','red').setColor('pink').save();
Come dichiarato dalla Gang of four inDesign Patterns dovresti preferire la strutturazione all'ereditarietà quando puoi. Ci sono validi motivi per utilizzare l'ereditarietà e altrettanto validi motivi per utilizzare la strutturazione.Il punto principale di questo assunto è che mentalmente sei portato a preferire l'ereditarietà. Prova a pensare alla strutturazione per risolvere il tuo problema: tante volte è davvero la soluzione migliore.Ti potresti chiedere: "Quando dovrei utilizzare l'ereditarietà?". Dipende dal problema che devi affrontare, ma c'è una discreta lista di suggerimenti che ti potrebbero aiutare a capire quando l'ereditarietà è meglio della strutturazione:
- L'estensione che stai mettendo in atto rappresenta una relazione di tipo "è-un" e non "ha-un" (Umano->Animale vs. Utente->DettagliUtente).
- Puoi riutilizzare il codice dalla classe padre
- Vuoi fare cambiamenti globali a tutte le classi estese tramite la classe di partenza.
Da evitare
classEmployee{constructor(name,email){this.name=name;this.email=email;}// ...}// Male because Employees "have" tax data. EmployeeTaxData is not a type of EmployeeclassEmployeeTaxDataextendsEmployee{constructor(ssn,salary){super();this.ssn=ssn;this.salary=salary;}// ...}
Bene:
classEmployeeTaxData{constructor(ssn,salary){this.ssn=ssn;this.salary=salary;}// ...}classEmployee{constructor(name,email){this.name=name;this.email=email;}setTaxData(ssn,salary){this.taxData=newEmployeeTaxData(ssn,salary);}// ...}
Come indicato inClean code, "Non dovrebbe mai esserci più di un solo motivo per modificare una classe". La tentazione è sempre quella di fare un'unica classe con molte funzionalità, come quando vuoi portarti un unico bagaglio a bordo.Il problema con questo approccio è che le tue classi non saranno concettualmente coese e ti potrebbero dare più di una ragione per modificarle in seguito.Minimizzare il numero di volte in cui modificare una classe è importante.È importante perchè quando ci sono troppe funzionalità in una classe è difficile capire che effetto avrà sulle classe che la estendono, nel caso in cui farai un cambiamento.
Da evitare
classUserSettings{constructor(user){this.user=user;}changeSettings(settings){if(this.verifyCredentials()){// ...}}verifyCredentials(){// ...}}
Bene:
classUserAuth{constructor(user){this.user=user;}verifyCredentials(){// ...}}classUserSettings{constructor(user){this.user=user;this.auth=newUserAuth(user);}changeSettings(settings){if(this.auth.verifyCredentials()){// ...}}}
Come dichiarato da Bertrand Meyer, "Le entità di un software (Classi, moduli, funzioni, etc.) dovrebbero essere aperte all'estensione, ma chiuse alla modifica. Cosa significa esattamente?Quello che intende è che dovresti dare ai tuoi utilizzatori la possibilità di aggiungere nuove funzionalità, non modificando quelle esistenti.
Da evitare
classAjaxAdapterextendsAdapter{constructor(){super();this.name='ajaxAdapter';}}classNodeAdapterextendsAdapter{constructor(){super();this.name='nodeAdapter';}}classHttpRequester{constructor(adapter){this.adapter=adapter;}fetch(url){if(this.adapter.name==='ajaxAdapter'){returnmakeAjaxCall(url).then((response)=>{// transform response and return});}elseif(this.adapter.name==='httpNodeAdapter'){returnmakeHttpCall(url).then((response)=>{// transform response and return});}}}functionmakeAjaxCall(url){// request and return promise}functionmakeHttpCall(url){// request and return promise}
Bene:
classAjaxAdapterextendsAdapter{constructor(){super();this.name='ajaxAdapter';}request(url){// request and return promise}}classNodeAdapterextendsAdapter{constructor(){super();this.name='nodeAdapter';}request(url){// request and return promise}}classHttpRequester{constructor(adapter){this.adapter=adapter;}fetch(url){returnthis.adapter.request(url).then((response)=>{// transform response and return});}}
Questo nome sembra molto più spaventoso di quello che in realtà significa.Formalmente la sua defnizione è "Se S è un sottotipo di T, allora gli oggetti di tipo T possono essere sostituiti con oggetti di tipo S (per esempio un oggetto di tipo S può sostituire un oggetto di tipo T) senza modificare alcuna della proprietà del software (correttezza, compito da svolgere, etc.)". Questa definizione suona comunque complessa.
Forse una spiegazione più esaustiva potrebbe essere: "Se hai una classefiglio che estende una classegenitore allora le due classi possono essere intercambiate all'interno del codice senza generare errori o risultati inattesi".Potrebbe esssere ancora poco chiaro, ma vediamo con un esempio (Quadrato/Rettangolo): matematicamente il Quadrato è un Rettangolo, ma se il tuo modello eredita una relazione di tipo "è-un", potresti avere presto qualche problema.
Da evitare
classRectangle{constructor(){this.width=0;this.height=0;}setColor(color){// ...}render(area){// ...}setWidth(width){this.width=width;}setHeight(height){this.height=height;}getArea(){returnthis.width*this.height;}}classSquareextendsRectangle{setWidth(width){this.width=width;this.height=width;}setHeight(height){this.width=height;this.height=height;}}functionrenderLargeRectangles(rectangles){rectangles.forEach((rectangle)=>{rectangle.setWidth(4);rectangle.setHeight(5);constarea=rectangle.getArea();// Sbagliato: Restituisce 25 anche// per il quadrato. Dovrebbe essere 20.rectangle.render(area);});}constrectangles=[newRectangle(),newRectangle(),newSquare()];renderLargeRectangles(rectangles);
Bene:
classShape{setColor(color){// ...}render(area){// ...}}classRectangleextendsShape{constructor(width,height){super();this.width=width;this.height=height;}getArea(){returnthis.width*this.height;}}classSquareextendsShape{constructor(length){super();this.length=length;}getArea(){returnthis.length*this.length;}}functionrenderLargeShapes(shapes){shapes.forEach((shape)=>{constarea=shape.getArea();shape.render(area);});}constshapes=[newRectangle(4,5),newRectangle(4,5),newSquare(5)];renderLargeShapes(shapes);
JavaScript non utilizza Interfacce, quindi non è possibile applicare questo principio alla lettera. Tuttavia è importante nonostante la sua mancanza di tipizzazione.
ISP indica che "Gli utenti non dovrebbero mai esssere forzati a dipendere da interfacce che non utilizza.". Le interfacce sono contratti impliciti in JavaScript per via delduck-typingUn buon esempio in JavaScript potrebbe essere fatto per le classi che richiedono la deinizione di un set di proprietà molto grande.Non utilizzare classi che richiedono la definizione di molte proprietà per essere istanziate è sicuramente un beneficio perchè spesso non tutte queste proprietà richiedono di essere impostate per utilizzare la classe.Rendere questi parametri opzionali evita di avere un'interfaccia pesante.
Da evitare
classDOMTraverser{constructor(settings){this.settings=settings;this.setup();}setup(){this.rootNode=this.settings.rootNode;this.animationModule.setup();}traverse(){// ...}}const$=newDOMTraverser({rootNode:document.getElementsByTagName('body'),animationModule(){}//Il più delle volte potremmo non dover animare questo oggetto// ...});
Bene:
classDOMTraverser{constructor(settings){this.settings=settings;this.options=settings.options;this.setup();}setup(){this.rootNode=this.settings.rootNode;this.setupOptions();}setupOptions(){if(this.options.animationModule){// ...}}traverse(){// ...}}const$=newDOMTraverser({rootNode:document.getElementsByTagName('body'),options:{animationModule(){}}});
Questo principio sostanzialmente indica due cose:
- Moduli ad alto livello non dovrebbero dipendere da molidi a basso livello. Entrambi dovrebbero dipendere da moduli astratti.
- L'astrazione non dovrebbe dipendere da classi concrete. Le classi concrete dovrebbero dipendere da astrazioni.
A primo impatto questo concetto potrebbe essere difficile da capire, ma nel caso in cui tu abbia lavorato con AngularJS avrai sicuramente visto l'applicazione di questo principio nel concetto diDependency injection (DI). Nonostante non sia esattamente identico come concetto, DIP evita che i moduli di alto livello conoscano i dettagli dei moduli di basso livello pur utilizzandoli.Uno dei benefici di questo utilizzo è che riduce la dipendenza tra due moduli.La dipendenza tra due moduli è un concetto negativo, perchè ne rende difficile il refactor.Come detto in precedenza, non essendoci il concetto di interfaccia in JavaScript, tutte le dipendenze sono contratte implicitamente.Nell'esempio successivo, la dipendenza implicita è che tutte le istanze diInventoryTracker avranno un metodorequestItems.
Da evitare
classInventoryRequester{constructor(){this.REQ_METHODS=['HTTP'];}requestItem(item){// ...}}classInventoryTracker{constructor(items){this.items=items;//Da evitare: abbiamo creato una dipendenza specifica per ogni istanza.//Dovremmo fare in modo che requestItems dipenda dal metodo `request`this.requester=newInventoryRequester();}requestItems(){this.items.forEach((item)=>{this.requester.requestItem(item);});}}constinventoryTracker=newInventoryTracker(['apples','bananas']);inventoryTracker.requestItems();
Bene:
classInventoryTracker{constructor(items,requester){this.items=items;this.requester=requester;}requestItems(){this.items.forEach((item)=>{this.requester.requestItem(item);});}}classInventoryRequesterV1{constructor(){this.REQ_METHODS=['HTTP'];}requestItem(item){// ...}}classInventoryRequesterV2{constructor(){this.REQ_METHODS=['WS'];}requestItem(item){// ...}}//Avendo dichiarato la nostra dipendenza esternamente ed aggiunta dall'esterno, la possiamo//sostituire facilemente con un'altra che utilizza WebSocketsconstinventoryTracker=newInventoryTracker(['apples','bananas'],newInventoryRequesterV2());inventoryTracker.requestItems();
Testare è più importante che rilasciare. Se non hai test o non ne hai un numero adeguato, non saprai se ad ogni rilascio puoi rompere qualcosa.Decidere quale sia il numero sufficiente di test dipende dal tuo team, ma cercare di coprire il 100% dei casi (per tutti gli stati ed i branch) vuol dire avere massima tranquillità durante i rilasci.Questo significa che oltre ad utilizzare una suite di test valida, dovresti utilizzare anche unbuon strumento di copertura.
Non ci sono scuse per non scrivere test. C'è un'abbondanza di ottimiframewerk per i test in JavaScript quindi cerca quello più adatto alle tue esigenze.Quando tu ed il tuo team avrete individuato quello più giusto per voi, dovrete iniziare sempre a scrivere i test per ogni modulo/feature che introdurrete nel vostro software.Se il vostro approccio preferito è quello del TestDrivenDevelopment (TDD) ottimo, ma assicurati di individuare i tuoi obiettivi prima di rilasciare ogni singola feature o eseguire il refactor di una esistente.
Da evitare
importassertfrom'assert';describe('MakeMomentJSGreatAgain',()=>{it('handles date boundaries',()=>{letdate;date=newMakeMomentJSGreatAgain('1/1/2015');date.addDays(30);assert.equal('1/31/2015',date);date=newMakeMomentJSGreatAgain('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);date=newMakeMomentJSGreatAgain('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});
Bene:
importassertfrom'assert';describe('MakeMomentJSGreatAgain',()=>{it('handles 30-day months',()=>{constdate=newMakeMomentJSGreatAgain('1/1/2015');date.addDays(30);assert.equal('1/31/2015',date);});it('handles leap year',()=>{constdate=newMakeMomentJSGreatAgain('2/1/2016');date.addDays(28);assert.equal('02/29/2016',date);});it('handles non-leap year',()=>{constdate=newMakeMomentJSGreatAgain('2/1/2015');date.addDays(28);assert.equal('03/01/2015',date);});});
Le funzioni di callback non sono sempre chiare e possono generare un eccessivo numero di nidificazioni. Con ES2015/ES6 sono nativamente e globalmente accessibili. Utilizzale!
Da evitare
import{get}from'request';import{writeFile}from'fs';get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin',(requestErr,response)=>{if(requestErr){console.error(requestErr);}else{writeFile('article.html',response.body,(writeErr)=>{if(writeErr){console.error(writeErr);}else{console.log('File written');}});}});
Bene:
import{get}from'request';import{writeFile}from'fs';get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin').then((response)=>{returnwriteFile('article.html',response);}).then(()=>{console.log('File written');}).catch((err)=>{console.error(err);});
Le Promise sono una valida e chiara alternativa alle funzioni di callback, ma ES2017/ES8 offrono anche async and await che possono essere addirittura una soluzione più migliore.Tutto quello che devi fare non è niente altro che scrivere una funzione che abbia prefissoasync e puoi scrivere la tua logica senza dover concatenare con la kywordthen.Utilizza questo approccio se hai la possibilità di utilizzare le feature ES2017/ES8!
Da evitare
import{get}from'request-promise';import{writeFile}from'fs-promise';get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin').then((response)=>{returnwriteFile('article.html',response);}).then(()=>{console.log('File written');}).catch((err)=>{console.error(err);});
Bene:
import{get}from'request-promise';import{writeFile}from'fs-promise';asyncfunctiongetCleanCodeArticle(){try{constresponse=awaitget('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');awaitwriteFile('article.html',response);console.log('File written');}catch(err){console.error(err);}}
Generare errori è una buona cosa. Vuol dire che l'esecuzione del tuo codice ha identificato precisamente quando nel tuo software qualcosa è andato storto e ti permette di interromperne l'esecuzione nello stack corrente terminando il processo (in Node), e notificandolo attraverso la console.
Non fare niente con gli errori intercettati non rende possibile correggerli o reagire all'errore. Loggare gli errori nella console (console.log) non ti assicura di non perderti nel mare di log stampati in console.Se invece inserisci il tuo codice all'interno del costruttotry/catch vuol dire riconoscere la possibilità che esista un errore nel caso mettere in piedi un modo per gestirlo.
Da evitare
try{functionThatMightThrow();}catch(error){console.log(error);}
Bene:
try{functionThatMightThrow();}catch(error){// Un'ozione (più visibile del console.log):console.error(error);// Un'altra opzione:notifyUserOfError(error);// Un'altra opzione:reportErrorToService(error);// Oppure usale tutte e tre!}
Per la stessa ragione per cui non dovresti ignorare gli errori contry/catch.
Da evitare
getdata().then((data)=>{functionThatMightThrow(data);}).catch((error)=>{console.log(error);});
Bene:
getdata().then((data)=>{functionThatMightThrow(data);}).catch((error)=>{// Un'ozione (più visibile del console.log):console.error(error);// Un'altra opzione:notifyUserOfError(error);// Un'altra opzione:reportErrorToService(error);// Oppure usale tutte e tre!});
La formattazione è soggettiva. Come molte di quelle sopracitate, non esiste una regola assoluta e veloce che devi seguire. Il punto principale, però, è NON DISCUTERE della formattazione.Ci sono unsacco di strumenti che automatizzano questo processo. Usane uno. È uno spreco di tempo e denaro per gli sviluppatori discutere della formattazione.
JavaScript non è tipizzato, per questo l'uso delle maiuscole può darti indicazioni sulle tue variabili, funzioni, etc. Queste regole sono soggettive, per questo tu ed il tuo team potrete scegliere quella che volete. Il punto è: non importa quale regola sceglierete, l'importante è essere consistenti.
Da evitare
constDAYS_IN_WEEK=7;constdaysInMonth=30;constsongs=['Back In Black','Stairway to Heaven','Hey Jude'];constArtists=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestore_database(){}classanimal{}classAlpaca{}
Bene:
constDAYS_IN_WEEK=7;constDAYS_IN_MONTH=30;constSONGS=['Back In Black','Stairway to Heaven','Hey Jude'];constARTISTS=['ACDC','Led Zeppelin','The Beatles'];functioneraseDatabase(){}functionrestoreDatabase(){}classAnimal{}classAlpaca{}
Se una funzione ne richiama un'altra, mantieni queste funzioni verticalmente vicine nel sorgente. Idealmente, mantieni il richiamo subito sopra la dichiarazione.Generalmente tendiamo a leggere il codice dall'alto verso il basso, come un giornale. Proprio per questo manteniamolo leggibile seguendo questa modalità.
Da evitare
classPerformanceReview{constructor(employee){this.employee=employee;}lookupPeers(){returndb.lookup(this.employee,'peers');}lookupManager(){returndb.lookup(this.employee,'manager');}getPeerReviews(){constpeers=this.lookupPeers();// ...}perfReview(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();}getManagerReview(){constmanager=this.lookupManager();}getSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.perfReview();
Bene:
classPerformanceReview{constructor(employee){this.employee=employee;}perfReview(){this.getPeerReviews();this.getManagerReview();this.getSelfReview();}getPeerReviews(){constpeers=this.lookupPeers();// ...}lookupPeers(){returndb.lookup(this.employee,'peers');}getManagerReview(){constmanager=this.lookupManager();}lookupManager(){returndb.lookup(this.employee,'manager');}getSelfReview(){// ...}}constreview=newPerformanceReview(employee);review.perfReview();
Commentare è una scusa, non un requisito. Un buon codicespesso si spiega da solo.Da evitare
functionhashIt(data){// The hashlethash=0;// Length of stringconstlength=data.length;// Loop through every character in datafor(leti=0;i<length;i++){// Get character code.constchar=data.charCodeAt(i);// Make the hashhash=((hash<<5)-hash)+char;// Convert to 32-bit integerhash&=hash;}}
Bene:
functionhashIt(data){lethash=0;constlength=data.length;for(leti=0;i<length;i++){constchar=data.charCodeAt(i);hash=((hash<<5)-hash)+char;// Convert to 32-bit integerhash&=hash;}}
I sistemi di versioning esistono per un motivo. Lascia il tuo codice vecchio alla storia.
Da evitare
doStuff();// doOtherStuff();// doSomeMoreStuff();// doSoMuchStuff();
Bene:
doStuff();
Ricordati di usare sistemi di version control. Non c'è motivo per cui codice non utilizzato, codice commentato e specialmente commenti con riferimenti a date esistano nel tuo file.Usagit log per avere lo storico!
Da evitare
/** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Removed type-checking (LI) * 2015-03-14: Added combine with type-checking (JR) */functioncombine(a,b){returna+b;}
Bene:
functioncombine(a,b){returna+b;}
Generalmente è solo fastidioso. Lascia che le tue funzioni e le tue variabili, insieme ad una corretta indentazioni ti diano una struttura visiva del tuo codice.
Da evitare
////////////////////////////////////////////////////////////////////////////////// Scope Model Instantiation////////////////////////////////////////////////////////////////////////////////$scope.model={menu:'foo',nav:'bar'};////////////////////////////////////////////////////////////////////////////////// Action setup////////////////////////////////////////////////////////////////////////////////constactions=function(){// ...};
Bene:
$scope.model={menu:'foo',nav:'bar'};constactions=function(){// ...};
Questa guida è disponibile in altre lingue:
Brazilian Portuguese:fesnt/clean-code-javascript
Spanish:andersontr15/clean-code-javascript
Chinese:
German:marcbruederlin/clean-code-javascript
Korean:qkraudghgh/clean-code-javascript-ko
Polish:greg-dev/clean-code-javascript-pl
Russian:
Vietnamese:hienvd/clean-code-javascript/
Japanese:mitsuruog/clean-code-javascript/
Indonesia:andirkh/clean-code-javascript/
Italiano:frappacchio/clean-code-javascript/
About
🛁 Clean Code concepts adapted for JavaScript
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Languages
- JavaScript100.0%
