ISP (Interface Segregation Principle) è il quarto principio SOLID, che afferma: i clienti non dovrebbero dipendere da metodi che non utilizzano. Il principio è stato formulato da Robert Martin nel contesto della progettazione di interfacce per sistemi orientati agli oggetti. Come descritto nel libro Clean Architecture (2017), il principio di segregazione delle interfacce richiede la creazione di interfacce strettamente specializzate invece di una singola universale, riducendo l'accoppiamento e semplificando l'apporto di modifiche.
Punti chiave
ISP (Interface Segregation Principle) è il principio di segregazione delle interfacce che vieta di creare interfacce “grasso” con metodi non utilizzati da tutti i clienti. Invece di un'interfaccia con una dozzina di metodi, vengono progettate diverse interfacce piccole, ciascuna per il proprio gruppo di clienti.
Il principio è stato introdotto da Robert Martin come soluzione al problema dell'“inquinamento delle interfacce”, quando una classe è costretta a implementare metodi di cui non ha bisogno solo perché dichiarati in un'interfaccia comune. Nei linguaggi tipizzati staticamente, ciò si traduce in implementazioni vuote o lancio di eccezioni — un segno diretto di violazione dell'ISP.
ISP e SRP si completano a vicenda: SRP riguarda la responsabilità della classe, ISP riguarda i contratti delle interfacce. SRP dice “una classe — una ragione per cambiare”, ISP dice “un'interfaccia — uno scenario cliente”. Insieme formano un'architettura modulare dove ogni elemento del sistema ha confini chiari.
Fat Interface — un'interfaccia contenente più metodi di quanto un cliente specifico necessiti. Ad esempio, un'interfaccia Worker con i metodi work, eat, sleep. Un robot lavoratore non dovrebbe implementare eat e sleep, ma è costretto a farlo. La soluzione è suddividere in Workable, Eatable, Sleepable. Ogni cliente ottiene esattamente ciò di cui ha bisogno.
Nello sviluppo mobile, le interfacce grasse si trovano nei protocolli delegati e DataSource. Un protocollo può contenere metodi per due scenari diversi (modifica + visualizzazione), anche se uno schermo specifico ne usa solo uno.
Implementare l'ISP inizia con l'analisi dei clienti di ciascuna interfaccia. Se due clienti utilizzano set diversi di metodi della stessa interfaccia — l'interfaccia dovrebbe essere suddivisa. Ogni nuova interfaccia raggruppa i metodi che vengono chiamati insieme all'interno di un singolo scenario.
Il meccanismo di suddivisione: l'interfaccia originale viene suddivisa in più interfacce strette, ciascuna ereditante la parte comune (se presente). I clienti passano a dipendere dall'interfaccia stretta necessaria invece di quella generale. Le classi che implementavano l'interfaccia originale ora implementano solo le interfacce strette di cui hanno effettivamente bisogno.
Un chiarimento importante: il grado di suddivisione è determinato dal numero di clienti e dai loro scenari. L'ISP non richiede la massima suddivisione (micro-interfacce con un metodo ciascuna). Ciò porterebbe a una complessità eccessiva. L'obiettivo è eliminare la dipendenza dei clienti da metodi non necessari, non minimizzare la dimensione di ciascuna interfaccia.
I principali segni di violazione dell'ISP includono: classi che implementano un'interfaccia con metodi vuoti (implementazione fittizia), lancio di UnsupportedOperationException nelle implementazioni, un gran numero di parametri o tipi restituiti non utilizzati da alcuni clienti, e frequenti modifiche all'interfaccia che interessano solo alcuni clienti.
Nello sviluppo Android, un esempio tipico di violazione dell'ISP è l'interfaccia OnItemClickListener, che include metodi per clic, clic lungo e scorrimento. Se uno schermo specifico utilizza solo il clic — i metodi rimanenti rimangono vuoti. La soluzione è suddividere in OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
Nello sviluppo iOS, la violazione dell'ISP si manifesta nei delegati UIKit: un protocollo contiene metodi per diversi stati del componente. UITableViewDelegate include metodi per visualizzazione, selezione, modifica e azione di scorrimento. Gli sviluppatori spesso implementano l'intero protocollo con una dozzina di metodi vuoti. La suddivisione in più protocolli per gruppi di responsabilità risolve il problema.
Il problema non è solo estetico. Quando un'interfaccia cambia (viene aggiunto un nuovo metodo), tutte le classi che la implementano devono essere aggiornate — anche quelle che non necessitano del nuovo metodo. Nello sviluppo mobile con decine di schermi, ciò porta a modifiche a cascata. L'ISP isola ogni cliente dalle modifiche che non lo riguardano.
La violazione implicita dell'ISP avviene attraverso parametri di configurazione. Se un metodo accetta un oggetto con molti campi e il cliente ne utilizza solo 2-3 — questo è un segnale per suddividere. Alternativa: diversi metodi specializzati con un set minimo di parametri.
Nello sviluppo Android, l'ISP viene violato quando si utilizza un singolo SharedPreferencesManager per leggere e scrivere tutte le impostazioni dell'applicazione. Un Fragment che ha solo bisogno di leggere il tema ottiene una dipendenza da un gestore globale con una dozzina di metodi per diversi tipi di dati. La suddivisione in ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — applicazione dell'ISP a livello di servizi di configurazione. Ogni fornitore contiene esattamente i metodi di cui i suoi clienti hanno bisogno.
Consideriamo un esempio Android con un'interfaccia per lavorare con i dati. Violazione dell'ISP: un'interfaccia per tutte le operazioni CRUD, anche se non tutti i clienti necessitano di tutte le operazioni.
// Violazione dell'ISP: interfaccia grassa
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// Dopo l'applicazione dell'ISP: interfacce strette
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel non dipende dai metodi di scrittura
class ReadOnlyViewModel(
private val reader: UserReader
)
Un esempio iOS con separazione dei protocolli per lavorare con i media:
// Violazione dell'ISP: un protocollo per tutto il lavoro con i media
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// Dopo l'ISP: separazione in protocolli per responsabilità
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModel non dipende dai metodi di download
class PlayerViewModel {
private let player: MediaPlayer
}
Conclusione pratica: l'ISP protegge i clienti dalle modifiche in parti non correlate dell'interfaccia. Suddividere UserRepository in UserReader e UserWriter significa che le modifiche in save non influenzano ReadOnlyViewModel, e viceversa. Ogni cliente è isolato dalle funzionalità che non utilizza e non richiede modifiche quando altre parti del sistema vengono modificate.
ISP e SRP — una coppia naturale. SRP definisce che una classe dovrebbe avere una sola ragione per cambiare. L'ISP applica la stessa logica alle interfacce: un'interfaccia dovrebbe servire uno scenario cliente. Una classe può implementare più interfacce strette (ciascuna corrispondente a una responsabilità), il che è più pulito di un'interfaccia grassa con molteplici responsabilità.
ISP e OCP sono anche correlati: le interfacce strette sono più facili da estendere. Aggiungere un nuovo metodo a un'interfaccia stretta influisce solo sui suoi clienti. Aggiungere un metodo a un'interfaccia grassa influisce su tutti i clienti — potenzialmente violando l'OCP se i clienti sono costretti a modificare la loro implementazione.
ISP e DIP lavorano insieme: DIP richiede la dipendenza dalle astrazioni. L'ISP rende queste astrazioni strette e focalizzate. Dipendere da un'interfaccia ampia è ancora una dipendenza da un'astrazione, ma un'astrazione “cattiva” dal punto di vista dell'ISP. Quattro principi (SRP, OCP, ISP, DIP) formano la “piramide della modularità”: SRP e ISP definiscono i confini, OCP e DIP definiscono i modi di estensione e accoppiamento.
L'architettura a componenti nei progetti mobili (moduli, funzionalità, layer) beneficia dell'ISP a livello di API pubbliche. Ogni modulo esporta interfacce strette per i suoi consumatori, invece di un'unica facciata comune. Ciò consente di modificare l'implementazione interna del modulo senza influenzare i consumatori che utilizzano solo una parte delle sue funzionalità.
Nei progetti Android con Clean Architecture, l'ISP viene applicato agli UseCase: ogni UseCase è un'interfaccia separata con un singolo metodo invoke o execute. Il cliente (ViewModel) dipende solo dall'UseCase di cui ha bisogno, invece di un intero repository. Ciò rende le dipendenze trasparenti e testabili.
Domande frequenti
Sì, una suddivisione eccessiva è possibile. L'ISP non richiede un'interfaccia per metodo. Il criterio è: esiste un cliente che necessita solo di una parte dei metodi dell'interfaccia? Se tutti i clienti utilizzano tutti i metodi — l'interfaccia non deve essere suddivisa. Il livello ottimale di suddivisione è determinato da scenari di utilizzo reali.
L'ISP a livello di parametri significa: una funzione non dovrebbe accettare oggetti con un gran numero di campi se ne utilizza solo una parte. Invece, dovrebbero essere passati solo i dati necessari o utilizzare interfacce specializzate (ad esempio, un'interfaccia Renderable invece di un User completo).
LSP riguarda l'ereditarietà corretta e la compatibilità comportamentale dei sottotipi. ISP riguarda la progettazione delle interfacce: i clienti non dovrebbero dipendere da metodi che non utilizzano. LSP risponde alla domanda “una sottoclasse può essere usata al posto della classe base?”, ISP risponde “il cliente ha bisogno dell'intera interfaccia?”
Le interfacce strette semplificano la creazione di oggetti mock: il test crea un mock con uno o due metodi, non con una dozzina. Meno metodi ha un'interfaccia, più facile è simulare il suo comportamento. Ciò riduce il carico cognitivo dello sviluppatore di test e diminuisce la probabilità di errori nella logica mock.
Se l'interfaccia è stabile e tutti i clienti utilizzano tutti i metodi — la suddivisione è ridondante. Un esempio tipico: i protocolli UIKit progettati da Apple. Suddividerli è rischioso perché UIKit si aspetta un'implementazione completa del delegato. In tali casi, la violazione dell'ISP è giustificata dalla stabilità dell'API.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche