If THen ELse…

journal de bord d'un aspirant codeur

EDIT: depuis la version 4 du SDK, l’intégration de composants de type Sprite a été simplifié grâce à l’utilisation des SpriteVisualElement. Plus d’informations ici.

Pour ceux qui ont un jour voulu utiliser l’API de dessin dans leur application Flex, l’expérience a dû être déconcertante:
en effet si vous faite un copié/collé des codes donnés en exemple dans le manuel officiel de Flex, vous réaliserez très vite qu’il y a un problème puisque aucune des formes que vous avez codé ne s’affichera.

// red triangle, starting at point 0, 0
triangle.graphics.beginFill(0xFF0000);
triangle.graphics.moveTo(triangleHeight/2, 0);
triangle.graphics.lineTo(triangleHeight, triangleHeight);
triangle.graphics.lineTo(0, triangleHeight);
triangle.graphics.lineTo(triangleHeight/2, 0);
 
// green triangle, starting at point 200, 0
triangle.graphics.beginFill(0x00FF00);
triangle.graphics.moveTo(200 + triangleHeight/2, 0);
triangle.graphics.lineTo(200 + triangleHeight, triangleHeight);
triangle.graphics.lineTo(200, triangleHeight);
triangle.graphics.lineTo(200 + triangleHeight/2, 0);
 
this.addChild(triangle); //l'erreur est ici

La raison est un peu technique: les classes qui permettent de dessiner (Shape et Sprite pour ne pas les nommer) n’implémentent pas IUIComponent, or vous pouvez uniquement insérer dans votre application des composants implémentant cette interface. Si la documentation de Flex n’en parle pas, c’est sans doute parce que les rédacteurs sont partis du principe lorsqu’ils l’ont écrite que vous voulez insérer ces éléments dans un objet de type Stage qui, si j’ai bien tout suivi, est le composant racine d’une application Flash.

Différentes techniques permettent de contourner ce problème. La plus simple consiste à créer un composant UIComponent qui contiendra tous les objets Shape et Sprite que vous désirez, et à l’ajouter comme enfant à votre application ou à un de ses sous-composants.

<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="100%" height="100%" creationComplete="init()">
	<mx:Script>
        <![CDATA[
		import flash.display.Sprite;
 
		public function init():void {
			var triangleHeight:uint = 100;
			var triangle:Sprite = new Sprite();
 
			triangle.graphics.beginFill(0xFF0000);
			triangle.graphics.moveTo(triangleHeight/2, 0);
			triangle.graphics.lineTo(triangleHeight, triangleHeight);
			triangle.graphics.lineTo(0, triangleHeight);
			triangle.graphics.lineTo(triangleHeight/2, 0);
 
 
			triangle.graphics.beginFill(0x00FF00);
			triangle.graphics.moveTo(200 + triangleHeight/2, 0);
			triangle.graphics.lineTo(200 + triangleHeight, triangleHeight);
			triangle.graphics.lineTo(200, triangleHeight);
			triangle.graphics.lineTo(200 + triangleHeight/2, 0);
 
			ui.addChild(triangle);
 
		}
	]]>
	</mx:Script>
 
	<mx:Canvas width="100%" height="100%" backgroundColor="#FF00FF">
		<mx:UIComponent id="ui" width="50%" height="50%" />
	</mx:Canvas>
</mx:Application>

Par défaut, un composant JComboBox ne permet pas de modifier directement les données qu’il contient: ce composant a été crée en suivant le concept de séparation de la vue et du modèle. En conséquence, si vous voulez ajouter ou supprimer des éléments de la liste, il vous faudra d’abord récupérer le modèle du composant. Cela peut être fait grâce à la méthode getModel() de JComboBox.

Par défaut, les JComboBox utilisent pour modèle des instances de la classe DefaultComboBoxModel. Cela suffira dans la plupart des cas, mais il peut parfois être intéressant de construire son propre modèle; il suffira alors d’implémenter l’interface MutableComboBoxModel. Un modèle peut être transmis à la vue facilement en appelant la méthode setModel de vos JComboBox.

Attention: une fois le modèle modifié il faut forcer la réactualisation de la vue en appelant updateUI(). Par ailleurs, je vous conseille de supprimer temporairement les écouteurs que vous aurez éventuellement crée pour capturer les évènement de vos JComboBox avant toute modification de votre modèle, l’affectation d’un nouveau modèle pouvant entraîner une modification de la sélection du JComboBox et donc des erreurs de traitement à l’intérieur des écouteurs. Pour autant ne supprimez pas tous les écouteurs du composant: certains ont été crée automatiquement lors de sa création et sont nécessaire pour assurer son bon fonctionnement.

En utilisant l’astuce figurant sur le site epivoila.
Au moment d’indiquer l’url du module à charger, il suffit d’ajouter le caractère « ? » suivi par les couples paramètre/valeur qui nous intéressent, à la manière d’un GET lorsqu’on envoie des données à un serveur HTML.
Il est possible de récupérer l’url du module en utilisant la variable loaderInfo, et d’extraire les paramètres.

Ceci dit cette méthode n’est pas idéale lorsqu’on souhaite transmettre des données complexes. Il est alors préférable de créer une interface et de la faire implémenter par l’application afin que le module puisse communiquer proprement avec elle.

Un petit programme java rapidement écrit hier soir: il extrait d’une adresse IPv4 au format CIDR des informations telles que l’adresse réseau, l’adresse machine, le nombre de sous-réseaux, la classe etc.
Il s’agit d’une application de mon cours de système et réseau, je ne suis pas un expert en la matière et je ne peux donc pas certifier à 100% de la validité des résultats affichés (disons 95%).

J’ai finalement trouvé un peu de temps libre pour essayer l’une des fonctionnalités les plus intéressantes d’AIR 2: la communication inter-processus.
Pour ce premier essais je vais tenter de lancer un programme java et de communiquer avec ce dernier.

Le code java:

public class JavaTest {
	public static void main(String[] args) {
		DataOutputStream out = new DataOutputStream(System.out);
		try {
			out.writeUTF(args[0]);
			Scanner in = new Scanner(System.in);
			String mot = in.nextLine();
			out.writeUTF("vous avez envoyé le mot \" " + mot + " \"");
			out.flush();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

Rien de bien sorcier: écriture sur la sortie standard de l’argument passé en paramètre du programme, puis lecture sur l’entrée standard d’une chaîne de caractère et confirmation de la réception en sortie. J’ai exporté ce programme en jar et l’ai placé dans le répertoire <application_air>/src/application/JavaTest.jar.

Le code AIR à présent. Il vous faudra bien entendu avoir installé la version beta, ainsi qu’avoir modifié le xml lié à votre application.
La balise application devra ainsi être sous cette forme:
<application xmlns= "http://ns.adobe.com/air/application/2.0beta2″>,
et vous devrez également ajouter cette balise:
<supportedProfiles>extendedDesktop</supportedProfiles>

Une fois que c’est fait, il ne vous reste qu’à écrire votre code:

private var processus:NativeProcess;
 
private function callNativeApp():void{
	// On vérifie d'abord que l'exécution de programmes tiers est permise.
	if(NativeProcess.isSupported) {
 
		// L'objet qui recueille les informations sur le programme qu'on s'apprête à exécuter.
		var lancement:NativeProcessStartupInfo = new NativeProcessStartupInfo();
 
		// La position du programme à exécuter
		lancement.executable = new File("C:/Program Files/Java/jre6/bin/java.exe");
 
		// Les arguments passés
		var args:Vector.<String> = new Vector.<String>();
		args.push("-jar");
		args.push("C:/Flex/CommInterProcessus/src/application/JavaTest.jar");
		args.push("Lancement depuis une application AIR");
		lancement.arguments = args;
 
		// On crée l'objet lié au processus à naître
		processus = new NativeProcess();
 
		// Un écouteur sur les données qui sortiront du processus
		processus.addEventListener(ProgressEvent.STANDARD_OUTPUT_DATA, onOutputData);
 
		// On lance le processus
		processus.start(lancement);
 
		// On écrit une chaîne de caractères sur l'entrée standard du processus
		processus.standardInput.writeMultiByte("test\n", "UTF-8");
	}
	else
		Alert.show("impossible à exécuter", "erreur");
}
 
public function onOutputData(event:ProgressEvent):void {
	// On affiche les données reçues
	Alert.show(processus.standardOutput.readUTF());
}

Les commentaires parlent d’eux même et l’exécution se déroule parfaitement.

Seulement il y a un hic: impossible d’envoyer autre chose que des chaînes de caractères sans générer une erreur.
Ainsi si on modifie notre programme java pour recevoir une valeur numérique codée sur un octet:

int valeur = in.nextByte();
out.writeUTF("vous avez envoyé la valeur \" " + valeur + " \"");

et qu’on modifie en conséquence notre programme AIR pour envoyer cet octet:

processus.standardInput.writeByte(10);

et bien l’octet semble ne jamais être réceptionné par notre application java puisque la sortie qui confirme la réception ne s’affiche pas. Au début j’ai pensé à un bête problème de flush, et j’ai essayé à tout hasard de fermer le flux en entrée juste après avoir envoyé l’octet:

processus.standardInput.writeByte(10);
processus.closeInput();

mais ça a eu pour unique conséquence de générer l’erreur suivante:
Error #2044: Unhandled IOErrorEvent:. text=Error #3218: Error while writing data to NativeProcess.standardInput.

J’ai eu beau chercher sur le net, je n’ai trouvé aucune information satisfaisante là dessus. Mon code est-il à l’origine du problème ou bien la beta porte t-elle bien son nom? J’aimerai bien avoir la réponse avant la Release Candidate donc si quelqu’un passe par ici et qu’il voit ce qui cloche, qu’il n’hésite pas à poster ^^

EDIT: Free ayant modifié le fonctionnement de la procédure de connexion, les informations ci dessous illustrant l’utilisation de la classe HttpsURLConnection ne sont plus d’actualité.
Vous pourrez trouver le code de connexion mis à jour ici.

Les connexions réseau sont très faciles à manipuler en Java, mais lorsqu’on souhaite communiquer des informations à un serveur HTML (pour s’identifier par exemple) les choses se corsent: il n’y a rien de plus barbare que d’écrire des entêtes HTTP à la main.

HttpURLConnection et HttpsURLConnection.
Heureusement il existe une classe, HttpURLConnection, qui simplifie ce travail en manipulant pour nous sockets et écriture d’entêtes. Encore mieux: il existe également une classe HttpsURLConnection qui se charge des connexions sécurisées (pour info cette classe hérite de la première qui elle même hérite de URLConnection, leur utilisation est donc similaire).

Voici comment la classe HttpURLConnection est présentée dans l’API:

Each HttpURLConnection instance is used to make a single request but the underlying network connection to the HTTP server may be transparently shared by other instances. Calling the close() methods on the InputStream or OutputStream of an HttpURLConnection after a request may free network resources associated with this instance but has no effect on any shared persistent connection. Calling the disconnect() method may close the underlying socket if a persistent connection is otherwise idle at that time.

Cela signifie qu’il faut recréer un objet HttpURLConnection pour chaque nouvelle requête, mais que les connexions sous-jacentes sont gérées en arrière plan de manière à ce qu’elles puissent être partagées entre plusieurs requêtes. On apprend également que la fermeture des objets de type InputStream / Outputstream liés à ces connexions libère les ressources mais n’a aucun effet sur la persistance.

Connexion sécurisée et certificat.
Avant de poursuivre, une petite précision: normalement les serveurs proposant un service sécurisé possèdent un certificat émis par une autorité reconnue. Ce certificat est transmis au client lors de la connexion. Lorsque le certificat n’a pas été validé pour l’une de ces autorités, votre navigateur vous demande en général de confirmer que vous souhaitez effectivement vous connecter à ce serveur. Or nombreux sont les sites ne possédant pas de certificat « valide », et une telle situation avec Java générera l’exception suivante:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path validation failed: java.security.cert.CertPathValidatorException: CA key usage check failed:
keyCertSign bit is not set

Pour résoudre ce problème il va falloir indiquer que la connexion à ce serveur est sûre. par chance quelqu’un s’est déjà occupé de la question, et vous trouverez comment faire ici. Pour ceux qui ne parlent pas anglais, il suffit donc d’exécuter ce programme avec en argument l’adresse du site qui pose problème. Un fichier appelé jssecacerts sera alors généré dans le répertoire courant, et il vous faudra le copier dans $JAVA_HOME/jre/lib/security.

Utilisation de HttpsURLConnexion.
À l’aide d’un exemple concret, comment se connecter au réseau Free-Wifi, je vais rapidement vous présenter les deux ou trois choses essentielles à savoir sur l’utilisation de HttpsURLConnection. La connexion s’effectue sur https://wifi.free.fr/, et le certificat n’a pas été validé par un organisme reconnu. Vous devrez donc effectuer l’opération indiquée plus haut pour pouvoir poursuivre.

Le code pour vous connecter:

// L'url du site auquel on s'apprête à se connecter
URL url = new URL("https://wifi.free.fr");
 
// Création de l'objet httpsURLConnection.
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
 
// Quelque headers de base
String navigateur = "Mozilla/4.0";
https.setRequestProperty("User-Agent", navigateur);
 
// On lance la connexion
https.connect();
 
// On extrait le flux d'entrée pour lire la réponse du serveur
BufferedReader in = new BufferedReader(new InputStreamReader(https.getInputStream()));
while (in.ready())
	System.out.println(in.readLine());

Surtout n’oubliez pas le « s » de https en écrivant l’URL, autrement vous vous retrouverez avec une erreur à l’exécution peu explicite et vous perdrez deux heures de votre temps pour en trouver la cause… Et comme je n’avais rien trouvé sur google pour me mettre sur la piste, j’espère que ceci permettra à d’autres étourdis de retrouver leur chemin:

Exception in thread "main" java.lang.ClassCastException: sun.net.www.protocol.http.HttpURLConnection cannot be cast to javax.net.ssl.HttpsURLConnection

Au moment où vous vous êtes connecté le serveur vous a attribué un identifiant. Vous le retrouverez dans le code HTML au niveau du formulaire. L’attribut s’appelle priv et il contient une suite de 128 caractères hexadécimaux. Une fois que vous l’avez extrait de la réponse alors vous pouvez vous logger facilement:

// La réponse à envoyer
String formulaire = "login=monLogin&password=monMotDePasse&priv=" + priv + "&submit=Valider";
 
// La réponse codée en UTF-8 et sous forme de tableau d'octets
byte[] post = formulaire.getBytes("UTF-8")
 
// On répète l'opération de tout à l'heure
URL url = new URL("https://wifi.free.fr");
HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
String navigateur = "Mozilla/4.0";
https.setRequestProperty("User-Agent", navigateur);
 
// On autorise l'envoi de données
https.setDoOutput(true);
 
// Méthode utilisée: POST
https.setRequestMethod("POST");
 
// Type de données envoyées: réponse à un formulaire
https.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
 
// Taille des données envoyées
https.setRequestProperty("Content-Length", String.valueOf(post.length));
 
// On extrait le flux en sortie et on envoit:	
DataOutputStream out = new DataOutputStream(https.getOutputStream());
out.write(post);

N’oubliez pas que les données doivent être encodées en UTF-8.

La deuxième version d’AIR devrait être disponible au début de l’année 2010. Parmi les nouveautés annoncées (une liste est disponible ici), celle qui m’intéresse le plus concerne le fait de pouvoir lancer d’autres processus et, plus important encore, de pouvoir communiquer avec eux. Un pas en avant important qui décuple les possibilités de ce RIA.

Rich Tretola a écrit un court mais intéressant article sur le sujet, où il montre comment lancer un programme C avec passage d’arguments.
Attention cependant: pour pouvoir profiter de cette nouvelle fonctionnalité, il faudra que l’application AIR ait été installée avec un installateur natif (une autre nouveauté qui permet de générer un installateur .exe pour windows, .dmg pour mac, .rpm ou .deb pour linux).

Je n’ai pas eu le temps de tester le code moi même, mais je compte m’y mettre sérieusement dans les prochaines semaines. Au programme: communication avec une application Java. Je posterai bien entendu le résultat de cette petite expérience ici.

Cet article fait suite au travail sur la transformée de Fourier rapide que j’ai dû réaliser pour la fac avec mon collègue Ivan. Je précise tout de suite que cette implémentation s’applique exclusivement aux images carrées, en niveaux de gris et dont la dimension est de la forme 2n.

L’utilisation de java n’est peut être pas idéale dans la mesure où il s’agit d’un algorithme ayant une complexité assez élevée, d’autant plus que l’API java ne propose pas de classe permettant de coder les nombres complexes (en tout cas pas à ma connaissance), mais dans la mesure où il nous fallait une interface graphique et où je n’ai pas encore trouvé le temps d’adopter une bibliothèque graphique en C++… Le côté positif est que la lecture du code en sera facilitée.

Le tout… n’est plus disponible du fait qu’un trop grand nombre d’étudiants médiocres semble s’être contenté de copier le code sans même dire merci.Hop.

Rappel.
Rappelons tout d’abord la formule de base de la transformée de Fourier discrète:

schema3

Concrètement, cette reformulation revient à séparer les pixels qui composent l’image en deux groupes, d’un côté les pixels dont la position est paire (à gauche sur ce schémas), de l’autre ceux dont la position est impaire (à droite).

schema1

Or les deux sommes qui composent notre nouvelle formule sont exprimées sous la même forme que la formule d’origine. On perçoit donc le premier intérêt de cette transformation: elle pourra être exprimée au moyen d’une fonction récursive. Le deuxième intérêt est que la complexité de cet algorithme est inférieure à l’originale: sur une image à deux dimensions on obtient une complexité de N²ln²(N) (contre N4 pour l’algorithme d’origine).

Pour appliquer cet algorithme sur une image à deux dimensions, il faut d’abord l’appliquer sur toutes les lignes de l’image, sauvegarder le résultat sous forme d’une image intermédiaire, puis l’appliquer de nouveau sur toutes les colonnes de ce résultat. Mais suivre à la lettre cette démarche nous obligerait à écrire deux fois l’algorithme de la transformation, une fois pour les lignes et une autre pour les colonnes, à cause de la gestion des indices. Pour contourner cette difficulté, nous travaillerons exclusivement sur des images carrées et nous effectuerons sur l’image intermédiaire une transformation qui inverse lignes et colonnes, procédé que nous appliquerons de nouveau sur le résultat final.

Implémentation de l’algorithme.
La première méthode qui vient à l’esprit pour implémenter cet algorithme consiste à regrouper dans des structures de données les pixels pairs d’un côté, les pixels impairs de l’autre, et à subdiviser ainsi chaque structures jusqu’à ce qu’elles ne contiennent plus qu’un pixel chacune.
Mais cette méthode est inutilement lourde; il est en effet possible de retrouver les pixels qui nous intéressent en sauvegardant seulement les quelques informations que sont la position du pixel de départ, le pas qui sépare les pixels les uns des autres et le nombre de pixels qui compose la subdivision.

schema2

Si l’on s’intéresse à la position des pixels dans le tableau d’origine, on constate que le pas d’un niveau donné est égal au pas du niveau supérieur que multiplie 2. On en déduit donc que, pour un niveau n donné (0 étant le sommet), les pixels seront séparés par un pas de 2n.

Maintenant que l’on connaît le pas qui sépare deux pixels, il nous faut déterminer la position du premier pixel de chaque subdivision. En fait, pour une subdivision paire, cette valeur sera égale à la position du premier pixel du niveau supérieur et, pour une subdivision impaire, à la position du premier pixel augmenté du pas du niveau supérieur.

Le nombre de pixels qui compose une subdivision est quant à lui égal au nombre de pixels qui compose la subdivision du niveau supérieur, divisé par deux.

On peut désormais écrire l’algorithme récursif de la transformée de Fourier rapide:

private Complexe fourierRapide(int size, int k0, int pas) {
	Complexe exposant = new Complexe(0, -2 * Math.PI * pixel / (size + 1));
	if (size > 0)
		return fourierRapide((size - 1) / 2, k0, pas * 2).add(exposant.exp().mult(fourierRapide((size - 1) / 2, k0 + pas, pas * 2)));
	else
		return exposant.mult(k0).exp().mult(image1D.getPixelComplexe(0, k0));
}

L’algorithme de la transformée inverse est en tout point identique, à l’exception du signe de la variable exposant qui devient positif.

Pour accélérer encore un peu plus la transformation, nous avons implémenté une version multithread de l’algorithme, chaque thread se partageant les lignes qui composent l’image. Un système de barrière permet de synchroniser les principales étapes de la transformation (calcul des lignes / passage des lignes aux colonnes / calcul des lignes / passage des lignes aux colonnes).

Notes pour plus tard:
Cesser d’utiliser bêtement des boucles pour réaliser des copies de tableaux, même partielles.
À la place, penser à faire appel à System.arrayCopy(Object src, int srcPos, Object dest, int destPos, int length) qui est plus rapide et facilite la relecture (merci aux considérations générales sur la performance des applications java d’Ibrahim Moukouop).

Si vous n’êtes pas encore familier avec les design pattern (patrons de conception en français), il vous est certainement arrivé lors de l’écriture de programmes un tant soit peu complexe de vous sentir un peu… perdu. Face à une multiplicité de classes toutes plus ou moins dépendantes les unes des autres, on a vite fait de se retrouver devant un code spaghetti impossible à maintenir. Ajoutez par dessus l’apparition d’un bug (ce qui arrivera presque toujours) et vous êtes bon pour une sérieuse migraine.
Heureusement, les design pattern sont là. Pour faire simple, ce concept désigne un ensemble de méthodes éprouvées pour concevoir efficacement un programme. En gros les classes sont vues comme des legos, et les patrons de conception vous indiquent comment les imbriquer entre eux.

Un exemple parmi d’autres: l’adaptateur
Un de vos programmes utilise une classe C1 implémentant l’interface I1. Un jour vous découvrez l’existence d’une classe C2 proposant les même fonctionnalités que C1, mais codée de manière plus efficace. Or C2 implémente l’interface I2, radicalement différente de I1.
Question: comment utiliser C2, sans devoir chambouler toute votre application?
Réponse: en codant un adapteur, un patron de conception de type structure. Dans cet exemple, concevoir un adapteur revient à écrire une interface I3 qui héritera de I1 et I2 et dont l’implémentation se chargera de dérouter tous les appels aux anciennes fonctions (celles de C1) vers les nouvelles (celles de C2). Ainsi, les appels de fonction à l’intérieur de votre application restent inchangés, mais en arrière plan se sont en réalités les fonctions de C2 qui sont utilisées.
L’idée est simple, il suffisait juste d’y penser, et les design pattern regorgent d’idées simples auxquelles on devrait toujours penser.

Un autre modèle de conception: l’observateur.
Utilisable typiquement quand vous codez une IHM, il ne s’agit ni plus ni moins que d’avertir proprement un objet lorsque l’état d’un autre objet a été modifié.
La mise en oeuvre de ce patron m’a demandé un peu plus de temps que celle de l’adaptateur parce qu’il fait appel à des éléments de l’API java que je n’avais pas l’habitude d’utiliser. Et si j’en juge par ce que j’ai trouvé sur le net après une rapide recherche, je dois pas être le seul dans ce cas, à vous d’en juger à partir des deux exemples de conception qui suivent. Dans les deux cas on possède deux classes, Observateur et Cible, et on souhaite notifier l’objet Observateur lorsque la Cible qui lui est passée en paramètre est modifiée.

Première solution.
Cette solution est proposée par le site vogella. je l’ai appliqué de manière à tenir compte de notre situation Observateur/Cible.

La cible pour commencer:

import java.beans.PropertyChangeListener;
 
public class Cible {
 
	private PropertyChangeListener listener;
 
	private int valeur;
 
	public Cible(int valeur) {
		this.valeur = valeur;
	}
 
	public int getValeur() {
		return valeur;
	}
 
	public void setValeur(int valeur) {
		this.valeur = valeur;
		listener.propertyChange(null);
	}
 
	public void addChangeListener(PropertyChangeListener newListener) {
		this.listener = newListener;
	}
}

La cible contient une valeur (l’élément que l’on souhaite en réalité surveiller) ainsi qu’un objet de type PropertyChangeListener (qui est en réalité une référence vers notre Observateur comme nous le verrons plus bas). On voit que la méthode addChangeListener permet justement à la cible d’obtenir la référence de notre observateur, et la méthode setValeur dont on voit à la fin qu’elle fait un appel à propertyChange de notre observateur. On imagine que c’est dans la méthode propertyChange que nous écriront le code qui traitera le changement d’état.

Bien c’est au tour de la classe Observateur à présent:

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
 
public class Observateur implements PropertyChangeListener {
	public Observateur(Cible cible) {
		cible.addChangeListener(this);
	}
 
	public void propertyChange(PropertyChangeEvent evt) {
		System.out.println("la cible a changé");
	}
}

Comme prévu la cible lui est passée en paramètre, et l’implémentation de l’interface PropertyChangeListener nécessite effectivement la présence de la méthode propertyChange. J’ai fait au plus simple pour le traitement du changement d’état: un Sopln fera l’affaire pour cet exemple.

Ajoutons à celà un main de base:

public class Main {
	public static void main(String[] args) {
		Cible cible = new Cible(5);
		Observateur obs = new Observateur(cible);
		cible.setValeur(10);
	}
}

Et voilà. Compilation, exécution, ça fonctionne.
Une rapide analyse du déroulement du programme: l’observateur possède une référence vers sa cible (elle lui est passée en paramètre du constructeur) et la cible une référence vers son observateur (grâce à addChangeListener). Dans le code de setValeur, une fois que la variable a été surveillée, on notifie l’observateur en appelant sa méthode propertyChange.
Ça fonctionne mais j’ai un peu tiqué en voyant le code, cette histoire de PropertyChangeListener qu’il faut déclarer dans la cible, et ce null placé en paramètre de propertyChange… on est pas censé avoir à s’occuper de ça ici… Il doit bien y avoir un moyen de déléguer ce travail à une autre classe… Et effectivement, il y a moyen. Et c’est tant mieux. Et c’est wikipedia qui nous le propose (comme quoi on peut y lire des articles tout à fait correct ["mais pas toujours!" me souffle mon prof de système d'exploitation...]).

Deuxième solution.
Voici donc une deuxième solution, plus claire et plus efficace:

import java.util.Observable;
 
public class Cible extends Observable {
	private int valeur;
 
	public Cible(int valeur) {
		this.valeur = valeur;
	}
 
	public int getValeur() {
		return valeur;
	}
 
	public void setValeur(int valeur) {
		this.valeur = valeur;
		this.setChanged();
		this.notifyObservers();
	}
}

Première nouveauté: la cible étend la classe Observable. C’est sans doute à l’intérieur de celle ci que la gestion de la communication avec l’écouteur a été placée. D’ailleurs autant s’en assurer, un petit tour dans le code de Observable, et voici ce qu’on y trouve:

public synchronized void addObserver(Observer o) {
	if (o == null)
		throw new NullPointerException();
	if (!obs.contains(o))
		obs.addElement(o);
}

Et en plus la classe gère les accès concurrents, parfait.
Mais revenons à notre classe Cible: dans setValeur on retrouve un appel à setChanged (qui se contente de mettre une variable de Observable à true) et un à notifyObserver, dont le rôle est d’avertir tous les écouteurs de notre cible que son état à changé. À noter que la méthode accepte également un argument de type Object, lequel sera distribué à tous les écouteurs notifiés; pratique pour indiquer quel attribut a été modifié, ou bien pour indiquer quels observateurs doivent réagir à ce changement.

L’observateur à présent:

import java.util.Observable;
import java.util.Observer;
 
public class Observateur implements Observer {
 
	public Observateur(Cible cible) {
		cible.addObserver(this);
	}
 
	public void update(Observable o, Object arg) {
		System.out.println("la cible a changé!");
	}
}

l’Observateur implémente à présent l’interface Observer, laquelle nous propose d’implémenter la méthode update. C’est l’équivalent de propertyChange que l’on a vu dans le premier exemple. C’est donc ici qu’on pourra écrire le code qui traitera le changement d’état de la cible. Sauf qu’en plus on aura accès à l’objet qui est à l’origine de la notification du changement d’état ainsi qu’à l’objet optionnel que nous propose notifyObservers.

Voilà ceci conclu mon premier article sur les patrons de conception, j’imagine qu’il y en aura d’autres au fur et à mesure de mes recherches.