Le basi dell’OOP e i vasi di Pandora

_Logo Alle volte ci si ritrova a perdere molto tempo per implementare una funzionalità all’apparenza banale ma che se inserita in un codice privo di architettura può provocare diversi mal di pancia. È come aprire un vaso di Pandora: tutto e il contrario di tutto rendono ogni modifica instabile come quelle che l’hanno preceduta.

Se la prima regola è quella di evitare un frettoloso refactoring e dedicarsi all’urgenza, la seconda è quella di non fare la fine del gatto che si morde la coda. Prevenire è sempre meglio che curare. Ma come evitare queste spiacevoli e fin troppo frequenti situazioni?

Due sono le regole che potrebbero aiutare e che si possono applicare durante la ristrutturazione del codice (parlare di refactoring è alle volte troppo benevolo e ricorda pratiche di miglioramento del codice e non di sopravvivenza):

  • Evitare il più possibile l’uso di flag (i mitici interruttori che determinano una funzionalità piuttosto che un’altra, cari alla flag oriented programming e spesso suggeriti dal richiedente con quell’innocua espressione del tipo: “a me basta solo un flag”).
  • Evitare il più possibile i cicli e le condizioni: sono difficili da leggere, rendono il codice complesso e sono il luogo perfetto per la proliferazione di bug.

Stiamo quindi parlando dei costrutti su cui si basano la maggior parte degli applicativi e gli unici che vengono affrontati a scuola. If, switch, for, while traboccano anche dei testi più attuali di programmazione, ma in realtà dovrebbero essere usati con cautela perché la programmazione ad oggetti (OOP) offre alternative molto più flessibili e semplici.

Oltre ai tanti principi e ai tanti pattern di cui ho in parte discusso in altri articoli, possiamo semplicemente partite dalla base di tutto: l’oggetto.

Un oggetto è un’entità definita da un tipo e che è in grado di incapsulare dati ed esporre proprietà e metodi. Questi membri lavorano direttamente sui dati dell’oggetto e quindi, rispetto alla programmazione procedurale, i dati sono legati alle operazioni e entrambi hanno un significato particolare proprio perché appartengono ad uno specifico tipo.

Il definire gli oggetti con i dati e le operazioni che devono viverci dentro è già un compito non banale, anche se il salto di qualità è pensare in modo più astratto e dinamico.

Comunque, sarebbe già un successo suddividere veramente in oggetti senza far diventare una classe una mera raccolta di procedure che col tempo diventano mastodontici vasi di Pandora.

Ma il vero vantaggio dell’OOP risiede nella possibilità di cambiare a runtime non solo l’oggetto in uso ma anche le sue operazioni: queste non sono staticamente fissate nel tempo ma possono essere determinate a runtime in base alle necessità (grazie al polimorfismo oppure tramite delegati).

È soprattutto un lavoro di concetto, di design: non basta avere un insieme di classi, che col tempo ingrassano diventano meri pezzi di codice procedurale, ma bisogna individuare la giusta forma delle entità e dei processi, entrambi rappresentabili da singole parti combinate a runtime.

Passando ad un esempio pratico e banale, diciamo di un applicativo in grado di avviarsi sia connesso ad un server sia in modalità stand-alone, trovare più volte i seguenti costrutti è sintomo di una lunga storia di smartellate:

if(session == null) { … } else { … }
if(IsOffline) session.DoThis(); else DoThat();
var appIcon = (session == null) ? Resources.ClientIcon : Resources.OfflineIcon;
if(session != null && session.IsLogged) { … }

Molto meglio creare un’astrazione sulla sessione. Una ISession con due implementazioni: una per la modalità client/server e una per la modalità offline. Quindi ISession esporrà l’icona corretta mentre DoThis e DoThat saranno lo stesso metodo ma implementato in modo differente. Un concetto abbastanza banale che se non affrontato per tempo può creare un pesante debito nel codice.

Il primo vantaggio dell’astrazione ISession è il non dover mai verificare se session è nullo. Un riferimento nullo è il male. Il secondo vantaggio è evitare continui blocchi condizionali. Il terzo è una semplificazione del codice e quindi della possibilità di completare un fix prima delle 18 di venerdì. Il quarto è che è semplice creare una nuova implementazione per affrontare nuovi requisiti senza toccare quanto già fatto.

Quelle condizioni viste poco più sopra possono (e lo fanno regolarmente) sfuggire di mano e moltiplicarsi senza controllo. Ad esempio, nel caricamento dei dati: in un caso tramite chiamata remota nell’altro con accesso ad una cache locale. Moltiplicate il tutto per un applicativo di medie dimensione e otterrete una buona metà di codice spazzatura.

Qualcuno potrebbe obiettare che nell’implementazione della ISession molti membri risulterebbero vuoti. Ma questo non significa che l’idea sia sbagliata: semplicemente la Session ha troppi compiti e deve essere suddivisa.

Si ritorna sempre al principio fondamentale di limitare la responsabilità di ogni oggetto e questo porta a dover utilizzare di volta in volta l’oggetto appropriato.

Con un codice mal strutturato va da sé che molta logica viene scaricata sul consumer e questo provoca delle duplicazioni irritanti. Il client non deve avere alcune responsabilità nell’uso del codice. Le responsabilità vanno nelle nostre classi.

I consigli sono sempre gli stessi: concentrarsi sugli oggetti, evitare i blocchi condizionali, evitare i cicli, nascondere i dettagli, usare oggetti immutabili. Attuare pratiche di defensive programming.

Tag: OOP