Scrivere codice chiaro

Il codice che funziona non è necessariamente un buon codice. Il codice deve anche essere facile da leggere, capire e modificare. Ha bisogno di chiarezza e per riuscirci deve essere organizzato bene, con un’attenta pianificazione e un’adeguata separazione di idee che precedono l’apertura dell’editor. Scrivere codice perché sia chiaro è qualcosa che separa i grandi sviluppatori da quelli solamente bravi e ci sono alcuni principi di base che possono metterci su un tale percorso.

L’articolo prosegue sotto

Nota: sebbene i principi in questo articolo siano applicabili a una varietà di linguaggi di programmazione, la maggior parte degli esempi deriva da JavaScript orientato agli oggetti. Se non ne avete familiarità, A List Apart ha degli articoli sul module pattern e su prototypal inheritance per aiutarvi a recuperare.

Il principio di singola responsabilità#section1

Immaginate di lavorare a un progetto in casa e di prendere un trapano per infilare una vite nel muro. Quando staccate il trapano dalla vite, scoprite che questo trapano ha una caratteristica interessante: schizza un composto di cartongesso ad asciugatura rapida sopra alla vite infilata per nasconderla. Beh, è ottimo se volete dipingere sopra la vite, ma non è sempre questo il caso. Non dovreste avere un secondo trapano solo per fare un buco in qualcosa. Il trapano sarebbe molto più usabile e affidabile se facesse solo una cosa e sarebbe anche abbastanza flessibile da poter essere usato in una serie di situazioni.

Il principio di singola responsabilità stabilisce che un blocco di codice dovrebbe fare una cosa sola e farla bene. Come il trapano di cui sopra, limitarne la funzionalità in realtà fa aumentare l’utilità di un blocco di codice. Programmare in questo modo non solo ci evita dei mal di testa, ma li eviterà anche ai futuri sviluppatori di questo progetto.

Pensate alle funzioni e ai metodi in termini di responsabilità. Man mano che aumentano le sue responsabilità, un blocco di codice diventa meno flessibile e affidabile, più dipendente dai cambiamenti e più suscettibile agli errori. Per maggior chiarezza, ogni funzione o metodo dovrebbe avere una responsabilità.

Se state descrivendo quello che fa una funzione e dovete usare la parola “e”, quella funzione probabilmente è troppo complessa. Quello che fa una funzione dovrebbe essere sufficientemente semplice da spiegare solo con un nome di funzione descrittivo e degli argomenti descrittivi.

Recentemente, mi è stato affidato il compito di creare una versione elettronica del test di personalità Myers-Briggs. L’avevo già fatto in passato e, quando mi ero avvicinato al problema alcuni anni fa, avevo programmato una funzione gigantesca chiamata processForm: raccoglieva i punteggi, generava i grafici e si occupava di tutto nel DOM per mostrare le cose all’utente.

Il problema era che se si doveva cambiare qualcosa, si doveva cercare in una valanga di codice per capire dove fare le modifiche. Inoltre, se qualcosa andava male nel mezzo della funzione, era molto più difficile trovare l’errore.

Quindi, quando mi sono trovato di fronte al problema questa volta, ho invece suddiviso tutto in funzioni a singola responsabilità impacchettate in un oggetto module. La funzione risultante chiamata a fronte dell’invio della form era così:

return {
    processForm: function() {
        getScores();
        calculatePercentages();
        createCharts();
        showResults();
    }
};

(Vedi la app completa qui)

Estremamente facile da leggere, comprendere e modificare: anche un non-programmatore può capirne il senso. E ognuna di quelle funzioni fa (avete indovinato!) una sola cosa. Questo è il principio di singola responsabilità in azione.

Se volessi aggiungere la validazione della form, piuttosto che dover modificare una gigantesca function funzionante (potenzialmente introducendovi degli errori), potrei semplicemente aggiungere un nuovo metodo. Questo approccio permette anche alla logica e alle variabili correlate di essere suddivise, riducendo i conflitti a fronte di una maggior affidabilità e rendendo molto semplice riutilizzare la funzione per altri scopi, se necessario.

Quindi, ricordate: una funzione, una responsabilità. Le grandi funzioni sono dove le classi vanno a nascondersi. Se una funzione fa molte cose strettamente collegate e che lavorano con gli stessi dati, avrebbe più senso spezzarla in un oggetto con dei metodi, in maniera simile a quello che ho fatto con la mia grande funzione per la form.

Command-query separation#section2

La catena di email più divertente che abbia mai visto è stata la serie di poster di Missing Missy di David Thorne riguardante un gatto scomparso. Ogni volta che la sua collega Shannon faceva una richiesta, David obbediva, ma ci metteva del suo e inviava qualcosa di diverso da ciò che ci si aspettava. Lo scambio è davvero divertente e vale la pena leggerlo, ma è meno divertente quando il vostro codice fa la stessa cosa.

La command-query separation fornisce una base per salvaguardare il vostro codice di fronte agli effetti collaterali involontari per evitare sorprese quando vengono fatte delle chiamate di funzioni. Le funzioni ricadono in una delle due seguenti categorie: comandi, che fanno un’azione, e queries, che rispondono a una domanda. Non dovreste mischiarle. Prendiamo in considerazione la seguente funzione:

function getFirstName() {
    var firstName = document.querySelector("#firstName").value;
    firstName = firstName.toLowerCase();
    setCookie("firstName", firstName);
    if (firstName === null) {
        return "";
    }
    return firstName;
}
 
var activeFirstName = getFirstName();

Questo è un esempio semplicistico: la maggior parte degli effetti collaterali sono difficili da trovare, ma potete vedere alcuni imprevisti effetti collaterali potenziali in azione.

Il nome della funzione, getFirstName, ci dice che la funzione ritornerà il nome proprio. Ma la prima cosa che fa è convertire il nome in lettere minuscole. Il nome dice che ottiene qualcosa (una query), ma sta anche cambiando lo stato dei dati (un comando): un effetto collaterale che non è chiaro dal nome della funzione.

Peggio ancora, la funzione poi imposta un cookie per il nome proprio senza dircelo, potenzialmente sovrascrivendo qualcosa su cui avremmo potuto fare affidamento. Una funzione query non dovrebbe mai, per nessun motivo, sovrascrivere dati.

Una buona regola pratica è che se la vostra funzione risponde a una domanda, dovrebbe ritornare un valore e non alterare lo stato dei dati. Di contro, se la vostra funzione fa qualcosa, dovrebbe alterare lo stato dei dati e non ritornare un valore. Per massima chiarezza, una funzione non dovrebbe mai ritornare un valore e alterare lo stato dei dati.

Una versione migliore del codice di cui sopra sarebbe:

function getFirstName() {
    var firstName = document.querySelector("#firstName").value;
    if (firstName === null) {
        return "";
    }
    return firstName;
}
 
setCookie("firstName", getFirstName().toLowerCase());

Questo è un esempio base, ma spero che possiate vedere come questa separazione possa chiarire lo scopo e prevenire errori. Al crescere delle dimensioni delle funzioni e della code base, la separazione diventa molto più importante, dal momento che cercare la definizione della funzione ogni volta che la si vuole usare solo per scoprire cosa fa non è un modo efficiente di sfruttare il tempo di qualcuno.

Loose coupling#section3

Consideriamo la differenza che c’è tra un puzzle e i mattoncini Lego. Con un puzzle, c’è solo un modo per mettere insieme i pezzi e c’è un unico prodotto finito. Con il Lego, potete mettere insieme i pezzi in qualsiasi maniera si voglia per ottenere qualunque risultato finale si desideri. Se doveste scegliere uno di questi tipi di giochi di costruzione con cui lavorare prima di sapere quello che creerete, quale scegliereste?

Coupling è una misura di quanto un’unità di programma faccia affidamento sulle altre. Troppo coupling (o un coupling troppo stringente) è rigido e andrebbe evitato: è il puzzle. Vogliamo che il nostro codice sia flessibile, come i mattoncini Lego. Questo è il loose coupling e generalmente risulta in maggiore chiarezza.

Ricordate: il codice dovrebbe essere sufficientemente flessibile da coprire un’ampia varietà di casi d’uso. Se vi trovate a copiare e incollare del codice facendo piccoli cambiamenti o a riscrivere codice perché il codice è cambiato da qualche altra parte, state osservando il tight coupling in azione. (Per esempio, per rendere la precedente funzione getFirstName riutilizzabile, potreste sostituire lo hard-coded firstName con un generico ID passato alla funzione). Altri segni di ciò includono gli ID hard-coded nelle funzioni, troppi parametri di funzione, molteplici funzioni simili e funzioni grandi che violano il principio di singola responsabilità.

Il tight coupling è più diffuso in un gruppo di funzioni e variabili che dovrebbero in realtà essere una classe, ma può anche succedere quando delle classi dipendono da metodi o proprietà di altre classi. Se avete dei problemi con le interdipendenze nelle funzioni, è probabilmente ora di pensare a suddividere le funzioni in una classe.

Mi ci sono imbattuto mentre osservavo del codice per una serie di manopole interattive. Le manopole avevano un numero di variabili, incluse le dimensioni, la taglia della manopola, la dimensione del perno e così via. A causa di tutto ciò, lo sviluppatore era costretto o a usare una quantità assurda di parametri di funzione o a creare molteplici copie di ogni funzione con le variabili hard-coded in ciascuna di esse. Inoltre, ogni manopola faceva qualcosa di diverso quando ci si interagiva. Questo ha portato a tre insiemi di funzioni quasi identiche, una per ciascuna manopola. In breve, il coupling è aumentato a causa dell’hard coding delle variabili e del comportamento, così, come in un puzzle, c’era solo un modo per mettere insieme quei pezzi. La codebase era inutilmente complessa.

Abbiamo risolto il problema suddividendo le funzioni e le variabili in una classe riutilizzabile che veniva istanziata per ognuna delle tre manopole. Abbiamo impostato la classe perché prendesse una funzione come argomento per l’output, quindi diversi risultati potrebbero essere configurati quando i singoli oggetti manopola vengono istanziati. Come risultato, avevamo meno funzioni e le variabili sono state memorizzate solo in un posto, rendendo gli aggiornamenti molto più semplici.

Le classi che interagiscono le une con le altre possono inoltre essere la causa di tight coupling. Supponiamo di avere una classe che può creare oggetti di un’altra classe, come un corso al college che possa creare degli studenti. La nostra classe CollegeCourse funziona bene. Ma poi abbiamo bisogno di aggiungere un parametro al costruttore della classe Student. Oh no! Adesso dobbiamo modificare la nostra classe CollegeCourse per tenere conto del cambiamento nella classe Student.

var CollegeCourse = (function() {
    function createStudent_WRONG(firstName, lastName, studentID) {
        /*
        If the Student constructor changes, we'll have to modify this method and all calls to it, too!
        */
    }

    function createStudent_RIGHT(optionsObject) {
        /*
        Passing an object as an argument allows the Student object to deal with the change. We may need to change this method, but we won’t need to change any existing calls to it.
        */
    }
}());

Non dovreste dover modificare una classe perché un’altra classe è cambiata. Questo è un classico caso di tight coupling. I parametri constructor possono essere passati come oggetto con l’oggetto ricevente che ha dei valori di fallback di default, che allenta il coupling e significa che il codice non smetterà di funzionare a dovere quando aggiungerete dei nuovi parametri.

Il punto è che dovreste costruire il vostro codice come dei mattoncini Lego non come dei pezzi di un puzzle. Se vi trovate ad affrontare problemi simili a quelli di cui sopra, il problema è probabilmente il tight coupling.

High cohesion#section4

Avete mai visto un bambino pulire una stanza mettendo tutto in un cassetto? Certo, funziona, ma è impossibile trovare una qualsiasi cosa e vengono messe assieme anche cose che non hanno alcuna relazione tra esse. Lo stesso può succedere con il nostro codice se non cerchiamo di ottenere un alto livello di coesione.

La cohesion è una misura di quanto le varie unità del programma possano andare bene assieme. Un alto livello di coesione va bene e aggiunge chiarezza ai blocchi di codice; un basso livello di coesione non va bene e porta a molta confusione. Le funzioni e i metodi in un blocco di codice dovrebbero aver senso insieme: ossia, avranno un alto livello di coesione.

High cohesion significa tenere insieme cose correlate, come le funzioni del database o le funzioni relative ad un particolare elemento, in un blocco unico o in un solo modulo. Questo aiuta non solo con la comprensione di come tali cose siano organizzate e dove trovarle, ma anche con la prevenzione di conflitti di naming. Se avete 30 funzioni, le probabilità di un conflitto di naming saranno molto più alte rispetto a quando avete 30 metodi suddivisi in quattro classi.

Se due o tre funzioni usano le stesse variabili, vanno messe insieme: si tratta di un ottimo caso per un oggetto. Se avete una serie di funzioni e variabili che controllano un elemento della pagina, come uno slider, è una grande opportunità per una high cohesion, quindi dovreste unirli in un oggetto.

Ricordate l’esempio che abbiamo fatto prima sulla classe che faceva decoupling della soluzione per la manopola? Si tratta di un ottimo esempio di high cohesion che cura il tight coupling. In quel caso, high cohesion e tight coupling erano ai lati opposti di una scala indicizzata e concentrarsi su una sistemava l’altra.

Il codice ripetuto è un segno sicuro di bassa coesione. Righe di codice simili dovrebbero essere scomposte in funzioni e funzioni simili dovrebbero essere scomposte in classi. La regola pratica qui è che una riga di codice non dovrebbe mai essere ripetuta due volte. In pratica, questo non è sempre possibile, ma per il bene della chiarezza, dovreste sempre pensare a come ridurre la ripetizione.

In maniera simile, lo stesso pezzo di dati non dovrebbe esistere in più di una variabile. Se state definendo lo stesso pezzo di dati in più posti, avete decisamente bisogno di una classe. Oppure, se vi trovate a passare dei riferimenti allo stesso elemento HTML a più funzioni, il riferimento dovrebbe probabilmente essere una proprietà in un’istanza di una classe.

Gli oggetti possono anche essere messi dentro ad altri oggetti per aumentare ulteriormente la cohesion. Per esempio, potreste mettere tutte le funzioni AJAX in un singolo modulo che includa oggetti per l’invio di form, per prendere il contenuto e la sintassi di login, così:

Ajax.Form.submitForm();
Ajax.Content.getContent(7);
Ajax.Login.validateUser(username, password);

Al contrario, non dovreste mettere insieme cose non collegate nella stessa classe. Un’agenzia per cui ho lavorato aveva una API interna con un oggetto chiamato Common che aveva un miscuglio di metodi e variabili comuni che non avevano nulla a che fare tra loro. La classe era diventata enorme e poco chiara semplicemente perché non si era pensato alla cohesion.

Se le proprietà non sono usate da molteplici metodi in una classe, questo può essere un segno di bassa o cattiva cohesion. In maniera simile, se i metodi non possono essere riutilizzati in alcune situazioni diverse, oppure se un metodo non è usato per niente, anche questo può essere un segno di bassa o cattiva cohesion.

La high cohesion aiuta ad alleviare il tight coupling e il tight coupling è un segno che occorre una cohesion maggiore. Se i due entreranno mai in conflitto, tuttavia, scegliete la cohesion. High cohesion è generalmente un aiuto maggiore per lo sviluppatore rispetto al coupling, sebbene entrambe possano solitamente essere ottenute assieme.

Conclusione#section5

Se il nostro codice non è immediatamente chiaro, ci saranno dei problemi. Ottenere la chiarezza richiede molto più di un’appropriata indentazione: ci vuole una pianificazione fin dall’inizio del progetto. Sebbene sia difficile da padroneggiare, rispettare il principio di singola responsabilità, la command-query separation, il loose coupling e la high cohesion può migliorare notevolmente la chiarezza del nostro codice, cosa che dovrebbe essere presa in considerazione in ogni progetto di programmazione significativo.

Nessun commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Altro da ALA