SOLID principles
I principi SOLID sono linee guida per progettare software manutenibile e flessibile. Promuovono la separazione delle responsabilità, consentono di estendere il codice senza modificarlo e assicurano che le classi siano sostituibili e modulari. Utilizzano interfacce specifiche e astrazioni, evitando dipendenze dirette tra classi di alto e basso livello.
Questi principi aiutano a creare sistemi chiari e facili da evolvere, possono essere definiti come segue:
- S - Single Responsibility Principle
- O - Open/Closed Principle
- L - Liskov Substitution Principle
- I - Interface Segregation Principle
- D - Dependency Inversion Principle
S - Single Responsibility Principle (SRP)
Una classe o una funzione dovrebbe essere responsabile di un solo scopo.
Esempio Errato
La classe ProductModel combina sia i dati del prodotto che la logica per il calcolo delle tasse.
public class ProductModel
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// This method violates the Single Responsibility Principle (SRP)
public decimal CalculatePrice(decimal taxRate)
{
return Price * taxRate;
}
}
Esempio Corretto
Occorre separare le responsabilità, la classe ProductModel rappresenta esclusivamente l'entità del database, mentre il calcolo delle tasse è una classe separata PriceCalculator.
public class ProductModel
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class PriceCalculator
{
public decimal CalculatePrice(ProductModel product, decimal taxRate) =>
product == null ? throw new ArgumentNullException(nameof(product)) : product.Price * taxRate;
}
O - Open/Closed Principle (OCP)
Le classi dovrebbero essere aperte all'estensione ma chiuse alla modifica.
Esempio Errato
Ogni volta che si aggiunge un nuovo tipo di prodotto è necessario modificare il codice della classe.
public class PriceCalculator
{
public decimal Price { get; set; }
// This method violates the Open/Closed Principle (OCP)
public decimal CalculatePrice(string productType) =>
productType switch
{
"Standard" => Price * 0.1m,
"Luxury" => Price * 0.2m,
_ => throw new ArgumentException()
};
}
Esempio Corretto
Creando le classi LuxuryPriceCalculator e StandardPriceCalculator implementazioni dell'interfaccia IPriceCalculator è possibile estendere i metodi di calcolo senza modificare la classe PriceCalculator.
public interface IPriceCalculator { decimal CalculatePrice(decimal price); }
public class StandardPriceCalculator : IPriceCalculator
{
public decimal CalculatePrice(decimal price) => price * 0.1m;
}
public class LuxuryPriceCalculator : IPriceCalculator
{
public decimal CalculatePrice(decimal price) => price * 0.2m;
}
public class PriceCalculator
{
public decimal Price { get; set; }
public decimal CalculatePrice() => PriceCalculator.CalculatePrice(Price);
}
L - Liskov Substitution Principle (LSP)
Le sottoclassi debbano essere sostituibili dalle loro classi d'origine.
Esempio Errato
La classe derivata FreeProductModel non è completamente sostituibile con la classe base ProductModel, lanciando una exception InvalidOperationException.
public class ProductModel
{
public int Id { get; set; }
public decimal Price { get; set; }
public virtual decimal CalculateDiscount()
{
return Price * 0.10m; // Sconto del 10%
}
}
public class FreeProductModel : ProductModel
{
// This method violates the Liskov Substitution Principle (LSP)
public override decimal CalculateDiscount() => throw new InvalidOperationException("Free products cannot have discounts.");
}
Esempio Corretto
Le sottoclassi DefaultDiscount e NoDiscount possono essere utilizzate senza dover gestire eccezioni inaspettate.
public interface IDiscount
{
decimal CalculateDiscount(decimal price);
}
public class DefaultDiscount : IDiscount
{
public decimal CalculateDiscount(decimal price) => price * 0.10m;
}
public class NoDiscount : IDiscount
{
public decimal CalculateDiscount(decimal price) => 0m;
}
I - Interface Segregation Principle (ISP)
E' necessario separare le responsabilità in interfacce più piccole e specifiche.
Esempio Errato
L'interfaccia IProductOperations obbliga le classi a implementare metodi non necessari, come SaveProduct e PrintProductDetails.
public interface IProductOperations
{
// This methods violates the Interface Segregation Principle (ISP)
decimal CalculatePrice(decimal taxRate);
void SaveProduct();
void PrintProductDetails();
}
Esempio Corretto
Occorre separare le responsabilità in interfacce più specifiche. Ogni interfaccia dovrebbe avere una sola responsabilità.
public interface IPriceable
{
decimal CalculatePrice(decimal taxRate);
}
public interface IProductStorage
{
void SaveProduct();
}
public interface IProductPrinter
{
void PrintProductDetails();
}
D - Dependency Inversion Principle (DIP)
Le dipendenze dovrebbero essere gestite tramite astrazioni, non dipendenze dagli oggetti.
Esempio Errato
La classe ProductService dipende direttamente da PriceCalculator, anziché dipendere da un'astrazione.
public class ProductService
{
// This property violates the Dependency Inversion Principle (DIP)
private readonly PriceCalculator _taxCalculator = new PriceCalculator();
public decimal GetPriceWithPrice(ProductModel product, decimal taxRate) =>
product.Price + _taxCalculator.CalculatePrice(product, taxRate);
}
public class PriceCalculator
{
public decimal CalculatePrice(ProductModel product, decimal taxRate) =>
product == null ? throw new ArgumentNullException(nameof(product)) : product.Price * taxRate;
}
Esempio Corretto
La classe ProductService dipende dall'astrazione IPriceCalculator, e l'implementazione concreta di IPriceCalculator viene iniettata tramite il costruttore.
public interface IPriceCalculator
{
decimal CalculatePrice(ProductModel product, decimal taxRate);
}
public class PriceCalculator : IPriceCalculator
{
public decimal CalculatePrice(ProductModel product, decimal taxRate) =>
product == null ? throw new ArgumentNullException(nameof(product)) : product.Price * taxRate;
}
public class ProductService
{
private readonly IPriceCalculator _taxCalculator;
public ProductService(IPriceCalculator taxCalculator) => _taxCalculator = taxCalculator;
public decimal GetPriceWithPrice(ProductModel product, decimal taxRate) =>
product.Price + _taxCalculator.CalculatePrice(product, taxRate);
}