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