I
principi della Programmazione Strutturata
Il principio fondante della programmazione strutturata
è che sono sufficienti appena tre strutture di
controllo per realizzare qualunque tipo di programma:
la sequenza, l'iterazione e la selezione. Le istruzioni
possibili appartengono a loro volta a tre sole categorie:
input, output ed assegnamento.
Attraverso
la scomposizione modulare del codice, resa possibile
dal costrutto della procedura (quello che in Java chiamiamo
metodo), è possibile suddividere un grosso problema
in sottoproblemi, permettendo un livello più
alto di organizzazione. Ogni procedura può essere
considerata a tutti gli effetti una sorta di estensione
al linguaggio: in altre parole, è possibile applicare
i costrutti fondamentali di sequenza, iterazione e selezione
su ciascuna procedura accessibile da un programma.
Resta
tuttavia aperta una questione: come facciamo a suddividere
un problema grosso in sotto-problemi di dimensioni inferiori?
Esistono delle pratiche che facilitino l'analisi e la
formalizzazione di problemi? Queste pratiche esistono,
e sono oggetto di studio di una scienza, detta "Ingegneria
del Software". Alla luce di quanto spiegato fino
ad ora, ci limiteremo ad analizzare i due approcci più
in uso nella programmazione strutturata: Top Down e
Bottom Up.
Sviluppo
Top-Down
Il principio su cui si fonda l'approccio Top Down è
lo stesso che permise a Cesare di conquistare la Gallia,
riassunto dalla celebre massima "Divide et Impera".
Quando ci si trova di fronte ad un problema troppo complesso
da poter essere affrontato in un colpo solo, l'approccio
migliore è quello di suddividerlo in passaggi
più semplici. Ogni singolo passaggio può
a sua volta essere suddiviso ulteriormente, fino a giungere
a un insieme di sottoproblemi facilmente risolvibile.
Questo modus operandi permette di portare a termine
un problema in modo incrementale, con un procedimento
che minimizza gli sforzi e massimizza i risultati. Proviamo
a vedere come affrontare un problema di una certa complessità
attraverso un approccio Top Down.
Un esempio pratico: Tavole Pitagoriche di dimensione
arbitraria
Si vuole realizzare un programma che, dopo aver chiesto
all'utente un valore compreso tra 10 e 100, calcola
una tavola pitagorica della dimensione specificata,
e quindi la stampa su schermo.
Al
momento di pianificare lo sviluppo di un simile programma,
dobbiamo anzitutto individuare quante e quali sono le
fasi in cui possiamo suddividere il problema. La più
intuitiva risulta essere la seguente:
1.
Richiesta della dimensione della tavola
2. Creazione in memoria della tavola
3. Stampa su schermo della tavola pitagorica
Ognuna
di queste fasi può essere risolta con un metodo
opportuno. Il primo di questi metodi si occuperà
dell'interrogazione utente, e restituirà un intero:
static
int chiediDimensioneTavola() {
}
Il
secondo costruirà una tavola pitagorica della
dimensione specificata in input, e restituirà
in output un vettore bidimensionale, ossia la rappresentazione
in memoria della tavola pitagorica stessa:
static
int[][] creaTavolaPitagorica(int dim) {
}
Infine
il terzo metodo stamperà su schermo il vettore
passato come parametro:
static
void stampaTavolaPitagorica(int[][] t) {
}
Sulla
base delle precedenti dichiarazioni di metodo, possiamo
già formulare il metodo main come segue:
public
static void main(String argv[]) {
int dim = chiediDimensioneTavola();
int[][] tabellina = creaTavolaPitagorica(dim);
stampaTavolaPitagorica(tabellina);
}
Da
un problema grosso, siamo passati a tre problemi piccoli,
ciascuno dei quali viene esemplificato dal metodo corrispondente.
Si noti che il fatto di aver definito le interfacce
di programmazione dei metodi (nome, parametri e il tipo
di ritorno) ci ha già permesso di isolare i sottoproblemi,
riducendo di gran lunga la difficoltà del problema
di ordine superiore. Vediamo ora come risolvere il primo
di questi sottoproblemi, ossia la richiesta di input
all'utente:
static
int chiediDimensioneTavola() {
// Richiede l'inserimento di un valore
String num = JOptionPane.showInputDialog(null,
"Inserisci un numero tra 10 e 100");
// trasforma l'input in un valore numerico
int n = Integer.parseInt(num);
if(n < 10) // verifica il rispetto del
limite inferiore
n = 10;
if(n > 100) // verifica il rispetto del
limite superiore
n = 100;
return n+1; // restituisce il valore maggiorato
di 1
}
La
prima istruzione visualizza su schermo una finestra
di input, che richiede l'inserimento di un numero da
10 a 100. La seconda trasforma l'input in un valore
int: questi due passaggi non possono essere spiegati
in dettaglio al momento attuale, dal momento che richiedono
una certa conoscenza di programmazione Object Oriented.
I due costrutti if seguenti servono a limitare il valore
di input: se l'utente segnala un valore inferiore a
10 o maggiore di 100, esso verrà comunque riportato
entro i limiti stabiliti. Infine, l'istruzione return
restituisce un valore maggiorato di 1, una scelta necessaria
per fare in modo che anche lo 0 sia incluso nella tavola
pitagorica.
Passiamo
ora al problema di creare in memoria la tavola pitagorica
vera e propria:
static
int[][] creaTavolaPitagorica(int dim) {
// crea un vettore bidimensionale
int[][] t = new int[dim][dim];
for ( int i = 0; i < dim; i++)
for ( int j = 0; j < dim;
j++)
// assegna ad ogni
elemento della tavola il suo valore
t[i][j] = i * j;
return t;
}
La
prima istruzione crea un vettore bidimensionale della
dimensione specificata dal parametro dim. Le istruzioni
successive, ricorrendo ad un ciclo for annidato, riempiono
la tavola con i valori corretti. Infine, l'istruzione
return restituisce il vettore appena creato.
Per
terminare il programma, resta solo da formulare l'ultima
funzione, quella che si occupa della stampa:
static
void stampaTavolaPitagorica(int[][] t) {
System.out.print("Tavola Pitagorica
del ");
System.out.println(t.length);
for ( int i = 0; i < t.length; i++) {
for ( int j = 0; j < t.length;
j++) {
System.out.print(t[i][j]);
System.out.print("\t");
}
System.out.println();
}
}
Le
prime due istruzioni stampano a video una semplice intestazione,
mentre i due cicli for innestati stampano a schermo
i valori del vettore, separando ogni valore dal successivo
con un carattere di tabulazione.
L'esempio
presentato ci ha permesso di creare un programma di
una certa complessità attraverso un processo
incrementale, che ci ha permesso di suddividere un problema
grosso in partenza in tre sotto-problemi molto più
semplici. Vediamo ora l'esempio completo:
public
class TavolaPitagorica {
static int chiediDimensioneTavola() {
// Richiede l'inserimento di
un valore
String num = JOptionPane.showInputDialog(null,
"Inserisci un numero tra 10 e 100");
// trasforma l'input in un valore
numerico
int n = Integer.parseInt(num);
if(n < 10) // verifica il
rispetto del limite inferiore
n = 10;
if(n > 100) // verifica il
rispetto del limite superiore
n = 100;
return n+1; // restituisce il
valore maggiorato di 1
}
static int[][] creaTavolaPitagorica(int
dim) {
// crea un vettore bidimensionale
int[][] t = new int[dim][dim];
for ( int i = 0; i < dim; i++)
for ( int j = 0; j < dim;
j++)
t[i][j] = i * j;
// assegna ad ogni elemento il suo valore
return t;
}
static
void stampaTavolaPitagorica(int[][] t) {
System.out.print("Tavola
Pitagorica del ");
System.out.println(t.length);
for ( int i = 0; i < t.length;
i++) {
for ( int j = 0;
j < t.length; j++) {
System.out.print(t[i][j]);
System.out.print("\t");
}
System.out.println();
}
}
public static void main(String argv[]) {
int dim = chiediDimensioneTavola();
int[][] tabellina = creaTavolaPitagorica(dim);
stampaTavolaPitagorica(tabellina);
}
}
Questo
esempio, per quanto sia stato costruito ad hoc, è
una buona dimostrazione di come mettere in pratica un
approccio Top Down per la risoluzione di un problema
reale. Esistono tuttavia dei limiti a questa metodologia,
che è importante chiarire.
Limiti della programmazione Top Down
Un tempo si pensava che l'approccio Top Down fosse la
chiave per affrontare qualunque problema di programmazione.
Ma a partire dagli anni '80 emersero almeno due grossi
problemi: in primo luogo, l'approccio Top Down è
ideale per problemi di dimensioni contenute, risolvibili
con programmi da poche centinaia o migliaia di righe
di codice. Di contro, diventa quasi impraticabile su
programmi di milioni di righe, sempre più comuni
ai giorni nostri.
In
secondo luogo, la programmazione strutturata non offre
un valido supporto alla progettazione di strutture di
dati, uno degli aspetti più importanti della
programmazione. La progettazione di strutture dati con
linguaggi procedurali è un tema talmente complesso
da scoraggiarne la trattazione anche solo in forma superficiale.
Come vedremo, i linguaggi ad oggetti offrono un approccio
molto più intuitivo e naturale all'argomento.
Infine,
la progettazione Top Down presuppone che il programmatore
affronti ogni nuovo problema partendo da zero, cosa
che nella realtà si verifica assai di rado. Pensiamo
ad un architetto che progetta una casa: sebbene ogni
casa sia diversa dalle altre, è anche vero che
in fase di progettazione ciascuna di esse presenta lo
stesso tipo di problematiche. Al momento di progettare
una casa, l'architetto non è costretto ogni volta
a considerare che la casa deve assolvere, tra gli altri,
anche il compito di permettere l'igiene personale, e
da questo dedurre che la soluzione più adatta
sia di costruire una stanza apposta. Al contrario, egli
sa con certezza che ogni casa deve possedere un bagno,
e si limita a prendere delle decisioni sulla disposizione
del bagno in rapporto agli altri locali, o a stabilire
come disporre i vari elementi all'interno del bagno
stesso. In conclusione, l'architetto ricorre ad un approccio
basato sulla conoscenza di determinati schemi progettuali
predefiniti, che vengono raffinati attraverso la composizione
di oggetti preesistenti (come lavandini o la vasche
da bagno). Questo approccio è attuabile ad ogni
livello del progetto, dalla singola camera, all'appartamento
fino ad arrivare a comprendere un intero edificio. A
ciascuno di questi livelli molti requisiti sono ben
noti, e pertanto non è necessario re inventarli.
La
programmazione ad oggetti, come vedremo, è basata
largamente sul principio della composizione e dell'astrazione
delle strutture dati. L'approccio Top Down è
comunque sempre presente, anche se su scala minore,
in determinati passaggi della stesura del codice.
Approccio Bottom Up e Funzioni di Libreria
L'approccio Bottom Up è l'altra metodologia di
sviluppo caratteristica della Programmazione Strutturata.
Il suo obbiettivo è l'opposto di quello Top Down:
invece lavorare sulla scomposizione di un problema sottoproblemi,
ha l'obiettivo di individuare algoritmi utili e di generalizzarli
in modo da creare un insieme di funzioni di libreria.
Ogni
linguaggio che si rispetti comprende per default un
buon numero funzioni di libreria, che coprono le esigenze
più svariate. Java non fa eccezione, ma nonostante
la dimensione di tali librerie, esistono ancora oggi
delle circostanze in cui ha un senso realizzare librerie
per uso personale. La particolarità di una funzione
di libreria è la sua versatilità: essa
non si deve limitare a risolvere un particolare problema,
ma una intera classe di problemi. Un tipico esempio
di funzioni riutilizzabili sono le librerie di funzioni
matematiche: il loro uso spazia dal calcolo scientifico
a quello commerciale, dalla grafica 3D ai videogames.Vediamo
ora qualche esempio pratico, per dimostrare come procedere
alla generalizzazione di un problema al fine di ricavarne
una funzione di libreria.
Valore
assoluto
Il valore assoluto di un numero è pari al numero
stesso, se esso è positivo, o al suo opposto
se è negativo. In altre parole, la funzione Valore
Assoluto prende in input un numero intero positivo o
negativo, e restituisce lo stesso valore privo di segno.
Possiamo facilmente creare una funzione di questo tipo
ricorrendo all'operatore condizionale '?':
static
int abs(int i) {
return i >= 0 ? i : - i;
}
Max
e Min
Max e Min sono altre funzioni facilmente realizzabili
con l'operatore condizionale. Esse ricevono in input
una coppia di valori interi, e restituiscono rispettivamente
il maggiore o il minore dei due:
static
int max(int n1, int n2) {
return n1 >= n2 ? n1 : n2;
}
static
int min(int n1, int n2) {
return n1 <= n2 ? n1 : n2;
}
Fattoriale
Abbiamo già visto come si può realizzare
in Java la funzione fattoriale. Per poterne realizzare
una versione di libreria, dobbiamo essere sicuri che
essa funzioni su qualsiasi input. Dal momento che non
è possibile calcolare il fattoriale di numeri
negativi, adottiamo la convenzione di restituire il
valore '-1' per segnalare la condizione di errore (si
noti tuttavia che questo metopdo non è in grado
di calcolare la funzione fattoriale per valori superiori
a al di sopra del 20: oltre tale soglia il valore calcolato
eccede la capacità di memorizzazione di una variabile
long)
static
long factorial(int i) {
if ( i < 0 )
return - 1; // seganla una condizione
di errore
long fact = 1;
for ( ; i > 0; i--)
fact = fact * i;
return fact;
}
Ordinamento di vettori
Per finire un esempio di difficoltà maggiore
rispetto ai precedenti: una procedura per l'ordinamento
di vettori di interi. Essa riceve in input un vettore
di interi, e lo restituisce ordinato, con i numeri più
bassi all'inizio e i valori più alti alla fine.
Esso scandisce la lista n volte, spostandosi ogni volta
in avanti di un elemento. Ad ogni iterazione, cerca
il numero più basso e lo sposta nella posizione
corrispondente. Al termine dell'operazione, restituisce
il vettore riordinato. Si noti che esistono algoritmi
più efficienti per l'ordinamento: questo esempio
ha voluto privilegiare la chiarezza di esposizione.
Chi fosse interessato ad approfondire la tematica degli
algoritmi di ordinamento, e degli algoritmi in generale,
può consultare la bibliografia.
static
int[] sort(int[] list) {
for(int i = 0 ; i < list.length ; i++
) {
int minIndex = i;
for(int j = i ; j < list.length
; j++) {
if ( list[j] <
list[minIndex] )
minIndex
= j;
}
int tmp = list[i];
list[i] = list[minIndex];
list[minIndex] = tmp;
}
return list;
}
Conclusioni
Abbiamo studiato a fondo i metodi visti come funzioni
matematiche. Abbiamo completato la panoramica sull'uso
base del linguaggio: è finalmente giunto il momento
di introdurre gli oggetti.
Bibliografia
Algoritmi in Java,
R. Sedgewick,
Addison-Wesley Italia, 1993.
Risorse
Scarica qui
i sorgenti presentati nell'articolo
|