Utente:LoStrangolatore/corsojava/ereditarietà

Da Wikiversità, l'apprendimento libero.

Problema[modifica]

Ci viene chiesto di scrivere un programma che prende in ingresso il testo di una e-mail, e determina se questa e-mail è probabilmente spam o è attendibile.

Prima di scrivere il codice, fermiamoci a riflettere. Cosa deve fare questo programma?

  1. Prelevare i dati di una e-mail
  2. analizzarli
  3. restituire un risultato all'utente.

Il primo passo è concettualmente facile da realizzare: bisogna stabilire una connessione con una fonte da cui attingere le e-mail, e da cui prelevare le e-mail. Nella nostra versione semplificata del programma, useremo come "fonte" il terminale, ovvero chiederemo i dati all'utente stesso. In una applicazione reale, probabilmente, stabiliremmo una connessione con il server di posta per scaricare le e-mail, oppure leggeremmo quelle che sono salvate su un file presente sul disco.

Il secondo passo, perché è quello più impegnativo. Cosa richiede? Richiede di svolgere dei controlli sulla mail per determinare se può essere spam. Questo ci pone due domande:

  • Quali parametri della e-mail possono dovranno essere disponibili al programma?
  • Come determinare se questi parametri indicano spam?

La risposta alla prima viene direttamente dalla seconda, e la risposta alla seconda è che oggi sono noti molti metodi di riconoscimento già collaudati: riconoscimento di parole sospette, white e black list, filtri bayesiani, ecc.

L'ultimo passo non sembra porre particolari problemi. Il programma può comunicare il risultato su terminale tramite System.out.println(), oppure visualizzare una interfaccia grafica con scritte e simboli che identifichino il risultato, oppure potrebbe salvarlo su file, e così via.


Ora dobbiamo decidere quali classi ci serviranno.

è indubbio che tutti i parametri delle e-mail vanno messi assieme. Il programma deve avere a disposizione un unico oggetto che dia accesso a tutti i parametri della e-mail. Questo oggetto ha bisogno di una classe che definisca come è fatto e quali operazioni supporta. Questa classe la chiameremo Mail, perché ogni oggetto Mail rappresenta una (e-)mail.

Quindi, il passo 1 di cui sopra consiste nella lettura dei dati della e-mail e nella creazione di un oggetto Mail.

Ma come analizzarli? Beh, esistono molti modi diversi di controllare una e-mail, e tutti seguono uno stesso modello generico: prendi in ingresso la mail e restituisci un responso del tipo "è spam / non è spam". Questo ci suggerisce che possiamo usare lo stesso trucco che abbiamo usato nel caso del cabarettista: diamo al programma la possibilità di creare oggetti che "ufficialmente" fanno la stessa cosa, perché hanno gli stessi metodi, ma nel concreto si comportano in modo diverso quando questi metodi vengono invocati. Nel nostro caso sarà sufficiente un solo metodo che prende in ingresso una e-mail e restituisce un valore del tipo Sì/No.
Quindi scriveremo classi diverse e il programma istanzierà alcuna di queste classi secondo come sarà necessario. Anche il secondo passo è andato.

Implementeremo il terzo passo comunicando con il solito System.out.println(), quindi questo non ci dà particolari problemi.

Mail[modifica]

Definire una classe che rappresenta una e-mail è facile.

// Mail.java

// Attualmente contiene solo due parametri: mittente e testo della e-mail.
// In futuro potranno essere aggiunti ulteriori parametri secondo quanto sarà necessario.
public class Mail {
    
    private final String mittente;
    private final String testo;
    
    public Mail(String mittente, String testo) {
        if (mittente == null || testo == null) throw new NullPointerException();
        
        this.mittente = mittente;
        this.testo = testo;
    }
    
    public String getMittente() { return mittente; }
    public String getTesto() { return testo; }
    
}

In questo esempio ci sono solo due parametri, per semplicità, ma ovviamente si possono includere tutti quelli che si vuole, quindi gli header, gli allegati, ecc.

Il costruttore prende in ingresso gli argomenti e li controlla. Se sono accettabili, prosegue senza problemi e ritorna; altrimenti lancia un'eccezione.

Questi parametri sono restituiti dagli argomenti getXXX().

Filtri[modifica]

Ci servono degli oggetti che apparentemente siano tutti uguali, ma nel concreto abbiano comportamenti diversi. "Comportamenti diversi" vuol dire che questi oggetti dovranno appartenere a classi diverse.

Ricordiamo che sarà sufficiente un solo metodo che prende in ingresso una e-mail e restituisce un valore del tipo Sì/No.

public class FiltroContenutoNonAmmesso {
    
    private final String contenuto;
    
    public FiltroContenutoNonAmmesso(String contenuto) {
        if (contenuto == null) throw new NullPointerException();
        
        this.contenuto = contenuto;
    }
    
    public boolean èSpam(Mail m) {
        if (m.getTesto().contains(contenuto))
            return true;
        else
            return false;
    }
    
}

Questa classe definisce un oggetto che analizza una mail e restituisce un risultato. Questo oggetto lo abbiamo chiamato filtro perché è proprio come un filtro che permette di selezionare le e-mail "buone" da quelle che non lo sono.

Abbiamo anche altri filtri:

public class FiltroWhitelist {
        ... // campi privati, costruttore ecc.
        
        public boolean èSpam(Mail m) { ... }
}
public class FiltroParanoico {
        public boolean èSpam(Mail m) {
            return true; // Tutto potrebbe essere spam!
        }
}


Esercizio: implementa un FiltroContenutiNonAmmessi per un numero arbitrario di stringhe. Le stringhe vengono passate al costruttore in un array e il metodo èSpam le cerca una per una all'interno del testo della e-mail. Se ne trova almeno una, allora considera la e-mail come spam, in caso contrario la considera una e-mail attendibile. Trovi una delle soluzioni possibili in fondo alla pagina.

Programma[modifica]

public class MailChecker {
    public static void main(String[] args) {
        // Passo 1: creo un canale verso il terminale
        // TODO BufferedReader attorno a System.in
        
        // Passo 1: ottengo i dati di una e-mail e creo un oggetto Mail
        System.out.println("Inserisci il testo della e-mail:");
        String testo = in.readLine();
        System.out.println("Inserisci il mittente della e-mail:");
        String mittente = in.readLine();
        Mail mail = new Mail(mittente, testo);
        
        // Passo 2: analizzo i dati della e-mail tramite un oggetto filtro
        FiltroContenutoNonAmmesso filtro = new FiltroContenutoNonAmmesso("xxx");
        boolean spam = filtro.èSpam(mail);
        
        // Passo 3: restituisco risultato all'utente
        if (spam)
            System.out.println("La e-mail è probabilmente spam");
        else
            System.out.println("La e-mail probabilmente non è spam");
        
    }
    
}

Miglioramenti[modifica]

Possiamo migliorare il programma da almeno due punti di vista.

Classe Mail[modifica]

Innanzitutto, il codice che comunica con l'utente per leggere i dati della e-mail non dovrebbe trovarsi nel main(). Cosa succede se un altro programmatore (o noi stessi) vuole riutilizzare la nostra classe per un programma diverso? Dovrà reimplementare il meccanismo che legge i dati della mail anche nel suo programma.
E cosa succede se un giorno vogliamo aggiungere il supporto per gli allegati? Dovremo modificare non solo il costruttore della classe Mail perché accetti ulteriori argomenti, ma anche tutti i programmi che usano quel costruttore e che creano oggetti Mail, e il codice sarà sostanzialmente lo stesso: qualcosa come System.out.println("Inserisci allegati:"); seguito dal codice che li legge. Questo copia-incolla è inaccettabile, perché se un giorno vogliamo cambiare il messaggio che viene stampato a video, oppure ci accorgiamo che queste istruzioni contengono un bug, oppure vogliamo riscriverle per renderle più chiare a chi legge il codice, dobbiamo ricordarci di tutti i punti in cui le abbiamo incollate e modificare di conseguenza, con il rischio di commettere ulteriori errori.

Il problema è che il main() deve svolgere un compito che non gli compete: creare un oggetto Mail a partire dai dati inseriti dall'utente. Invece, qualunque programma che usi la classe Mail dovrebbe essere in grado di comunicare alla classe: Leggi i dati di una e-mail da terminale, e restituisci un oggetto Mail con questi dati. Possiamo farlo:

public class Mail {
    
    public static Mail leggiDaTerminale() {
        System.out.println("Inserisci il testo della e-mail:");
        String testo = in.readLine();
        System.out.println("Inserisci il mittente della e-mail:");
        String mittente = in.readLine();
        
        Mail mail = new Mail(mittente, testo);
        return mail;
    }
    
    private Mail(String mittente, String testo) {
        ... // Il codice di prima
    }
    
    ... // Campi e metodi di istanza: il codice di prima
    
}

Qui abbiamo definito un metodo leggiDaTerminale(). Questo metodo comunica con l'utente e restituisce un oggetto Mail. Il metodo è static perché vogliamo poterlo invocare direttamente sulla classe e non su un oggetto già creato. Quindi possiamo scrivere:

Mail.leggiDaTerminale()

e questo si comporterà come una normale espressione di tipo Mail.

Il costruttore è stato definito come private, perché non ci serve che sia visibile all'esterno. Se un giorno aggiungiamo alla classe il supporto per gli allegati, potremo modificare il costruttore e i campi della classe senza problemi, perché saremo sicuri che non c'è nessuna altra classe che li usa. Le classi che usano il metodo leggiDaTerminale() non dovranno essere modificate.

Ora possiamo snellire il codice della classe MailChecker:

public class MailChecker {
    public static void main(String[] args) {
        // Passo 1: creo un canale verso il terminale
        // TODO BufferedReader attorno a System.in
        
        // Passo 1: ottengo i dati di una e-mail e creo un oggetto Mail
        Mail mail = Mail.leggiDaTerminale();
        
        // Passo 2: analizzo i dati della e-mail tramite un oggetto filtro
        FiltroContenutoNonAmmesso filtro = new FiltroContenutoNonAmmesso("xxx");
        boolean spam = filtro.èSpam(mail);
        
        // Passo 3: restituisco risultato all'utente
        if (spam)
            System.out.println("La e-mail è probabilmente spam");
        else
            System.out.println("La e-mail probabilmente non è spam");
        
    }
    
}

Filtri[modifica]

Supponiamo che il nostro programma abbia avuto successo, e ora l'azienda ci chieda di migliorare il sistema dei filtri. Ci viene chiesto di modificare il programma perché

  1. siano utilizzati più filtri
  2. i filtri siano letti dai file di configurazione che contengono le preferenze utente.

Queste due richieste mettono in luce un problema più generale nel programma: il main() è stato scritto per lavorare con un solo tipo di filtro, cioè FiltroContenutoNonAmmesso. Dovremo aggiungere del codice che accede al file di configuazione del programma, estrae i dati e crea il filtro. Quindi dovremo sostituire in qualche modo la riga che crea il filtro tramite istruzione new affinché restituisca un qualunque filtro letto da file di configurazione.

Si presenta lo stesso problema della classe Mail: è proprio al main() che spetta questo compito? Sarebbe giusto modificare il main() se un giorno volessimo scaricare le definizioni dei filtri da Internet anziché leggerli da file di configurazione? Probabilmente no, perché si ripresenterebbe il problema del copia-incolla nel caso in cui volessimo integrare i filtri anche in un altro programma. Quindi spostiamo questo codice in una classe apposita, la istanziamo, e chiediamo a questo oggetto Crea e restituisci i filtri che dovrò usare.

public class CreatoreDiFiltri {
    
    public FiltroContenutoNonAmmesso creaFiltro() {
        return new FiltroContenutoNonAmmesso("xxx");
    }
    
}
public class MailChecker {
    public static void main(String[] args) {
        // Passo 1: creo un canale verso il terminale
        // TODO BufferedReader attorno a System.in
        
        CreatoreDiFiltri factory = new CreatoreDiFiltri();
 
        // Passo 1: ottengo i dati di una e-mail e creo un oggetto Mail
        Mail mail = Mail.leggiDaTerminale();
        
        // Passo 2: analizzo i dati della e-mail tramite un filtro
        FiltroContenutoNonAmmesso filtro = factory.creaFiltro();
        boolean spam = filtro.èSpam(mail);
 
        // Passo 3: restituisco risultato all'utente
        if (spam)
            System.out.println("La e-mail è probabilmente spam");
        else
            System.out.println("La e-mail probabilmente non è spam");
 
    }
 
}

Una versione diversa del CreatoreDiFiltri potrebbe leggere i parametri da file di configurazione o scaricarli da un dizionario su Internet; ma soprattutto, essa potrebbe essere condivisa tra più programmi.

Prossima lezione

Il creatore di filtri presenta un problema. Cosa succede se vogliamo usare un filtro diverso, ad esempio un FiltroBlackList? Dovremo modificare la classe CreatoreDiFiltri, ma anche tutti i programmi che ne fanno uso, ad esempio il MailChecker. Nella prossima lezione vedremo come fare.

Esercizi[modifica]

Di seguito alcune delle possibili soluzioni agli esercizi presentati (non esiste una unica soluzione "giusta" o "sbagliata").

FiltroContenutiNonAmmessi
public class FiltroParoleNonAmmesse {
    
    private final String[] paroleAmmesse;
    
    public FiltroParoleNonAmmesse(String[] parole) {
        if (parole == null) throw new NullPointerException();
        for(String parola : parole)
            if (parola == null) throw new NullPointerException();
        
        this.paroleAmmesse = parole;
    }
    
    public boolean èSpam(Mail m) {
        String testo = m.getTesto();
        
        for(String parola : paroleAmmesse)
            if (testo.contains(parola))
                return true;
        
        return false;
    }
    
}