Newer
Older
Dans ce TP, vous allez vous familiariser avec le patron de conception observateurs/observable.
D'abord, en implémentant sa structure et son mécanisme.
Puis, en utilisant cette couche abstraite pour réaliser un petit programme qui suit la progression d'objectifs sur des saisies utilisateurs.
Pour commencer, cloner ce projet, éventuellement en faisant un fork préalable.
Si vous rencontrez des problèmes sur intelliJ au niveau des tests pour importer Observer ou Observable, supprimez les modules .iml.
La figure suivante vous présente le patron Observateurs (ici « Observer ») / Observable.
L’idée est qu’un ou plusieurs observateurs peuvent s’inscrire auprès d’un Observable et être avertis d’évènements qui se produisent sur ce dernier.
La méthode « update() » des Observateurs est déclenchée par l’Observable chaque fois qu’il veut prévenir d’un évènement.
Il utilise pour ça sa propre méthode « notifyObservers() ».
class Observable
<<Abstract>> Observable
Observable --> "*" Observer : #observers
Observable ..> Observer : notifies
Observable : +attach(Observer) void
Observable : +detach(Observer) void
Observable : #notifyObservers() void
Observable : #notifyObservers(Object) void
Observer : +update(Observable) void
Observer : +update(Observable, Object) void
Compléter la classe abstraite Observable de manière à pouvoir disposer du mécanisme Observateur/Observable.
Vérifiez la validité de votre implémentation à l'aide des tests fournis.
Lors du dernier test, une *ConcurrentModificationException* surviendra peut-être selon votre implémentation.
En effet, si l'observateur se désabonne pendant la boucle de notification, le risque de est modifier la structure sur laquelle on itère.
Pour remédier à ce problème, deux pistes s'offrent à vous :
- [trouver une autre collection](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Collection.html) qui ne posera pas cet inconvénient
- modifier le code d'Observable pour y pallier.
package fr.univlille.iut.r304.tp3.q1;
public void attach(Observer obs) {
}
public void detach(Observer obs) {
}
protected void notifyObservers() {
}
protected void notifyObservers(Object data) {
}
On souhaite implémenter un premier suivi d'objectif simple sur un texte, qui se met à jour à chaque nouvelle ligne saisie sur l'entrée standard.
Nous utiliserons le patron observateurs/observé pour l'implémenter.
On va donc disposer d'une classe Texte, encapsulant un Scanner pour traiter un flux, ici l'enrée standard :
```mermaid
classDiagram
class Texte
Texte : +Texte(InputStream)
Texte --> "1" Scanner : #scan
```
ainsi que d'une classe Objectif, chargée d'afficher au fur et à mesure la progression sur l'objectif donné, ici simplement en terme de lignes à saisir :
On devra par exemple pouvoir dérouler le scénario suivant (pour y parvenir quelques questions ci-dessous pour vous guider) :
<pre>
>>>Objectif 1 - nombre de lignes : encore 3<<<
<b>Hello</b>
>>>Objectif 1 - nombre de lignes : encore 2<<<
<b>The answer is 42</b>
>>>Objectif 1 - nombre de lignes : encore 1<<<
<b>Sayonara</b>
>>>Objectif 1 - nombre de lignes atteint<<<
<b>Mission accomplie</b>
<b>!fin</b>
Fin de la saisie.
</pre>
*En gras, les saisies utilisateurs, les autres chaînes sont affichées par le programme*
*La lecture de texte prend fin lorsqu'on rencontre la chaîne "!fin"*
* Qu'est-ce qui est observable ici ? \
Faîtes hériter d' *Observable* la classe correspondante.
* Quel sera l'observateur ? \
Faîtes implémenter *Observer* à la classe correpsondante.
* En mode *push*, quelle *data* va-t-on récupérer de l'*Observable* qu'on suit lorsqu'il appellera notre méthode *update(Observable o, Object data)* ? \
Faîtes en sorte qu'elle soit bien envoyée par l'observable.
* Dans un *Main*, instancier les différentes classes nécessaires à obtenir le scénario ci-dessus en pensant bien à gérer l'abonnement et le désabonnement de l'observateur à l'observable.
Écrivez des tests qui démontrent le bon fonctionnement de votre implémentation.
Pour ce faire, on va bien sûr éviter d'aller lire sur l'entrée standard...
À la place, on pourra donner un flux d'entrée vers une chaîne de caractères, par exemple
```java
InputStream is = new ByteArrayInputStream(text.getBytes());
On va désormais ajouter d'autres objectifs, sur les mêmes saisies, mais vérifiant un critère plus fin, qu'un caractère donné ait été saisi un certain nombre de fois.
Créer une classe pour ce faire, de manière à mutualiser le plus possible le code avec **Objectif**, en n'hésitant pas à refactoriser au besoin.
Là encore, vous réaliserez un Main et des tests.
Voici un exemple de comportement attendu :
<pre>
>>>Objectif 1 - nombre de lignes : encore 3<<<
<b>Hello</b>
>>>Objectif 1 - nombre de lignes : encore 2<<<
>>>Objectif 2 - nombre de e : encore 4<<<
<b>The answer is 42</b>
>>>Objectif 1 - nombre de lignes : encore 1<<<
>>>Objectif 2 - nombre de e : encore 2<<<
<b>Sayonara</b>
>>>Objectif 1 - nombre de lignes atteint<<<
>>>Objectif 2 - nombre de e : encore 2<<<
<b>The end</b>
>>>Objectif 2 - nombre de e atteint<<<
<b>Rideau</b>
<b>!fin</b>
Fin de la saisie.
</pre>
Construisez 1 ou 2 autres objectifs sur d'autres critères, par exemple avec un nombre de mots d'une taille donnée ou un nombre de majuscules ou chiffres à atteindre.
Quels sont les avantages de se baser sur le patron Observer/Observable ?
JavaFX défini la notion de propriété observable (javafx.beans.value. ObservableValue) avec un mécanisme qui permet à un observateur d’être averti de tout changement d’état (i.e. de valeur) de la propriété (javafx.beans. property.Property).
À ceci s’ajoute les connexions qui permettent à des propriétés de s’observer mutuellement et que l’une réagisse automatiquement aux changement d’état (i.e. de valeur) de l’autre.
Il y a deux types de connexion entre les propriétés : mono ou bi-directionnelle.
La monodirectionnelle propage les changement d’une propriété (« source ») vers l’autre (« destination »), la bidirectionnelle propage les changements de n’importe laquelle des deux propriétés vers l’autre.
Ce sont des mécanismes pratiques mais qui obligent à importer JavaFX ce qui n’est pas toujours souhaitable dans un projet.
Vous devez réimplémenter ces mécanismes des propriétés.
Outre le fait qu'elle soit observable (attach, detach), cette classe doit avoir une propriété à laquelle on peut accéder :
Bien sûr, le fait de changer la valeur de la propriété doit être notifié aux observateurs de celle-ci.
Notez que par simplicité, on utilisera une propriété de type `Object` (donc n'importe quel type).
On aurait aussi pu définir une classe générique avec un type paramétrique pour typer cette propriété.
Vérifier la validité de votre implémentation en exécutant les tests associés à cette question.
Implémentez la classe ConnectableProperty pour qu'elle puisse se connecter de façon mono ou bi-directionnelle (elle aura donc à la fois le rôle d'observateur et d'observable) :
public class ConnectableProperty extends ObservableProperty ... {
public void connectTo(ConnectableProperty other) {
}
public void biconnectTo(ConnectableProperty other) {
}
public void unconnectFrom(ConnectableProperty other) {
}
## Partie 4 : Application avec la conversion de devises
On souhaite réaliser une application de portefeuille gérant la conversion dans différentes monnaies.
Dans un premier temps, on va disposer d'un montant en euros et souhaiter l'afficher aussi en dollars.
Dans un sous-paquetage *q4*, implémentez une classe **Devise** héritant de *ConnectableProperty* de manière à pouvoir instancier deux devises différentes, l'une correspondant aux euros, l'autre aux dollars.
Les valeurs n'étant pas les mêmes dans les deux monnaies, la méthode *void update(Subject other, Object data)* va être à redéfinir de manière à opérer la conversion.
Une fois la classe implémentée, réalisez un *Main* avec 2 devises synchronisées, l'ajoutant ou le retrait d'un montant dans l'une se répercutant automatiquement sur l'autre.
Voici un exemple de scénario :
```
Ajout 20$
18,00 €
20,00 $
Retrait 11€
7,00 €
7,78 $
```
Ajoutez une troisième devise et démontrez la validité de votre solution à l'aide de tests.