I Principi SOLID: Le Fondamenta dell'Architettura Software
I principi SOLID sono cinque linee guida fondamentali per scrivere codice orientato agli oggetti di qualità. Introdotti da Robert C. Martin (Uncle Bob), questi principi aiutano a creare software manutenibile, estensibile e testabile. SOLID è un acronimo che rappresenta: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation e Dependency Inversion.
🎯 I 5 Principi SOLID
- S - Single Responsibility Principle (SRP)
- O - Open/Closed Principle (OCP)
- L - Liskov Substitution Principle (LSP)
- I - Interface Segregation Principle (ISP)
- D - Dependency Inversion Principle (DIP)
1. Single Responsibility Principle (SRP)
"Una classe dovrebbe avere un solo motivo per cambiare"
Ogni classe deve avere una singola responsabilità ben definita. Questo rende il codice più facile da capire, testare e modificare.
❌ Violazione SRP:
class User {{ '{' }}
constructor(
public name: string,
public email: string
) {{ '{' }}{{ '}' }}
// Responsabilità 1: Validazione
validateEmail(): boolean {{ '{' }}
return /\S+@\S+\.\S+/.test(this.email);
{{ '}' }}
// Responsabilità 2: Persistenza
save(): void {{ '{' }}
// Salva nel database
console.log("Salvato nel DB");
{{ '}' }}
// Responsabilità 3: Notifiche
sendWelcomeEmail(): void {{ '{' }}
console.log(`Email a ${{ '{' }}this.email{{ '}' }}`);
{{ '}' }}
{{ '}' }}
✅ Rispetta SRP:
// Una responsabilità per classe
class User {{ '{' }}
constructor(
public name: string,
public email: string
) {{ '{' }}{{ '}' }}
{{ '}' }}
class UserValidator {{ '{' }}
validateEmail(email: string): boolean {{ '{' }}
return /\S+@\S+\.\S+/.test(email);
{{ '}' }}
{{ '}' }}
class UserRepository {{ '{' }}
save(user: User): void {{ '{' }}
console.log("Salvato nel DB");
{{ '}' }}
{{ '}' }}
class EmailService {{ '{' }}
sendWelcome(user: User): void {{ '{' }}
console.log(`Email a ${{ '{' }}user.email{{ '}' }}`);
{{ '}' }}
{{ '}' }}
2. Open/Closed Principle (OCP)
"Le classi dovrebbero essere aperte all'estensione ma chiuse alla modifica"
Dovresti poter aggiungere nuove funzionalità estendendo il codice esistente, non modificandolo. Usa astrazioni (interfacce, classi astratte) per permettere estensioni.
class DiscountCalculator {{ '{' }}
calculate(type: string, amount: number): number {{ '{' }}
// Ogni nuovo tipo richiede modifica di questa classe!
if (type === 'student') {{ '{' }}
return amount * 0.9; // 10% sconto
{{ '}' }} else if (type === 'senior') {{ '{' }}
return amount * 0.85; // 15% sconto
{{ '}' }} else if (type === 'vip') {{ '{' }}
return amount * 0.8; // 20% sconto
{{ '}' }}
return amount;
{{ '}' }}
{{ '}' }}
// Interfaccia per estensione
interface DiscountStrategy {{ '{' }}
apply(amount: number): number;
{{ '}' }}
class StudentDiscount implements DiscountStrategy {{ '{' }}
apply(amount: number): number {{ '{' }}
return amount * 0.9;
{{ '}' }}
{{ '}' }}
class SeniorDiscount implements DiscountStrategy {{ '{' }}
apply(amount: number): number {{ '{' }}
return amount * 0.85;
{{ '}' }}
{{ '}' }}
class VIPDiscount implements DiscountStrategy {{ '{' }}
apply(amount: number): number {{ '{' }}
return amount * 0.8;
{{ '}' }}
{{ '}' }}
// Chiusa alla modifica, aperta all'estensione
class DiscountCalculatorOCP {{ '{' }}
calculate(strategy: DiscountStrategy, amount: number): number {{ '{' }}
return strategy.apply(amount);
{{ '}' }}
{{ '}' }}
// Aggiungere nuovo sconto = creare nuova classe, non modificare esistente
const calc = new DiscountCalculatorOCP();
console.log(calc.calculate(new StudentDiscount(), 100)); // 90
console.log(calc.calculate(new VIPDiscount(), 100)); // 80
3. Liskov Substitution Principle (LSP)
"Gli oggetti di una sottoclasse devono poter sostituire gli oggetti della classe base senza alterare il comportamento corretto del programma"
Le sottoclassi devono estendere il comportamento della classe base, non cambiarlo in modi che rompano le aspettative.
class Rectangle {{ '{' }}
constructor(protected width: number, protected height: number) {{ '{' }}{{ '}' }}
setWidth(width: number): void {{ '{' }}
this.width = width;
{{ '}' }}
setHeight(height: number): void {{ '{' }}
this.height = height;
{{ '}' }}
getArea(): number {{ '{' }}
return this.width * this.height;
{{ '}' }}
{{ '}' }}
// Viola LSP: Square non può sostituire Rectangle
class Square extends Rectangle {{ '{' }}
setWidth(width: number): void {{ '{' }}
this.width = width;
this.height = width; // Effetto collaterale inaspettato!
{{ '}' }}
setHeight(height: number): void {{ '{' }}
this.width = height;
this.height = height; // Effetto collaterale inaspettato!
{{ '}' }}
{{ '}' }}
function increaseRectangleWidth(rect: Rectangle): void {{ '{' }}
rect.setWidth(rect.getArea() + 10);
// Aspettativa: solo la larghezza cambia
// Realtà con Square: anche l'altezza cambia!
{{ '}' }}
// Usa composizione invece di ereditarietà problematica
interface Shape {{ '{' }}
getArea(): number;
{{ '}' }}
class RectangleLSP implements Shape {{ '{' }}
constructor(private width: number, private height: number) {{ '{' }}{{ '}' }}
setWidth(width: number): void {{ '{' }}
this.width = width;
{{ '}' }}
setHeight(height: number): void {{ '{' }}
this.height = height;
{{ '}' }}
getArea(): number {{ '{' }}
return this.width * this.height;
{{ '}' }}
{{ '}' }}
class SquareLSP implements Shape {{ '{' }}
constructor(private side: number) {{ '{' }}{{ '}' }}
setSide(side: number): void {{ '{' }}
this.side = side;
{{ '}' }}
getArea(): number {{ '{' }}
return this.side * this.side;
{{ '}' }}
{{ '}' }}
// Funziona con qualsiasi Shape senza sorprese
function printArea(shape: Shape): void {{ '{' }}
console.log(`Area: ${{ '{' }}shape.getArea(){{ '}' }}`);
{{ '}' }}
4. Interface Segregation Principle (ISP)
"I client non dovrebbero essere forzati a dipendere da interfacce che non usano"
Meglio avere interfacce piccole e specifiche che interfacce grandi e generiche. Evita "interfacce grasse" che costringono le classi a implementare metodi inutili.
// Interfaccia troppo grande
interface Worker {{ '{' }}
work(): void;
eat(): void;
sleep(): void;
{{ '}' }}
class HumanWorker implements Worker {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro"); {{ '}' }}
eat(): void {{ '{' }} console.log("Mangio"); {{ '}' }}
sleep(): void {{ '{' }} console.log("Dormo"); {{ '}' }}
{{ '}' }}
// Robot non mangia né dorme, ma è forzato a implementarli!
class RobotWorker implements Worker {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro"); {{ '}' }}
eat(): void {{ '{' }} throw new Error("Robot non mangia"); {{ '}' }}
sleep(): void {{ '{' }} throw new Error("Robot non dorme"); {{ '}' }}
{{ '}' }}
// Interfacce segregate per responsabilità
interface Workable {{ '{' }}
work(): void;
{{ '}' }}
interface Eatable {{ '{' }}
eat(): void;
{{ '}' }}
interface Sleepable {{ '{' }}
sleep(): void;
{{ '}' }}
// Umano implementa tutte le interfacce
class HumanWorkerISP implements Workable, Eatable, Sleepable {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro"); {{ '}' }}
eat(): void {{ '{' }} console.log("Mangio"); {{ '}' }}
sleep(): void {{ '{' }} console.log("Dormo"); {{ '}' }}
{{ '}' }}
// Robot implementa solo ciò che serve
class RobotWorkerISP implements Workable {{ '{' }}
work(): void {{ '{' }} console.log("Lavoro 24/7"); {{ '}' }}
{{ '}' }}
// Funzioni specifiche per interfacce specifiche
function makeWork(worker: Workable): void {{ '{' }}
worker.work();
{{ '}' }}
function feedWorker(worker: Eatable): void {{ '{' }}
worker.eat();
{{ '}' }}
const human = new HumanWorkerISP();
const robot = new RobotWorkerISP();
makeWork(human); // OK
makeWork(robot); // OK
feedWorker(human); // OK
// feedWorker(robot); // ❌ Errore compile-time: robot non è Eatable
5. Dependency Inversion Principle (DIP)
"Dipendi da astrazioni, non da implementazioni concrete"
I moduli di alto livello non devono dipendere da moduli di basso livello. Entrambi devono dipendere da astrazioni (interfacce). Le astrazioni non devono dipendere dai dettagli, ma i dettagli devono dipendere dalle astrazioni.
// Dipendenza diretta da implementazione concreta
class MySQLDatabase {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in MySQL: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// UserService dipende dalla classe concreta MySQLDatabase
class UserService {{ '{' }}
private db = new MySQLDatabase(); // Accoppiamento forte!
createUser(name: string): void {{ '{' }}
this.db.save(name);
{{ '}' }}
{{ '}' }}
// Cambiare database richiede modifica di UserService
// Astrazione (interfaccia)
interface Database {{ '{' }}
save(data: string): void;
{{ '}' }}
// Implementazioni concrete dipendono dall'astrazione
class MySQLDatabaseDIP implements Database {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in MySQL: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class MongoDBDatabase implements Database {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in MongoDB: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class PostgreSQLDatabase implements Database {{ '{' }}
save(data: string): void {{ '{' }}
console.log(`Salvato in PostgreSQL: ${{ '{' }}data{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// UserService dipende dall'astrazione, non dall'implementazione
class UserServiceDIP {{ '{' }}
// Dependency Injection: il database viene iniettato
constructor(private db: Database) {{ '{' }}{{ '}' }}
createUser(name: string): void {{ '{' }}
this.db.save(name);
{{ '}' }}
{{ '}' }}
// Facile cambiare implementazione senza modificare UserService
const mysqlService = new UserServiceDIP(new MySQLDatabaseDIP());
const mongoService = new UserServiceDIP(new MongoDBDatabase());
const pgService = new UserServiceDIP(new PostgreSQLDatabase());
mysqlService.createUser("Alice"); // MySQL
mongoService.createUser("Bob"); // MongoDB
pgService.createUser("Charlie"); // PostgreSQL
SOLID in Pratica: Esempio Completo
Vediamo come applicare tutti i principi SOLID insieme in un sistema di notifiche:
// ISP: Interfacce segregate
interface MessageSender {{ '{' }}
send(message: string, recipient: string): void;
{{ '}' }}
// DIP: Implementazioni concrete dipendono da astrazione
class EmailSender implements MessageSender {{ '{' }}
send(message: string, recipient: string): void {{ '{' }}
console.log(`📧 Email a ${{ '{' }}recipient{{ '}' }}: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class SMSSender implements MessageSender {{ '{' }}
send(message: string, recipient: string): void {{ '{' }}
console.log(`📱 SMS a ${{ '{' }}recipient{{ '}' }}: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
class PushNotificationSender implements MessageSender {{ '{' }}
send(message: string, recipient: string): void {{ '{' }}
console.log(`🔔 Push a ${{ '{' }}recipient{{ '}' }}: ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// SRP: Una sola responsabilità - gestione notifiche
class NotificationService {{ '{' }}
constructor(private sender: MessageSender) {{ '{' }}{{ '}' }} // DIP
notify(message: string, recipient: string): void {{ '{' }}
this.sender.send(message, recipient);
{{ '}' }}
{{ '}' }}
// OCP: Aperto all'estensione (nuovi logger) chiuso alla modifica
interface Logger {{ '{' }}
log(message: string): void;
{{ '}' }}
class ConsoleLogger implements Logger {{ '{' }}
log(message: string): void {{ '{' }}
console.log(`[LOG] ${{ '{' }}message{{ '}' }}`);
{{ '}' }}
{{ '}' }}
// SRP: Responsabilità separata per logging
class NotificationServiceWithLogging {{ '{' }}
constructor(
private sender: MessageSender,
private logger: Logger
) {{ '{' }}{{ '}' }}
notify(message: string, recipient: string): void {{ '{' }}
this.logger.log(`Invio notifica a ${{ '{' }}recipient{{ '}' }}`);
this.sender.send(message, recipient);
{{ '}' }}
{{ '}' }}
// Utilizzo
const emailService = new NotificationServiceWithLogging(
new EmailSender(),
new ConsoleLogger()
);
const smsService = new NotificationServiceWithLogging(
new SMSSender(),
new ConsoleLogger()
);
emailService.notify("Benvenuto!", "user@example.com");
smsService.notify("Codice: 1234", "+39123456789");
Vantaggi dei Principi SOLID
✅ Benefici
- Manutenibilità: Codice più facile da capire e modificare
- Estensibilità: Aggiungi funzionalità senza rompere l'esistente
- Testabilità: Classi piccole e focalizzate sono più facili da testare
- Riutilizzo: Componenti modulari riutilizzabili
- Flessibilità: Cambi implementazioni senza riscrivere tutto
Conclusione
I principi SOLID non sono regole rigide ma linee guida per scrivere codice di qualità. Applicarli richiede pratica e giudizio: a volte violarli leggermente è accettabile per semplicità. L'obiettivo è codice manutenibile, testabile e estensibile, non seguire dogmaticamente ogni principio.
🎯 Punti Chiave
- SRP: Una classe = una responsabilità
- OCP: Aperto all'estensione, chiuso alla modifica
- LSP: Sottoclassi sostituibili senza sorprese
- ISP: Interfacce piccole e specifiche
- DIP: Dipendi da astrazioni, non da concrete
Domande frequenti
Quali sono i cinque principi fondamentali dell'Architettura Software?
I cinque principi SOLID sono Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation e Dependency Inversion.
Cosa significa il principio di Single Responsibility (SRP)?
Il principio SRP afferma che ogni classe dovrebbe avere un solo motivo per cambiare, rendendo il codice più facile da capire, testare e modificare.
Come si applica il principio Open/Closed (OCP) nel codice?
Il principio OCP consiglia di aprire le classi all'estensione ma chiuderle alla modifica, utilizzando astrazioni per permettere estensioni senza dover modificarne il codice esistente.
Qual è l'obiettivo principale del principio Dependency Inversion (DIP)?
L'obiettivo del DIP è quello di dipendere da astrazioni, non da implementazioni concrete, in modo che i moduli di alto livello non siano vincolati a specifiche implementazioni di basso livello.

Commenti
Caricamento commenti...
Accedi per commentare