SOLID: principi per codice che invecchia bene 🧱
Perché SOLID? 🤔
Quando un progetto cresce, il codice tende a diventare un condominio con fili elettrici volanti, tubi rumorosi e vicini che litigano sulle dipendenze. I principi SOLID sono le regole del buon vicinato: niente magia, solo buone pratiche che rendono il software più semplice da estendere, testare e mantenere.
In breve: meno accoppiamento, più coesione, dipendenze più sane. Risultato? Refactoring meno dolorosi e feature che entrano senza demolire mezzo edificio.
SRP: responsabilità singola 🧹
Il principio di responsabilità singola dice che un modulo dovrebbe avere una sola ragione per cambiare. Tradotto: evita le classi “coltellino svizzero”.
- In pratica: separa orchestrazione, dominio e I/O. Una classe che valida dati non dovrebbe anche inviare email e scrivere su database.
- Segnali d’allarme: metodi con nomi eterogenei, dipendenze a cascata, test che devono istanziare “mezza città”.
- Refactoring utile: estrai servizi dedicati, usa composizione, introduci adapter per I/O e mantieni il core di dominio puro.
Benefici: test più piccoli, cambi locali e meno regressioni.
Esempio TypeScript 🟦
|
|
Esempio Go 🟩
|
|
OCP: aperto/chiuso 🚪
“Open for extension, closed for modification”: il comportamento si estende senza modificare il codice esistente.
- In pratica: programma su astrazioni; usa strategie, plugin, eventi, feature flag. Aggiungi nuovi comportamenti creando nuove implementazioni, non toccando quelle vecchie.
- Segnali d’allarme: switch/if su tipi sparsi ovunque, classi che cambiano ogni volta che aggiungi un caso.
- Refactoring utile: Strategy/Factory/Template Method, polimorfismo, registri di handler, configurazioni dichiarative.
Benefici: minor rischio di rompere il passato, rilascio di estensioni indipendenti.
Esempio TypeScript 🟦
|
|
Esempio Go 🟩
|
|
LSP: sostituzione di Liskov 🔁
I sottotipi devono poter sostituire i loro base type senza sorprendere nessuno. Se un figlio rompe le assunzioni del padre, c’è un problema di modellazione.
- In pratica: rispetta pre/post-condizioni e invarianti; non “specializzare” rompendo il contratto.
- Segnali d’allarme: override che lancia eccezioni “non supportato”, metodi che cambiano semantica o requisiti.
- Refactoring utile: rivedi la gerarchia; considera composizione > ereditarietà; spezza responsabilità in interfacce più piccole.
Benefici: polimorfismo affidabile, test più semplici e meno rami condizionali.
Esempio TypeScript 🟦
|
|
Esempio Go 🟩
|
|
ISP: segregazione delle interfacce ✂️
I client non devono dipendere da metodi che non usano. Le interfacce “gonfie” costringono le implementazioni a comportamenti innaturali.
- In pratica: preferisci interfacce piccole e coerenti; separa comandi da query; evita “manager” onniscienti.
- Segnali d’allarme: metodi no-op, eccezioni “non implementato”, mock complicatissimi.
- Refactoring utile: estrai interfacce per capability (es. Reader/Writer), adotta CQRS dove ha senso, usa adapter per scenari legacy.
Benefici: accoppiamento ridotto e test più dichiarativi.
Esempio TypeScript 🟦
|
|
Esempio Go 🟩
|
|
Nota: per semplicità didattica qui Reader/Writer usano string; nella stdlib Go io.Reader / io.Writer gestiscono stream di byte e errori.
DIP: inversione delle dipendenze 🔌
I moduli di alto livello non dovrebbero dipendere dai dettagli di basso livello: entrambi dipendono da astrazioni. E le astrazioni non dovrebbero dipendere dai dettagli.
- In pratica: definisci contratti nel dominio; implementazioni concrete vivono ai bordi (I/O, framework, DB). Usa dependency injection (anche manuale!), container solo dove serve.
- Segnali d’allarme: new sparsi ovunque, funzioni che conoscono la tecnologia di storage, logica di business che importa SDK esterni.
- Refactoring utile: introduci porte/adapter, costruttori che accettano interfacce, factory per risorse costose, inversion of control per orchestrazione.
Benefici: testabilità, sostituibilità dei dettagli, migrazioni tecnologiche meno traumatiche.
Esempio TypeScript 🟦
|
|
Esempio Go 🟩
|
|
Metterli insieme nel mondo reale 🧩
Un flusso sano spesso segue questo schema:
- Modella il dominio con entità e servizi coesi (SRP).
- Espandi i comportamenti tramite strategie/handler configurabili (OCP).
- Evita gerarchie profonde, preferisci composizione e contratti chiari (LSP/ISP).
- Tieni i dettagli alle periferie e dipendi da interfacce (DIP).
Suggerimento: struttura a strati (domain → application → infrastructure) o esagonale. Il core non importa framework; gli adapter sì.
Checklist veloce ✅
- Una classe ha una sola ragione di cambiare? (SRP)
- Posso aggiungere un comportamento senza toccare codice esistente? (OCP)
- I sottotipi rispettano il contratto del tipo base? (LSP)
- Le interfacce sono piccole e coerenti con i client reali? (ISP)
- Il dominio dipende da astrazioni, non da dettagli o framework? (DIP)
Se hai risposto “sì” cinque volte, probabilmente dormirai meglio stanotte.