"Ciao, Amigo! Oggi ti dirò alcune cose interessanti sulla classe BufferedInputStream, ma cominciamo con « involucri » e un « sacchetto di zucchero »."

"Cosa intendi per «incarto» e «bustina di zucchero»?"

"Queste sono metafore. Ascolta. Quindi..."

Il design pattern «wrapper» (o «decorator») è un meccanismo abbastanza semplice e conveniente per estendere la funzionalità dell'oggetto senza usare l'ereditarietà.

BufferedInputStream - 1

Supponiamo di avere una classe Cat con due metodi: getName e setName:

codice java Descrizione
class Cat
{
 private String name;
 public Cat(String name)
 {
  this.name = name;
 }
 public String getName()
 {
  return this.name;
 }
 public void setName(String name)
 {
  this.name = name;
 }
}
La classe Cat ha due metodi: getName e setName
public static void main(String[] args)
{
 Cat cat = new Cat("Oscar");

 printName(cat);
}

public static void printName(Cat cat)
{
 System.out.println(cat.getName());
}
Un esempio di come potrebbe essere utilizzato.

«Oscar» verrà visualizzato sulla console.

Supponiamo di dover intercettare una chiamata di metodo su un oggetto cat e forse apportare alcune piccole modifiche. Per questo, dobbiamo avvolgerlo nella sua classe wrapper.

Se vogliamo "avvolgere" il nostro codice attorno alle chiamate del metodo su un oggetto, allora dobbiamo:

1) Creare la nostra classe wrapper ed ereditare dalla stessa classe/interfaccia dell'oggetto da avvolgere.

2) Passare l'oggetto da avvolgere al costruttore della nostra classe.

3) Sovrascrivi tutti i metodi nella nostra nuova classe. Richiamare i metodi dell'oggetto avvolto all'interno di ciascuno dei metodi sottoposti a override.

4) Apporta le modifiche che desideri: cambia ciò che fanno le chiamate al metodo, cambia i loro parametri e/o fai qualcos'altro.

Nell'esempio seguente, intercettiamo le chiamate al metodo getName di un oggetto Cat e modifichiamo leggermente il suo valore restituito.

codice java Descrizione
class Cat
{
 private String name;
 public Cat(String name)
 {
  this.name = name;
 }
 public String getName()
 {
  return this.name;
 }
 public void setName(String name)
 {
  this.name = name;
 }
}
La classe Cat contiene due metodi: getName e setName.
class CatWrapper extends Cat
{
 private Cat original;
 public CatWrapper (Cat cat)
 {
  super(cat.getName());
  this.original = cat;
 }

 public String getName()
 {
  return "A cat named " + original.getName();
 }

 public void setName(String name)
 {
  original.setName(name);
 }
}
La classe wrapper. La classe non memorizza alcun dato tranne un riferimento all'oggetto originale.
La classe è in grado di "lanciare" chiamate all'oggetto originale (setName) passato al costruttore. Può anche "catturare" queste chiamate e modificarne i parametri e/oi risultati .
public static void main(String[] args)
{
 Cat cat = new Cat("Oscar");
 Cat catWrap = new CatWrapper (cat);
 printName(catWrap);
}

public static void printName(Cat named)
{
 System.out.println(named.getName());
}
Un esempio di come potrebbe essere utilizzato.

«Un gatto di nome Oscar».
verrà visualizzato sulla console

In altre parole, sostituiamo silenziosamente ogni oggetto originale con un oggetto wrapper, che riceve un collegamento all'oggetto originale. Tutte le chiamate di metodo sul wrapper vengono inoltrate all'oggetto originale e tutto funziona come un orologio.

"Mi piace. La soluzione è semplice e funzionale."

"Ti parlerò anche di un «sacchetto di zucchero». Questa è una metafora piuttosto che un modello di progettazione. Una metafora per la parola buffer e buffering. Cos'è il buffering e perché ne abbiamo bisogno?"

BufferedInputStream - 2

Diciamo che oggi tocca a Rishi cucinare e tu lo stai aiutando. Rishi non è ancora arrivato, ma voglio bere il tè. Ti chiedo di portarmi un cucchiaio di zucchero. Vai nel seminterrato e trovi un sacchetto di zucchero. Puoi portarmi l'intera borsa, ma non mi serve la borsa. Mi basta un cucchiaio. Poi, da bravo robot, prendi un cucchiaio e me lo porti. Lo aggiungo al tè, ma non è ancora abbastanza dolce. E ti chiedo di portarmene un altro. Vai di nuovo nel seminterrato e porta un altro cucchiaio. Poi arriva Ellie e ti chiedo di portarle dello zucchero... Tutto questo richiede troppo tempo ed è inefficiente.

Rishi arriva, vede tutto questo e ti chiede di portargli una zuccheriera piena di zucchero. Poi Ellie ed io iniziamo a chiedere a Rishi dello zucchero. Ce lo serve semplicemente dalla zuccheriera, e basta.

Quello che è successo dopo che Rishi si è presentato si chiama tampone : la zuccheriera è un tampone. Grazie al buffering, i "client" possono leggere i dati da un buffer in piccole porzioni , mentre il buffer, per risparmiare tempo e fatica, li legge dalla sorgente in grandi porzioni .

"È un bell'esempio, Kim. Capisco perfettamente. La richiesta di un cucchiaio di zucchero è come leggere un byte da un flusso."

"Esattamente. La classe BufferedInputStream è un classico esempio di wrapper bufferizzato. Fa il wrapping della classe InputStream. Legge i dati dall'InputStream originale in blocchi di grandi dimensioni in un buffer, quindi li estrae dal buffer pezzo per pezzo mentre leggere da esso."

"Molto bene. È tutto chiaro. Ci sono buffer per la scrittura?"

"Oh certo."

"Forse un esempio?"

"Immagina un bidone della spazzatura. Invece di uscire ogni volta per mettere la spazzatura in un inceneritore, la butti semplicemente nel bidone della spazzatura. Poi Bubba porta fuori il bidone una volta ogni due settimane. Un classico tampone."

"Che interessante! E molto più chiaro di un sacchetto di zucchero, tra l'altro."

"E il metodo flush() è come portare subito fuori la spazzatura. Puoi usarlo prima che arrivino gli ospiti."