Semplificare il codice creando un’interfaccia
Uno degli argomenti più interessanti che sto affrontando nel progetto su cui sto lavorando è la programmazione di un contatore elettronico attraverso un dispositivo Pocket PC. Con “contatore elettronico” intendo i contatori dell’energia elettrica, quelli che sono installati nelle nostre case e che misurano i consumi in modo tale che poi arrivi la brava bolletta da pagare. Il software che stiamo sviluppando permette ad un tecnico di andare in giro per la città con un palmare e di elaborare uno alla volta gli ordini che ha ricevuto la mattina in ufficio. Ciascun ordine – detto semplicemente – consiste fra le altre cose nella programmazione del contatore di turno, scrivendo una miriade di informazioni riguardanti: cliente, tariffe, calendario, festività, azzeramento dei consumi, settaggio data ed ora e così via. Il tutto, come dicevo, attraverso un normalissimo Pocket PC che monta anche una fotocamera in bianco/nero ed un lettore ottico su porta seriale che viene utilizzato per la comunicazione con il contatore vero e proprio.
Dal punto di vista più tecnico, la programmazione avviene attraverso l’utilizzo di una classe che espone un metodo, che chiameremo WriteCommand, che prende in input una stringa formattata in modo un po’ particolare. I primi due caratteri identificano il tipo di operazione (lettura o scrittura), i successivi quattro identificano il registro sul quale lavorare e quelli dopo ancora rappresentano i dati da scrivere (quando l’operazione è di scrittura, ovviamente). Il metodo WriteCommand ritorna un intero: se vale zero, l’operazione ha avuto successo; se diverso da zero, l’operazione per qualche motivo è fallita.
Inizialmente avevamo studiato un modo un po’ spartano: nel codice compariva un elenco brutale di chiamate, circa un centinaio, a WriteCommand. Tecnicamente il tempo richiesto per queste operazioni è pochissimo, dell’ordine dei decimi di secondo. In realtà, il tutto è notevolmente più complesso, perchè le informazioni da inviare al contatore arrivano da un file XML e sono sottoposte anche a controlli di validità. Non appena una delle WriteCommand fallisce, la programmazione del contatore viene interrotta e viene data comunicazione al tecnico che qualcosa non è andata a buon fine. Alla fine mi sono ritrovato con una classe con un metodo con qualche centinaio di righe: assolutamente inaccettabile.
Ci ho messo due giorni interi, ma ho fatto un bel refactoring del codice, passando da un’interfaccia ICEProgramming definita in questo modo:
interface ICEProgramming { // Properties int ErrorCode { get; } string MessageError { get; } string ProgrammingPhase { get; } CENativeClass NativeClass { set; } StreamWriter Logger { get; set; } // Methods Dictionary<string, string> CallNativeFunction(Dictionary<string, string> inputParameters); }
Ho poi creato un certo numero di classi che implementano questa interfaccia, ciascuna delle quali si occupa di programmatore il contatore con un particolare set di dati (anagrafica cliente, tariffe, calendario, festività, potenze e consumi previsti dal contratto, etc. etc.). Ciascuna di queste classi deve quindi implementare i seguenti membri pubblici:
- ErrorCode, che ritorna lo stesso valore ritornato dalla WriteCommand relativa. Non appena un’operazione di scrittura fallisce, la programmazione viene interrotta ed il flusso del codice ritorna al chiamante, fino a visualizzare una MessageBox al tecnico.
- MessageError, che contiene l’errore ritornato direttamente dal contatore.
- ProgrammingPhase, che serve invece per visualizzare un messaggio in chiaro al tecnico. Se la programmazione non riesce, questo membro è in grado di dirci a che punto eravamo effettivamente arrivati.
- NativeClass, la classe nativa, cioè quella che espone il metodo WriteCommand.
- Logger, che è importante, perchè permette di scrivere su un file .log tutto quello che stiamo facendo. In pratica, appena dopo l’esecuzione di una WriteCommand, inviamo sullo stream di output il comando che abbiamo appena eseguito, così possiamo eventualmente vedere in un secondo momento quello che è stato inviato al contatore.
- CallNativeFunction, nome ancora in beta. Questo è il metodo che effettivamente esegue la programmazione del contatore. Questo metodo prende in input un Dictionary<string, string>, che contiene eventuali parametri che servono al metodo. Ritorna ancora un Dictionary<string, string>: questo perchè può essere che abbiamo bisogno di avere un certo numero di valori di ritorno. All’uscita da questo metodo viene sempre controllato il valore di ErrorCode, per controllare che tutto sia andato bene oppure no.
Nel progetto ho creato classi come CEPowers (scrittura delle potenze), CEDisplay (configurazione del display del contatore), CEStartup (scrittura dei valori di default) e così via. Tutte le classi implementano ICEProgramming, come dicevo, e tutte hanno un approccio simile, anche se poi l’implementazione del metodo CallNativeFunction è molto, molto diversa. Tutti i Dictionary<string, string> vengono alla fine fusi in un unico Dictionary<string, string>, che viene scritto su database e che servirà in un secondo momento per motivi che esulano dallo scopo di questo post.
In questo modo ho potuto spezzare il codice suddividendolo in diverse classi, ciascuna con il proprio scopo ben definito.
Il codice è molto più leggibile e manutenibile.
Technorati Tags: .NET programming