Sai usare un Command Design Pattern? Ti sei mai chiesto cos’è un Abstract Factory? O com’è fatto un Proxy? In questo articolo introduciamo i Design Pattern raccontandoti un caso reale di applicazione.
Il problema
Qualche anno fa un cliente ci ha chiesto di rifare un componente di un’applicazione scritta con Java.
Il componente riceveva informazioni (o messaggi) dall’interfaccia utente e da altri dispositivi fisici esterni: tastiera dedicata, lettori di carte, scanner… e trasformava i messaggi ricevuti in azioni dell’applicazione. Non c’era un ordine predefinito con cui le fonti inviavano i messaggi e i passi che l’applicazione doveva eseguire dipendevano sia dalle informazioni, sia dall’esito della loro elaborazione.
L’implementazione esistente era fragile: qualche volta si bloccava e l’applicazione doveva essere chiusa e riavviata.
L’approccio semplice
Per mostrarti la differenza tra l’approccio procedurale e quello a oggetti basato sul Design Pattern ‘Command’, cominciamo a vedere una soluzione procedurale.
Cominciamo a costruire un ciclo principale che serve per ricevere ed elaborare tutti i messaggi dalle varie fonti.
Potrebbe assumere questo aspetto:
while (true) { Object message = readNextMessage(); // fai qualcosa con il messaggio }
Il primo problema è: come gestiamo il messaggio? Ogni fonte manda messaggi con struttura differente che devono essere elaborati in modi diversi.
Per riconoscere il tipo di messaggio potremmo aggiungergli un attributo che lo qualifica (type)
e sulla base del suo valore decidere come procedere:
switch (message.type) { case INPUT_FROM_KEYBOARD: //Elabora messaggio da tastiera case INPUT_FROM_SCANNER: //Elabora messaggio da scanner … }
Le diverse informazioni che accompagnano il messaggio possono essere registrate in una mappa. Ad esempio, per leggere il valore del tasto premuto dall’utente sulla tastiera, potremmo scrivere:
value = message.get(“key”);
Il secondo problema è: come facciamo l’elaborazione dei messaggi? Possiamo pensare a un metodo dedicato per ogni tipo di messaggio.
Bene. Ma c’è un altro problema: ogni elaborazione produce un risultato che può richiedere ulteriori passaggi, dipendenti dal risultato stesso e dal tipo di messaggio.
Ora puoi ben immaginare il codice che deriva da questa soluzione:
- if nidificati a più livelli
- istruzioni duplicate
- metodi mediamente lunghi.
Insomma questa strada genera un codice molto complicato, difficile da scrivere, da capire e da manutenere.
L’approccio scelto: applichiamo il Design Pattern ‘Command’
La prima considerazione che abbiamo fatto è stata questa: ogni messaggio ricevuto dall’esterno poteva essere rappresentato con un oggetto a sé stante. Infatti ogni messaggio aveva una struttura diversa e conteneva tutte le informazioni necessarie per la successiva elaborazione: con questa premessa l’elaborazione poteva essere fatta dall’oggetto stesso, invece che da una funzione esterna.
Non volevamo ricorrere all’uso di instanceof e del cast per identificare i vari tipi di messaggio: questa soluzione avrebbe penalizzato l’estensibilità del componente e la sua futura evoluzione. L’alternativa era dare a tutti i messaggi la stessa interfaccia e contare sul polimorfismo.
I passi opzionali e variabili da eseguire dopo l’elaborazione dei messaggi potevano essere modellati nello stesso modo, cioè definendo un’interfaccia comune per rendere più semplice e omogenea l’interazione.
Abbiamo ritenuto che il Design Pattern Command fosse la soluzione giusta. Così abbiamo deciso di rappresentare ogni elaborazione e ogni passo successivo con un Command di interfaccia:
interface Operation { public void execute(Context ctx); }
Ecco come si presentava una classe che implementava questa interfaccia:
class MessageFromKeyboard implements Operation { private String key; public void execute(Context ctx) { // elabora i dati e produce un risultato // crea i passi successivi e li inserisce nella lista delle operazioni da eseguire tramite Context) } }
Context rappresentava un oggetto che l’operazione poteva utilizzare per svolgere compiti particolari, per esempio registrare informazioni sul log dell’applicazione e accodare i successivi oggetti di tipo Operation.
Alla fine il componente da sviluppare assumeva l’aspetto di un CommandProcessor, un oggetto che esegue la logica di altri oggetti di tipo Operation.
La soluzione
Con questa idea il problema diventava gestibile: non era più necessario prefigurarsi tutti gli scenari possibili, corrispondenti a numerose combinazioni di if. Ogni Command poteva svolgere il proprio lavoro in modo lineare, valutare il risultato e se necessario costruire i passi successivi come ulteriori Command da mettere in coda perché il CommandProcessor li elaborasse.
Questo approccio aveva un altro indiscutibile vantaggio: ogni singolo Command poteva essere isolato e testato, permettendoci di costruire una suite di test automatici che garantivano il corretto funzionamento di tutte le parti del componente. Un passo chiave per il successo dell’intero progetto.
La soluzione prefigurata prometteva bene e così abbiamo cominciato a sviluppare il componente. A mano a mano che procedevamo nello sviluppo la soluzione si mostrava corretta. Non solo: era tanto flessibile da accogliere anche tutte quelle funzionalità necessarie ma non prevedibili all’inizio.
Dopo tre mesi di sviluppo il componente era pronto: integrato nell’applicazione con facilità, è entrato in produzione presso tutti i clienti senza manifestare problemi.
Conclusione
I Design Pattern sono uno strumento prezioso per progettisti e sviluppatori di software. Spesso aiutano a trovare soluzioni efficaci che migliorano il lavoro di codifica, semplificano la scrittura e rendono più economica la manutenzione delle applicazioni.
Ma c’è una premessa fondamentale per poterli usare bene: capire e conoscere a fondo ciascun Pattern.
Se ancora non padroneggi i Design Pattern puoi seguire il nostro corso.
Lascia un commento
Devi essere connesso per inviare un commento.