Scopri i Principi SOLID: Fondamenti per uno Sviluppo con Swift Efficiente

I principi SOLID sono cinque principi fondamentali della programmazione orientata agli oggetti, formulati da Robert C. Martin (Uncle Bob), per scrivere codice più leggibile, manutenibile ed estendibile.
Chi è Robert C. Martin
Robert C. Martin, noto come Uncle Bob, è un ingegnere del software, autore, consulente e formatore nel campo dello sviluppo software. È uno dei principali esponenti della programmazione orientata agli oggetti e dello sviluppo Agile, ed è famoso per aver formalizzato i principi SOLID, fondamentali per scrivere codice pulito e manutenibile. Nato il 5 Dicembre del 1952 Uncle Bob ha scoperto la programmazione a dodici anni. Oggi continua a lavorare dal sito Cleancoder.com e a proporre i suoi principi e insegnamenti tramite il sito Clean Coders. Tiene conferenze e corsi e ha scritto nel tempo libri che sono pilastri della buona programmazione, come Clean Code, Clean Architecture, Clean Agile e Clean Craftsmanship. E’ soprannominato Uncle Bob, zio Bob in quanto nel mondo anglosassone c’è anche un modo di dire che recita Bob’s your uncle, Bob è tuo zio, per dire è tutto ok.
Il nome SOLID è un acronimo che rappresenta cinque concetti chiave:
- S - Single Responsibility Principle (SRP) - Principio di Responsabilità Unica
- O - Open/Closed Principle (OCP) - Principio Aperto/Chiuso
- L - Liskov Substitution Principle (LSP) - Principio di Sostituzione di Liskov
- I - Interface Segregation Principle (ISP) - Principio di Segregazione delle Interfacce
- D - Dependency Inversion Principle (DIP) - Principio di Inversione delle Dipendenze
Oggi affronteremo il primo dei principi SOLID, il Principio di Responsabilità Unica.
Single Responsibility Principle (SRP)
Una classe dovrebbe avere una e una sola ragione per cambiare. (“A class should have only one reason to change.”) In altre parole: Una classe dovrebbe avere una sola responsabilità.
Ritengo sia uno dei principi più importanti ma anche forse il più bistrattato, il meno “rispettato”.
Un errore comune nello sviluppo è la creazione di classi che gestiscono più responsabilità.
Ogni classe dovrebbe avere un singolo scopo ben definito. Ogni classe, modulo o funzione nel codice deve avere una sola responsabilità e un solo motivo per essere modificata. Se una classe ha più responsabilità, allora ha più di un motivo per cambiare, e questo può portare a problemi di manutenibilità.
Perché è importante il SRP?
- Riduce la complessità – Se una classe ha solo una responsabilità, è più facile da capire.
- Migliora la manutenibilità – Se una parte del codice deve cambiare, si modifica solo il punto specifico, senza effetti collaterali.
- Favorisce il riuso – Classi più piccole e mirate possono essere riutilizzate più facilmente in altri contesti.
- Facilita il testing – Una classe con una sola responsabilità è più facile da testare.
Esempio di errata applicazione del principio:
class DatabaseManager {
static let shared = DatabaseManager ()
private init() {}
func getUsers() -> [User] {
// Send API request for get all users
}
func saveDataToDB(users: [User]) {
// Save user data into database
}
}
Questa classe “DatabaseManager” si occupa di gestire il salvataggio degli utenti nel database ma al tempo stesso effettua una chiamata API per recuperare gli utenti.
Quali sono i problemi in questa classe DatabaseManager?
- Gestisce una chiamata di rete (getUsers),
- Gestisce la persistenza dei dati salvandoli nel database (saveDataToDB).
Se cambiano il metodo di salvataggio nel database o la modalità di recupero degli utenti, dovrai modificare questa classe, aumentando il rischio di introdurre bug.
Esempio di corretta applicazione del principio:
class DatabaseManager {
static let shared = DatabaseManager ()
private init() {}
func saveDataToDB(users: [User]) {
// Save user data into database
}
}
class NetworkManager {
static let shared = NetworkManager ()
private init() {}
func getUsers() -> [User] {
// Send API request for get all users
}
}
In questo caso ogni classe ha una specifica responsabilità.
L’esempio esposto viola inoltre altri principi SOLID, in particolare:
Open/Closed Principle (OCP)
Principio “Aperto/Chiuso”: una classe dovrebbe essere aperta all’estensione ma chiusa alla modifica.
Questo vuol dire che:
- Se volessimo cambiare la modalità di persistenza dei dati (es. cambiare tipologia di database), dovremmo modificare direttamente la classe DatabaseManager.
- Non ci sono interfacce o protocolli che permettano di estendere il comportamento senza cambiare il codice esistente.
Immagina di avere una classe per gestire diverse tipologie di notifiche:
enum TipoNotifica {
case email
case push
case sms
}
class GestoreNotifiche {
func inviaNotifica(tipo: TipoNotifica, messaggio: String, utente: String) {
if tipo == .email {
// Logica per inviare una email
print("Invio email a \(utente): \(messaggio)")
} else if tipo == .push {
// Logica per inviare una notifica push
print("Invio notifica push a \(utente): \(messaggio)")
} else if tipo == .sms {
// Logica per inviare un SMS
print("Invio SMS a \(utente): \(messaggio)")
}
}
}
Ogni volta che devi aggiungere un nuovo tipo di notifica (es. WhatsApp), devi modificare la classe GestoreNotifiche, violando l’OCP. Questo vuol dire che la classe diventa sempre più grande e complessa, rendendo difficile la manutenzione.
Per rendere il codice coerente con il principio di cui sopra, si potrebbe modificare il codice in questo modo:
protocol Notificabile {
func invia(messaggio: String, utente: String)
}
class EmailNotifica: Notificabile {
func invia(messaggio: String, utente: String) {
// Logica per inviare una email
print("Invio email a \(utente): \(messaggio)")
}
}
class PushNotifica: Notificabile {
func invia(messaggio: String, utente: String) {
// Logica per inviare una notifica push
print("Invio notifica push a \(utente): \(messaggio)")
}
}
class SMSNotifica: Notificabile {
func invia(messaggio: String, utente: String) {
// Logica per inviare un SMS
print("Invio SMS a \(utente): \(messaggio)")
}
}
class GestoreNotifiche {
func inviaNotifica(notifica: Notificabile, messaggio: String, utente: String) {
notifica.invia(messaggio: messaggio, utente: utente)
}
}
Per aggiungere un nuovo tipo di notifica (es. WhatsApp), crei semplicemente una nuova classe che implementa il protocollo Notificabile senza modificare il codice esistente. Il codice è più modulare, flessibile e facile da testare.
Liskov Substitution Principle (LSP)
Il Liskov Substitution Principle riguarda l’ereditarietà. Nell’esempio proposto, essendo una classe singleton, non è possibile creare una sottoclasse di DatabaseManager per cambiarne il comportamento, e quindi si viola questo principio.
Interface Segregation Principle (ISP)
I client non dovrebbero essere costretti a dipendere da interfacce che non usano.
In questo caso:
- La classe espone metodi che potrebbero non essere sempre necessari per chi la utilizza.
- Se un client ha bisogno solo di leggere la lista utenti, si trova comunque a dipendere anche da metodi di salvataggio sul database.
class DatabaseManager {
static let shared = DatabaseManager ()
private init() {}
func saveDataToDB(users: [User]) {
// Save user data into database <--- metodo non per forza necessario a tutti i client
}
}
Dependency Inversion Principle (DIP)
I moduli di alto livello non dovrebbero dipendere da moduli di basso livello.
Per correggere l’esempio, entrambi dovrebbero dipendere da astrazioni (es. protocolli).
In questo caso: il DatabaseManager dipende direttamente dall’implementazione concreta (ossia database e API). Non ci sono protocolli o astrazioni. Non è facile sostituire il database o la fonte dei dati (es. per fare mocking nei test).
Conclusioni
In sintesi, i principi SOLID, introdotti da Robert C. Martin, sono un insieme di linee guida fondamentali per lo sviluppo di software orientato agli oggetti, mirati a creare codice più leggibile, manutenibile, testabile e riutilizzabile.
Applicare questi principi, come il Single Responsibility Principle che prevede che ogni classe abbia una sola responsabilità, porta a una maggiore coesione del codice, riducendo la complessità e facilitando la manutenzione nel tempo. In progetti di grandi dimensioni, l’adozione dei principi SOLID si traduce in una migliore scalabilità, tempi di sviluppo più rapidi e una maggiore adattabilità ai cambiamenti.
E tu, quali sfide hai incontrato nell’implementazione dei principi SOLID e come li hai superati?