Cicli di Vita

E’ un po’ di tempo che mi trovo spesso a parlare di cicli di vita degli oggetti. Il ciclo di vita e la sua massimizzazione e’ un criterio che ho sempre dato per scontato, seguendolo quasi per senso estetico, e mi viene naturale usarlo per motivare certe scelte.

Visto che il natale a casa dei miei presenta molto tempo libero e pochi computer ho pensato di sfruttare l’occasione per buttare giu’ qualche linea esplicativa del problema dei cicli di vita cosi’ come lo vedo io. Se non altro questo post mi permettera’ di far finta di non aver perso un sacco di tempo lontano dal computer e davanti alla tv.

(Riguardo alla tv, ho visto per la prima volta Piccolo Lord, bellissimo)

Immagino di avere un’applicazione che viene lanciata e rimane in ascolto di richieste di qualche tipo, la classe centrale a questo software avanzatissimo si chiama A, se non altro perche’ non ho idea di quello che il sistema debba fare di queste richieste, so solo che a un certo punto verranno stampate, quindi il nome A va bene come un altro :

one1DoSomething e’ il metodo che viene invocato a ogni richiesta e di questa richiesta riceve un valore molto importante “K”.

Il metodo e’ chiaramente separato in tre parti :

– La configurazione, con le new del Chooser e del Printer

– La logica, vedi l’IF

– L’output, con la Choice che si esegue e si stampa tramite il printer

Una sequenza classica.

Dovendo identificare la distribuzione di responsabilita’ tra gli oggetti non ci troveremmo molto di male, ci sono un sacco di oggetti, praticamente uno per linea, il printer ha la responsabilita’ di stampare, il chooser di scegliere, le choice 1 e 2 di eseguire quello che devono eseguire. Volendo fare i pignoli potremmo dire che la responsabilita’ di fare una scelta e’ distribuita tra il Chooser e A. Si perche’ il Chooser dice se K ha cosato i cosi oppure no, ma e’ A che decide che se K ha cosato i cosi allora bisogna usare Choice1, altrimenti Choice2.

Esporre l’associazione tra il valore ritornato dal Chooser e la Choice concreta non e’ bello, potremmo invece nasconderla tutta dentro al Chooser, che sembra avere ottime ragioni di contenerla (Tell, don’t ask per dirne una).

Eppure questo codice, per quanto faccia uso di un sacco di oggetti e contenga forse solo un punto veramente negativo non e’ affatto estensibile. Anzi, qualunque modifica richiedera’ di infilare le mani dentro ad A. Bisogna stampare su un printer diverso? Via a pacioccare. Bisogna cambiare la Choice quando K == Pippo? Via a pacioccare, dentro ad A o dentro a Chooser, poco importa.

Tutti questi oggetti e non ho nessun vantaggio.

La cosa da fare mi e’ sempre parsa ovvia, ma ho visto talmente tanto codice di questo tipo, con tutte le responsabilita’ belle divise nei vari oggetti, ma con tutti gli oggetti creati al volo, che credo valga la pena di non dare nulla per scontato.

Inizio con un primo esempio di quello che intendo con “massimizzazione del ciclo di vita”. Il diagramma che ho immaginato per caratterizzare i cicli di vita degli oggetti e’ un po’ uno spettrogramma, all’estrema sinistra dell’asse si trovano gli oggetti con un periodo di vita piu’ lungo, mentre andando verso destra i periodi di vita si accorciano e quindi, sempre rimanendo nella metafora spettrale, le frequenze di creazione e distruzione aumentano verso destra.

Ecco come si piazzano gli oggetti sullo spettro :

one-phase

Il ciclo di vita piu’ lungo e’ quello della init dell’applicazione e li’ si trova solo A, poi ci sono gli oggetti che nascono e muoiono ad ogni request : Chooser e Printer. Infine ci sono Choice1 e Choice2 che compaiono solo in uno specifico branch dell’IF.

Volendo posso evidenziare con simpatici colori pastello le aree delle tre frequenze che si manifestano nel codice. Verde e’ la frequenza di inizializzazione dell’applicazione, giallo quella del flusso delle request e rosso quella dell’IF :

one-colors

Ora, se si guarda Printer si vede che non c’e’ nulla che impedisca di inizializzarlo assieme ad A invece che ad ogni richiesta.

C’e’ pero’ anche il problema delle Choice1 e 2 gestite da A invece che dal Chooser. Quindi passo ad applicare entrambe le modifiche.

two-a1

Questa e’ la classe A modificata, l’if e’ finito nel Chooser, la responsabilita’ di scegliere l’azione da eseguire in funzione di K e’ ben incapsulata nel Chooser e ad A rimane il puro coordinamento.

Sicuramente un miglioramento netto, almeno, a guardarlo. All’atto pratico il codice non e’ piu’ chiuso al cambiamento di prima. La differenza forse e’ che dovro’ mettere le mani in pasta in Chooser se voglio cambiare l’associazione K -> Choice, piuttosto che in A.

Infatti la posizione dell’IF e’ un’altra, ma Choice1 e 2 ricadono sempre nella stessa banda di frequenza, nascono nell’IF :

two-chooser

Dal punto di vista della chiusura al cambiamento e apertura all’estensione l’unico miglioramento reale e’ dato dallo spostamento di printer. Effettivamente ora se bisogna cambiare la strategia di printing non e’ piu’ necessario toccare A.

Sullo spettrogramma e’ cambiato qualcosa :

two-phase

Il Printer ora nasce e muore con l’applicazione, possiamo immaginare che diventi configurabile anche senza ricompilare, ad esempio se l’inizializzazione avviene con Spring, ma non e’ necessario spingersi fin li’, il vantaggio e’ gia’ evidente se pensiamo che ora possiamo avere differenti main nella nostra applicazione, con A che gestisce richieste che vengono stampate in un modo o nell’altro semplicemente scegliendo il main da lanciare.

Sarebbe bello poter configurare ogni aspetto del nostro server, non solo il modo in cui stampa, ma al momento siamo lontani. L’obiettivo pero’ dovrebbe essere chiaro a questo punto, fare uscire il maggior numero di oggetti possibili e farli passare alla banda piu’ bassa, la init. La banda di base ha il pregio di essere un punto in cui la composizione degli oggetti si trasforma immediatamente in configurabilita’ del sistema intero.

Tornando al Chooser, e’ immediatamente evidente che le due Choice non hanno bisogno di nascere nell’IF, non necessitano di K per esistere, anzi, a K non sono per niente interessate. Sempre con l’idea di massimizzare il ciclo di vita degli oggetti possiamo tirare fuori le new delle choice dall’if e farci passare le choice nel costruttore per poi usarle come variabili d’istanza. A questo punto il polimorfismo che l’interfaccia Choice ci permette torna utile anche a Chooser, non solo ad A :

three-chooser

Piu’ in “alto” si riesce a far galleggiare un’interfaccia piu’ saranno i vantaggi ottenuti, spostare le new, col fatto che sono estremamente concrete, aiuta ad estendere l’applicazione dell’interfaccia e ridurre le dipendenze concrete. In effetti questa stessa mossa, che presento sulla base del ciclo di vita, ha la sua motivazione nel principio di inversione delle dipendenze : meglio non avere qualcosa di concreto (Chooser) che dipende da qualcos’altro di concreto (Choice1 e Choice2).

A dire il vero tutto quello che riguarda il ciclo di vita degli oggetti puo’ essere descritto e motivato da SRP (una responsabilita’ e’ la configurazione, un’altra la logica di esecuzione) e da DIP, l’obiettivo raggiunto in fine (la composizione) puo’ essere descritto in termini di OCP, come avevo gia’ accennato sopra. Puntare l’occhio sul ciclo di vita per me e’ un comodo cambio di paradigma che mi permette di vedere le forze messe in campo da questi tre principi come un unico criterio, dall’applicabilita’ certamente ridotta, ma molto limpida. Almeno per me!

Quando all’inizio ho detto che quel codice non aveva particolari debolezze di design non era vero, in verita’ ogni new rappresentava una responsabilita’ di troppo, per quanto le responsabilita’ esecutive fossero delegate agli oggetti, A aveva un sacco di responsabilita’ di configurazione che non doveva avere. Allo stesso tempo A era una grossa classe concreta che dipendeva da un sacco di classi concrete. Volendo uscire dai principi di design raccolti da Martin avevo gia’ citato Tell don’t Ask, ma si puo’ anche fare entrare in campo responsabilita’-collaborazione, con l’ambiguita’ del ruolo di A e di Chooser.

Insomma, era un gran guazzabuglio. Eppure chissa’ perche’ e’ un guazzabuglio che risulta poco evidente, altrimenti non mi spiegerei perche’ vedo cosi’ spesso delle new sparse in giro quando il loro ciclo di vita dovrebbe portarle a galleggiare verso l’alto e verso le basse frequenze.

Tornando allo spettrogramma ecco come si presenta ora :

three-phase

A questo punto l’ultimo passo e’ facile, ne’ Chooser ne’ le due Choice hanno bisogno di K per esistere e quindi le si puo’ far saltare nella frequenza piu’ bassa :

four-a

Cosi’ alla fine, se guardo le frequenze degli oggetti del sistema ho questo diagramma :

four-phase

Questo diagramma significa che la init ora potra’ avere questo aspetto :

four-init

La parte dichiarativa del nostro sistema adesso e’ ricca, mentre il flusso appare semplificato. Volendo togliere l’IF l’avrei potuto fare subito sostituendolo ad esempio con una mappa che associasse i valori possibili di K con una Choice particolare, ma per come era distribuita la costruzione degli oggetti mi sarei trovato a riempire una mappa dalla vita effimera, dentro a doSomething. Ridicolo. Ora invece e’ legittimo immaginare di avere un Chooser a cui vengono fatte delle chooser.set(“K1”,new Choice1()) etc. etc. Sempre nella Init, ovviamente.

Poteva anche andare peggio, il salto sulle basse frequenze e’ possibile solo se K lo concede, in caso K fosse stato un parametro necessario alla costruzione delle Choice queste sarebbero rimaste inchiodate sul rosso.

In quel caso si sarebbero forse usate delle factories per fare un ponte verso le basse frequenze : Choice1 nasce nel rosso, ma la decisione che e’ Choice1 a nascere, piuttosto che Choice2, viene fatta nel verde sfruttando una factory : chooser.set(“K1”,new Choice1Factory());

Credo pero’ che il factory pattern possa spesso essere evitato riflettendo sulle interfacce : il vantaggio di avere un parametro nel costruttore e’ enorme, perche’ permette di avere un’interfaccia che non dipende da quel medesimo parametro (il metodo non espone alcun argomento e quindi alcuna dipendenza), ma non sempre questo grado di incapsulamento e’ necessario. A volte si possono evitare catene di factories che sono li’ solo per permettere di avere configurabilita’ attraverso tre bande di frequenze diverse semplicemente ammettendo di servire queste tre frequenze con dei parametri nei metodi dell’interfaccia.

In breve avrei potuto riassumere tutto con un : ma insomma, quando fai la fatica di definire una nuova classe e dargli una responsabilita devi portare l’inizializzazione dell’oggeto il piu’ in alto possibile, se no non te ne fai niente. Ma visto quanto e’ comune il non portarlo su ho pensato che il mio piccolo modello frequenziale potesse spiegare bene l’evidenza.

Prima di un altro post del genere credo che dovro’ aspettare pasqua.