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 conMusicalInstrument
).2. La Factory
CountedInstrumentFactory
che implementa l’interfacciaAbstractInstr
⌋umentFactory
e permette di creare strumenti musicali nella loro versione “contata” (vedi 6.1.1).3. Il Decorator
ObservableInstrument
che implementa l’interfacciaMusicalInstrument
e svolge il ruolo di Subject nel pattern Observer e decora ilMusicalInstrument
passato al suo costruttore in modo che l’invocazione del metodoplay
possa essere osservata.4. L’Observer
InstrumentLoggerObserver
in modalità PULL che emette sullo standard error, per ogni invocazione diplay
, 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(finalString name)
che, dato un nome di classe come argomento, restituisce il numero di volte per cuiplay
è 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’interfacciaAbstractInst
⌋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 DecoratorObservableInstrument
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 metodoplay
usando almeno il metodonotifyObservers
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 invocandogetSimpleName
detto altrimenti, dato un oggetto definito comeAClass anObject
= newAClass()
, l’espressioneanObjec
⌋t.getClass().getSimpleName()
ha valore "AClass"3. la classe
InstrumentLoggerObserver
emetterà i messaggi invocando opportuni metodi diSystem.err
.4. la classe
InstrumentCounterObserver
può usare ad esempio una implementazione diMap
(di tipo specificoMap<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 genericiMusicalInstrument
. L’operazione è compatibile con l’annotazione@Override dato cheMusicalInstrumentCounter
è un sot-totipo diMusicalInstrument
: 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 iMusicalInstrument
creati saranno di tipo “normale”, usando un oggettoCountedInstrumentFactory
avremo invece sempre deiMusicalInstrumentCounter
.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
eObserver
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 unSubject
deve essere possibile “abbonare” unObserver
(con il metodoregisterObserver
), il quale a questo punto riceve una notifica per ogni chiamata diplay
; 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 degliObserver
) usiamo un double object (creato conMockito.mock
) sul quale andremo a verificare la notifica (conMockito.verify
). Si noti che l’interfacciaObserver
è sufficiente per creare il mock object, non serve avere un’implementazione concreta.Volendo verificare anche l’effettiva chiamata di
notifyObservers
per ogniplay
, 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 streamSystem.err
in unaString
. A questo scopo build.gradle con-tiene già una dipendenza dei test dalla libreriacom.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 dalSubject
.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 unaHashMap
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. IlModel
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 traView
eModel
ed interpreta l’input dell’utente (l’inserimento di un valore nelTextField
). Un’istanza diController
(una per ogni vista) a fronte dell’interazione dell’utente deve aggiornare ilModel
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 ilController
(L’EventHandler
dell’⌋ActionEvent
);– il
Controller
interroga laView
collegata chiedendo il valore della temperatura;– la
View
si occupa di tradurlo dalla sua scala di visualizzazione a quella Celsius usata dalModel
; - ilController
chiama ilModel
passando come parametro il nuovo valore della temperatura; - ilModel
aggiorna il proprio stato interno (nuovo valore per la temperatura); - ilModel
notifica tutte leView
registrate sulModel
comeObserver
; - ogniView
chiede alModel
(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