Principi SOLID in Java
Applica i principi SOLID — SRP, OCP, LSP, ISP, DIP — alla progettazione Java.
SOLID è un insieme di cinque principi di progettazione orientata agli oggetti — reso popolare da Robert C. Martin — che mantengono il codice Java facile da modificare, testare ed estendere man mano che cresce. Non sono regole sintattiche imposte dal compilatore; sono linee guida per dove tracciare i confini tra le classi in modo che una singola modifica non si propaghi attraverso tutto il codebase. L'acronimo sta per Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation e Dependency Inversion.
Questi principi si basano sui fondamenti orientati agli oggetti: vedrai interfacce, ereditarietà e polimorfismo utilizzati ovunque. Se questi concetti non sono ancora chiari, rivedili prima — SOLID è essenzialmente buon giudizio su dove applicarli.
I cinque principi in sintesi
Ogni lettera affronta uno specifico tipo di problema di progettazione. Tieni questa tabella a portata di mano mentre leggi il resto del capitolo:
| Lettera | Principio | Obiettivo in una riga |
|---|---|---|
| S | Single Responsibility | Una classe dovrebbe avere un solo motivo per cambiare |
| O | Open/Closed | Aperta all'estensione, chiusa alla modifica |
| L | Liskov Substitution | I sottotipi devono essere utilizzabili ovunque sia previsto il loro tipo base |
| I | Interface Segregation | Molte interfacce piccole sono meglio di una grande |
| D | Dependency Inversion | Dipendi dalle astrazioni, non dalle classi concrete |
I principi si rafforzano a vicenda. Nel codice ben strutturato raramente se ne applica solo uno — una piccola interfaccia (ISP) da cui dipende il codice di alto livello (DIP) è esattamente ciò che consente di aggiungere una nuova implementazione (OCP) senza toccare il chiamante.
S — Single Responsibility Principle
Una classe dovrebbe fare una cosa sola e avere un solo motivo per cambiare. Quando preoccupazioni non correlate — regole di business e consegna dei messaggi, ad esempio — condividono una classe, una modifica a qualsiasi delle due ti obbliga a ritestare entrambe. Separarle isola il cambiamento.
// Mixes WHEN to alert with HOW to deliver -- two reasons to change.
class BadAlertService {
void raise(String user, int errors) {
if (errors > 0) {
// ...build an email, open an SMTP connection, send...
}
}
}
// One responsibility: deciding when to alert. Delivery lives elsewhere.
class AlertService {
private final Notifier notifier;
AlertService(Notifier notifier) { this.notifier = notifier; }
void raise(String user, int errors) {
if (errors > 0) notifier.send(user, errors + " error(s) detected");
}
}O — Open/Closed Principle
Le entità software dovrebbero essere aperte all'estensione ma chiuse alla modifica. Dovresti essere in grado di aggiungere nuovi comportamenti scrivendo nuovo codice, non modificando — e rischiando — codice che già funziona. In Java lo strumento abituale è un'interfaccia stabile con nuove implementazioni.
interface Notifier { void send(String to, String message); }
class EmailNotifier implements Notifier { /* ... */ }
class SmsNotifier implements Notifier { /* ... */ } // new feature = new class
// AlertService never changes when a new channel appears.Aggiungere le notifiche push in seguito significa scrivere PushNotifier implements Notifier — AlertService rimane invariato, quindi non richiede revisione né comporta rischi di regressione.
L — Liskov Substitution Principle
Se S è un sottotipo di T, allora gli oggetti di tipo T possono essere sostituiti con oggetti di tipo S senza interrompere il programma. Una sottoclasse deve rispettare il contratto del suo genitore — stesse aspettative, nessuna eccezione inattesa, nessuna precondizione più restrittiva.
abstract class Shape { abstract double area(); }
class Rectangle extends Shape { /* area() = w * h */ }
class Circle extends Shape { /* area() = PI * r * r */ }
// Works for ANY Shape, present or future, without inspecting the concrete type.
double totalArea(List<Shape> shapes) {
return shapes.stream().mapToDouble(Shape::area).sum();
}La classica violazione è Square extends Rectangle: se impostare la larghezza modifica anche l'altezza, il codice scritto per un Rectangle si rompe quando gli viene passato uno Square. La soluzione è modellarli come fratelli sotto Shape, non come coppia padre-figlio. (Vedi classi astratte per la base Shape utilizzata qui.)
I — Interface Segregation Principle
I client non dovrebbero essere costretti a dipendere da metodi che non utilizzano. Preferisci diverse interfacce piccole e mirate rispetto a una grande — altrimenti un'implementazione viene trascinata a stub di metodi che non può rispettare.
// Fat interface: a read-only source is forced to implement write().
interface Storage { String read(); void write(String data); }
// Segregated: implement only what you can honor.
interface Readable { String read(); }
interface Writable { void write(String data); }
class ConfigFile implements Readable { // no empty write() stub
public String read() { return "mode=prod"; }
}D — Dependency Inversion Principle
I moduli di alto livello non dovrebbero dipendere dai moduli di basso livello; entrambi dovrebbero dipendere dalle astrazioni. In pratica: programma contro le interfacce e inietta l'implementazione concreta (l'injection tramite costruttore è la forma più semplice). È questo che fa sì che gli altri principi ripagino — e che rende una classe testabile, poiché puoi passare un oggetto finto.
// AlertService depends on the Notifier interface, not EmailNotifier.
AlertService alerts = new AlertService(new EmailNotifier());
// In a test, inject a fake Notifier and assert on what it recorded.Un esempio pratico: tutti e cinque in un unico programma
Questo programma collega i principi insieme — un singolo AlertService (SRP) comunica con un Notifier iniettato (DIP), passa da un EmailNotifier a un SmsNotifier senza modifiche (OCP), legge un ConfigFile solo Readable (ISP) e somma le aree tra i sottotipi di Shape uniformemente (LSP). Verifica i propri risultati così puoi vedere ogni principio in azione.
Cosa trarre dall'esecuzione:
email sent: [EMAIL -> alice: 3 error(s) detected]contiene solo una voce —bobaveva zero errori quindiraisenon ha inviato nulla.AlertServiceha l'unica responsabilità di decidere quando avvisare (SRP); non costruisce mai il corpo del messaggio né apre una connessione.- La stessa classe
AlertServiceha gestito sia unEmailNotifierche unSmsNotifierperché la dipendenza veniva passata tramite il suo costruttore (DIP). La logica di alerting di alto livello dipende solo dall'interfacciaNotifier, mai da un mittente concreto. OCP check : ... unchanged = trueconferma che entrambi gli oggetti di alert sono la stessa classeAlertService: aggiungere il supporto SMS ha significato scrivere un nuovoSmsNotifier, con zero modifiche adAlertService— aperta all'estensione, chiusa alla modifica.ISP check : is Writable? falsemostra cheConfigFileimplementa soloReadable. Poiché le interfacce sono segregate, la sorgente di sola lettura non è mai stata costretta a fornire uno stubwriteprivo di significato.LSP area : 9.142è la somma di un rettangolo 2×3 (6.0) e di un cerchio con raggio 1 (≈3.142).totalAreaha iterato su riferimentiShapee chiamatoarea()senza verificare quale sottotipo fosse — ogni sottotipo era sostituibile al suo tipo base (LSP).