Pratiquer le design-pattern Strategy en 15 min

Dans ce billet, nous poursuivons les notions de design-pattern (motifs de conception).
Après le design-pattern Observer, étudions un autre motif de conception Strategy qui appartient aussi au groupe de comportement.
Ce motif de conception permet de définir plusieurs algorithmes interchangeables dynamiquement.

Encore un motif de conception qui répond à la question : Comment faire simple quand on peut faire compliqué ?

En fait, cela reste simple si on n'oublie pas de le pratiquer tôt. Sinon, ce sont les développements, les tests et les évolutions qui deviennent compliqués.
Mais, la revue de code permet aussi d'éviter de rendre les développements plus compliqués et de tirer profit de la mise en place de ces design.

D'un point de vue théorique, il est facile de l'appréhender. Néanmoins, la mise en pratique (avec le langage Java) de ce design reste un peu moins évidente.

Beaucoup d'entre nous l'ont déjà pratiqué sans, forcément, le savoir.
Un exemple de l'API Java est la méthode Collections.sort ayant deux arguments dont le second est l'algorithme de comparaison pour effectuer le tri.
Ainsi, cette méthode est fournie avec une implémentation par défaut qui convient à bon nombre de types de données avec la possibilité de redéfinir dynamiquement de nouveaux algorithmes de tri.

Notons enfin que dans l'exemple d'illustration ci-après, nous allons comparer deux entreprises déclarées dans des répertoires différents
Et par conséquent, les codes de référence et libellés sont variés et distincts d'un répertoire à un autre.
La comparaison est basée sur un score de pertinence qui est une moyenne de(s) différente(s) note(s) calculée(s).

Un seul pré-requis pour ce billet : connaître la notion de composition en programmation objet.

Situation/problème:

Un objet doit définir plusieurs algorithmes interchangeables dynamiquement. Ces algorithmes bien qu'ils répondent à la même interface peuvent évoluer indépendamment.

Solution:

Le design Strategy répond à cette problématique par l'introduction de la composition dans la classe essentielle qui doit contenir l'algorithme.

Avantages:

Mise en place de bonne pratique,
Code extensible avec aisance d'inter changer les algorithmes,
Code facile à maintenir en cas d'évolution/changement.

Mise en pratique:

Notre exemple pratique, tiré d'une situation réelle, a pour but de comparer deux entreprises déclarées dans deux répertoires distincts
Par exemple, le premier est le répertoire FINESS et le second est SIRENE de l'INSEE.
Les déclarations peuvent complètement différer d'un répertoire à un autre sur :
Les formats des données,
Les codes de référence d'adresse ou de géolocalisation,
Bien d'autres indicateurs.

Exemple:

Pour fixer les idées prenons cet exemple:
La première entreprise a pour enseigne:
SARL AMBULANCE TAXI PARIS dans le premier répertoire disons par exemple le répertoire de l'INSEE.
et la seconde (qui peut être la même) est enregistrée encore dans un second répertoire qui gère spécialement le métier des ambulances médicales:
SARL TRANSPORT AMBULANCE PARIS

En résumé, la comparaison de ces deux entités va reposer sur un algorithme qui est interchangeable dynamiquement:

  • Le premier algorithme, celui par défaut, qui compare l'égalité des enseignes (chaînes de caractères). Rarement utile. D'où le second
  • Le second qui parcourt les mots qui composent chacune des enseignes afin de les comparer et puis établir des scores.

On a simplifié le problème en ne s'intéressant qu'à la notion de l'enseigne comme suit:
On affecte la note total de 100 lorsque les deux enseignes sont identiques (cad les deux chaînes de caractères sont identiques).
Sinon, on affecte une note liée à la comparaison de chaque mot qui compose ces enseignes
Les détails de cette comparaison n'est pas utile pour la compréhension du billet.

Les quatre classes sont de simples POJO utilisées pour la suite.

/****Classe EtabCommune***/

public class EtabCommune  implements  Serializable{
 
	private static final long serialVersionUID = 1L;
	private String nom;
	public String getNom() {
		return nom;
	}
	public void setNom(String nom) {
		this.nom = nom;
	}	
	public String toString(){
		return 	"Nom:"+getNom();
	}
}

/****EtabFiness****/

public class EtabFiness extends EtabCommune  {
	private int codeCategorie;
	public int getCodeCategorie() {
		return codeCategorie;
	}
	public void setCodeCategorie(int codeCategorie) {
		this.codeCategorie = codeCategorie;
	}
	public String toString(){
		return "Finess:"+super.toString()+"\tCodeCategorie="+getCodeCategorie();
	}	
}

/********EtabSirene************/

public class EtabSirene extends EtabCommune implements Serializable {
 
	private static final long serialVersionUID = 1L;
	int type;
	public int getType() {
		return type;
	}
	public void setType(int type) {
		this.type = type;
	}
	public String toString(){
		return "Sirene:"+super.toString()+"\tType="+type;
	}
}

/******NotesComparaison************/

public class NotesComparaison  implements Serializable{
	private static final long serialVersionUID = 1L;
	double noteEnseigne;
	public double getNoteEnseigne() {
		return noteEnseigne;
	}
	public void setNoteEnseigne(double noteEnseigne) {
		this.noteEnseigne = noteEnseigne;
	}
	public String toString()
	{ 
		return "noteEnseigne="+noteEnseigne;//+autres composantes de la note
	}
}

Voici les algorithmes de comparaison avec implémentation par défaut (dans la class AbstractAlgorithme) et une seconde (pour démo).
Le client, un peu plus loin, va illustrer l'emploi des deux implémentations.

/*****Algorithme (interface,classe abstraite, implémentation )*****/

/**** IAlgorithme ***/

public interface IAlgorithme {
	public double calculateNoteEnseigne(String nomFiness,String nomSirene) ;
}

/**** AbstractAlgorithme (alg utilisé par defaut)***/

public abstract class AbstractAlgorithme implements IAlgorithme {
	public double calculateNoteEnseigne(String nomFiness,String nomSirene) {	
		if(nomFiness==null) return 0.0;
		if(nomFiness.equals(nomSirene))return 100.0;
		return 0.0;
	}
}

/**** ConcreteAlgorithme: démo ***/

public class ConcreteAlgorithme extends AbstractAlgorithme {
	public double calculateNoteEnseigne(String nomFiness,String nomSirene) {
		if(nomFiness==null || nomFiness.trim().equals("") ||  
                            nomSirene==null || nomSirene.trim().equals(""))return 0.0;
 
                final String[] listeFinessEnseigne=nomFiness.split(" ");
		final String[] listeSireneEnseigne=nomSirene.split(" ");
		double[] notes =new double[listeFinessEnseigne.length];
		int dimension=notes.length;
		for(int i=0;i<dimension;i++) notes[i]=0.0;
		int k=0;
		for( String mot:listeFinessEnseigne){
		  for (String terme: listeSireneEnseigne ){
			if (mot.equals(terme)) notes[k]=100.0; 
		  }
		  k++;	 
		}
		double note=0.0;
		for(double n:notes){
		   note+=n;	
		}
		return note/dimension; //DIV ZERO!
	}
}

La classe Comparaison contient les détails de l'implémentation du motif stratégie.
En effet, la classe Comparaison délègue l'algorithmique de comparaison à la classe/interfae AbstractAlgorithme/IAlgorithme via l'attribut algorithme.
Néanmoins, cette classe fournit, dans le constructeur sans argument, une implémentation par défaut.
Le client peut redéfinir dynamiquement l'algorithme de comparaison, comme illustré dans le client ci-après

/****Comparaison (interface et implémentation)***/

public interface IComparaison {
	public NotesComparaison compare(EtabFiness finess, EtabSirene sirene);
	public void setAlgorithme(IAlgorithme algorithme) ;
 
}

/************ classe importante implémentant le motif stratégie************************/

public class Comparaison implements IComparaison{
	private IAlgorithme algorithme;
	public Comparaison(IAlgorithme algorithme){
		this.algorithme=algorithme;
	}
	public Comparaison(){
		this.algorithme=new Algorithme();
	}
	/***Methode prinicpale...***/	
	public NotesComparaison compare(EtabFiness finess,EtabSirene sirene){
		NotesComparaison notesComparaison=new NotesComparaison();	
		/*****noteEnseigne à claculer...****/
		double noteEnseigne=0.0;	
		noteEnseigne=algorithme.calculateNoteEnseigne(finess.getNom(), sirene.getNom());
		notesComparaison.setNoteEnseigne(noteEnseigne);	
		return notesComparaison;
	}
	//setters...	
	public void setAlgorithme(IAlgorithme algorithme) {
		this.algorithme = algorithme;
	}
	//inner class
	private class Algorithme extends AbstractAlgorithme{	
	}
}

Avant de présenter le client, voici le fichier de configuration spring qui va être utilisé:
/**** spring.xml****/

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
		xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"
		>
<bean id="finess1" class="fr.netapsys.designpatterns.entites.EtabFiness">
	<property name="nom" value="SARL AMBULANCE TAXI PARIS"/>
	<property name="codeCategorie" value="100"/>
</bean> 
<bean id="sirene1" class="fr.netapsys.designpatterns.entites.EtabSirene">
	<property name="nom" value="SARL TRANSPORT AMBULANCE PARIS"/>
	<property name="type" value="1"/>
</bean>
</beans>

/****************** Main client**************/

public class Main {
final private static Logger logger=Logger.getLogger("fr.netapsys.designpatterns.main.Main.class");
	public static void main(String[] args) {
		/* point d entree de spring: déclarer dans le fichier spring.xml le bean */  
		ApplicationContext ctx=(ApplicationContext)
					new ClassPathXmlApplicationContext(new String[]{"resources/spring.xml"});
		//Exemple de finess structure ...
		EtabFiness finess1 = defineExempleEtabFiness(ctx);		
		//exemple de sirene structure ...**************************
		EtabSirene sirene1 = defineExempleEtabSirene(ctx);
		/********************** TEST 1: Comparaison par defaut (cad algorithme par defaut qui opere) ***************/	
		IComparaison comparaison=new Comparaison();
		NotesComparaison notesComparaison=comparaison.compare(finess1, sirene1);
		logger.info("Résultat de comparaison par defaut de
"+finess1+"
et de
"+sirene1 +
				"

-->NOTES:"+notesComparaison+"
"  );
		/********************** TEST 2: attache un algorithme concret à la comparaison ***************/		
		//definir algo de la comparaison 
		IAlgorithme algorithme = defineConcreteAlgorithme();
		//attacher cet algo à Comparaison
		//comparaison=new Comparaison(algorithme);
		comparaison.setAlgorithme(algorithme);
		notesComparaison=comparaison.compare(finess1, sirene1);
		logger.info("Resultat de comparaison avec algorithme concret de
"+finess1+"
et de
"+sirene1 +
				"

-->NOTES:"+notesComparaison  );
	}
	private static IAlgorithme defineConcreteAlgorithme() {
		IAlgorithme algorithme=new ConcreteAlgorithme();
	   return algorithme;
	}	
	private static EtabSirene defineExempleEtabSirene(ApplicationContext ctx) {
			return (EtabSirene)ctx.getBean("sirene1");
	}
	private static EtabFiness defineExempleEtabFiness(ApplicationContext ctx) {
		 return (EtabFiness)ctx.getBean("finess1");
	}
}

L'exécution de la classe Main produit la sortie suivante:

INFO  ( Main.java:54) Résultat de comparaison par defaut de
Finess:Nom:SARL AMBULANCE TAXI PARIS	CodeCategorie=100
et de
Sirene:Nom:SARL TRANSPORT AMBULANCE PARIS	Type=1
-->NOTES:noteEnseigne=0.0
 INFO  ( Main.java:64) Resultat de comparaison avec algorithme concret de
Finess:Nom:SARL AMBULANCE TAXI PARIS	CodeCategorie=100
et de
Sirene:Nom:SARL TRANSPORT AMBULANCE PARIS	Type=1
-->NOTES:noteEnseigne=75.0

Voilà, ça fait beaucoup de lignes de codes mais le résultat est intéressant puisque changer/redéfinir l'algorithme est dynamique.
Vous pouvez en ajouter/définir un autre adapté aux nouvelles règles métier sans modifier la classe Comparaison.

Laisser un commentaire

Votre adresse e-mail 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.