OCP, Open-Closed principle

_Logo Il principio Open/Closed (OCP) può essere sintetizzato definendo che per estendere un'applicazione non deve essere necessario modificarne il codice. Formalmente di parla di apertura all'estensione e chiusura alle modifiche. Un principio che se seguito offre al proprio codice importanti benefeci di manutenibilità ed estendibilità.

Il principio si applica a qualsiasi elemento del progetto, come i moduli, le classi e i membri delle classi stesse. Infatti, se voglio poter estendere l’Applicazione senza modificarla, significa che tale principio è stato applicato trasversalmente e tale possibilità deve essere prevista. Possibilità e previsione rappresentano la criticità del principio stesso, che si presta a diverse interpretazione e critiche come descritto più avanti.

Il principio è semplice: il codice non deve essere modificato se esteso e si concretizza sfruttando le astrazioni. Le astrazioni permettono di non avere limiti sul numero e sulle modalità di implementazione delle astrazioni stesse.

I problemi che possono sorgere col tempo e che possono portare ad applicare tale principio sono i seguenti:

  • L’estendere una funzionalità richiede la modifica del codice dell’applicazione.
  • Una modifica influenza a cascata diversi elementi.
  • Le richieste di estensioni non sono sporadiche.
  • L’applicativo è critico e il suo aggiornamento richiede una serie di procedure complesse.

Le problematiche citate vengono arginate grazie all'applicazione degli altri principi SOLID, ma l'OCP risulta particolarmente utile in fase di refactoring o di estensione di un'applicazione legacy. Vale il principio secondo cui ogni modifica può portare all'introduzione di nuovi bug e quindi comportare la necessità di modificare e riapplicare i test delle parti modificate.

Il principio è una forma di difesa per mitigare i rischi collegati all’introduzione di nuove funzionalità: il codice esistente si presume testato e consolidato, e applicativi critici già in produzione possono essere soggetti a diversi vincoli. L’applicabilità è quindi molto importante e dipende da tanti fattori: se l’applicazione è critica, se il deploy è una procedura lunga, se i sorgenti sono completamente sotto il proprio controllo, se le richieste sono frequenti…

Si preferisce quindi creare nuove classi per implementare l'estensione e questo permette di limitare l'impatto sul codice esistente e di avere oggetti ben definiti e con ridotti legami ad altri elementi dell'applicazione. Proprio per questo motivo anche la scrittura dei nuovi test risulta semplificata.

Approcci

Vi sono tre possibili approcci:

  • Per i linguaggi procedurali si utilizzano i parametri con i quali il client (il chiamante) definisce il comportamento (in generale con delegate e lambda expression).
  • Con la tecnica denominata Inheritance/Template Method Pattern, in cui le classi derivate possono re-implementare un comportamento tramite override.
  • Applicando il Composition/Strategy Pattern, una soluzione alla base dei modelli plug-in in cui un client dipende dalle astrazione che possono essere personalizzate esternamente tramite l'implementazione di interfacce o la derivazione di classi base. Sarà poi il client a comporre le varie implementazioni disponibili (si parla appunto di Composition). Si applica quindi lo Strategy Pattern.

Esempio

Un possibile esempio riguarda un semplice sistema di elaborazione file in cui una classe di elaborazione applica un insieme di regole ad un file in input. Tali regole possono cambiare nel tempo e richiedono quindi un architettura estendibile, mentre il motore che applicare le regole rimane immutato: il suo scopo è di applicare le regole definite sui file da processare, indipendentemente dal numero e dal tipo delle regole disponibili.

Invece di avere un tipico blocco condizionale per stabilire quale regola applicare è possibile generalizzare la regola con un'astrazione e applicare una semplice iterazione:

public void ProcessFile(string filePath)
{
  foreach (var rule in _Rules)
  {
    if (rule.IsMatch(filePath))
      rule.Apply(filePath);
  }         
}

Abbiamo quindi l'astrazione della regola IProcessFileRule che espone i due metodo IsMatch e ApplyRule:

public interface IProccessFileRule
{
  bool IsMatch(string filePath);
  void Apply(string filePath);
}

Da notare che l'interfaccia è semplice in linea con i principi del clean code: si riduce a un quando e a un cosa fare. Paradossalmente, più complessa diventa l’astrazione è più è probabile che possa essere modificata successivamente: più funzionalità vengono considerate (magari proprio per stabilizzare l’interfaccia) e più possibilità vi sono che ne suia richiesta la modifica (SRP).

L'insieme _Rules sarà popolato in modo dinamico all'avvio dell'applicazione con uno dei tanti metodi a disposizione: dall'analisi degli assembly di una specifica cartella all'utilizzo di un Dependency Injection Container.

Anche la parte che si occupa di elaborare i file potrebbe essere astratta, in modo da poterla estendere senza modificare quanto fatto: si dovrà poi applicare lo Strategy Pattern per passare al chiamante l’implementazione desiderata.

Considerazioni

E’ difficile prevedere tutte le modifiche e spesso non ne vale la pena. Nell’esempio precedente la possibilità per un regola di bloccare l'esecuzione delle altre non è prevista, è richiederebbe quantomeno il ritorno di un booleano da ProcessFile(): ciò comporterebbe la modifica dell'interfaccia e di tutte le impetrazione realizzate.

Come sempre vale il principio di non applicare un pattern se non necessario, pena l'aumento della complessità del sistema. E' una questioni di valutazione: se le estensioni richieste sono circoscritte a un ritocco di quanto inizialmente implementato allora è necessario intervenire sul codice dell'applicativo, altrimenti se le modifiche diventano più frequenti e personalizzate è il caso di applicare l'OCP tramite refactoring della parte da estendere.

In ogni caso è difficile che un applicativo, di una certa complessità, sia immune da modifiche e prevedere in ogni parte l'estendibilità non è un buon approccio. Bisogna intervenire nelle situazioni di necessità, individuando per prima cosa se è possibile realmente applicare la chiusura alle modifiche.

Una conseguenza positiva dell’applicazione di tale principio riguarda la miglior definizione della collaborazione tra classi e la creazione di un livello di astrazione che rende il codice strutturalmente pulito_ definisco l’algoritmo e l’astrazione da applicare, senza pensare ai dettagli delle implementazioni.

Non mancano le critiche visto che alcuni definiscono il principio inapplicabile o troppo soggetto a interpretazioni, riducendolo spesso ad un pattern. Tra i vari articoli disponibili in rete ne segnalo un paio:

Ben di diverso avviso è Uncle Bob, che lo considera il centro morale dell’architettura software, con alcune importanti precisazioni.

Riferimenti

Pattern e principi collegati:

Tag: SOLID, OCP