4Programmazione top-down

Transcript

4Programmazione top-down
4
Programmazione
top-down
Contenuto
4.1
Risolviamo un mistero
4.2
La programmazione top-down ed una applicazione database
4.3
Sottoprogrammi
4.4
Sottoprogrammi con variabili interne
4.5
Sottoprogrammi con parametri array
4.6
Esempi di comunicazione fra sottoprogrammi
4.7
Memorizzare e stampare dati con un database
4.8
Rappresentazione delle domande e ricerca delle risposte appropriate
4.9
Assemblare l’applicazione database e aggiungere i commenti
4.10 *Ricorsione
4.11 Sommario
4.1
Risolviamo un mistero
Mentre il sole pomeridiano svaniva sul parco del paese, l’ispettore capo Brown stava accendendo la sua pipa della sera quando giunse una telefonata urgente da Villa Dunsmore.
Era miss Secrest, terrorizzata, che aveva appena trovato Lord Peter Dunsmore crollato sul
pavimento del salotto. Era morto. Lady Emily era rientrata nella sua camera in stato di
shock e l’ispettore capo veniva implorato di correre lì sulla scena. Questo è ciò che egli
fece e, dopo aver esaminato la salma, annunciò la causa del decesso: una dose letale di
veleno, somministrata da poche ore.
La famiglia e gli inservienti erano troppo sconvolti per essere interrogati, ma un piccolo bizzarro terminale all’angolo della stanza consentiva l’accesso ad alcune informazioni
sulla famiglia. L’ispettore si sedette ed iniziò a scrivere:
Chi ha visitato la villa oggi?
La macchina rispose:
%LHUPDQQSGI
122
Programmazione top-down
Mr. Mason è entrato alle 15:00.
Il professore è entrato alle 15:00.
Miss Secrest è entrata alle 17:00.
Dimmi qualcosa di più riguardo Mr. Mason.
Mr. Mason ha l’hobby del tennis.
Mr. Mason è entrato alle 15:00.
Mr. Mason è un farmacista.
L’ispettore digitò una lunga serie di domande, fumando lentamente la sua pipa, ed alla fine
giunse ad un sospetto. Egli era in grado di identificare soltanto una persona in possesso di
tutti e tre i requisiti: un movente, l’accesso al veleno ed un modo per somministrarlo alla
vittima.
Per quanto possiamo essere interessati a conoscere l’identità dell’assassino, come informatici siamo ancor più interessati a scoprire come funziona il programma di quel computer. In questo capitolo vediamo delle metodologie per creare programmi più grandi e
complessi rispetto a quelli visti nei capitoli precedenti ed uno di questi è un programma
in grado di rispondere a domande, come quello utilizzato dall’ispettore. In particolare,
studiamo la metodologia di programmazione top-down, l’uso di sottoprogrammi e le tecniche per mantenere sotto controllo la sempre maggiore complessità dei programmi di
grandi dimensioni. Nelle sezioni finali di questo capitolo applichiamo tali metodologie
ad altri problemi di esempio.
4.2
La programmazione top-down ed una applicazione
database
Frequentemente, in informatica, ci si trova di fronte a problemi complessi.
affrontarli? Ci sono due tecniche importanti a cui ricorrere:
Come
1. rappresentare il problema in modo che possa essere affrontato con facilità.
2. decomporre il problema in sottoproblemi più semplici e quindi decomporre ulteriormente tali sottoproblemi fino a quando non si giunge al livello più basso, dove
ogni sottoproblema rimanente risulta di facile comprensione (Figura 4.1). La soluzione del problema originale si ottiene assemblando opportunamente le soluzioni
dei sottoproblemi individuati durante la decomposizione. Due ingredienti essenziali per il felice esito dell’operazione sono l’uso di semplici decomposizioni e la
facilità di soluzione dei sottoproblemi al livello più basso; il tutto va naturalmente
eseguito senza introdurre errori.
Applichiamo queste idee per la creazione del programma che risponde alle domande
dell’ispettore. Un insieme di informazioni come quello riguardante la famiglia Dunsmore
%LHUPDQQSGI
4.2 La programmazione top-down ed una applicazione database
123
Figura 4.1
viene detto database (base di dati). Un programma che memorizza tali informazioni e
risponde a domande riguardo ad esse è chiamato applicazione database. Il nostro attuale obiettivo è costruirne una, sebbene dalla discussione precedente emerga chiaramente
il rischio di imbarcarsi in una impresa titanica. Se vogliamo raggiungere il successo
dobbiamo trovare dei sistemi per ridurre la complessità del problema.
Il nostro primo compito nell’affrontare questo problema consiste nell’individuare una
rappresentazione che sia facile da comprendere e ci conduca al più semplice programma
possibile. Come prima semplificazione, supponiamo che le informazioni sulla famiglia
siano memorizzate sotto forma di un insieme di fatti, rappresentati da dichiarazioni del
tipo:
Mr. Mason è entrato alle 15:00.
Mr. Mason è un farmacista.
Dopo aver preso questa decisione, non abbiamo più bisogno di dover afferrare la vaga
idea di “informazione” e il compito del programmatore diviene quello di stabilire i modi
di memorizzare e recuperare frasi. Semplifichiamo ulteriormente le cose supponendo
che sia possibile rispondere a tutte le domande semplicemente cercando e restituendo
alcune delle frasi memorizzate, quelle cioè che rispondono alla domanda dell’utente. Ad
una domanda dell’utente, dunque, la macchina non deve far altro che determinare se vi
siano delle affermazioni nel database che rispondono a tale domanda e, nel caso vi siano,
stamparle. Non consideriamo il caso in cui venga chiesto al programma di inferire alcuni
fatti nuovi che non sono esplicitamente memorizzati nel database.
Avendo deciso una rappresentazione che indichi chiaramente cosa si intenda per “informazione” e la natura stessa dell’informazione, possiamo iniziare a vedere le elaborazioni necessarie alla risoluzione del problema. Il secondo passo nel ridurre la complessità consiste nel decomporre l’intero compito in un insieme di compiti più semplici, che
possano essere facilmente eseguiti.
%LHUPDQQSGI
124
Programmazione top-down
Figura 4.2
L’applicazione necessiterà di due abilità principali: leggere delle affermazioni e rispondere a domande ad esse collegate. Costruiremo il programma con una struttura a
comando che utilizza quattro comandi:
cerca
Riceve domande e stampa tutte le affermazioni rilevanti
inserisci
Legge delle affermazioni
stampa
Stampa tutte le affermazioni memorizzate
esci
Termina l’esecuzione del programma
È ancora prematuro preoccuparsi del codice finale, ma, data la conoscenza degli argomenti trattati nei capitoli precedenti, immaginiamo un pulsante (così come varie etichette e
TextField ) per ciascuno di questi quattro comandi. Graficamente, l’applicazione database
è stata decomposta in quattro sottoproblemi (Figura 4.2).
Esaminiamo ora i tre sottoproblemi non banali: Inserisci, Cerca e Stampa. Consideriamo la funzione Inserisci. Si deve innanzitutto creare una nuova locazione in memoria,
dopodiché si memorizza in tale posizione l’affermazione appena letta:
Funzione Inserisci
Trova una nuova locazione in memoria.
Leggi una nuova affermazione e memorizzala in tale posto.
Anche la funzione Stampa è semplice:
Funzione Stampa
Per ogni locazione di memoria che contiene una affermazione, stampane il contenuto.
La funzione Cerca leggerà inizialmente la domanda dell’utente. Poi controllerà ogni affermazione memorizzata e stamperà quelle affermazioni che aiutino a fornire una risposta
alla domanda. Per esempio, quando l’ispettore ha richiesto informazioni riguardo a Mr.
Mason, il programma ha stampato tutte le affermazioni che citavano Mr. Mason.
Funzione Cerca
Leggi la domanda dell’utente.
Per ogni locazione di memoria contenente una affermazione,
se tale affermazione aiuta a rispondere alla domanda, stampala.
L’ultima parte della funzione Cerca sembra complicata poiché non sappiamo quando una
affermazione “aiuta a rispondere alla domanda.” Decomponiamo allora ulteriormente
%LHUPDQQSGI
4.2 La programmazione top-down ed una applicazione database
125
Figura 4.3
questo processo, e creiamo un comparatore domanda-affermazione, funzione che esaminerà la domanda e l’affermazione, restituendo “risponde” oppure “no” a seconda del
fatto che l’affermazione risponda o meno alla domanda. Utilizzando questa funzione, la
funzione Cerca è ora abbastanza semplice:
Funzione Cerca
Leggi la domanda dell’utente.
Per ogni locazione di memoria contenente una affermazione,
chiama il comparatore domanda-affermazione.
Se il comparatore restituisce “risponde”, allora
stampa l’affermazione.
Naturalmente, il comparatore domanda-affermazione potrebbe essere una funzione
complessa, ma questo è un problema da esaminare in seguito.
Il programma database è dunque evoluto nello stato mostrato in Figura 4.3. Una volta
scoperto il modo per programmare il comparatore domanda-affermazione, il programma
sarà ridotto ad una serie di semplici sottoproblemi. Infatti, esaminando il risultato della
decomposizione si nota immediatamente come il programma database, apparentemente
complicato, sia stato in realtà ridotto a un insieme di semplici componenti. Prima di
dettagliare ulteriormente l’analisi è tuttavia importante considerare attentamente il concetto di sottoprogramma, strumento fondamentale per la realizzazione del processo di
decomposizione.
Esercizio
1. Supponete di voler realizzare un programma che legga nome e grado di istruzione di una serie di uomini e donne e proceda poi accoppiandoli, ponendo insieme
le persone aventi grado di istruzione simile. Il programma dovrebbe stampare la
lista delle coppie. Come dovrebbero essere rappresentate le informazioni? Mostrate come decomporre questo problema in una serie di sottoproblemi facilmente
programmabili.
%LHUPDQQSGI
126
Programmazione top-down
4.3
Sottoprogrammi
Un sottoprogramma è una sequenza di istruzioni finalizzate al conseguimento di un stesso
obiettivo. Ogni sottoprogramma è individuato da un nome che viene utilizzato per farvi
riferimento. In Java abbiamo già incontrato i sottoprogrammi: sono chiamati metodi.
I termini subroutine, sottoprogramma, metodo, funzione e procedura sono considerati
sinonimi da molti programmatori. Presentiamo di seguito un esempio di definizione di un
metodo chiamato ScrittoDa . Supponendo che i TextField m1 ed m2 siano già stati impostati
all’avvio dell’applet, il sottoprogramma stampa un messaggio con il nome dell’autore:
void S c r i t t o D a ( )
{
m1 . s e t T e x t ( " Q u e s t o programma e ’ s t a t o s c r i t t o da " ) ;
m2 . s e t T e x t ( " Lady Emily Dunsmore " ) ;
}
Questo metodo svolge il compito di stampare le due righe mostrate. Il programmatore
che vuole stampare queste due righe, non deve far altro che includere l’istruzione
ScrittoDa ();
per invocare (o chiamare) l’esecuzione di tale metodo. Supponiamo che Lady Emily abbia
scritto il programma DataBase ed abbia deciso che, ogni volta che il programma viene
utilizzato, esso debba iniziare stampando queste due righe e debba terminare stampandole
di nuovo. Se la sua applet originaria avesse la seguente forma:
public c l a s s DataBase extends j a v a . a p p l e t . Applet
implements A c t i o n L i s t e n e r
{
// dichiarazioni
public void i n i t ( )
{
/ / codice di i n i z i a l i z z a z i o n e
}
public void actionPerformed ( ActionEvent evento )
{
Object causa = evento . getSource ( ) ;
/ / codice di r i s p o s t a per I n s e r i s c i
/ / c o d i c e d i r i s p o s t a p e r Stampa
/ / c o d i c e d i r i s p o s t a p e r Cerca
/ / codice di r i s p o s t a per Esci
}
}
la nuova versione potrebbe essere codificata in questo modo:
%LHUPDQQSGI
4.3 Sottoprogrammi
127
public c l a s s DataBase extends j a v a . a p p l e t . Applet
implements A c t i o n L i s t e n e r
{
T e x t F i e l d m1 , m2 ;
Button bEsci ;
//
altre dichiarazioni
public void i n i t ( )
{
m1 = new T e x t F i e l d ( 7 0 ) ;
m2 = new T e x t F i e l d ( 7 0 ) ;
b E s c i = new B u t t o n ( " E s c i " ) ;
/ / altro codice di i n i z i a l i z z a z i o n e
ScrittoDa ( ) ;
/ / Qui v i e n e u t i l i z z a t o
i l metodo S c r i t t o D a .
}
public void actionPerformed ( ActionEvent evento )
{
Object causa = evento . getSource ( ) ;
/ / codice di r i s p o s t a per I n s e r i s c i
/ / c o d i c e d i r i s p o s t a p e r Stampa
/ / c o d i c e d i r i s p o s t a p e r Cerca
i f ( c a u s a == b E s c i )
{
S c r i t t o D a ( ) ; / / Qui v i e n e u t i l i z z a t o
}
i l metodo S c r i t t o D a .
}
}
Questo codice invoca ScrittoDa al principio durante l’inizializzazione dell’applet e
poi di nuovo nel metodo actionPerformed , dove viene invocato quando l’utente preme il
pulsante Esci.
In termini di funzionalità offerte dal programma, non abbiamo introdotto nulla di
nuovo. I messaggi vengono mostrati dall’applet durante l’inizializzazione e poi di nuovo
interattivamente in risposta ad una pressione del pulsante.
Ciò che è nuovo è il modo in cui ciò viene realizzato. Definendo il metodo ScrittoDa
prima di utilizzarlo, possiamo invocarlo in qualsiasi istante semplicemente scrivendo
l’istruzione: ScrittoDa (); .
C’è tuttavia un problema con questa nuova versione: essa non mostra la definizione
del metodo. Questa definizione è posta di seguito alle altre dichiarazioni e prima di init :
%LHUPDQQSGI
128
Programmazione top-down
public c l a s s DataBase extends j a v a . a p p l e t . Applet
implements A c t i o n L i s t e n e r
{
T e x t F i e l d m1 , m2 ;
Button bEsci ;
//
altre dichiarazioni
v o i d S c r i t t o D a ( ) / / Qui v i e n e d e f i n i t o i l metodo S c r i t t o D a .
{
m1 . s e t T e x t ( " Q u e s t o programma e ’ s t a t o s c r i t t o da " ) ;
m2 . s e t T e x t ( " Lady Emily Dunsmore " ) ;
}
public void i n i t ( )
{
m1 = new T e x t F i e l d ( 7 0 ) ;
m2 = new T e x t F i e l d ( 7 0 ) ;
b E s c i = new B u t t o n ( " E s c i " ) ;
/ / altro codice di i n i z i a l i z z a z i o n e
S c r i t t o D a ( ) ; / / Qui v i e n e u t i l i z z a t o i l metodo S c r i t t o D a .
}
public void actionPerformed ( ActionEvent evento )
{
Object causa = evento . getSource ( ) ;
/ / codice di r i s p o s t a per I n s e r i s c i
/ / c o d i c e d i r i s p o s t a p e r Stampa
/ / c o d i c e d i r i s p o s t a p e r Cerca
i f ( c a u s a == b E s c i )
{
S c r i t t o D a ( ) ; / / Qui v i e n e u t i l i z z a t o
}
i l metodo S c r i t t o D a .
}
}
Il programma esegue le istruzioni nel seguente ordine: la computazione ha inizio con
il metodo init , le cui istrizioni vengono eseguite in sequenza. L’ultima è l’istruzione ScrittoDa (); . Questa provoca l’esecuzione in sequenza delle istruzioni del metodo
ScrittoDa .
Dopo che ScrittoDa è stato completamente eseguito, viene terminata questa prima
chiamata a ScrittoDa e si ritorna ad eseguire le eventuali successive istruzioni del metodo
init (ma non ve ne sono). Terminata l’inizializzazione, il programma attenderà qualche
%LHUPDQQSGI
4.3 Sottoprogrammi
129
Figura 4.4
evento, come la pressione di un pulsante.
In seguito alla pressione del pulsante Esci, il computer eseguirà sequenzialmente le
istruzioni del metodo actionPerformed , una alla volta. Alla fine giungerà all’istruzione
if , in cui stabilirà che è stata la pressione del pulsante Esci a causare l’esecuzione di
actionPerformed . Quindi eseguirà di nuovo ScrittoDa (); , provocando dunque l’esecuzione
sequenziale delle istruzioni ivi contenute.
Questi eventi sono illustrati in Figura 4.4, in cui la linea spessa indica l’esecuzione e
la linea sottile indica i salti. Il percorso mostra le due chiamate ed i conseguenti salti al
corpo del metodo.
I sottoprogrammi sono di grande aiuto ai programmatori per due motivi. Innanzitutto,
forniscono un modo per evitare di scrivere le stesse righe più e più volte. Se bisogna
svolgere lo stesso compito due volte, come nel programma DataBase, o più volte, come
in altri programmi, non è necessario digitare ripetutamente le stesse righe di codice. Le
istruzioni sono scritte soltanto una volta all’interno della definizione del sottoprogramma
e, quando desiderato, è possibile invocare questo codice scrivendo semplicemente il nome
del sottoprogramma. Poiché il programma globale sarà più breve, esso richiederà meno
%LHUPDQQSGI
130
Programmazione top-down
memoria del computer e sarà quindi meno avido di risorse. Inoltre, dato che i sottoprogrammi possono essere lunghi anche decine di righe di codice ed essere invocati molto
frequentemente, tale risparmio può essere notevole.
Il secondo motivo per cui i sottoprogrammi sono utili è ancor più importante del
primo. Essi consentono al programmatore di isolare un compito specifico dagli altri e
semplificare dunque la risoluzione del problema. Per esempio, programmare la funzione
Cerca nell’applicazione DataBase può sembrare un problema abbastanza complesso. Il
programma deve ricevere una domanda dall’utente e poi scrivere tutte le affermazioni che
contribuiscono alla risposta. In realtà, possiamo semplificare grandemente il problema
isolando parte del lavoro in un sottoprogramma da scrivere successivamente. Digitiamo
dunque il codice che riceve la domanda e poi, per ogni affermazione, utilizziamo un sottoprogramma, il comparatore domanda-affermazione, per controllare se l’affermazione fa
parte della risposta. Se è così, il codice stamperà tale affermazione:
Funzione Cerca
Leggi la domanda dell’utente.
Per ogni locazione di memoria contenente una affermazione,
chiama il comparatore domanda-affermazione.
Se il comparatore restituisce “risponde”, allora
stampa l’affermazione.
Spostando parte del lavoro nel sottoprogramma del comparatore domanda-affermazione,
possiamo efficacemente scrivere parte del codice per risolvere il problema. Più avanti
potremo concentrare le nostre energie su questo specifico sottoprogramma. Avendo diviso
il problema in vari pezzi possiamo sperare che ognuno di questi sia piccolo abbastanza da
essere programmato facilmente.
Potrebbe risultare comodo modificare il metodo ScrittoDa in modo tale che stampi
qualsiasi nome e non soltanto quello di Lady Emily. Ecco le necessarie modifiche:
v o i d S c r i t t o D a 2 ( S t r i n g nome )
{
m1 . s e t T e x t ( " Q u e s t o programma e ’ s t a t o s c r i t t o da " ) ;
m2 . s e t T e x t ( " " + nome ) ;
}
La nuova variabile di tipo String , nome, fa riferimento al nome che desideriamo stampare,
che potrebbe essere una qualunque sequenza di caratteri. La variabile nome è chiamata
parametro ed è identificata come tale quando viene posta all’interno delle parentesi nella
definizione del metodo, dopo il suo nome ScrittoDa2 . (Abbiamo già incontrato i parametri
in precedenza, come evento , in public void actionPerformed (ActionEvent evento) .)
La variabile nome è differente da una variabile ordinaria di un programma, poiché ai
parametri dichiarati nel modo ora mostrato non viene assegnata una propria locazione
%LHUPDQQSGI