Published on

SOLID per i comuni mortali

Authors

Se hai lavorato almeno qualche ora con me, allora avrò sicuramente menzionato i principi SOLID. Li tratto come una specie di Bibbia da seguire per scrivere del buon codice.

Ma la domanda sorge spontanea: cos'è un "buon codice"? Come posso distinguere una codebase buona da una che fa cagare? Questa domanda all'apparenza semplice si rivela in realtà di difficile risposta. La "bellezza" e la "leggibilità" della codebase sono parametri fin troppo soggettivi per essere considerati in una misura di questa natura.

Come valuto una codebase, allora? Semplice: più è facile sviluppare nuove funzionalità senza introdurre regressioni (ossia bug dovuti alle nuove aggiunte e modifiche), più la codebase è di livello elevato.

Idealmente, un team di sviluppatori dovrebbe lavorare esclusivamente sulle feature del prodotto. Introdurne di nuove, modificarne le esistenti sulla base dell'evoluzione dei sempre malleabili requisiti del cliente, eccetera... è uno dei requisiti fondamentali per essere AGILE.

Nel volgo, questo parametro di "pulizia" talvolta prende il nome di manutenibilità del software, ma secondo me è molto più di questo. "Manutenzione" non rende giustizia al concetto, poiché sembra quasi un afterthought, qualcosa di pensato dopo per migliorare la vita di chi dovrà star dietro al nostro codice. Ma così è troppo riduttivo. Si tratta, invece, della vera essenza del ragionamento da ingegnere del software quando posto dinanzi a un problema.

Ma, si sa, tra il dire e il fare... i bug sono all'ordine del giorno, e la maggior parte di questi sono, indovinate un po', regressioni. Allora come possiamo fare noi sviluppatori ad evitare quanto più possibile questi fastidiosi problemi e concentrarci sul miglioramento del prodotto?

È proprio qui che entrano in gioco i principi SOLID. E, in questa caldissima (veramente calda) serata di Luglio, sono qui a spiegarveli in maniera semplice, come la spiegherei ad un collega in ufficio. Cominciamo.

Come nasce SOLID?

I principi SOLID sono 5 linee guida da seguire per migliorare la qualità del codice. Sono nati nel contesto della OOP (Object Oriented Programming) da Uncle Bob, un vero colosso della programmazione, e uno dei miei punti di riferimento assieme a Martin Fowler (raccomando di leggere i suoi articoli perché sono fottute miniere d'oro).

Andiamo a sviscerarli uno ad uno, così è più chiaro:

S - Single Responsibility Principle 👨‍⚕️👨‍🔧👨‍🍳👨‍🏭

In assoluto il principio più importante e fondamentale di tutti (non per altro è il primo). Afferma che una classe (o una funzione in contesto funzionale) deve avere una ed una sola responsabilità.

Qualora si dovessero presentare funzionalità diverse relative a contesti diversi dello stesso dominio, va pensata l'introduzione di una nuova classe o funzione in grado di incapsulare entrambe le funzionalità, tenendole singolarmente quanto più separate possibile.

Potrà sembrare scontato, ma questo principio è difficilissimo da applicare nella pratica. Esiste tuttavia un trucco per capire se il codice che stiamo scrivendo rispetta o no questo principio. Basta porsi la seguente domanda: "Questa classe/funzione ha una e una sola ragione per cambiare?". Se sì, stiamo rispettando il principio, altrimenti non lo stiamo rispettando.

Già solo seguire questo principio aumenta immensamente la flessibilità della codebase!

O - Open/Closed Principle 🔒🔓

Questo principio afferma che le classi/funzioni devono essere aperte alle estensioni (open) e chiuse alle modifiche (closed). Che cosa significa?

Significa che una volta ultimato un modulo o una funzionalità, dovremmo cercare di evitare di andare a modificare direttamente il suo codice sorgente per espanderla. Dovremmo, invece, pianificare il codice in maniera tale che sia facile "estendere" le funzionalità senza stravolgere tutto ogni volta.

Questo è molto importante in ambienti di produzione: per ridurre al minimo la dipendenza ossessiva dai test (che, si sa, sono purtroppo molto difficili da scrivere efficaciemente e da mantenere) occorre ridurre al minimo le modifiche al codice originale, se non per risolvere bug evidenti.

Tuttavia, ciò è possibile solo se il codice originale è predisposto a questo tipo di interazioni, dunque deve fare ampio utilizzo di interfaces (e di conseguenza architetturato seguendo il principio di composition over inheritance per semplificare la gestione delle dipendenze). Pensare in soli termini di input/output aiuta molto a rispettare questo principio.

Se, invece, dobbiamo andare necessariamente ad interpellare l'ereditarietà, allora è fondamentale seguire a modo il prossimo principio:

L - Liskov Substitution Principle 🧔👥

Il principio di sostituzione di Liskov afferma che le sottoclassi devono essere in grado di sostituire pienamente le loro classi padre, senza alterare in alcun modo il comportamento del programma.

Questo è un altro di quei principi difficilissimi da seguire, poiché (vi chiederete) che senso ha l'ereditarietà se non posso espandere il comportamento di una super-classe?

La chiave di volta è pensare in termini di "contratto". Se una classe rispetta un determinato contratto, allora anche tutte le sotto-classi devono rispettarlo. Altrimenti non devono essere una sotto-classe.

Come è possibile notare, i principi SOLID non sono semplici concetti puramente teorici, ma sono immediatamente applicabili nella pratica! Per questo mi piacciono moltissimo.

I - Interface Segregation Principle 📦⛓️🤝

Il principio afferma che una classe che implementa un'interfaccia non dovrebbe mai dipendere da metodi che non è in grado di implementare (o che non usa). In soldoni significa che è vietato scrivere interfacce enormi con un sacco di metodi (cosa che di solito i Service di dominio fanno spesso e volentieri), ma è meglio introdurre tante interfacce diverse, ognuna che adempie ad un compito specifico (si torna al primo principio). Lo scopo è evitare che i metodi inizino a dipendere troppo da loro, innescando un pericoloso ciclo di spaghettification della codebase.

RICORDARE A VITA: l'obiettivo è sempre il decoupling! Dividere, dividere, dividere! E sempre ragionando in termini di interfacce (input/output).

Ragionare in questo modo permette anche di dividere gli applicativi in microservizi molto agevolmente. In pratica se non si segue questo principio è impossibile "disaccopiare" pezzi di un monolite.

D - Dependency Inversion Principle 💉

Questo è importantissimo, ma a memoria lo ricordo solo in inglese: "A class should always depend on abstractions rather than concrete implementations".

Significa che una classe deve sempre dipendere dalle astrazioni. Ossia? Banalmente, le dipendenze di una classe (ossia ciò di cui la classe ha bisogno per funzionare) devono essere interfacce astratte, implementate altrove.

In praticamente ogni linguaggio esiste un modo per ottenere esattamente questo (in TypeScript/Java/Go tramite le interfaces, in Rust tramite i traits). In Spring è presente un intero framework di dependency injection sottostante atto a "iniettare" le implementazioni delle classi all'interno di altre classi. L'idea alla base è quella di rendere semplice il mocking allo scopo di migliorare la testabilità della codebase, ma i vantaggi sono molteplici anche in altri campi, specie se sono stati applicati anche tutti gli altri principi.

Un po' come i pezzi di Exodia o le sette sfere del drago, se si riesce a mettere assieme tutti questi principi, allora si diventa fortissimi!

Concludendo...

Ci sono veramente tantissimi articoli e discussioni riguardanti questi principi (il quesito su quando non applicarli, ad esempio, è ancora ampiamente acceso). Vi basterà una veloce ricerca sul web per approfondirli ulteriormente, e vi consiglio vivamente di farlo! Ecco alcune risorse per iniziare:

Consiglio di iniziare ad applicare i principi già da subito per internalizzarli, allo scopo di migliorare vertiginosamente la qualità del tuo codice!

Alla prossima!