W3docs

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:

LetteraPrincipioObiettivo in una riga
SSingle ResponsibilityUna classe dovrebbe avere un solo motivo per cambiare
OOpen/ClosedAperta all'estensione, chiusa alla modifica
LLiskov SubstitutionI sottotipi devono essere utilizzabili ovunque sia previsto il loro tipo base
IInterface SegregationMolte interfacce piccole sono meglio di una grande
DDependency InversionDipendi 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 NotifierAlertService 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.

java— editable, runs on the server

Cosa trarre dall'esecuzione:

  • email sent: [EMAIL -> alice: 3 error(s) detected] contiene solo una voce — bob aveva zero errori quindi raise non ha inviato nulla. AlertService ha l'unica responsabilità di decidere quando avvisare (SRP); non costruisce mai il corpo del messaggio né apre una connessione.
  • La stessa classe AlertService ha gestito sia un EmailNotifier che un SmsNotifier perché la dipendenza veniva passata tramite il suo costruttore (DIP). La logica di alerting di alto livello dipende solo dall'interfaccia Notifier, mai da un mittente concreto.
  • OCP check : ... unchanged = true conferma che entrambi gli oggetti di alert sono la stessa classe AlertService: aggiungere il supporto SMS ha significato scrivere un nuovo SmsNotifier, con zero modifiche ad AlertService — aperta all'estensione, chiusa alla modifica.
  • ISP check : is Writable? false mostra che ConfigFile implementa solo Readable. Poiché le interfacce sono segregate, la sorgente di sola lettura non è mai stata costretta a fornire uno stub write privo di significato.
  • LSP area : 9.142 è la somma di un rettangolo 2×3 (6.0) e di un cerchio con raggio 1 (≈3.142). totalArea ha iterato su riferimenti Shape e chiamato area() senza verificare quale sottotipo fosse — ogni sottotipo era sostituibile al suo tipo base (LSP).

Esercitati

Pratica
Una classe chiamata ReportGenerator sia formatta i dati del report sia li scrive su disco, quindi qualsiasi modifica alle regole di formattazione o al layout del file ti obbliga a modificare e ritestare la stessa classe. Quale principio SOLID viola più direttamente?
Una classe chiamata ReportGenerator sia formatta i dati del report sia li scrive su disco, quindi qualsiasi modifica alle regole di formattazione o al layout del file ti obbliga a modificare e ritestare la stessa classe. Quale principio SOLID viola più direttamente?
Was this page helpful?