OCP (Open/Closed Principle) è il secondo principio del SOLID, che stabilisce: le entità software dovrebbero essere aperte all'estensione ma chiuse alla modifica. Questo principio, formulato da Bertrand Meyer nel 1988, permette di aggiungere nuove funzionalità senza modificare il codice esistente. Secondo il libro di Robert Martin Clean Architecture (2017), il principio di apertura viene implementato attraverso astrazioni e polimorfismo, minimizzando il rischio di errori di regressione.
Punti Chiave
OCP (Open/Closed Principle) — il principio di apertura all'estensione e chiusura alla modifica. Classi, moduli e funzioni dovrebbero essere progettati in modo che nuovi comportamenti possano essere aggiunti senza modificare il loro codice sorgente. L'estensione si ottiene attraverso ereditarietà, composizione o sostituzione di implementazioni di interfacce.
Bertrand Meyer nel suo libro Object-Oriented Software Construction (1988) descrisse per la prima volta OCP attraverso l'ereditarietà: la classe base rimane invariata, mentre le sottoclassi ne estendono il comportamento. L'interpretazione moderna di OCP, proposta da Robert Martin, si basa su polimorfismo e interfacce: invece dell'ereditarietà, vengono utilizzati contratti astratti.
La differenza tra gli approcci è significativa. L'ereditarietà crea un forte accoppiamento tra classi base e derivate. Le interfacce e la composizione offrono flessibilità: l'implementazione può essere scambiata senza modificare il codice client. OCP moderno riguarda l'astrazione, non l'ereditarietà.
OCP polimorfico utilizza classi astratte o interfacce per definire un contratto. Il codice client lavora con l'astrazione senza conoscere l'implementazione concreta. Nuove funzionalità vengono aggiunte creando una nuova classe che implementa la stessa interfaccia — senza una singola modifica al codice esistente. Ciò rende il sistema resistente ai cambiamenti e prevedibile per l'estensione.
Nello sviluppo mobile, questo approccio è onnipresente: il pattern Strategy permette di scambiare algoritmi (compressione immagini, caching, autenticazione) attraverso un'interfaccia comune. Aggiungere una nuova strategia non richiede di modificare il codice che la utilizza.
Implementare OCP inizia isolando il comportamento variabile in un'astrazione. Se c'è una costruzione switch o una catena if-else che controlla il tipo di un oggetto nel codice — è un segnale per applicare OCP. Ogni ramo condizionale potenzialmente richiede l'aggiunta di un nuovo ramo quando si estende.
Il processo di refactoring sotto OCP include tre passaggi: identificare l'aspetto variabile (ciò che può essere esteso), isolarlo in un'interfaccia o classe astratta, riscrivere il codice client per lavorare con l'astrazione invece della classe concreta. Dopo ciò, nuove funzionalità vengono aggiunte senza modificare il client.
Un chiarimento importante: la chiusura alla modifica non è assoluta. Se un cambiamento di requisito riguarda l'astrazione stessa o il contratto — il cambiamento è inevitabile. OCP protegge dai cambiamenti nelle implementazioni, non nei contratti. Un buon design assume che i contratti siano stabili e le implementazioni siano variabili.
Nel valutare la compatibilità OCP di un'architettura, è utile guardare ai punti di estensione. Ogni punto in cui uno sviluppatore aggiunge if-else o switch per un nuovo tipo è candidato per l'astrazione. Un sistema progettato secondo OCP ha punti di estensione prevedibili: interfacce con documentazione che dice "implementa questa interfaccia per aggiungere un nuovo tipo". In Android, il pattern Factory con ViewModelProvider.Factory è un chiaro esempio — aggiungere un nuovo tipo di ViewModel non richiede di modificare le factory esistenti.
I pattern più efficaci per rispettare OCP nello sviluppo mobile includono Strategy, Template Method, Decorator e Factory. Ciascuno risolve il problema di estendere il comportamento senza modificare il codice esistente attraverso diversi meccanismi di progettazione orientata agli oggetti.
Strategy permette di scambiare algoritmi al volo attraverso un'interfaccia comune. Nello sviluppo iOS, le strategie sono utilizzate per animazioni e validazione di form. Template Method definisce lo scheletro di un algoritmo in una classe base, e le sottoclassi sovrascrivono i passaggi — adatto per schermi con struttura comune ma contenuto diverso.
Decorator aggiunge dinamicamente comportamento a un oggetto senza modificarne la classe. In Android, Decorator viene utilizzato per avvolgere un Repository con un livello di caching o logging. Factory Method crea oggetti attraverso un'interfaccia, permettendo alle sottoclassi di decidere quale classe istanziare — la base della creazione di dipendenze compatibile con OCP.
La scelta del pattern dipende dalla stabilità del comportamento esteso. Strategy è ottimale quando gli algoritmi vengono sostituiti completamente. Template Method — quando la struttura è fissa ma i passaggi variano. Decorator — quando l'estensione deve essere trasparente per il client. Per la maggior parte degli scenari in Android e iOS, Strategy + dependency injection è sufficiente.
Applicare questi pattern senza OCP è tecnicamente possibile ma perde significato. È OCP che giustifica perché introduciamo un ulteriore livello di astrazione: affinché il sistema possa crescere senza riscrivere il codice esistente.
Consideriamo un esempio Android con elaborazione pagamenti. Senza OCP, ogni nuovo sistema di pagamento richiede modifiche alla classe gestore. Con OCP, viene aggiunta una nuova implementazione dell'interfaccia senza modificare il codice esistente.
// Violazione OCP: switch richiede modifica quando si aggiunge un nuovo sistema
class BadPaymentProcessor {
fun process(type: String) {
when (type) {
"card" -> // elaborazione carta
"paypal" -> // elaborazione PayPal
}
}
}
// Design compatibile con OCP
interface PaymentMethod {
fun pay(amount: Double)
}
class CardPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
class PayPalPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
// Nuovo sistema — nuova classe, senza modificare il codice esistente
class ApplePayPayment : PaymentMethod {
override fun pay(amount: Double) { }
}
Un esempio iOS con validazione di campi di testo dimostra la stessa logica attraverso i protocolli Swift:
// Validazione compatibile con OCP
protocol ValidationRule {
func validate(_ input: String) -> Bool
}
struct EmailRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.contains("@")
}
}
struct PhoneRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count == 11
}
}
// Aggiungere una nuova regola non richiede di modificare il codice del validatore
struct PasswordRule: ValidationRule {
func validate(_ input: String) -> Bool {
return input.count >= 8
}
}
Il vantaggio chiave di OCP in questi esempi: aggiungere ApplePay o PasswordRule non richiede di modificare le classi esistenti. Il codice si espande orizzontalmente — attraverso nuovi file, non modificando quelli vecchi. Ciò riduce il rischio di regressione e accelera l'implementazione di nuove funzionalità.
La violazione più comune è una costruzione switch o when basata sul tipo di oggetto. Ogni volta che viene aggiunto un nuovo tipo, bisogna trovare tutti questi switch nel codice e aggiungere un nuovo ramo. Uno switch dimenticato è un errore di runtime difficile da rilevare in fase di compilazione.
Nello sviluppo mobile, OCP viene violato utilizzando classi enum giganti con metodi che dipendono dal valore dell'enum. Aggiungere un nuovo elemento enum richiede di modificare ogni switch in tutto il progetto. L'alternativa è il polimorfismo attraverso un'interfaccia, dove ogni tipo implementa il proprio comportamento.
Un'altra violazione tipica è il God Adapter: RecyclerView.Adapter (Android) o UITableViewDataSource (iOS) che gestisce diversi tipi di cella attraverso if-else. Ogni nuovo tipo di cella richiede di estendere l'adattatore. La soluzione è un ViewHolder polimorfico con un metodo bind comune, dove ogni tipo di cella è responsabile del proprio rendering.
Misure preventive includono: evitare switch basato sul tipo a favore del polimorfismo, iniettare dipendenze attraverso interfacce e utilizzare il pattern Factory per creare oggetti in base alla configurazione. Analizzare il codice per "interruttori per tipo" è una parte obbligatoria della revisione del codice nei team orientati a OCP.
Refactoring di una violazione esistente di OCP viene effettuato tramite Replace Conditional with Polymorphism: ogni ramo condizionale diventa una classe separata che implementa un'interfaccia comune. Il codice client viene riscritto per lavorare con l'interfaccia, e l'implementazione concreta viene fornita tramite una factory o un contenitore DI.
È importante capire che OCP e polimorfismo non risolvono tutti i problemi di estensione. Se l'architettura è scelta in modo errato, aggiungere nuove funzionalità richiederà di modificare non solo le implementazioni ma anche i contratti. Una buona architettura predice le direzioni di estensione e posiziona le astrazioni esattamente in quei punti. Gli investimenti in OCP sono tanto più remunerativi quanto più il progetto vive a lungo e quanto più spesso cambiano i requisiti dei moduli specifici.
Domande Frequenti
No. OCP vieta di modificare il codice esistente quando si aggiungono nuove funzionalità relative alla stessa astrazione. Modificare un contratto, correggere bug e fare refactoring non sono violazioni di OCP — il principio protegge dai cambiamenti a cascata durante l'estensione.
Strategy è un'implementazione diretta di OCP. L'interfaccia della strategia definisce il contratto, il client dipende dall'astrazione e le strategie concrete implementano il comportamento variabile. Aggiungere una nuova strategia non richiede di modificare il client — questa è apertura all'estensione con chiusura alla modifica.
Sì, attraverso ereditarietà e Template Method: la classe base definisce lo scheletro dell'algoritmo e le sottoclassi sovrascrivono i passaggi. Tuttavia, l'ereditarietà crea un forte accoppiamento ed è meno flessibile delle interfacce. Nello sviluppo moderno, interfacce e composizione sono considerate il metodo preferito per implementare OCP.
Il codice compatibile con OCP semplifica i test: ogni implementazione di interfaccia viene testata isolatamente. Il codice client viene testato con un'implementazione mock, permettendo di verificare la logica senza essere legati a un comportamento specifico. Espandere il sistema non richiede di riscrivere i test esistenti.
No. OCP è giustificato quando l'estensione funzionale è prevedibile. Per codice stabile che non si prevede di estendere, un'astrazione aggiuntiva è eccessiva. YAGNI (You Ain't Gonna Need It) è un buon contrappeso a OCP: l'astrazione viene introdotta quando appare una seconda variante di comportamento, non preventivamente.
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