• Non ci sono risultati.

1.2 Hello World!

7.1.1 Requisiti

Obiettivo dell’esercizio è progettare e realizzare un insieme di classi che consentano di:

• contare il numero di suoni emessi per tipologia di strumento musicale (ossia per ogni nome di classe che implementa l’interfaccia

MusicalInstrument

,

• emettere su standard error un messaggio di log contenente il nome del tipo per ogni suono emesso.

Obiettivi

Tale obiettivo deve essere raggiunto usando il design pattern denominato Observer; più in det-taglio, è richiesta la realizzazione, nel package it.unimi.di.prog2.lab07, delle seguenti classi:

1. La Factory

MusicalInstrumentFactory

che implementa l’interfaccia AbstractInstru-mentFactory e permette di creare strumenti musicali nella loro versione “normale” (se necessario adattata per renderla compatibile con

MusicalInstrument

).

2. La Factory

CountedInstrumentFactory

che implementa l’interfaccia

AbstractInstr

umentFactory

e permette di creare strumenti musicali nella loro versione “contata” (vedi 6.1.1).

3. Il Decorator

ObservableInstrument

che implementa l’interfaccia

MusicalInstrument

e svolge il ruolo di Subject nel pattern Observer e decora il

MusicalInstrument

passato al suo costruttore in modo che l’invocazione del metodo

play

possa essere osservata.

4. L’Observer

InstrumentLoggerObserver

in modalità PULL che emette sullo standard error, per ogni invocazione di

play

, il nome della classe sulla cui istanza osservata è stato invocato;

5. L’Observer

InstrumentCounterObserver

in modalità PUSH che ha un metodo con signature public int getCount(final

String name)

che, dato un nome di classe come argomento, restituisce il numero di volte per cui

play

è stato invocato su istanze osservate di tale classe e un metodo con signature public void resetCount() che azzera tutti i conteggi;

6. La Factory

ObservedInstrumentFactory

che implementa l’interfaccia

AbstractInst

rumentFactory

e il cui costruttore accetta una lista di osservatori (entrambi i tipi) di cui sopra e costruisca le istanze di vari tipi di strumenti opportunamente avvolte dal Decorator

ObservableInstrument

e poste sotto osservazione dagli Observer passati al costruttore.

Suggerimenti

Nell’implementazione delle classi sopra specificate può essere utile tenere conto dei seguenti suggerimenti:

1. la classe

ObservableInstrument

è bene che decori il metodo

play

usando almeno il metodo

notifyObservers

necessario a rendere le invocazioni osservabili dai due Observer da implementare;

2. la classe di cui un oggetto è istanza può essere ottenuta invocando

getClass

, data una classe è possibile ottenere il suo nome invocando

getSimpleName

detto altrimenti, dato un oggetto definito come

AClass anObject

= new

AClass()

, l’espressione

anObjec

t.getClass().getSimpleName()

ha valore "AClass"

3. la classe

InstrumentLoggerObserver

emetterà i messaggi invocando opportuni metodi di

System.err

.

4. la classe

InstrumentCounterObserver

può usare ad esempio una implementazione di

Map

(di tipo specifico

Map<String,Integer>

) per tener traccia del numero di invocazioni per classe.

7.2 Soluzioni

Iniziamo importando il progetto (in modalità Gradle) che contiene, nelpackage

it.unimi.d

i.prog2.lab06una possibile soluzione degli esercizi del Capitolo 6.

La Factory MusicalInstrumentFactory che implementa l’interfaccia AbstractInstrument-Factory e permette di creare strumenti musicali nella loro versione “normale” (se necessario adattata per renderla compatibile conMusicalInstrument).

L’interfaccia AbstractInstrumentFactory è già data nel codice dell’esercizio:

import it.unimi.di.prog2.lab06.MusicalInstrument;

interface AbstractInstrumentFactory { MusicalInstrument createTrumpet();

MusicalInstrument createHorn();

MusicalInstrument createWaterGlass();

MusicalInstrument createIronRod();

}

Partiamo (secondo un approccio test driven, vedi6.2) come di consueto da un test. Avremmo quattro metodi da implementare, ma possiamo fare un unico test perché si tratta di metodi davvero banali: altrimenti rischieremmo di avere codice di test più complicato di quello da verificare, una situazione da evitare.

7.2 Soluzioni

import static org.junit.Assert.assertEquals;

import it.unimi.di.prog2.lab06.Orchestra;

import org.junit.Test;

public class InstrumentFactoryTest {

@Test

public void testMusicalIInstrumentFactory() {

AbstractInstrumentFactory factory = new MusicalInstrumentFactory();

Orchestra orchestra = new Orchestra();

orchestra.add(factory.createTrumpet());

orchestra.add(factory.createHorn());

orchestra.add(factory.createWaterGlass());

orchestra.add(factory.createIronRod());

assertEquals("pepepe\npapapa\ndiding\ntatang", orchestra.play());

} }

Come previsto l’implementazione è quasi banale: l’unica attenzione è per gli oggetti che necessitano di adattamento.

import it.unimi.di.prog2.lab06.GermanInstrument.IronRod;

import it.unimi.di.prog2.lab06.*;

public class MusicalInstrumentFactory implements AbstractInstrumentFactory {

@Override

public MusicalInstrument createTrumpet() { return new Trumpet();

}

@Override

public MusicalInstrument createHorn() { return new Horn();

}

@Override

public MusicalInstrument createWaterGlass() { return new WaterGlassInstrument();

}

@Override

public MusicalInstrument createIronRod() {

return new GermanMusicalInstrument(new IronRod());

} }

La FactoryCountedInstrumentFactoryche implementa l’interfaccia AbstractInstrum entFactorye permette di creare strumenti musicali nella loro versione “contata”.

Anche in questo caso un solo test è sufficiente.

@Test

public void testCountedInstrumentFactory() {

AbstractInstrumentFactory factory = new CountedInstrumentFactory();

Orchestra orchestra = new Orchestra();

orchestra.add(factory.createTrumpet());

orchestra.add(factory.createHorn());

orchestra.add(factory.createWaterGlass());

orchestra.add(factory.createIronRod());

assertEquals("pepepe\npapapa\ndiding\ntatang", orchestra.play());

assertEquals(4, MusicalInstrumentCounter.getCount());

}

@Before

public void setUp() {

MusicalInstrumentCounter.resetCount();

}

L’implementazione è anche in questo caso immediata.

import it.unimi.di.prog2.lab06.GermanInstrument.IronRod;

import it.unimi.di.prog2.lab06.*;

public class CountedInstrumentFactory implements AbstractInstrumentFactory {

@Override

public MusicalInstrument createTrumpet() {

return new MusicalInstrumentCounter(new Trumpet());

}

@Override

public MusicalInstrument createHorn() {

return new MusicalInstrumentCounter(new Horn());

}

@Override

public MusicalInstrument createWaterGlass() {

return new MusicalInstrumentCounter(new WaterGlassInstrument());

}

@Override

public MusicalInstrument createIronRod() {

return new MusicalInstrumentCounter(new GermanMusicalInstrument(new IronRod()));

} }

O, meglio, per comunicare in maniera più evidente che questa Factory costruisce specifica-tamente

MusicalInstrumentCounter

e non dei generici

MusicalInstrument

. L’operazione è compatibile con l’annotazione@Override dato che

MusicalInstrumentCounter

è un sot-totipo di

MusicalInstrument

: trattandosi del tipo del valore di ritorno stiamo rispettando pienamente il principio di Liskov che per la sostituibilità richiede post-condizioni uguali o più stringenti; non sarebbe così se cambiassimo un parametro di un metodo: in questo caso restrin-geremmo una pre-condizione (il tipo che si aspetta il metodo) e perderemmo la garanzia della sostituibilità secondo Liskov (infatti l’operazione in Java è vietata, o meglio, non è compatibile con l’annotazione@Override).

import it.unimi.di.prog2.lab06.GermanInstrument.IronRod;

import it.unimi.di.prog2.lab06.*;

public class CountedInstrumentFactory implements AbstractInstrumentFactory {

7.2 Soluzioni

@Override

public MusicalInstrumentCounter createTrumpet() { return new MusicalInstrumentCounter(new Trumpet());

}

@Override

public MusicalInstrumentCounter createHorn() { return new MusicalInstrumentCounter(new Horn());

}

@Override

public MusicalInstrumentCounter createWaterGlass() {

return new MusicalInstrumentCounter(new WaterGlassInstrument());

}

@Override

public MusicalInstrumentCounter createIronRod() {

return new MusicalInstrumentCounter(new GermanMusicalInstrument(new IronRod()));

} }

Il pattern “Factory” ha proprio lo scopo di rendere omogenea la creazione degli oggetti di una stessa “famiglia” in modo da poterli usare automaticamente in maniera coerente, senza dover curare le modalità di creazione: usando un oggetto

MusicalInstrumentFactory

siamo sicuri che tutti i

MusicalInstrument

creati saranno di tipo “normale”, usando un oggetto

CountedInstrumentFactory

avremo invece sempre dei

MusicalInstrumentCounter

.

Il Decorator ObservableInstrument che implementa l’interfaccia MusicalInstrumente svolge il ruolo di Subject nel pattern Observer e decora il MusicalInstrument passato al suo costruttore in modo che l’invocazione del metodo playpossa essere osservata.

In questo caso per procedere con un approccio test driven è utile sfruttare librerie di mocking, perché le funzionalità richieste necessitano l’interazione di diverse classi. Utilizziamomockito, già

inserito fra le dipendenze dei test nel build.gradle (

testCompile

"org.mockito:mockito-core:2.+").

Subject

e

Observer

saranno chiaramente due interfacce, senza funzionalità concrete: non c’è quindi nulla da verificare, se non la coerenza sintattica delle signature. Possiamo già scriverle.

public interface Subject {

void registerObserver(Observer o);

void removeObserver(Observer o);

void notifyObservers();

}

public interface Observer {

void update(Subject s, Object state);

}

La vera funzionalità che vogliamo realizzare (e quindi verificare) è in

ObservableInstru

ment

: affinché funzioni correttamente come un

Subject

deve essere possibile “abbonare” un

Observer

(con il metodo

registerObserver

), il quale a questo punto riceve una notifica per ogni chiamata di

play

; togliendo l’osservatore dall’elenco degli “abbonati”, non c’è più alcuna notifica.

Dato che non abbiamo ancora implementato nessun

Observer

(e comunque in questo test non siamo interessati a verificare la correttezza degli

Observer

) usiamo un double object (creato con

Mockito.mock

) sul quale andremo a verificare la notifica (con

Mockito.verify

). Si noti che l’interfaccia

Observer

è sufficiente per creare il mock object, non serve avere un’implementazione concreta.

Volendo verificare anche l’effettiva chiamata di

notifyObservers

per ogni

play

, abbiamo bisogno di uno spy object (altrimenti le chiamate ai metodi dell’oggetto non vengono tracciate da mockito).

@Test

public void testObservableInstrument() {

ObservableInstrument instrument = Mockito.spy(new ObservableInstrument(new Trumpet()));

Observer obs = Mockito.mock(Observer.class);

instrument.registerObserver(obs);

public class ObservableInstrument implements MusicalInstrument, Subject { private List<Observer> observers = new ArrayList<>();

public ObservableInstrument(MusicalInstrument instrument) { this.instrument = instrument;

}

private MusicalInstrument instrument;

@Override

public String play() {

String result = instrument.play();

notifyObservers();

return result;

}

@Override

public void registerObserver(Observer o) { observers.add(o);

}

@Override

public void removeObserver(Observer o) {

7.2 Soluzioni

observers.remove(o);

}

@Override

public void notifyObservers() {

for (Observer observer : observers) {

observer.update(this, getInstrumentName());

} }

public String getInstrumentName() {

return instrument.getClass().getSimpleName();

}

}

L’ObserverInstrumentLoggerObserverin modalità PULL che emette sullo standard error, per ogni invocazione diplay, il nome della classe sulla cui istanza osservata è stato invocato.

Dato che ci viene chiesto di scrivere sullo standard error (cioè

System.err

, generalmente associato al terminale da cui opera l’utente) è utile usare una libreria di test per catturare ciò che viene scritto sullo stream

System.err

in una

String

. A questo scopo build.gradle con-tiene già una dipendenza dei test dalla libreria

com.github.stefanbirkner:system-rules

. Basterà quindi aggiungere ai test una @Ruleappropriata.

@Rule

public final SystemErrRule stderr = new

SystemErrRule().enableLog().muteForSuccessfulTests();

In questo modo ciò che viene scritto sullo standard error (per esempio con

System.err.p

rintln()) è disponibile come stringa eseguendo

stderr.getLog()

(oltre che, nel caso di test falliti, sullo standard error).

Possiamo quindi scrivere un test che traduca l’obiettivo atteso.

@Test

public void testInstrumentLoggerObserver() {

ObservableInstrument instrument = new ObservableInstrument(new Trumpet());

InstrumentLoggerObserver obs = new InstrumentLoggerObserver();

instrument.registerObserver(obs);

instrument.play();

assertEquals("Trumpet\n", stderr.getLog());

}

Implementiamo l’

Observer

in modalità PULL, cioè traendo l’informazione che ci interessa direttamente dal

Subject

.

public class InstrumentLoggerObserver implements Observer {

@Override

public void update(Subject s, Object state) { if (s instanceof ObservableInstrument) {

String name = ((ObservableInstrument) s).getInstrumentName();

System.err.println(name);

} } }

L’ObserverInstrumentCounterObserver in modalità PUSH che ha un metodo con signa-ture public int getCount(final String name) che, dato un nome di classe come argo-mento, restituisce il numero di volte per cui play è stato invocato su istanze osservate di tale classe e un metodo con signaturepublic void resetCount()che azzera tutti i conteggi.

Quello che si vuole ottenere è:

@Test

public void testInstrumentCounterObserver() {

ObservableInstrument instrument = new ObservableInstrument(new Trumpet());

ObservableInstrument anotherInstrument = new ObservableInstrument(new Horn());

InstrumentCounterObserver obs = new InstrumentCounterObserver();

instrument.registerObserver(obs);

anotherInstrument.registerObserver(obs);

instrument.play();

anotherInstrument.play();

instrument.play();

assertEquals(2, obs.getCount("Trumpet"));

assertEquals(1, obs.getCount("Horn"));

assertEquals(0, obs.getCount("Violin"));

obs.resetCount();

assertEquals(0, obs.getCount("Trumpet"));

assertEquals(0, obs.getCount("Horn"));

}

L’implementazione PUSH, che cioè usa il secondo parametro dell’

update

, che dovrà quindi contenere l’informazione desiderata. Per tenere traccia delle varie sonate usiamo una

HashMap

che mantenga i contatori per ogni classe.

import java.util.HashMap;

import java.util.Map;

public class InstrumentCounterObserver implements Observer { private Map<String, Integer> counters = new HashMap<>();

@Override

public void update(Subject s, Object state) { if (state instanceof String) {

counters.put((String) state, counters.getOrDefault(state, 0) + 1);

} }

public int getCount(final String name) { return counters.getOrDefault(name, 0);

}

public void resetCount() { counters.clear();

} }

La FactoryObservedInstrumentFactory che implementa l’interfaccia AbstractInstru

mentFactorye il cui costruttore accetta una lista di osservatori (entrambi i tipi) di cui sopra e

7.2 Soluzioni

costruisca le istanze di vari tipi di strumenti opportunamente avvolte dal DecoratorObservab

leInstrument e poste sotto osservazione dagli Observer passati al costruttore.

Quest’ultimo obiettivo diventa un test piuttosto articolato.

@Test

public void testObservableInstrumentFactory() { List<Observer> audience = new ArrayList<>();

InstrumentCounterObserver counter = new InstrumentCounterObserver();

InstrumentLoggerObserver logger = new InstrumentLoggerObserver();

audience.add(counter);

audience.add(logger);

AbstractInstrumentFactory factory = new ObservedInstrumentFactory(audience);

Orchestra orchestra = new Orchestra();

orchestra.add(factory.createTrumpet());

orchestra.add(factory.createHorn());

orchestra.add(factory.createWaterGlass());

orchestra.add(factory.createIronRod());

orchestra.play();

for (String instrument : new ArrayList<String>(

asList("Trumpet", "Horn", "WaterGlassInstrument", "GermanMusicalInstrument"))) {

A questo punto l’implementazione è quasi ovvia.

import it.unimi.di.prog2.lab06.*;

import it.unimi.di.prog2.lab06.GermanInstrument.IronRod;

import java.util.ArrayList;

import java.util.List;

public class ObservedInstrumentFactory implements AbstractInstrumentFactory { private List<Observer> observers = new ArrayList<>();

public ObservedInstrumentFactory(List<Observer> audience) { observers = audience;

}

@Override

public ObservableInstrument createTrumpet() {

ObservableInstrument instrument = new ObservableInstrument(new Trumpet());

for (Observer observer : observers) { instrument.registerObserver(observer);

}

return instrument;

}

@Override

public ObservableInstrument createHorn() {

ObservableInstrument instrument = new ObservableInstrument(new Horn());

for (Observer observer : observers) { instrument.registerObserver(observer);

}return instrument;

}

@Override

public ObservableInstrument createWaterGlass() {

ObservableInstrument instrument = new ObservableInstrument(new WaterGlassInstrument());

for (Observer observer : observers) { instrument.registerObserver(observer);

}

return instrument;

}

@Override

public ObservableInstrument createIronRod() { ObservableInstrument instrument =

new ObservableInstrument(new GermanMusicalInstrument(new IronRod()));

for (Observer observer : observers) { instrument.registerObserver(observer);

}return instrument;

} }

8 Lab08: Temperature

Il punto di partenza di questo laboratorio è disponibile all’indirizzo: https://bitbucket.org/

prog2unimi/p2-lab08-2019.

8.1 Esercizi

8.1.1 Requisiti

Creare un programma Java che permetta di gestire la temperatura di attivazione di un termo-stato. La possibilità di leggere o impostare la temperatura può avvenire tramite diverse viste, in particolare la temperatura viene visualizzata sia in gradi Celsius che Fahrenheit; l’utente può impostare la temperatura in entrambe le scale, tutte le viste devono essere coordinate sulla temperatura di attivazione mostrata all’utente.

8.1.2 Obiettivi

Il progetto deve tener conto dei seguenti requisiti:

• Le viste ci permettono di leggere e di impostare la temperatura usando diverse scale (unità di misura). Sono richieste 2 viste:

1. un TextFieldin gradi Celsius, 2. un TextFieldin gradi Fahrenheit.

Tutte le viste devono essere in grado di osservare un cambiamento di temperatura nel termostato in modo da aggiornarsi ed essere allineate sul valore mostrato all’utente.

• Una componente chiamata

Model

si occupa di mantenere il dato temperatura tramite un doubleche rappresenta la misura in gradi Celsius. È unica e comune a tutte le viste. Il

Model

a fronte di un cambiamento della temperatura, deve occuparsi di notificare tutte le viste. Internamente mantiene le temperature secondo la scala Celsius.

• Una componente chiamata

Controller

sta nel mezzo tra

View

e

Model

ed interpreta l’input dell’utente (l’inserimento di un valore nel

TextField

). Un’istanza di

Controller

(una per ogni vista) a fronte dell’interazione dell’utente deve aggiornare il

Model

con la nuova temperatura appena immessa.

Di seguito viene fornita la definizione di quella che sarà la funzionalità dell’MVC tramite un sequence diagram.

Lo schema riporta le azioni intraprese a fronte di una modifica di una vista:

– l’interfaccia grafica (

JavaFXLoop

) richiama il

Controller

(L’

EventHandler

dell’

ActionEvent

);

– il

Controller

interroga la

View

collegata chiedendo il valore della temperatura;

– la

View

si occupa di tradurlo dalla sua scala di visualizzazione a quella Celsius usata dal

Model

; - il

Controller

chiama il

Model

passando come parametro il nuovo valore della temperatura; - il

Model

aggiorna il proprio stato interno (nuovo valore per la temperatura); - il

Model

notifica tutte le

View

registrate sul

Model

come

Observer

; - ogni

View

chiede al

Model

(modalità PULL) il nuovo valore e aggiorna l’interfaccia grafica dopo averlo convertito nella propria scala.

Suggerimenti

Di seguito si accenna ad alcuni design pattern che risultano essere particolarmente indicati per svolgere l’esercizio:

• L’aggiornamento delle viste a fronte di un cambiamento del Model può essere realizzata attraverso il pattern Observer. Le viste interessate si registrano sul

Model

che è il Subject.

• Per consentire l’utilizzo di diverse scale (bisogna trattare almeno Celsius e Fahrenheit, ma è previsto ce ne saranno altre) per la rappresentazione della temperatura è possibile utilizzare il pattern Strategy.

8.2 Soluzioni

• Ogni Strategy essendo stateless può essere implementata come un Singleton.

• Una

View

potrebbe essere implementata attraverso l’uso di un Composite. Infatti una vista è solitamente una gerarchia di componenti grafici (label, button, ecc.).

Documenti correlati