Viaggio tra i paradigmi

Un’introduzione ai 3 principali paradigmi di programmazione

Annunci

Un paradigma di programmazione è un insieme di concetti che permette allo sviluppatore di modellare il problema da risolvere secondo un gruppo ben definito di regole.

I fondamentali paradigmi di programmazione sono:

  • La programmazione strutturata
  • La programmazione orientata agli oggetti
  • La programmazione funzionale

I 3 precedenti paradigmi sono stati utilizzati nell’ordine in cui sono scritti, ma, curiosamente sono stati inventati nell’ordine opposto.

Programmazione strutturata

La programmazione strutturata è stata per la prima volta teorizzata da Edsger Dijkstra, matematico e informatico olandese, uno dei padri della computer science.

Dijkstra era convinto che, per validare i programmi, fosse necessario dimostrarli matematicamente. Riuscì a dimostrare la validità di alcune strutture di controllo, come sequenze di operazioni, cicli e istruzioni condizionali.
Scoprì inoltre che era impossibile dimostrare la correttezza di programmi che contenessero salti incondizionati (goto), e, alla fine degli anni 60, pubblicò un articolo in cui metteva in evidenza i rischi dell’uso di tali costrutti.

Questo aprì una diatriba durata una decina d’anni, ma alla fine degli anni 70 fu pienamente accettato che i soli costrutti validi per il controllo di flusso fossero le istruzioni di tipo if … then … else e do … while.
Il famigerato goto venne messo all’angolo e non più permesso in molti linguaggi per un po’ si accettò che il goto fosse utilizzato nella gestione degli errori; poi con l’avvento delle eccezioni anche questo tipo di utilizzo fu abbandonato.

Dijkstra sosteneva inoltre che ogni funzione dovesse avere un solo punto di ingresso e un solo punto di uscita; in realtà negli ultimi decenni è stato in parte sdoganato il fatto di poter avere più di un punto di uscita: infatti in alcuni casi può essere più espressivo avere 2 o 3 punti di uscita, ma solo a patto che la funzione sia molto corta.

La dimostrabilità dei programmi invece non prese mai piede, per vari motivi:

  • Dopo Dijkstra non fu più portato avanti l’impianto matematico teorico che sarebbe servito per dare fondamento alla dimostrabilità dei programmi
  • Non tutti i programmatori sono dei matematici
  • I tempi per la dimostrazione di ogni pezzo di programma probabilmente non sarebbero compatibili con i tempi di consegna (ma questo non possiamo dirlo, infatti si sarebbero potuti creare degli strumenti automatici per la dimostrazione)

Ancora oggi che usiamo il paradigma ad oggetti o quello funzionale, il corpo degli algoritmi è scritto secondo le regole della programmazione strutturata, che quindi costituisce l’ossatura dei nostri programmi.
 

Programmazione ad oggetti

La programmazione ad oggetti viene spesso descritta ponendo l’accento sui dati e dicendo che permette di avere delle strutture che contengono sia i dati che le funzioni (metodi) che servono per manipolarli.
Questo in realtà è vero solo perché i linguaggi OO usano una notazione del tipo oggetto.funzione(), che però non è diverso da dire funzione(oggetto).
Una definizione che mi piace di più è quella che la programmazione ad oggetti permette di rappresentare in modo più chiaro le relazioni esistenti tra le entità del sistema (contenimento, specializzazione, uso).

Il paradigma è inoltre identificato con le 3 funzionalità principali, che sono:

  • Incapsulamento
  • Ereditarietà
  • Polimorfismo

In realtà l’incapsulamento, cioè la separazione tra l’interfaccia e l’implementazione, già era presente in C. Infatti tutto ciò che serviva al programma client per usare una libreria era il file .h da includere. Con i linguaggi ad oggetti l’incapsulamento diventa comunque più facile da ottenere.

L’ereditarietà era in qualche misura già possibile, perché si poteva utilizzare una struct al posto di un’altra, a patto che i campi comuni seguissero lo stesso ordine e fossero dello stesso tipo.
Sicuramente i linguaggi ad oggetti hanno reso più semplice e sicuro l’uso dell’ereditarietà, che prima doveva essere costruita e gestita manualmente.

Il polimorfismo, che forse costituisce l’apporto più importante della programmazione ad oggetti, ha permesso di fare in modo chiaro ed esplicito ciò che prima si poteva ottenere in modo più macchinoso, utilizzando i puntatori a funzione.

Quindi potremmo dire che la programmazione ad oggetti ha rivestito in modo più chiaro e sicuro una serie di operazioni già possibili in precedenza, ma che costavano molto dal punto di vista della chiarezza del codice, della possibilità di errori e della produttività:

  • Ha permesso di definire in modo più chiaro quali sono gli attori che fanno parte dell’applicazione e come interagiscono tra loro attraverso lo scambio di messaggi.
  • Ha reso più semplice, sicuro e controllato dal compilatore l’utilizzo dell’ereditarietà e del polimorfismo, permettendo a tutti di utilizzare queste funzionalità. Questo ha determinato un notevole aumento di produttività rispetto ai linguaggi precedenti.
  • Attraverso il polimorfismo e quel che ne consegue, ha permesso di introdurre il concetto di separazione e disaccoppiamento tra i componenti di un’applicazione, permettendoci di generalizzare certe parti di codice e di renderle indipendenti dai cambiamenti (o renderle almeno più resistenti).
  • Con la dependency injection ha permesso di inserire la dipendenza direttamente a run-time, consentendo di costruire applicazioni ancora più generiche; questo anche grazie a numerosi framework nati attorno a questi linguaggi.

Programmazione funzionale

La programmazione funzionale è stato il primo paradigma ad essere emerso (risale agli studi del matematico Alonzo Church negli anni 30), e l’ultimo ad essere adottato.
Questo stile di programmazione ha alcune caratteristiche che lo contraddistinguono dagli altri:

  • L’immutabilità delle variabili
  • La funzione come unità di base
  • La struttura dichiarativa

L’immutabilità (quasi totale) si attua nel fatto che se si vuole modificare il valore di una variabile, è necessario crearne una nuova con il nuovo valore.
L’immutabilità delle variabili in realtà non è assoluta: esistono infatti meccanismi per modificare il valore delle variabili, ma sono espliciti ed estremamente controllati, ad esempio con l’uso di una memoria transazionale (simile al funzionamento delle transazioni su un database).

L’immutabilità è il motivo per cui, negli ultimi anni, la programmazione funzionale è uscita dall’ambito prettamente accademico: infatti tutti i problemi che derivano dalla concorrenza e dal multi threading sono dovuti ai cambiamenti di stato delle variabili:

  • Se le variabili non sono protette, si possono verificare delle variazioni indesiderate (un thread modifica un valore, e il thread successivo trova quel valore modificato).
  • D’altra parte, se le variabili sono protette con dei lock, si può incorrere in condizioni di deadlock.

L’immutabilità ci ripara da questi effetti indesiderati.

Il prezzo da pagare è un cambiamento di prospettiva che di solito mette in difficoltà gli sviluppatori che utilizzano altri paradigmi:

  • In primo luogo l’unità fondamentale è la funzione, che riceve una serie di parametri in ingresso (che possono essere a loro volta funzioni) e che dà un risultato (anch’esso può essere una funzione). Queste funzioni possono essere assimilate alle funzioni nel senso matematico del termine, e, a parità di ingressi, generano sempre la stessa uscita (funzioni pure).
  • Il codice è poi scritto in maniera dichiarativa anziché imperativa (sequenza di comandi) come avviene nei linguaggi OO e in quelli procedurali.
  • L’iterazione è sostituita dalla ricorsione. Ottimizzazioni effettuate dal compilatore permettono in alcuni casi di risparmiare spazio sullo stack e di lo stesso codice macchina che si sarebbe ottenuto se si fosse usata un’iterazione

I linguaggi puramente funzionali non sono ancora molto diffusi. Essi hanno però influenzato gli altri linguaggi (per esempio le ultime versioni di Javascript, Python, C# hanno delle caratteristiche funzionali, come le espressioni lambda, ma non solo).
Inoltre si sono diffuse delle buone pratiche nella scrittura delle funzioni nei linguaggi non funzionali, come per esempio evitare di generare degli effetti collaterali, se non esplicitamente indicato.

Viaggi in Liguria

Ho viaggiato molto spesso per lavoro, avendo modo di soggiornare in varie parti d’Italia.
Qui voglio parlare di alcuni luoghi interessanti che ho scoperto in Liguria durante queste trasferte di lavoro.

Altare (Savona)

La prima volta che ho lavorato in Liguria è stato per una vetreria in provincia di Savona, dove ho sviluppato un sistema di gestione di magazzino.
Più precisamente ero ad Altare, nella vallata di confine tra le Alpi e gli Appennini, dove passa l’autostrada Torino – Savona.
La valle è un po’ cupa, ma Altare è un comune interessante da visitare. Ci si può trovare una chiesa che, pare, sia stata costruita dai templari. Si dice che siano stati proprio i templari a dare inizio alla produzione del vetro in questa valle, facendo arrivare maestri vetrai dalla Francia.
Nei pressi di Altare si trova l’autobar Marenco, quello che viene definito l’autogrill più bello d’Italia: devo dire che si mangia davvero bene, con prodotti di eccellenza e molti di produzione propria (se non ricordo male si trova nella corsia dell’autostrada in direzione Piemonte). All’epoca si poteva accedere a piedi senza dover per forza passare dall’autostrada.
Ho spesso pranzato al ristorante Quintilio, che si trova sulla guida Michelin: in quel periodo a pranzo c’era un menù del giorno a prezzi contenuti (ma sono anni che non passo di lì). L’ottima cucina è di confine tra Piemonte e Liguria.
Ho sempre dormito ad Albisola, che alla sera offriva la possibilità di una passeggiata lungo mare. Albisola è attaccata a Savona, quindi la scelta di ristoranti, pub e itinerari a piedi è molto ampia.

Pieve di Teco

In anni più recenti ho lavorato per un’azienda che commercializza olio, prodotti confezionati, salumi e formaggi di alta qualità specialmente nel nord Europa. In questa ditta, specializzata nella vendita telefonica e web, ho sviluppato un software di controllo di una linea di smistamento colli e pick to light.
La sede è a Pieve di Teco, un bel comune nella valle Arroscia, sopra Albenga.
La zona di Albenga è uno dei più grandi spazi pianeggianti in tutta la Liguria; la vocazione della valle è prevalentemente agricola e si vedono ovunque coltivazioni e serre (un comune della valle si chiama Ortovero).
Salendo da Albenga verso Pieve di Teco non è raro trovarsi davanti un’Apecar che trasporta cassette di ortaggi, impossibile da sorpassare in quella strada stretta e piena di curve.
Dal punto di vista architettonico Pieve di Teco ha un impianto medievale: la via centrale porticata è assolutamente da vedere.
Poco più giù di Pieve di Teco, scendendo verso Albenga, si trova il comune di Vessalico, famosa per l’aglio. Lì io e i miei colleghi andavamo spesso a pranzo al ristorante Da Maria, dove si terminava il pasto con un tiramisù veramente spettacolare.
Durante l’estate alloggiavamo all’hotel Lorenzina al colle di Nava, a circa 1000 metri sul livello del mare. Devo dire che nella stagione calda era una scelta azzeccata. L’hotel è dotato di un ottimo ristorante e di una notevole cantina. A fine agosto si possono gustare ottimi piatti a base di funghi raccolti in giornata nei boschi attorno all’hotel.
La struttura era abitata da 3 categorie di avventori:
• Noi che eravamo lì per lavorare
• Gruppi di motociclisti (lì intorno ci sono ottimi percorsi per moto da cross)
• Pensionati di Torino e dintorni che stavano in villeggiatura al fresco: uno spaccato d’altri tempi.
Alla sera, nelle frazioni vicine c’erano varie feste all’aperto dove passare qualche ora ballando e ascoltando musica.
L’hotel in autunno chiude e, a quanto mi dicevano, durante l’inverno la strada del colle di Nava è impraticabile.
Così nei mesi freddi stavamo ad Albenga, all’hotel Magnolia.
Questo albergo era abitato da pensionati che svernavano al clima più mite della Liguria (non so se erano gli stessi che in estate stavano da Lorenzina). Dicono che la colazione fosse davvero ottima, ma purtroppo, data l’atmosfera di villeggiatura, la servivano a partire dalle 8:00. Noi a quell’ora eravamo già sulla strada per Pieve di Teco, dove il cliente ci aspettava con ansia.
Sul lungo mare a 2 passi dall’hotel c’è un piccolo ristorante costruito sull’acqua, tutto di legno bianco: non sono più riuscito a trovare il nome: comunque è veramente caratteristico e si mangia un ottimo fritto di pesce. Da evitare durante le mareggiate.

Sull’autostrada in direzione Savona

Se avete superato Genova e state guidando in direzione Savona, poco dopo Arenzano c’è l’area di servizio Piani di Invrea Nord. Vale sicuramente la pena fare una sosta, perché l’autogrill è dotato di una terrazza all’aperto con tavoli vista mare, incorniciata da pini marittimi. Credo che sia utile fare una pausa rilassante e godere per qualche minuto di un bel panorama, specie se si sta viaggiando per lavoro.

Chiavari

Spesso, tornando dalla Toscana mi sono fermato a cena a Chiavari, che è una bella località balneare poco prima di Genova, venendo dalla Toscana. In estate è davvero piacevole fermarsi qui: a giugno e luglio c’è gente sulla spiaggia fino alle 8 di sera e si riesce ad ammirare un bel tramonto. Tra il lungomare e la spiaggia si trova un largo marciapiede con palme, giardinetti e fontane, con gente che passeggia e che corre. In inverno c’è ovviamente meno vita, ma, se non c’è vento, le luci e le fontane rendono comunque interessante fare una passeggiata sul lungomare.
Di solito ceno all’hotel Zia Piera, sul lungo mare: il ristorante offre cucina ligure, pizza e l’immancabile focaccia di Recco, ripiena di formaggio.

 

 

State pattern nel mondo reale

Le macchine a stati sono argomento di studio per tutti coloro che seguono i corsi di informatica o ingegneria informatica. Spesso restano un argomento di studio perché appaiono lontane dalla realtà di tutti i giorni.

In realtà tutti i nostri programmi hanno uno stato interno che varia di continuo. Di solito però non abbiamo bisogno di formalizzarli come macchine a stati.

Ad esempio la seguente macchina a stati

Untitled Diagram(1)

potrebbe modellare questo programma:

void Main() {
    while (true) {
    string res = Console.ReadLine();
    Console.WriteLine(res);
    }
}

Implementare una macchina a stati per descrivere il comportamento del programma precedente sarebbe un’inutile complicazione e renderebbe il programma molto più lungo.

Però ci sono casi in cui una macchina a stati può venire in aiuto.
Ad esempio, nell’ultimo periodo ho dovuto modellare un sistema di approvvigionamento di una linea di assemblaggio che utilizza dei robot per portare la materia prima da alcuni magazzini automatici verso le macchine dell’assemblaggio.
Gli attori coinvolti sono:

  • le macchine assemblatrici
  • il sistema che gestisce i robot
  • i magazzini automatici

Le situazioni da controllare sono molte, ad esempio:

  • verificare se è richiesta una nuova cassetta dall’assemblaggio
  • comandare l’uscita di una cassetta da uno dei magazzini automatici
  • verificare se, per una certa macchina assemblatrice, una cassetta sta già uscendo
  • controllare se le uscite dei magazzini automatici sono libere o occupate e scegliere di conseguenza l’uscita dove convogliare la cassetta richiesta
  • comandare un trasferimento al sistema di gestione dei robot
  • verificare lo stato dei trasferimenti pendenti

In un caso come questo, data la complessità, è molto più facile utilizzare una macchina a stati che non utilizzarla.
Questa è la versione semplificata della macchina a stati che ho usato in questa situazione:

macchina_stati_ciclo_pieni

Per implementare via codice una macchina a stati ho utilizzato lo state pattern, qui rappresentato in UML:

state_pattern_uml

In pratica gli stati sono oggetti e la classe Context contiene il puntatore allo stato attuale.
Secondo il diagramma UML di cui sopra, la logica per calcolare le transizioni di stato si trova all’interno dei singoli stati (il metodo goNext di State1 richiama infatti il metodo setState di Context passandogli lo stato successivo State2).
Alternativamente si può mettere questa logica all’interno del contesto.
Il primo metodo è, secondo me, più leggibile, anche se ha lo svantaggio che gli stati si devono conoscere tra loro, in altre parole sono accoppiati.
Il secondo metodo non ha questo problema di accoppiamento, ma rende più difficile seguire il flusso logico della macchina a stati quando si legge il codice.
Inoltre esiste anche in questo caso un accoppiamento, questa volta tra il contesto e gli stati.

Nell’implementazione ho usato il primo metodo.
Mi è venuto comodo identificare il contesto con la macchina a stati: infatti è qualcosa che tiene traccia dello stato corrente e contiene i metodi con cui il client può comandare i passaggi di stato.

Questa è l’interfaccia con cui ho rappresentato il contesto:

public interface IStateMachine 
{ 
	void goNext(); 
	void setState(IState state); 
	IState getState(); 
	void Log(string message); 
} 

Oltre ai metodi goNext() e setState() richiesti dal pattern, ho inserito getState() per poter leggere dal client lo stato attuale, e Log() per ovvi motivi di logging.

Questa è l’implementazione dell’interfaccia:

public class StateMachineAssemblyCycle : IStateMachine
{
    private IState _currentState;
    public StateMachineAssemblyCycle(AssemblyLine assemblyLine, IState startState)
    {
        _assemblyLine = assemblyLine;
        _currentState = startState;
    }
    public void goNext()
    {
        _currentState.goNext(this);
    }
    public void setState(IState state)
    {
        ...
        _currentState = state;
    } 
    public IState getState() { return _currentState; }
    public void Log(string message)
    {
        ... 
    }
}

Lo stato è rappresentato attraverso l’interfaccia IState

public interface IState
{
    void goNext(IStateMachine stateMachine);
}

Questo è il codice relativo ad uno stato specifico, in cui si attende che il robot abbia terminato la missione che gli è stata assegnata.
Sono evidenziate le righe che riguardano i passaggi di stato.

public class StateWaitForRobotMissionConclusion : IState
{
    private static IState _instance; 
    public static IState GetInstance() 
    { 
        if (_instance == null) 
            _instance = new StateWaitForRobotMissionConclusion(); 
        return _instance; 
    } 
    private StateWaitForRobotMissionConclusion() { } 
    public override void goNext(IStateMachine stateMachine) 
    { 
        AssemblyLine assemblyLine = GetAssemblyLine(stateMachine); 
        Request request = assemblyLine.GetRobotRequest(); 
        if (request == null) { 
            stateMachine.Log("Non trovata nessuna richiesta a Robot pendente"); 
            stateMachine.setState(StateErrorRobotMission.GetInstance); 
        } 
        else 
            switch (request.GetRequestStatus()) { 
                case Request.State.Running: { 
                    stateMachine.Log("Richiesta in corso"); 
                    stateMachine.setState(this); break; 
                } 
                case Request.State.Ended: { 
                    stateMachine.Log("Arrivata cassetta " + request.BoxNumber); 
                    if (request.BoxNumberConsistentWithRobotMission()) 
                    { 
                        stateMachine.Log(string.Format("Cassetta {0} coerente con la missione", request.BoxNumber)); 
                        request.AdvanceRobotMissionState(); 
                        stateMachine.setState(StateWaitForBoxEnteringAssemblyLine.GetInstance); 
                    } 
                    else 
                    { 
                        string errorMessage = 
                            assemblyLine.BoxNumberNotRead ? 
                            "Cassetta non letta" : 
                            string.Format("Cassetta non coerente con la missione: attesa {0}, rilevata {1}", 
                                          request.Missione.BoxNumber, 
                                          request.BoxNumber); 
                        stateMachine.Log(errorMessage); 
                        stateMachine.setState(StateErrorBoxNonConsistentWithRobotMission.GetInstance); 
                    } 
                    break; 
                } 
            } 
    }
    ...
}

Ogni stato deve essere istanziato una sola volta: sarebbe inutile e dispendioso in termini di memoria creare più copie dello stesso stato, dal momento che lo stato contiene solo codice da eseguire e non dei dati.
Ho risolto questa problematica rendendo lo stato un singleton: il costruttore è privato e il metodo GetInstance() fornisce sempre lo stesso oggetto registrato in una variabile static della classe (si noti che questa implementazione del singleton non è thread-safe, quindi non è adatta nel multi-threading).

Questo, infine, è il codice del client (il controller della macchina assemblatrice):

class AssemblyLine
{
    private IStateMachine _smCycle;
    public AssemblyLine(...)
    {
        _smCycle = new StateMachineAssemblyCycle(this, StateWaitingForCycle.GetInstance());
        //[...]
    }
    public void DoCycle()
    {
        //[...]
        if (isOk)
            _smCycle.goNext();
    }
    //[...]
}

Su Wikipedia si possono trovare molte altre informazioni sullo state pattern.
Il testo di riferimento è Design Patterns della Gang of Four.

 

Clean Code

Clean Code è un libro che ogni sviluppatore dovrebbe leggere. In questo articolo riporto, a grandi linee, alcuni degli importanti concetti espressi nel libro.

Clean Code è stato scritto da Rober Martin (noto anche come Uncle Bob) e alcuni coautori.
È un libro che ogni sviluppatore dovrebbe leggere, magari dopo aver lavorato per qualche tempo.
Personalmente vorrei averlo letto prima, perché mi ha aiutato, nel tempo, a cambiare il mio modo di scrivere codice, permettendomi, di realizzare prodotti migliori faticando di meno.

In questo articolo riporto, a grandi linee, alcuni degli importanti concetti espressi nel libro.

All’inizio l’autore descrive una situazione che tutti abbiamo sperimentato e cioè che, man mano che un programma cresce, diventa sempre più complesso e disordinato, e ogni nuova modifica impiega un tempo sempre maggiore per essere portata a termine.
Questa situazione è conosciuta come code entropy, e dipende ma molti fattori, tra cui:

  • Codice scritto velocemente perché deve andare in produzione in fretta
  • Codice scritto da più persone che non comunicano bene tra loro
  • Decisioni facili al momento ma non sostenibili quando il progetto cresce di dimensioni (debito tecnologico)

Per evitare questa situazione occorre una continua attenzione alla pulizia del codice, con frequenti refactoring laddove il programma sia disordinato e poco leggibile.

I primi capitoli puntano l’attenzione sullo stile: molto spazio viene dedicato alla nomenclatura di variabili, metodi e classi, perché la leggibilità del codice è fondamentale. Quindi occorre evitare nomi troppo corti, non esplicativi, non legati al dominio del progetto, o con prefissi inutili.
Si parla anche di indentazione e di spaziatura: sembrano cose banali, ma permettono di migliorare la leggibilità del codice.

Interessante anche discorso sui commenti: Martin sostiene che i commenti dovrebbero essere utilizzati solo per dichiarare l’intento, avvisare sull’uso di certe funzionalità o amplificare il significato di qualche parte del codice a cui, altrimenti, non si darebbe la dovuta importanza.
Negli altri casi i commenti andrebbero evitati, perché il codice dovrebbe essere auto esplicativo: utilizzando la corretta nomenclatura per variabili, metodi e classi, la maggior parte dei commenti diventerebbero ridondanti, quindi inutili.

Particolare enfasi viene data a come scrivere le funzioni: il mantra è “una funzione dovrebbe fare una sola cosa“, quindi se una funzione fa più di una cosa dovrebbe essere spezzata; inoltre, una funzione dovrebbe riferirsi ad un solo livello di astrazione e dovrebbe avere il minimo numero possibile di parametri (meglio se nessuno).

Ci sono tantissimi consigli su come scrivere dei metodi il più possibile espressivi e comprensibili: leggendo il codice si dovrebbe riuscire a capire il flusso del programma senza essere obbligati ad entrare troppo nel dettaglio.
I metodi dovrebbero essere inoltre apparire nel sorgente per livello di astrazione decrescente: dopo una funzione dovrebbero seguire quelle di livello inferiore da essa utilizzate, e così via.

Le idee espresse sulle funzioni vengono sviluppate per le classi, che pure dovrebbero essere il più piccole possibile. Per capire se e quando dobbiamo spezzare una classe in classi più piccole, possiamo provare a descrivere che cosa fa quella classe: se nella descrizione c’è almeno una congiunzione “e”, allora probabilmente la classe va spezzata.
Altro metodo è quello del valutare la coesione dei metodi: quest’ultima è tanto maggiore quanto maggiore è il numero di variabili di istanza utilizzate da un metodo. Se una funzione ne usa poche, probabilmente non c’entra molto con quella classe e andrebbe messa in una classe a parte.

Nella scrittura delle classi è cruciale il Single Responsibility Principle (SRP), in base al quale un modulo dovrebbe avere una sola ragione per cambiare. Questo concetto è strettamente legato alla coesione. L’autore fa un interessante esempio in cui una classe per la generazione di numeri primi viene spezzata in più classi ad elevata coesione.

Altro principio estremamente importante, che serve per isolare le classi dai cambiamenti esterni, è il Dependency Inversion Principle (DIP): questo afferma che per isolare una classe dai cambiamenti, essa dovrebbe dipendere non da classi concrete (implementazioni), ma da astrazioni (interfacce). La dipendenza dovrebbe poi essere “iniettata” dall’esterno (Dependency Injection).

Il capitolo sulla gestione degli errori contiene interessanti consigli su come affrontare queste delicata materia. Si ricollega alla scrittura dei metodi il fatto che la gestione degli errori non dovrebbe essere mescolata al codice vero e proprio: infatti una funzione deve fare una cosa sola, e la gestione degli errori è appunto una cosa. In questo modo le funzioni che descrivono gli algoritmi restano pulite e separate dal resto.

I margini, i confini delle nostre applicazioni sono i punti in cui utilizziamo componenti di terze parti (framework e librerie varie). Secondo Uncle Bob questi punti sono da controllare e difendere accuratamente, perché dobbiamo evitare di dipendere troppo da componenti esterni. Quindi una buona pratica è quella di racchiudere le parti esterne dentro dei wrapper, in modo da isolarle dal resto del nostro codice. In questo modo, per sostituire i componenti esterni dovremo soltanto intervenire sui wrapper, mentre il resto del nostro codice sarà al sicuro.

L’autore parla diffusamente dei test: questi sono essenziali non solo per assicurarci che le nostre applicazioni funzionino correttamente, ma anche per permetterci di eseguire in sicurezza le modifiche evolutive, le correzioni e i necessari refactoring.

Egli pone inoltre l’accento sul fatto che il codice di test deve essere pulito come il codice di produzione: infatti, se non adeguatamente curato, anche il codice di test tende a corrompersi e a diventare ingestibile; a questo punto non possiamo più utilizzarlo e i test perdono lo loro utilità.
Gli unici “peccati” che possiamo compiere con il codice di test riguardano l’uso della memoria e le prestazioni.

Il concetto di clean code si può applicare anche a livello più alto, cioè a livello sistema, o architetturale. Un sistema è un’interconnessione di componenti che collaborano tra loro.
Il punto chiave per ottenere un’architettura pulita è mantenere separate le responsabilità: per esempio è molto utile separare la costruzione del sistema (cioè la generazione degli oggetti che lo compongono) dall’uso degli stessi.

Il meccanismo di base per ottenere separazione di responsabilità è la Dependency Injection (strettamente legata all’Inversion of Control).

Esistono framework che servono di costruire sistemi sulla base di questi principi. Uno di questi è Spring, che permette anche un elevato livello di disaccoppiamento del codice dal framework stesso: infatti non occorre derivare le classi da un oggetto base del framework e bastano poche righe per generare gli oggetti attraverso le classi e i metodi del framework (queste righe possono essere facilmente isolate e modificate nel caso si decida di cambiare framework).

Successivamente l’autore parla di multithreading, che, in alcuni casi, permette di migliorare le prestazioni: questo è vero solo quando ci sono tempi di attesa che si possono sfruttare per interrompere un thread e farne partire un altro (questo, ad esempio, si verifica se sono coinvolte connessioni di rete, oppure, in misura maggiore, quando c’è interazione con qualche utente).

Il multithreading non si può improvvisare, altrimenti si incorre in comportamenti inspiegabili e imprevedibili. Allora occorre progettare i programmi in modo accurato, dividendo il codice che gestisce la concorrenza dall’altro codice, riducendo al minimo il numero di oggetti condivisi e limitandone lo scope e rendendo i thread il più possibile indipendenti.
Occorre inoltre conoscere molto bene le librerie che si utilizzano, prediligendo le classi thread-safe, e soprattutto bisogna conoscere bene gli algoritmi coinvolti.

Fatto questo occorre eseguire i test in modo molto accurato e mirato: il codice normale (non di multithreading) deve essere testato a parte, mentre quello che gestisce il multithreading deve essere stressato in modo da far emergere eventuali debolezze, cercando di forzare il più possibile il passaggio da un thread all’altro (esistono strumenti che permettono di automatizzare queste operazioni).

Un’ampia sezione del libro è dedicata all’analisi di pezzi di codice e al refactoring, per ripulirlo e migliorarlo. Sono capitoli impegnativi, ma vale davvero la pena di analizzare il codice e il modo in cui viene modificato per renderlo migliore. L’autore ha raggruppato una serie di regole che servono per individuare le code smells (punti in cui il codice è disordinato, poco comprensibile, male organizzato, ecc.) e applicare i correttivi necessari: queste regole hanno un codice, che viene richiamato laddove vengono applicate al codice.

Personalmente ho trovato molto utile analizzare i miei programmi con davanti questo elenco di regole, e cercare di applicarle una dopo l’altra (almeno le più importanti): i risultati sono sorprendenti.

A questo punto, non mi resta che augurarvi buona lettura.