4 regole da seguire per un codice pulito

Pochi e semplici consigli su come scrivere codice manutenibile

Gli sviluppatori che lavorano in team adottano spesso delle convenzioni per far sì che il codice sia leggibile e modificabile da chiunque, nel tempo. Quando si è alle prime esperienze in team, però, non è sempre facile scrivere (o meglio, progettare) il codice in modo che un altro sviluppatore, leggendolo, non impieghi due ore a capire di cosa si tratta. Si scrivono metodi lunghi chilometri, si fa incetta di variabili, si utilizzano controlli e condizioni come se si avessero dei coupon e, quando andiamo a rileggere il codice qualche giorno dopo, sembra che sia passata una scimmia a battere freneticamente sulla tastiera.

La necessità di seguire delle convenzioni, di utilizzare le best practices e l'importanza che si attribuisce alla manutenibilità del codice deriva dall’esperienza, dall’essere già passati attraverso progetti in cui la gestione della codebase era andata alla deriva, impossibile da recuperare. Quindi è del tutto normale che chi programma da poco non senta questa necessità, cullandosi nella convinzione del “non potrà mai andare così male”. Ma tranquilli, se non ci pensate voi, ci penserà la vita a mettervi davanti al fatto compiuto: vi aspettano intere giornate (se non settimane) di refactoring, debugging e tanto caffè. E, nei casi più estremi, se colti da sfinimento, potreste arrivare a riscrivere l’intero progetto.

E non pensate che il refactoring sia una pratica così facile. Anche per quello ci vuole esperienza e, cosa non da poco, bisogna avere una conoscenza di tutta l’applicazione per sapere dove e cosa spostare, cosa si può eliminare o se una modifica potrebbe distruggere tutto.

Panico? Fate un respiro profondo. Come potete fare per non finire nel vortice della follia? Beh, gli antichi dicevano che se non si vuole cadere nel pozzo, bisogna starci lontano. Ecco, quindi, qualche consiglio da ripetere come un mantra ogni volta che scrivete codice.

# Write what you mean

Se il metodo che stiamo scrivendo deve verificare che l'utente sia loggato, non serve a nulla chiamarlo check. Potreste chiamarlo isUserLoggedIn. Molto più chiaro, no? Cercate di evitare:

  • nomi generici (ad es. a, check, items, data, array, obj, showItems, etc.)
  • abbreviazioni inutili, ambigue o che utilizzate solo voi
  • nomi troppo lunghi (ad es. getAllInvoicesFilteredByDate potrebbe diventare invoicesByDate)
  • rispettate le naming conventions del linguaggio che state utilizzando

Per prendere l’abitudine, ogni volta che assegnate un nome ad una variabile o ad un metodo, chiedetevi se solo tramite il nome riuscite a capirne lo scopo.

# Scusi, dove finisce il metodo?

Se per leggere il corpo di un metodo dovete scrollare verso il basso, c’è qualcosa che non va. Spesso si aggiungono controlli e condizioni superflue, che rendono difficile comprendere il flusso del codice e, quindi, il debug. In generale, se il corpo di un metodo è venuto più lungo del solito, probabilmente potrete estrapolare dal suo interno altri metodi (vedi il paragrafo successivo). In generale, per una buona scrittura di un metodo, si seguono alcune regole:

  • dichiarate la variabile che volete restituire in cima, come prima istruzione, e assegnate un valore iniziale. Se è un oggetto, assegnate dei valori di default alle sue proprietà;
  • evitate return multipli e scrivetene solo uno, come ultima istruzione;
  • evitate condizioni superflue e/o annidate: spesso un ramo di una condizione si può sostituire inizializzando una variabile.

In generale, semplificate. Ma attenzione, semplificare non vuol dire fare a gara a chi scrive meno codice. L’importante è che ogni cosa sia fatta con un perché.

Alcuni team, per aiutarsi, impongono una lunghezza massima (di righe) per il corpo di un metodo. Ma non c’è un’unica regola: chi dice 5, chi 10, chi 20. Se pensate possa aiutarvi, provate.

# ThisAndThat

Non è solo questione di lunghezza, come dicevano i latini. Quante cose fa il vostro metodo? Un segnale inequivocabile che state sbagliando qualcosa è se il vostro metodo fa troppe cose diverse (anche due potrebbero non andare bene). Il vostro intento era scrivere un metodo per fare una chiamata fetch, poi avete aggiunto un ciclo sul risultato della chiamata, poi avete aggiunto un calcolo dentro il ciclo e infine avete anche creato la stringa da far visualizzare all'utente. Perfetto, ora avete almeno 4 metodi da scrivere. Evitate il cambio di contesto.

# DRY (Don’t Repeat Yourself)

Anche se è un grande classico e potreste averlo già sentito, è meglio ripeterlo (ma solo in questo caso!): non duplicate il codice. Seguite la regola del tre: la prima copia potrebbe andare, anche se dovreste già sentire puzza di bruciato; alla seconda copia dovete fermarvi, la stanza va a fuoco! Di solito, nel momento in cui si copia qualcosa da una parte all’altra del progetto, probabilmente quel metodo (o la parte di esso che stiamo copiando) deve essere estrapolato per poter essere riutilizzato in punti diversi.

# Extra: no comment!

Ultimo, ma non meno importante, un campanello d'allarme. Se per il metodo che avete appena scritto state lasciando 20 linee di commento, c’è qualcosa che non va. Qualche anno fa sarebbe stato normale. Quando frequentavo l’università i professori insistevano molto affinchè gli studenti imparassero a commentare il codice. Oggi i commenti sono, in generale, indice di cattiva scrittura. Cercate di scrivere codice che si spieghi da solo.

# Esempio

Proviamo a riassumere tutti questi consigli con uno splendido esempio. Immaginiamo di avere degli items che rappresentano dei prodotti di un magazzino. Ogni tipologia di prodotto ha un fattore di moltiplicazione da associare al valore di base per calcolare il costo finale. Il nostro obiettivo, avendo a disposizione i prodotti selezionati dall’utente, è calcolare il costo finale dell’ordine, il numero di prodotti selezionati ed elaborare una stringa riepilogativa da mostrare all’utente.

Prima del refactoring (quanti errori trovate?):

const calculate = (items) => {
  if (items.length === 0) {
    return 0;
  }

  let totalItems = 0;
  let totalValue = 0;
  let str = '';

  items.forEach(item => {
    if (item) {
      if (item.type === 'A1' && item.value !== 0) {
        totalItems += 1;
        totalValue += item.value * 1.1;
        str += '1 prodotto di tipo A1: ' + (item.value * 1.1) + ' €;';
      } else if (item.type === 'A2' && item.value !== 0) {
        totalItems += 1;
        totalValue += item.value * 1.2;
        str += '1 prodotto di tipo A2: ' + (item.value * 1.2) + ' €;';
      } else {
        totalItems += 1;
        totalValue += item.value * 1.0;
        str += '1 prodotto di tipo generico: ' + (item.value * 1.0) + ' €;';
      }
    }
  });

  return { totalItems, totalValue, str };
}

Dopo una birra e un primo refactoring:

const getOrderFor = (products) => {
  let order = {
    price: 0,
    count: 0,
    summary: `Nessun prodotto presente nell’ordine`
  }

  products.forEach(product => {
    let multiplier = 1;
    const productType = product.type;

    if (productType === 'A1') {
      multiplier = 1.1;
    } else if (productType === 'A2') {
      multiplier = 1.2;
    }

    const productPrice = product.value * multiplier;

    order = {
      price: order.price + productPrice,
      count: order.count + 1,
      summary: order.summary + `1 prodotto di tipo ${productType}: ${productPrice} €;`
    };
  });

  return order;
}

Le modifiche fatte, nell'ordine, sono:

  • ho cambiato il nome del metodo, dandogli un nome che faccia capire il suo scopo;
  • ho cambiato il nome dell'array in input: sono prodotti, perchè nasconderlo?;
  • ho stabilito il tipo di oggetto che restituirà la funzione, in modo da effettuare un solo return, ed ho assegnato dei valori di default;
  • ho eliminato il controllo iniziale, che a questo punto non serviva a nulla: se non abbiamo elementi, il forEach non verrà eseguito e restituiremo il nostro oggetto con i suoi valori di default;
  • ho eliminato le istruzioni ripetute all'interno degli if, poiché inutili, e le ho spostate fuori;
  • ho sostituito il ramo dell'else con un'inizializzazione (multiplier = 1);
  • ho salvato in una variabile il price del prodotto poiché utilizzato due volte nel codice;

Ma il codice è ancora troppo, per un solo metodo. Possiamo estrapolare la parte relativa al calcolo del prezzo del prodotto.

Dopo due birre e un secondo refactoring:

const getProductPrice = (value, type) => {
  let price = value;
  let multiplier = 1;

  if (type === 'A1') {
    multiplier = 1.1;
  } else if (type === 'A2') {
    multiplier = 1.2;
  }

  price = price * multiplier
  return price;
}

const getOrderFor = (products) => {
  // ...

  products.forEach(product => {
    const productType = product.type;
    const productValue = product.value;
    const productPrice = getProductPrice(productValue, productType);

    order = {
      price: order.price + productPrice,
      count: order.count + 1,
      summary: order.summary + `1 prodotto di tipo ${productType}: ${productPrice} €;`
    };
  });

  return order;
}

Ricordate: semplificate. Buon codice a tutti!