Si desidera gestire una rubrica telefonica. Si vuole gestire un certo numero di nominativi, memorizzando nome, cognome, data di nascita. Per ogni nominativo inoltre, sarà memorizzato, in alternativa, l'indirizzo email, oppure il numero di telefono. Su queste informazioni, si effettuano operazioni di ricerca per nominativo, oppure anche ricerche di compleanni. Al momento non interessa la memorizzazione della rubrica su supporto persistente (file, ecc.). Come pure non interessa un'interfaccia utente avanzata; sarà sufficiente poter effettuare operazioni con semplici righe di testo, ad esempio se l'utente digita:
add Mario Rossi 10/09/1973 email rossi@somewhere.net
verrebbe aggiunto nella rubrica il Sig. Rossi.
Come al solito il committente (detto anche cliente, colui che desidera un software, detto anche progetto, commessa), non sa manco lui quello che vuole, e già un testo come quello sopra è una descrizione piuttosto dettagliata di cosa c'è da fare. Spesso ci si arriva con un lavoro, fatto non solo da sviluppatori, ma anche da esperti del dominio del problema da affrontare, dai futuri utenti del software da sviluppare, persino da psicologi, esperti di interfacce utente, e altri consulenti di altri settori, più o meno inutili, tutti strapagati...
In ogni caso in genere lo sviluppatore sceglie di lavorare a partire da testi come questo sopra (della serie "Bando alle ciance e ditemi cosa c'è da fare..." ;-) ). Il quale, pur essendo non vago, è comunque ancora impreciso e incompleto. Il primo passo quindi è chiarire tutte le ambiguità, le cose che entreranno in gioco, ma che per ora non sono state neanche pensate, altri punti non strettamente relativi alla programmazione, ma non per questo meno importanti, del tipo: su che macchine dovrà girare? (Segue eventuale studio di fattibilità, con analisi dell'HW e SW necessari e costi relativi) Quanto tempo si dà a disposizione? Quanto pagate? Tutte cose che tipicamente si decidono assieme al cliente.
Per le questioni più tecniche invece, vediamo di lavorare a questo esempio.
Il buon sviluppatore di solito inizia a prendere i cosiddetti "appunti sulla carta del formaggio", cioè scrive quello che glie pare, come glie pare, in modo che possa capire a grandi linea bene lui cosa abbiamo in mano. Cosa fare in questa fase è piuttosto libero, ma il formalismo grafico di fig. FIG_ROLES può essere utile (vedere appendice su UML per dettagli...). In pratica si individuano i vari attori (disegnati come omini) che costituiscono il sistema, ovvero le entità che interagiscono tra loro, e i ruoli, o le azioni (gli ellissi) che questi svolgono. Gli attori del sistema possono essere persone fisiche (ad un omino può corrispondere la stessa persona, perché ad esempio un utente di un sito web può agire sia come amministratore, che come persona che cerca dati), oppure componenti software o hardware. In genere le parti del testo (paragrafi, capitoli, ecc. ) corrispondono ad altrettanti attori o azioni.
Spesso uno schema come quello in fig. (o qualunque altra roba di simile si preferisca disegnare), aiuta a capire meglio cosa si deve fare. Nel nostro caso è subito chiaro che le azioni che l'utente può eseguire sulla rubrica, sono state specificate in maniera del tutto generica. Diventa quindi necessario 1) Elencare per esteso quello che si potrà fare 2) concordarlo col cliente.
In questo caso avremo quanto segue.
L'utente può:
aggiungere un nominativo
cancellare un nominativo
cercare uno o più nominativi, anche solo per cognome, solo per nome, in entrambi i casi anche specificando le sole iniziali.
modificare i dati di un dato nominativo
ricercare i nominativi che compiono gli anni in un dato periodo di tempo
Si concorda che al momento non sono richieste altre possibili ricerche.
Si concorda inoltre che per le funzioni 1) e 2) occorrerà specificare almeno le iniziali del cognome, dopodiché il sistema eseguirà la relativa funzione, se troverà un solo nominativo, altrimenti visualizzerà un messaggio di errore.
Quello che abbiamo appena scritto potrebbe essere il testo di un verbale di riunione. Abbiamo fatto due cose: chiarito meglio le funzionalità richieste, chiarito più in dettaglio alcune di queste (ad esempio la ricerca o l'ultima annotazione).
Ma non è finita. Infatti ancora diverse cose sono poco chiare. Riportiamo di seguito le domande che ci si potrebbero ancora fare e le possibili risposte.
Che formato si usa per le
date?
Risposta: si decide di usare il formato italiano: g/m/a.
Giorno e mese saranno di almeno una cifra. L'anno sempre di 4.
Come si specifica un indirizzo
email piuttosto che numero di telefono?
Risposta: sono state
valutate due alternative. Decidere sulla base dell'analisi di quanto
digitato dall'utente (ad esempio, è chiaro che se in una
stringa c'è un carattere '@' si tratta di un indirizzo di
posta elettronica); oppure chiedere all'utente di inserire, prima di
un telefono la stringa "telefono", prima di un indirizzo
email, "email". Per semplicità e questioni di
budget, si opta per questa seconda alternativa
Che formato ha il
telefono?
Risposta: Sempre del tipo +39 02 1234, ovvero un +, un
prefisso internazionale, uno locale, il numero.
Quanto al formato delle mail, si decide di aderire allo standard: nome, chiocciola, dominio. Si rimanda alle specifiche MIME per ulteriori dettagli.
E anche qui abbiamo fatto diversi passi avanti:
abbiamo precisato meglio le cose
abbiamo fatto delle scelte, tra varie opzioni possibili, sia per ragioni ovvie (il punto 1) sulle date), che meno ovvie (le considerazioni sul telefono).
Al terzo punto si può osservare l'uso non raro di rimandi a cose già eseistenti.
Bene, come vedremo, la strada verso una definizione più precisa del problema, è ancora lunga, ma possiamo ora cominciare a tracciare un' idea di base di come sarà fatto il nostro aggeggio. Dato che lavorare a oggetti è cosa buona e giusta... buttiamo giù un diagramma delle classi che saranno coinvolte nel nostro software. Ancora una volta lo schema di FIGCLASSES usa il simbolismo UML, ma per i programmatori in erba va bene qualunque schema che gli assomigli. L'importante è rappresentare:
le classi e le interfacce che saranno utilizzate, alcune delle quali corrisponderanno agli attori o ai ruoli tracciati in precedenza (gli omini e gli ovali). Distinguere tra classi e oggetti dipende dalla semantica di quello che si fa, ricordando, in prima battuta, che un'interfaccia definisce delle operazioni, delegando il come eseguirle, una classe le esegue effettivamente.
le relazioni tra le classi, ovvero chi è figlia di chi (Indirizzo, Email, Telefono), chi usa che cosa (Persona che usa data), chi è legata a chi (Rubrica che contiene da 0 a n persone).
Di solito è conveniente non pensare subito alle variabili e ai metodi delle classi, meglio concentrarsi sulle questioni più generali, specificando magari solo i metodi e le variabili più importanti.
Riguardando a tutta la faccenda, dovrebbe nascere una domanda: Cosa succede dopo che l'utente ha
digitato una riga come quella indicata all'inizio, per chiedere di inserire un nominativo?
Ebbene, bisognerà analizzarla, ovvero, come si dice in letteratura, farne il parsing, in modo da capire se è stato scritto qualcosa di accettabile, e nel caso ricavarne un qualche cosa di utilizzabile per eseguire l'operazione richiesta (come aggiungere un nominativo) . Ergo, probabilmente bisogna pensare di aggiungere ancora qualche altra classe.
A questo punto magari val la pena di farsi un'altra domanda: perché ci viene in mente questa questione del parsing (e relative classi) solo ora? Risposta: perché occorre distinguere tra requisiti e implementazione. I primi sono le cose che sono richieste da fare, la seconda è come facciamo le cose. La teoria vuole che prima di pensi a cosa fare e poi a come farlo. Questo perché sono in effetti questioni separate, e ad una specifica di requisiti possono corrispondere molte possibili implementazioni. Ad esempio, tutta questa faccenda del parsing potrebbe già essere disponibile in qualche modo (in effetti esiste un programma che si chiama yacc che in pratica fa questo), mentre sicuramente sono da pensare entità come Persona o Rubrica, dato che riguarda il cosa si vuole fare. In pratica, quasi mai la linea di confine tra requisiti e realizzazione è così netta, per cui, in diverse fasi di sviluppo di un software, le cose si mescolano tra loro. Sta al bravo sviluppatore cercare di avere una visione il più possibile separata.
Comunque la domanda da cui eravamo partiti ci porta alle classi di FIGPARSER, in cui abbiamo pensato alla classe RubricaParser, che viene inizializzata con una stringa, ovvero la linea da analizzare, ed è in grado di scomporla in token, ovvero pezzi di stringa separati da spazi o altri caratteri speciali (come le virgolette). Parser è inoltre in grado di implementare l'interfaccia Comando, la quale avrà l'obbiettivo di eseguire la particolare azione analizzata, col metodo esegui(). Dato che l'esecuzione dei comandi avviene sulla rubrica, Parser memorizza anche un oggetto di tipo Rubrica con cui lavorare.
Come detto qualche pagina fa, i chiarimenti stabiliti per la descrizione del software da sviluppare, non erano ancora sufficienti. Infatti sono ancora irrisolte questioni come queste:
qual è esattamente il formato di un nome e di un cognome?
cosa esattamente scrive l'utente per richiedere l'esecuzione di un'azione sulla rubrica?
Sul punto 1) si chiede al committente e questo ci risponde che sono stringhe di 30 caratteri. Bravo. Ma non basta. Infatti avevamo già pensato in precedenza di considerare le virgolette o gli spazi dei separatori. Allora, quali caratteri contiene un nome o un cognome? Risposta del cliente: "A me non me ne frega niente se vuoi usare gli spazi come separatore, per me un nome può avere spazi". Soluzione nostra: vabbeh, allora separiamo le cose con i punti e virgola. Un cognome (o un nome) può contenere solo lettere e/o numeri e le virgolette semplici ('). Spesso non ce la si cava con così poco, bisogna trattare molto e, peggio ancora, non ci si rende conto di dettagli come questo degli spazi, finché non ci si trova ad eseguire una prima versione del software. Che fare allora? Non c'è una risposta. Bisogna essere bravi, esperti, cercare di pensare a tutte le casisitiche possibili. Negli ultimi anni si è cominciato anche a dire di fare "early prototyping", cioè pensare di fare subito, non il prodotto finito, ma solo un prototipo, da far vedere al cliente, in modo da ottenere altro feedback, altri chiarimenti, e sviluppare una versione più completa ("Extreme programming" è l'argomento che tratta quest'ultimo punto). Ovviamente è cruciale, per lavorare così, programmare bene, fare codice flessibile, modulare, estendibile. Figo... ;-)
Veniamo ora al punto 2). Qui è utile, non tanto per interagire col cliente, quanto come riferimento tra programmatori, definire quelle che si chiamano specifiche formali. Pensiamoci un attimo: una cosa come
add Mario Rossi
non è altro che una frase scritta con la grammatica di un linguaggio. Quindi tutto verrebbe depurato di ambiguità e malintesi, se descrivessimo formalmente (matematicamente) questo linguaggio. A questo punto vengono in aiuto i tanti strumenti teorici, formali, matematici sviluppati nel corso del tempo. In questo caso è naturale scrivere la grammatica richiesta col formalismo BNF. In TABBNF il risultato.
E' una specifica formale? In effetti non esattamente. Perché ad esempio abbiamo dato per scontato cose come il formato del n. di telefono, o dell'email, dato che ne abbiamo già parlato sopra e dovrebbe essere abbastanza chiaro. E allora? E allora i formalismi, come i diagrammi, gli schemi, ecc. vanno usati cum grano salis: quando servono, né troppo né troppo poco...
Precisiamo infine anche per i formalismi si distingue tra la descrizione di cosa si vuole fare (specifiche formali, come in questo caso) e quella del come lo si fà (specifiche di implementazione). Esistono una pletora di simbolismi grafici e linguaggi matematici per entrambi gli obbiettivi, per cui si rimanda alla letteratura.
Bene. Ne sappiamo abbastanza per cominciare a pensare a qualcosa di più concreto, e magari scrivere un po' di codice.
In questa fase di progetto, detta anche design, che precede la scrittura del codice vero e proprio, o durante la quale si scrive solo una parte del codice (le dichiarazioni di metodi e variabili) gli strumenti che tornano utili possono essere questi:
innanzi tutto conviene definire
un po' bene i dettagli delle classi, ovvero variabili e metodi. Poi,
magari assieme a questo, conviene cominciare a pensare cosa accade
all'interno delle classi, i loro metodi, ecc. In FIGUMLDETAILS
abbiamo riportato alcuni dettagli sui metodi. Qui abbiamo fatto le
seguenti assunzioni:
molte variabili di classe le abbiamo marcate come "readonly", significa che dall'esterno potranno essere solo lette, non modificate. Ci sono diversi modi di codificare questo. In Java, ad esempio per Nome, si fa una variabile privata nome, e il metodo getNome().
Abbiamo fatto si, grazie all'accorgimento precedente, che diverse classi implementino oggetti algebrici immutabili. Ad esempio una volta definita una new Person (...), non è possibile cambiare i singoli valori di questa istanza, perché non ci sono i corrispondenti metodi. Questo non è sempre corretto, ma spesso offre alcuni vantaggi, il principale dei quali è che il corrispondente oggetto è stateless, ovvero non ci sono informazioni di stato, interne all'oggetto, che ne influenzano il comportamento esterno.
Il codice vero e proprio. Magari
più che Java o un altro linguaggio, qualcosa di meno
informale, linguaggi fittizi, di cui non esiste un vero e proprio
compilatore, ma che sono abbastanza chiari agli umani. Ad esempio,
in TABMAIN, abbiamo provato a scrivere il ciclo "leggi comando
/ analizzalo / eseguilo". E' utile inoltre, scrivere il codice per gradi:
prima le definzioni delle classi, con le definzioni dei metodi e delle
variabili di classe più importanti (senza implementazioni). Poi nelle
implementazioni, scriversi prima qualche riga che dice come funzionerà
l'algoritmo che si sta per codificare, e dopo scrivere il codice.
CLICCKA PER PSEUDOCODICE
Diagrammi e schemi, come i diagrammi di flusso e quelli delle interazioni tra oggetti. Ad esempio in fig. FIGUMLSEQ è riportato un diagramma temporale, che descrive come interagiscono alcuno componenti nel corso del tempo.
Altri formalismi più o meno matematici, che qui forse è meglio tralasciare.
Al passaggio precedente abbiamo scoperto (FIG...), ad esempio che un comando può essere scritto non correttamente. Il ché genera delle condizioni di errore. Ops! Finora ci siamo preoccupati di come deve funzionare il nostro software normalmente. Non abbiamo pensato a condizioni anomale come questa detta. C'è tutta una serie di errori, situazioni non previste, non normali, che va pensata e va pensato a come gestire questi casi. Quando pensare a questo? Tipicamente tra l'analisi dei requisiti e il progetto, dato che questo argomento fa parte del come si fanno le cose. Spesso il committente manco immagina queste cose, per cui decidono gli sviluppatori, per altre situazioni invece si debbono chiedere chiarimenti, magari pensando in anticipo ad una serie di soluzioni possibili. Esempio: è ovvio che un comando digitato male porta a un messaggio di errore. Meno ovvio è il caso di una data digitata con due cifre per l'anno invece che quattro; al cliente si dovrebbe chiedere se va considerata come data del 1900, oppure come errore.
Dopo un'attenta analisi, svolta più o meno come visto qui, dovrebbe rendere la scrittura del codice meno faticosa possibile. In realtà spesso questo non accade, dato che, anche se si è meticolosi e non si ha fretta di precipitarsi a programmare, molti dettagli vengono lasciati proprio alla stesura dei programmi veri e propri. Nel nostro caso della rubrica, abbiamo lasciato alcune decisioni proprio a questa fase:
Decidere come suddivedere le cose in package (vedere listati per dettagli)
Decidere tutte le condizioni di errore, anomalie, ecc.
Decidere i test da effettuare. Spesso le prime versioni di un programma sono testate molto sommariamente: si fanno solo quelle quattro prove che mostrano che più o meno funzionano. In realtà alcuni test andrebbero pensati già in fase di definizione dei requisiti (i test di accettazione, con i quali si stabilisce cosa si aspetterà di vedere funzionare l'utente), altri durante le specifiche formali (es.: pensare a delle stringhe della grammatica dei comandi e pensare a dei test che controllano se il parser funziona o meno). C'è in letteratura tutta una teoria del testing, a cui si rimanda.
Domanda: perché tutto sto calderone? Non si poteva fare un'unica classe parser, che richiama le varie operazioni della classe Rubrica.
Risposta: si, in questo caso probabilmente si. In generale la soluzione mostrata è molto più flessibile. Ad esempio: la classe Rubrica non è fornita da altri, oppure la si vuole pilotare sia con righe di comando, che con un'interfaccia grafica. O ancora: si vuole aggiungere altri comandi, ma senza la necessità di cambiare la classe Rubrica.
Domanda: nel diagramma di classi c'era Indirizzo, Mail, Telefono. Ora nel codice c'è solo Indirizzo, in cui sono fuse le funzionalità di entrambe le ultime due. E' un errore?
Risposta: dipende... Quando si stravolge il codice, rispetto all'analisi iniziale, questo è sicuramente sbagliato. Si dovrebbe almeno ri-documentare la cosa. Quando invece si introducono cambiamenti non sostanziali, e lo si evidenzia chiaramente, la cosa è considerata lecita. Si ricordi che gli schemi spesso nascondono molti dettagli del codice effettivo.
Domanda: i commenti al codice sono un po' strani. Ad esempio iniziano sempre con /**, oppure hanno sequenze di formattazione HTML, come <P>, o ancora, hanno cose strane tipo @return.
Risposta: i commenti sono scritti seguendo le regole fissate dal tool JavaDoc (www.javasoft.com), il quale riesce a leggere i commenti inseriti in uno o più package Java e ricavarne pagine Web strutturate, ovvero con elenco dei package, delle classi, delle variabili e metodi di classe. La documentazione di riferimento di JDK è stata prodotta in questo modo.
Domanda: se non so l'UML e tutta sta roba formale e semi-formale non so programmare?
Risposta: dipende... c'è gente che ha scritto pezzi interi di Linux sapendo solo il C. Ci sono consulenti in giacca e cravatta che conoscono quattro grafici in croce e programmano schifezze... Diciamo che le cose viste qui fanno parte della scatola degli attrezzi di chi sviluppa software, soprattutto questo modo di ragionare a 360 gradi, non limitato al solo smanettare. Detto questo, qui si è voluto dare un esempio realistico e omni-comprensivo, in cui si sono gettati anni di esperienza in materia. Per cui non ci si spaventi per un eventuale disorientamento in cui ci si può trovare leggendo queste pagine.
UML sta per Unified Modelling Language, definito da un consorzio di imprese ed istituzioni pubbliche, chiamato Object Management Group (OMG). E' un linguaggio grafico che aiuta in diversi modi durante lo sviluppo del software. UML è organizzato in molte sottoparti: ce n'è per definire le classi, le interazioni tra oggetti, i vecchi diagrammi di flusso per rappresentare gli algoritmi, la struttura hardware dove girerà un software e il dislocamento del codice su questa (il cosiddetto deploy). Ciò che abbiamo visto in questo testo, è soprattutto il modo di rappresentare le classi e le relazioni tra loro. In FIGUMLRESUME è riportata una breve legenda relativa a questo tipo di diagramma.
Per ulteriori informazioni su UML, si rimanda a:
http://web.cefriel.it/~alfonso/WebBook/indice.htm