-
Fabien Delecroix authoredFabien Delecroix authored
TP3 - Observateurs/Observable
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.
Partie 1 : Le patron Observateurs / Observable
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() ».
classDiagram
direction LR
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
class Observer
<<interface>> Observer
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 qui ne posera pas cet inconvénient
- modifier le code d'Observable pour y pallier.
package fr.univlille.iut.r304.tp3.q1;
public abstract class Observable {
public void attach(Observer obs) {
}
public void detach(Observer obs) {
}
protected void notifyObservers() {
}
protected void notifyObservers(Object data) {
}
}
Partie 2 : Application
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 :
classDiagram
class Texte
Texte : +Texte(InputStream)
Texte : +lancer() void
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 :
classDiagram
class Objectif {
#nbRestant : int
#nom : String
}
Implémentez ces classes dans un sous-paquetage q2.
On devra par exemple pouvoir dérouler le scénario suivant (pour y parvenir quelques questions ci-dessous pour vous guider) :
>>>Objectif 1 - nombre de lignes : encore 3<<< Hello >>>Objectif 1 - nombre de lignes : encore 2<<< The answer is 42 >>>Objectif 1 - nombre de lignes : encore 1<<< Sayonara >>>Objectif 1 - nombre de lignes atteint<<< Mission accomplie !fin Fin de la saisie.
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
String text = "abc\ndef\n";
InputStream is = new ByteArrayInputStream(text.getBytes());
Autres objectifs
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 :
>>>Objectif 1 - nombre de lignes : encore 3<<< nombre de e : encore 5 Hello >>>Objectif 1 - nombre de lignes : encore 2<<< >>>Objectif 2 - nombre de e : encore 4<<< The answer is 42 >>>Objectif 1 - nombre de lignes : encore 1<<< >>>Objectif 2 - nombre de e : encore 2<<< Sayonara >>>Objectif 1 - nombre de lignes atteint<<< >>>Objectif 2 - nombre de e : encore 2<<< The end >>>Objectif 2 - nombre de e atteint<<< Rideau !fin Fin de la saisie.
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 ?
Partie 3 : Propriétés observables
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.
Rendez la classe ObservableProperty observable. Outre le fait qu'elle soit observable (attach, detach), cette classe doit avoir une propriété à laquelle on peut accéder :
package fr.univlille.iut.r304.tp3.q3;
public class ObservableProperty {
public Object getValue() {
}
public void setValue(Object val) {
}
...
}
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) :
package fr.univlille.iut.r304.tp3.q3;
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.