Design pattern observer: Pratiquer, en mieux, le design Observer en java 8

blog_observer_design_1_titreblog_observer_design1-Titre2

En 2009, j'ai présenté ce billet pour expliquer comment pratiquer en quinze minutes le design pattern Observer.

Je le reprends aujourd'hui afin de mieux le pratiquer. Essentiellement, nous allons voir comment affaiblir le couplage entre objets !

Et pour la démo, nous avons intégré cette contrainte : Il n'est pas toujours possible de changer la signature de certaines classes existantes.

Comme d'habitude, nous présentons une démo pratique en java 8, bien que l'explication donnée reste valable pour les autres versions de java.

J'ai décidé de ne pas traduire les termes "Observable" et "Observer" pour ne pas rajouter de la confusion:-)

Un diagramme de classe détaillé et commenté est fourni pour expliciter les relations et les enjeux. Et un test JUnit illustre le comportement attendu.

Notez enfin que le code du projet sera fourni mais je vous recommande de lire les concepts avant de toucher le code pour mieux saisir les enjeux.

Pour exécuter le test Junit du démo, il suffit de lancer la commande maven: mvn test.

C'est quoi ce design pattern observer ?

L'idée est simple. Un objet (observable) peut avoir un attribut ou un comportement qui en cas de changement peut intéresser d'autres objets (observer).

Ces derniers doivent effectuer des traitements en fonction de ce changement.

Le design pattern Observer suit le principe de "publish-subscribe".

Ainsi, le besoin consiste à ce que l'observable notifie tout changement de son état aux objets abonnés (observers) qui à leur tour effectuent les traitements adéquats.

Comme vous allez le constater, ces traitements sont effectués de manière complètement découplée: l'observable n'a aucune idée de ce que va faire les observers!

Le premier objet est nommé « observable » et les objets abonnés sont nommés « observer ».

Le rôle de l'observable est double :

  • Permettre l'abonnement (ou désabonnement) des objets intéressés. C'est pour cela qu'il maintient une liste des observers,
  • Notifier les observers abonnés du changement de l'état.

On peut ajouter/retirer un observateur à tout moment sans affecter le code existant.

Ce n'est pas encore tout à fait clair ? Illustrons avec un exemple simple.

Use Case de notre démo

Prenons un exemple (simpliste) qui servira de base à notre démo.

Une entreprise (morale) s'intéresse au turn-over effectif de ses propres établissements.

Ici l'entreprise est un observer candidat qui s'intéresse au changement de l'effectif d'un établissement (observable).

Ainsi, l'objet Etablissement est un Observable et il doit fournir les deux méthodes citées précédemment.

Résumons: L'objet Etablissement est un Observable. L'objet Entreprise est un Observer.

Or, avec cette formulation, nous sommes tentés de penser à l'héritage et d'écrire ce code:
public class Etablissement extends Observable

Est-ce judicieux ?

En réalité non, car d'une part, nous ne pouvons pas toujours ajouter l'héritage ou changer la signature d'un objet legacy et d'autre part, en terme de conception, un héritage entraîne un couplage fort inutile.

Scénario de test ou user story (US)

Voici les étapes de notre scénario de test :

Étant donné une entreprise nommée FanDeFleurs (effectif global à zéro).

Et étant donné un premier établissement nommé FanDeFleursBleus (nbEffectif à zéro).

Et étant donné un second établissement nommé FanDeFleursViolettes (nbEffectif à zéro).

Lorsque l'établissement FanDeFleursBleus embauche un salarié.

Alors l'effectif global de l'entreprise FanDeFleurs s'incrémente de 1 (donc égal à 1).

Lorsque l'établissement FanDeFleursBleus embauche un second salarié.

Alors l'effectif global de l'entreprise FanDeFleurs devient égal à 2.

Lorsque l'établissement FanDeFleursViolettes embauche un salarié.

Alors l'effectif global de l'entreprise FanDeFleurs devient égal à 3.

Lorsqu'un salarié quitte l'un des deux établissements.

Alors l'effectif global de l'entreprise FanDefleurs redevient égal à 2.

Le test JUnit donné plus loin doit répondre à ce scénario.

Observable: Où se cache t-il?

Nous avons compris que l'objet Etablissement est l'Observable mais en réalité il n'est pas, conceptuellement, de nature Observable.

C'est pour cette raison que nous allons utiliser la composition au lieu de l'héritage.
Donc, Etablissement est composé d'un Observable.

Or, comme seul l'attribut nbEffectif intéresse les observers, nous isolons l'attribut nbEffectif dans une classe POJO (réduite) nommée Effectif.

Faisant de la sorte, nous allégeons les liens entre les références de l'Observable et de ses observers,

Et pour la suite la classe Effectif joue le rôle d'observable et non Etablissement.

Ce qui donne une classe Effectif (POJO simple) dont voici un extrait de code :

package fr.netapsys.abdou.observer.model;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

public class Effectif {

	private int nbEffectif;
	
	private int previousNbEffectif;

	private LocalDateTime dateUpdate;
	
	private transient Observable observable = new Observable();
	
	private transient Set<Observer> observers = new HashSet<>();
	
	public synchronized void addObserver(Observer observer){
		observers.add(observer);
	}
	public synchronized void removeObserver(Observer observer){
		observers.remove(observer);
	}
	
	public void addOneSalarie(){
		.....
	}

	public void removeOneSalarie(){
	    .....
	}
	private void setNbEffectif(int nbEffectif) {
		this.nbEffectif = nbEffectif;
	}
	//..le reste des getters setters ...
}

Il est important de remarquer que le setter de l'attribut nbEffectif est private.

Ainsi, nous garantissons que seules les deux méthodes public void addOneSalarie, public void removeOneSalarie modifient l'état nbEFfectif.

Donc, les deux méthodes qui se chargent de la modification de l'état et de la notification des observers sont :

	public void addOneSalarie(){
		.....;
	}

	public void removeOneSalarie(){
		...;
	}

Vous avez certes constaté l'attribut observable (transient) assurant à la classe Effectif le rôle d'Observable.

C'est pour cette raison que la classe Effectif maintienne une liste d'observer à notifier.

Voici ci-après les deux versions du code de la classe Effectif.

La première version sans les lambdas de java8:

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.*;

public class Effectif {

	private int nbEffectif;
	
	private int previousNbEffectif;

	private LocalDateTime dateUpdate;
	
	private transient Observable observable = new Observable();
	
	private transient Set<Observer> observers = new HashSet<>();
	
	public synchronized void addObserver(Observer observer){
		observers.add(observer);
	}
	public synchronized void removeObserver(Observer observer){
		observers.remove(observer);
	}
		
	public void addOneSalarie(){
		addOrRemoveOneSalarie(true);
	}

	public void removeOneSalarie(){
		addOrRemoveOneSalarie(false);
	}

	private void addOrRemoveOneSalarie(final boolean isAdd) {
		int oldEffectif = this.getNbEffectif();
		this.setPreviousNbEffectif(this.getNbEffectif());
		if(isAdd){
			this.setNbEffectif(++oldEffectif);
		}else{
			this.setNbEffectif(--oldEffectif);	
		}
		this.setDateUpdate(LocalDateTime.now());
		//notify
		for(Observer obs:getObservers()){
			obs.update(observable, this);
		}
		//observable.notifyObservers();
	}

	private void setNbEffectif(int nbEffectif) {
		this.nbEffectif = nbEffectif;		
	}
	/********** le reste des getters setters *****/
}

Et la classe Etablissement (POJO simple) désormais allégée devient:

public class Etablissement  implements Serializable{
	
    private String siret;
    
    private LocalDate dateCreation;
    
    private Effectif effectif  = new Effectif();
    	
....getters setters
}

Et la seconde version du code Effectif avec les lambdas:

public class Effectif {

	private int nbEffectif;
	
	private int previousNbEffectif;

	private LocalDateTime dateUpdate;
	
	private transient Observable observable = new Observable();
	
	private transient Set<Observer> observers = new HashSet<>();
	
	public synchronized void addObserver(Observer observer){
		observers.add(observer);
	}
	public synchronized void removeObserver(Observer observer){
		observers.remove(observer);
	}
		
	public void addOneSalarie(){
		addOrRemoveOneSalarie(true);
	}

	public void removeOneSalarie(){
		addOrRemoveOneSalarie(false);
	}

	private void addOrRemoveOneSalarie(final boolean isAdd) {
		int oldEffectif = this.getNbEffectif();
		this.setPreviousNbEffectif(this.getNbEffectif());
		if(isAdd){
			this.setNbEffectif(++oldEffectif);
		}else{
			this.setNbEffectif(--oldEffectif);	
		}
		this.setDateUpdate(LocalDateTime.now());
		
		getObservers().stream().forEach( o -> o.update(observable, this) );

	}

	private void setNbEffectif(int nbEffectif) {
		this.nbEffectif = nbEffectif;		
	}

Dans cette seconde version, la seule différence réside dans ce morceau de code:

getObservers().stream().forEach( o -> o.update(observable, this) );

qui permet de notifier les observers.

Résumé

En résumé, seule la classe Effectif assure le rôle d'observable.

Elle fournit une méthode pour ajouter/retirer un observer et notifie tout observer qui s'est enregistré !

Observer: Où se cache t-il?

Dans notre démo, l'Entreprise étant le seul observer, il doit impérativement implémenter le contrat (interface) Observer.

Ce contrat possède une seule méthode: update(Observable o, Object obj);

Donc dans la démo, c'est l'objet Effectif qui est transmis lorsque les observers sont notifiés.

En résumé, la classe Entreprise joue le rôle d'observer et implémente la méthode update(Observable o, Object obj)!

Qu'est-ce qu'est présenté ici en mieux ?

Nous avons, avec la composition, affaibli le couplage entre objets Observable et Observers !

En isolant l'attribut à surveiller, nous avons affaibli les liens entre les références des objets observable et observers.

Avant d'entrer dans le code, jetons un œil sur le diagramme de classe commenté.

Diagramme de classe

Voici une vue explicitée et commentée des classes de la démo (merci à ce site):

blog_observer_design2

Qui se charge de brancher les observers et l'observable?

Nous avons vu que l'observable fournit deux méthodes addObserver et removeObserver.

Mais qui se charge alors de les appeler pour faire la soudure entre observable et observers?

Dans la démo, cette soudure est faite au moment de rajouter un établissement à son entreprise.

Je vous laisse découvrir dans le test suivant (regarder les commentaires dans le code).

Notre test JUnit

Maintenant que nous avons précisé les rôles de nos objets, passons au test JUnit qui doit répondre au scénario donné ci-dessus.

Ci-dessous le code du test (qui peut être simplifié et optimisé).

import java.time.LocalDateTime;
import org.junit.Assert;
import org.junit.Test;
import fr.netapsys.abdou.observer.model.Entreprise;
import fr.netapsys.abdou.observer.model.Etablissement;

public class EmployeurSuiviEffectifObserverTest {

	@Test
	public void test() throws InterruptedException {
		//creer une entreprise et ses deux etablissements
		Entreprise fanDeFleurs = new Entreprise();		
		//etab1
		Etablissement etabFanDeFleursBleus = new Etablissement();
		etabFanDeFleursBleus.setDateCreation(LocalDateTime.of(2017,01,26,12,10));
		//etab 2
		Etablissement etabFanDeFleursVioletes = new Etablissement();
		etabFanDeFleursVioletes.setDateCreation(LocalDateTime.of(2017,01,29,11,00));
		fanDeFleurs.addEtablissement(etabFanDeFleursBleus);
		fanDeFleurs.addEtablissement(etabFanDeFleursVioletes);
		//addObserver fait dans addEtablissement de Entreprise
		//etabFanDeFleurs.addOneSalarie();
		etabFanDeFleursBleus.getEffectif().addOneSalarie();
		Assert.assertEquals(fanDeFleurs.getEffectifGlobal(),1);
		
		etabFanDeFleursVioletes.getEffectif().addOneSalarie();
		Assert.assertEquals(fanDeFleurs.getEffectifGlobal(),2);		
		//encore du changement effectif dans etabFanDeFleures
		etabFanDeFleursBleus.getEffectif().addOneSalarie();
		Assert.assertEquals(fanDeFleurs.getEffectifGlobal(),3);
		//remove one salarie
		etabFanDeFleursBleus.getEffectif().removeOneSalarie();
		Assert.assertEquals(fanDeFleurs.getEffectifGlobal(),2);
	}
}
Junit test

Je crois que l'article a fait avec détail le tour du design pattern observer.

Vous l'avez forcément rencontré dans diverses situations.

Vous avez le zip du code ici.

Conclusion

La démarche ici est différente de l'article de 2009 puisque nous ne faisons pas d'héritage pour l'observable.

Le contrat Observer est facile à intégrer dans le code existant.

Ensuite un peu de rigueur est le design pattern Observer est en place.

Ce design est une "best practices" qui participe à construire des applications robustes et faciles à maintenir.

@Enjoy

Enregistrer

Enregistrer

Enregistrer

2 commentaires

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.