Utente:LoStrangolatore/corsojava/programmazione a oggetti

public class Cabaret {
    
    public static void main(String[] args) {
        System.out.println("Un cane dice a una papera:");
        System.out.println("Dove abiti?");
        System.out.println("E la papera risponde:");
        System.out.println("Qua!");
    }
    
}

Cos'ha di brutto questo programma? Nulla. Proviamo a rendere le cose più lunghe:

public class CabaretConScelta {
    
    public static void main(String[] args) {
        //TODO definire uno Scanner su system.in
        
        System.out.println("Cab. > Buonasera, signori e signore.");
        
        do {
            final int numero = Math.random(); //TODO è l'uso giusto?
            switch(numero) {
            case 1:
                    System.out.println("Cab. > Un cane dice a una papera:");
                    System.out.println("Dove abiti?");
                    System.out.println("Cab. > E la papera risponde:");
                    System.out.println("Qua!");
                    System.out.println("Pub. > ...");
            break;
            case 2:
                    // TODO mettere qualcosa di divertente
            break;
            case 3:
                    System.out.println("Cab. > Mi dispiace, ma stasera non ho l'ispirazione.");
            break;
            }
            
            System.out.println("Continuare? (inserire 'e' per uscire)");
        } while(in.nextChar() != 'e');
    }
    
}

Questo programma mette sulla scena un cabarettista finché l'utente non inserisce il carattere 'e'. Qualunque carattere diverso da 'e' non supera il test in.nextChar() != 'e', quindi il programma termina.

Cab. indica il cabarettista, mentre Pub. indica il pubblico. Cos'ha di brutto questo programma? Il fatto che è troppo lungo. Non si coglie a colpo d'occhio il flusso di esecuzione: o si legge attentamente il codice e ci si arriva da soli, oppure si rende necessario un commento aggiuntivo. Inoltre, questo programma è poco manutenibile: se un giorno voglio aggiungere o togliere un dialogo del cabarettista devo ricordarmi di aggiornare la riga che usa Math.random(), perché lo switch è stato scritto per gestire un numero limitato di casi.

Inoltre sarebbe bello permettere all'utente di scegliere un cabarettista migliore, con un set di dialoghi diverso. Magari un set di dialoghi letto da file, oppure il programma potrebbe connnettersi a Internet per scaricarli direttamente dal sito dell'azienda che ci ha commissionato il programma.
Se aggiungessimo tutte queste possibilità direttamente nella classe CabaretConScelta, renderemmo il metodo main() eccessivamente lungo e poco gestibile.


Riflettiamo

modifica

Cosa succede esattamente nel metodo main()?

  1. Presentazione del cabarettista.
  2. Almeno una volta, e finché l'utente non decide di terminare il programma,
    1. scegli un dialogo a caso tra quelli del repertorio del cabarettista;
    2. mettilo in scena (stampa a video).

In questa descrizione appaiono due "attori": l'utente e un "cabarettista" che recita sul palco. In realtà, nel programma non esiste nessun cabarettista, perché le scritte del dialogo sono realizzate tutte tramite System.out.println(). Usiamo System.out.println() per le scenette, così come per le "scritte di servizio" quale Continuare? (inserire 'e' per uscire); dal punto di vista del programma è la stessa cosa, ma per noi non lo è. L'unico attore "reale" resta l'utente, il quale comunica le sue scelte tramite la console.

Il codice del main() svolge due compiti diversi: gestisce il flusso di esecuzione e il set di dialoghi. È naturale pensare che il main() abbia il "diritto" di gestire il primo (in fondo, esiste proprio per questo); tuttavia non gli compete la gestione del secondo. Dovrebbe essere possibile avere un vero e proprio cabarettista al servizio del programma, e potergli chiedere: stampa a video uno dei tuoi dialoghi con una sola istruzione, esattamente come si chiede all'utente: inserisci una lettera tramite in.nextChar().

Si può fare. Scriveremo un automa che è al servizio del metodo main() e che rappresenta un cabarettista.

Separiamo il codice

modifica

Creiamo due file, Cabarettista.java e Cabaret3.java. Nel primo scriviamo:

public class Cabarettista {
    
    public void presentati() {
        System.out.println("Cab. > Buonasera, signori e signore.");
    }
    
    public void unDialogoACaso() {
        final int numero = Math.random(); //TODO correggere con l'uso corretto (controllare il javadoc di Math.random())
        switch(numero) {
        case 1:
                System.out.println("Cab. > Un cane dice a una papera:");
                System.out.println("Dove abiti?");
                System.out.println("Cab. > E la papera risponde:");
                System.out.println("Qua!");
                System.out.println("Pub. > ...");
        break;
        case 2:
                // TODO mettere qualcosa di divertente
        break;
        case 3:
                System.out.println("Cab. > Mi dispiace, ma stasera non ho l'ispirazione.");
        break;
        }
    }
    
}

Nel secondo scriviamo:

public class Cabaret3.java {
    
    public static void main(String[] args) {
        //TODO definire uno Scanner su system.in
        
        Cabarettista cab = new Cabarettista();
        cab.presentati();
        
        do {
            cab.unDialogoACaso();
            
            System.out.println("Continuare? (inserire 'e' per uscire)");
        } while(in.nextChar() != 'e');
    }
    
}

Compiliamo con javac:
javac -cp . *.java

e lanciamo la macchina virtuale:
java -cp . Cabaret

Ora usiamo *.java perché ci sono due file invece che uno (*.java indica qualunque file che abbia estensione .java, quindi il compilatore compilerà sia il file Cabaret3.java, sia il file Cabarettista.java.)

Cabarettista è un tipo personalizzato. I progettisti del linguaggio non sanno che esiste un tipo che si chiama così; ma il nostro programma lo sa, perché lo abbiamo definito noi. Si dice che cabarettista è una classe, perché è stata definita con la parola-chiave class.

Come si è scritto sopra, ora il main() ha a disposizione un automa, un robot, che rappresenta un cabarettista. A livello tecnico, si tratta di un automa a stati finiti. In programmazione si dice che esso è un oggetto.

L'automa "nasce" quando il programma incontra l'istruzione new Cabarettista() e viene allocato in una area di memoria specializzata per gli oggetti e chiamata heap. Resta lì finché necessario, poi viene eliminato in automatico per liberare memoria rendendola disponibile per la creazione di nuovi oggetti.

Se l'oggetto è nello heap, come fa l'applicazione a richiamarlo? Semplice: definisce una variabile e ci infila dentro un reference che punta a quell'oggetto. Il reference è come un link che punta a quell'oggetto. Per intenderci, è l'analogo del puntatore del C e del C++.
Quando dichiariamo la variabile cab di tipo Cabarettista, stiamo dicendo al programma: Dichiaro una variabile, la chiamo cab, e stabilisco fin da ora che essa conterrà solo reference a oggetti di tipo Cabarettista.

L'automa che abbiamo creato riconosce solo due ordini: presentati() e unDialogoACaso(). Questi ordini sono chiamati metodi. Quindi, l'istruzione cab.presentati(); dice all'oggetto: presentati(); e l'oggetto esegue. L'oggetto cerca il metodo public void presentati() nella sua classe, cioè Cabarettista, ed esegue tutte le istruzioni che trova lì una per una. Nel frattempo, il main() aspetta pazientemente. Quando l'oggetto ha finito, il main() prosegue con l'istruzione successiva.

La classe Cabarettista è come il libretto delle istruzioni di cui ogni oggetto di tipo Cabarettista è fornito, e che indica all'automa che cosa deve fare nello specifico.


Questo è un modo di vedere le cose abbastanza astratto. Gli oggetti sono una astrazione a cui, per comodità, attribuiamo l'esecuzione delle istruzioni come se fossero qualcosa di autonomo; ma in realtà, a tempo di esecuzione, esiste solo una entità che esegue le istruzioni, e cioè la macchina virtuale, anche se è concettualmente più comodo (perché più semplice) "fare finta" che non sia così.
Quando la macchina virtuale incontra l'istruzione cab.presentati() fa questo:

  1. legge il contenuto della variabile cab;
  2. riconosce che dentro c'è il reference di un oggetto;
  3. segue il reference, arriva all'oggetto che è nello heap, e
  4. richiama il metodo presentati() su questo oggetto.

Il quarto passo significa

  1. cercare la classe di cui l'oggetto è istanza, quindi la classe Cabarettista;
  2. cercare il metodo presentati();
  3. spostare il flusso di esecuzione all'inizio di questo metodo.

La prima (e unica) istruzione che la VM incontra nel metodo è System.out.println("Cab. > Buonasera, signori e signore."); quindi la esegue. A questo punto, la VM si accorge che non ci sono altre istruzioni nel metodo, quindi la macchina virtuale sposta il flusso di esecuzione dove lo aveva lasciato: nella classe Cabaret3; e prosegue l'esecuzione del main() continuando dall'istruzione successiva a cab.presentati().
Questo succede per ogni invocazione a metodo: ad esempio, ogni volta che la VM incontra l'istruzione cab.unDialogoACaso(), sospende l'esecuzione del main(), salta al metodo unDialogoACaso() della classe Cabarettista, esegue in sequenza tutte le istruzioni, e quando ha finito ritorna alla classe Cabaret3.

Riassumendo

modifica

Un tipo definisce un insieme di valori e un modello generico di comportamento per questi valori. Questo vale per i tipi base così come per le classi.
Ad esempio, sappiamo già che il tipo int definisce i numeri compresi tra (TODO limiti minimo e massimo del tipo int) e un insieme di operatori aritmetici che "fanno qualcosa" con questi numeri.
Per le classi è la stessa cosa: il tipo Cabarettista definisce un insieme di valori che sono chiamati oggetti, e un insieme di operazioni su questi oggetti che sono chiamati metodi. Gli oggetti si creano con la parola-chiave new.

Un programma può creare un numero teoricamente infinito di oggetti; a livello pratico, l'area di memoria che la VM riserva agli oggetti, cioè lo heap, è molto grande, ma non è infinita, quindi si possono creare oggetti fino ad esaurimento di questa zona di memoria.

Veniamo alle operazioni. Così come un operatore numerico prende in ingresso uno o più numeri e restituisce un risultato, così un metodo prende in ingresso zero o più valori, chiamati argomenti, e restituisce un risultato. La differenza fondamentale tra metodi e operatori è la seguente: un metodo viene sempre invocato su un oggetto. La notazione è: oggetto . nomeDelMetodo ( eventuali argomenti ). Ad esempio, cab.unDialogoACaso(). Le parentesi tonde sono obbligatorie.


L'istruzione Cabarettista cab = new Cabarettista(); fa tre cose:

  1. Cabarettista cab dichiara una variabile di tipo Cabarettista;
  2. new Cabarettista() crea un oggetto di tipo Cabarettista;
  3. l'operatore di assegnazione (=) infila nella variabile un riferimento all'oggetto appena creato.

Ampliamo il programma

modifica

Come promesso, volevamo permettere all'utente di scegliere un cabaterrista migliore. Si può fare.

Supponiamo di essere in grado di scrivere del codice che legge un dialogo da file o da connessione Internet. Lo salveremo in un file CabarettistaMigliore.java:

public class CabarettistaMigliore {
    
    public void presentati() {
        System.out.println("Cab. > Buonasera, signori e signore.");
    }
    
    public void unDialogoACaso() {
        ... // Qui c'è del codice che legge un dialogo a caso da connessione Internet o da altra fonte
    }
    
}

In Cabaret3.java dobbiamo modificare solo una riga, quella che crea il cabarettista: invece di
Cabarettista cab = new Cabarettista(); scriveremo CabarettistaMigliore cab = new CabarettistaMigliore();

In questo modo possiamo creare programmi anche molto grandi, ma "a pezzi", e mettere insieme i pezzi che ci servono.

Usi diversi

modifica

Un altro vantaggio della OOP è che possiamo usare il cabarettista anche in un contesto completamente diverso.
Supponiamo che l'azienda che ci ha commissionato il programma voglia ampliare il target dei suoi clienti e punti a coloro che hanno difficoltà visive. Ad esempio ci chiede di creare una versione del programma per supportare i dispositivi per la lettura Braille, oppure per leggere a voce i dialoghi visualizzati.

Se procedessimo come nel caso del CabaretConScelta, dovremmo copiare ed incollare tutto il codice dei dialoghi, e mettere nel main() molto altro codice per la connessione ai dispositivi o alle librerie per la sintesi vocale di quanto sarà scritto sul terminale tramite System.out.println. Il risultato sarebbe ingestibile.
Se usiamo la programmazione a oggetti, possiamo scrivere le diverse versioni del programma in modo che tutte usino la stessa classe Cabarettista, quindi

  • non dovremo copincollare i dialoghi in ogni programma;
  • se vorremo aggiornare il set di dialoghi, dovremo modificare e ricompilare solo la classe Cabarettista.