Skip to content

Instantly share code, notes, and snippets.

@ValentinaServile
Last active May 2, 2023 14:00
  • Star 9 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save ValentinaServile/05b154217522c7d723ff to your computer and use it in GitHub Desktop.
Appunti sui principi del Clean Code

Clean Code ITA

Nomi

  • Tutte le variabili, costanti, funzioni, classi etc devono avere un nome significativo. Se il nome ha bisogno di un commento per essere spiegato non è abbastanza significativo.

  • Sono da evitare nomi che possano sviare dal vero significato della variabile

  • E' da evitare l'utilizzo di noise words come "data", "info", "the" etc. nei nomi di variabile

  • E' da evitare lasciare numeri dal significato non chiaro all'interno del codice quando si possono rappresentare con costanti dal nome significativo

  • Sono da evitare nomi impronunciabili e di conseguenza suffissi e prefissi innecessari per indicare il tipo di una variabile

  • La lunghezza del nome di una variabile dev'essere direttamente proporzionale alla dimensione del suo scope: più lo scope è piccolo e più il nome può essere corto perché la variabile è stata dichiarata poco prima, viceversa se lo scope è molto grande la variabile avrà bisogno di un nome molto descrittivo in quanto altrimenti dovrà essere cercata la sua dichiarazione in altre parti del codice

  • La lunghezza del nome di una funzione o una classe dev'essere inversamente proporzionale alla dimensione del suo scope: più lo scope è grande e più si rivela necessario avere un nome corto e semplice in quanto verrà chiamata più volte in diverse parti del codice, più lo scope è piccolo e più ci si può permettere di allungare il nome per renderlo più esplicativo possibile in quanto la classe o funzione avrà una ragione di esistere molto specifica.

  • Ogni funzione deve avere un verbo come nome. In particolare un metodo può avere nomi utili come setValue, getValue oppure isSomething per facilitarne l'utilizzo nelle righe "imperative" e negli if statement.

Funzioni

Le funzioni in generale devono essere molto piccole (5-6 righe), fare solamente una cosa e contenere al massimo uno o due livelli di innestamento, e pertanto devono avere nomi molto significativi.

Questo perché funzioni lunghe spesso contengono delle classi nascoste o si occupano di più di un livello di astrazione.

Per scomporre una funzione molto lunga ed ottenere una struttura coerente con la descrizione di cui sopra è possibile:

  • provare a scomporla in funzioni più piccole finché non è più possibile scomporre.

  • verificare che non lavori con più livelli di astrazione (es. mischiando righe con compiti di basso livello come appendere un carattere a una stringa a righe con compiti di alto livello come istanziare una classe)

  • verificare che risponda a una domanda o faccia qualcosa (es. che dia informazioni riguardo a un oggetto o cambi lo stato dell'oggetto, non entrambe le cose)

  • fare in modo che i metodi che si occupano di un livello di astrazione più alto chiamino sempre e solo metodi che si occupano di un livello di astrazione più basso, creando una struttura a cascata

Classi

Le classi devono essere molto piccole. Mentre per le funzioni la misura era definita in righe effettive, per le classi la si può definire come numero di responsabilità: ogni classe deve averne una e una soltanto. Si può determinare il numero di responsabilità di una classe contando le motivazioni per cui potrebbe ragionevolmente essere modificata.

Le classi dovrebbero avere un numero ridotto di variabili, ed ogni metodo dovrebbe far uso di più variabili possibile: questo principio si chiama coesione, e determina un'alta co-dipendenza fra tutte le variabili e i metodi della classe stessa rendendola coerente e atomica a livello logico.

Inoltre una classe deve seguire il principio OCP (Open Closed Principle), che stabilisce che debba essere sempre aperta alle estensioni (via ereditarietà) ma chiusa rispetto ad eventuali modifiche.

Metodi e variabili pubblici o importanti vanno indicati all'inizio della classe, e quelli privati o poco importanti alla fine per facilitare la lettura, andando in ordine di livelli di astrazione (metodo top-down)

In generale inoltre, i metodi vanno divisi in setter e getter.

  • i metodi getter che hanno un valore di ritorno non devono cambiare lo stato del sistema (sarebbe un side-effect inaspettato)

  • i metodi setter che cambiano lo stato del sistema non devono avere un valore di ritorno (finirebbe per essere uno dei codici di errore di cui sopra)

E' meglio limitare al più possibile le funzioni getter, in quanto non è buona pratica prendere decisioni sulla base di uno stato dell'oggetto al di fuori dell'oggetto stesso per non violare il suo dominio di competenza (principio "Tell Don't Ask").

Differenza tra oggetti e strutture dati

Gli oggetti sono astrazioni che nascondono i loro dati ed espongono solo le funzioni per manipolarli (rendono semplice aggiungere nuovi oggetti e difficile aggiungere nuove funzionalità in quanto dovranno essere aggiunte a tutti gli oggetti esistenti), mentre le strutture dati espongono variabili e non hanno funzioni significative (rendono semplice aggiungere nuove funzionalità e difficile aggiungere nuovi dati alla funzione che le sfrutta). E' pertanto buona pratica non mischiare le due logiche e scegliere quella più adatta al contesto.

Error handling

Le funzioni non devono ritornare codici di errore, in quanto forzano il programmatore a dover gestire l'errore subito portando a strutture di if innestate a molti livelli di questo tipo:

if (deletePage(page) == E_OK) {
    if (registry.deleteReference(page.name) == E_OK) {
        if (configKeys.deleteKey(page.name.makeKey()) == E_OK){
            logger.log("page deleted");
        } else {
            logger.log("configKey not deleted");
        }
    } else {
        logger.log("deleteReference from registry failed");
    }
} else {
    logger.log("delete failed");
    return E_ERROR;
}

E' meglio fare in modo che lancino eccezioni che verranno gestite con un blocco try-catch, possibilmente astratto dalla struttura. Es.

public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    }
    catch (Exception e) {
        logError(e);
    }
}
private void deletePageAndAllReferences(Page page) throws Exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
    logger.log(e.getMessage());
}

Temporal coupling

Il concetto di temporal coupling si riferisce alla dipendenza del funzionamento di una serie di operazioni dal loro ordine. Es. openfile e closefile. A volte in sistemi molto grandi questo può essere controintuitivo (specialmente se non si tratta di operazioni ovvie come openfile e closefile) ed è pertanto necessario raggruppare le operazioni da eseguire in un particolare ordine in un blocco simile a:

public void open(file f, fileCommand c) {
    f.open();
    c.process(f);
    f.close();
}

così che il sistema dopo la chiamata ritorni allo stato precedente eliminando dipendenze temporali.

Commenti

I commenti sono generalmente da evitare in quanto il codice dovrebbe possibilmente commentarsi da sé, tuttavia alcuni sono necessari.

  • Le header con le note legali (che non dovrebbero contenere l'intero contratto ma un riferimento)

  • Dichiarazioni dell'intento

  • Warning particolari

  • TO-DOs

  • docs nelle api

Forma

E' necessario utilizzare la spaziatura verticale in modo da separare nettamente i blocchi logici. Ogni riga inoltre non deve mai eccedere la larghezza di uno schermo standard (non bisogna mai dover scrollare orizzontalmente perché questo interrompe il flusso di lettura).

Codice di terze parti

È consigliabile incapsulare le funzionalità delle librerie di terze parti all'interno di delle classi personalizzate, per evitare di dover mettere le mani ovunque successivamente nel codice nel caso queste venissero aggiornate e/o cambiate.

Architettura del software e dipendenze

Il codice di ogni applicazione non dovrebbe essere focalizzato sul metodo di consegna (es. console, web, etc) ma sui suoi use case. L'architettura vera e propria dell'applicazione dovrebbe essere astratta da vari framework e architetture e consistere di 3 oggetti distinguibili:

  • entities

contengono funzionalità indipendenti dall'applicazione in cui questi oggetti sono utilizzati e sono il livello più astratto

  • interactors

contengono funzionalità dipendenti dall'applicazione e dai suoi use cases, e sfrutta le funzionalità delle entities per soddisfare le richieste dell'applicazione

  • boundaries

oggetti che interagiscono con l'interfaccia accettando un input dall'utente e restituendogli un output comunicando con gli interactors, provvedendo un ponte fra la parte astratta dell'applicazione e l'architettura con cui è implementata

Eventuali framework MVC comunicano con i boundaries, rimanendo quindi indipendenti dall'architettura generale. Il database sarà invece gestito dagli interactors che tradurranno i dati statici in entities.

Nell'applicazione devono essere nettamente distinguibili:

  • I moduli core

  • I moduli di "plug-in" e main

Le dipendenze fra i due devono sempre e solo puntare verso i moduli core, che non devono sapere nulla dei moduli di plug-in.

In generale le dipendenze di ogni modulo devono puntare sempre verso l'alto (la parte astratta dell'applicazione), mai viceversa.

Code smells

I principali indicatori di un codice poco pulito sono:

  • Codice duplicato

Quando si ha codice duplicato in più metodi di una classe è meglio riassumerlo in un metodo solo, mentre se la duplicazione si trova in due classi diverse è necessario decidere a quale delle due classi il metodo derivato apparterrebbe.

  • Metodi lunghi

Più una procedura è lunga e più è difficile da analizzare. La chiave per un codice pulito è utilizzare tanti piccoli metodi con dei nomi molto significativi. Ogni volta che sentiamo il bisogno di aggiungere un commento possiamo invece creare un nuovo metodo che contiene il codice che sarebbe stato commentato, e che spiega il suo intento con il suo nome.

  • Classi molto grandi

Quando una classe cerca di fare troppo nella maggior parte dei casi ha moltissime variabili all'interno. E' buona pratica raggruppare quelle accumunabili a livello logico e separarle in delle piccole sottoclassi.

  • Metodi con troppi parametri

Le lunghe liste di parametri sono difficili da comprendere ad un primo impatto, sono difficili da usare e vengono costantemente modificate quando c'è bisogno di nuovi dati. Invece di passare ad un metodo tutto ciò di cui ha bisogno, è meglio passargli lo stretto necessario per raggiungere tutto ciò di cui ha bisogno (del resto molte delle cose di cui ha bisogno sono probabilmente già disponibili nella sua classe).

  • Cambiamento divergente

Quando una classe è modificata spesso, in modi diversi e per ragioni diverse allora probabilmente sarebbe il caso di dividerla e prevedere più casistiche.

  • Shotgun surgery

Quando per fare una piccola modifica sono necessari tanti cambiamenti in tante altre classi, i cambiamenti diventano presto difficili da rintracciare e una causa molto probabile di infiniti bug. E' idealmente meglio prevedere un legame 1:1 fra i cambiamenti più frequenti e le nostre classi.

  • Feature envy

Lo scopo degli oggetti è impacchettare dati e operazioni comuni effettuate su quei dati, e quando un metodo è interessato più alle proprietà di un'altra classe che a quelle della sua, si parla di feature envy (il focus più comune sono le proprietà, ma possono anche essere altri metodi). E' necessario spostare quel metodo nella classe a cui realmente appartiene. Talvolta solo parte del metodo soffre di feature envy, e in quel caso va spezzato.

  • Grumi di dati

Talvolta si vedono le stesse tre o quattro proprietà insieme in più punti del codice: in questo caso vanno raggruppati e messi in un oggetto. Un modo per verificare se questo tipo di refactoring è necessario per un gruppo di proprietà è domandarsi se queste, tolta una a caso, avrebbero comunque senso. Se la risposta è no si tratta chiaramente di un oggetto mancato.

  • Gerarchie di classi parallele

Come nel caso della shotgun surgery, le gerarchie di classi parallele implicano la necessità di cambiamenti in più parti del codice quando si rende necessaria una singola modifica.

  • Classi che non fanno abbastanza

Ogni classe rappresenta un costo di manutenzione, pertanto se non svolge abbastanza funzioni oppure è stata rimpicciolita troppo con il refactoring va eliminata.

  • Generalizzazioni

Quando si pensa troppo avanti nel codice, cercando di predisporlo a funzionalità future che probabilmente non verranno mai implementate si rischia di scrivere delle generalizzazioni non necessarie. E' meglio collassare le gerarchie di classi astratte che non hanno una vera ragione di esistere.

  • Variabili temporanee

Talvolta si incontrano oggetti che fanno uso di alcune variabili solo in determinate circostanze. Questo rende il codice poco chiaro perché in genere ci si aspetta che un oggetto abbia bisogno di tutte le sue variabili, ed è il caso di estrarre quelle raramente utilizzate in una classe a sé stante.

  • Catene di oggetti

Si verificano quando un client chiede ad un oggetto ad un altro oggetto, a cui poi chiede un altro oggetto e così via (catene di metodi getThis().getThat()). Questo è poco pratico perché quella parte di codice è poi dipendente e deve conoscere le relazioni delle classi nell'applicazione, cosa che rende il refactoring molto difficile (nel caso una di queste relazioni dovesse cambiare dovrà cambiare di conseguenza anche il client). E' meglio spostare i metodi in punti più convenienti.

  • Delegazione eccessiva

Quando una classe delega troppe funzionalità ad un'altra classe è necessario rimuoverla del tutto e consultare direttamente l'oggetto che sa cosa sta succedendo oppure trasformare la delegazione in una gerarchia.

  • Troppa intimità fra classi

Talvolta due classi passano troppo tempo a consultare variabili che dovrebbero essere private l'una dell'altra. Quando questo si verifica si possono spostare alcuni metodi in favore di una delle due per trasformare questa dipendenza bidirezionale in una dipendenza unidirezionale, oppure se queste classi hanno molto in comune è il caso di estrarre le parti che interessano entrambe in una classe terza.

  • Classi composte solo di dati

Sono delle classi che hanno solamente un gruppo di variabili e dei metodi setter e getter per quelle variabili, diventando solamente dei contenitori di dati senza comportamento che spesso vengono modificati troppo estensivamente da altre classi. In questi casi si possono prendere i metodi che modificano le loro variabili dalle altre classi e attribuirgli un po' di responsabilità.

  • Rifiuto dell'eredità

A volte si incontrano sottoclassi che fanno poco uso dei metodi e delle variabili della classe madre. Questo molte volte rappresenta una gerarchia pensata nel modo sbagliato, e bisogna spostare i metodi della classe madre utilizzati solo da alcune figlie direttamente nelle classi figlie interessate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment