Salta al contenuto principale

05 - Principi SOLID: Le Basi dell\

I Principi SOLID: Le Fondamenta dell'Architettura Software I principi SOLID sono cinque linee guida fondamentali per scrivere codice orientato agli...

Cosa ne pensi di questo articolo?

05 - Principi SOLID: Le Basi dell\
OOPFoto:Daniil KomovPexels

In Breve

I Principi SOLID: Le Fondamenta dell'Architettura Software I principi SOLID sono cinque linee guida fondamentali per scrivere codice orientato agli...

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.

❌ Violazione OCP
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;
  {{ '}' }}
{{ '}' }}
✅ Rispetta OCP
// 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.

❌ Violazione LSP
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!
{{ '}' }}
✅ Rispetta LSP
// 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.

❌ Violazione ISP
// 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"); {{ '}' }}
{{ '}' }}
✅ Rispetta ISP
// 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.

❌ Violazione DIP
// 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
✅ Rispetta DIP
// 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:

Sistema di Notifiche SOLID
// 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.

Fonti e approfondimenti

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti