Habituellement, la recherche d'éléments dans une liste consiste à écrire une boucle de parcours d'éléments, un test pour savoir si l'élément parcouru est un de ceux que l'on cherche, puis à ajouter cet élément dans la liste de résultats. L'écriture de ce code est redondant, source d'erreurs et peut devenir complexe à lire et à maintenir.
Les prédicats permettent de simplifier l'écriture de méthodes de recherche d'éléments dans une liste.
En effet, l'interface Predicate permet d'externaliser la méthode de sélection des éléments recherchés.
Nous nous concentrons sur l'intention : que les éléments trouvés correspondent bien à ce que nous cherchons.
Ainsi, nous verrons une recherche classique de livres correspondant à certains critères. Ensuite nous verrons comment les bibliothèques common-collections d'Apache et guava de Google nous permettent de simplifier l'écriture de cette recherche à l'aide notamment de l'interface Predicate.
Exemple de recherche
Nous allons prendre comme exemple une bibliothèque qui dispose d'une liste de livre.
Pour chaque livre, nous disposons des informations suivantes : le titre, l'auteur et l'année de publication.
Nous recherchons les livres répondant aux critères suivant :
- auteur : Rudyard Kipling
- titre : le titre comporte les mots "jungle" et "grand"
Voyons comment nous pouvons effectuer cette recherche.
Recherche classique
Voici une boucle de recherche classique :
// Livres trouvés par la recherche List<Livre> livreTrouves = new ArrayList<Livre>(); // Boucle de recherche for(Livre livre : livres) { // Test des critères boolean estTrouve = StringUtils.containsIgnoreCase(livre.getAuteur(), "Kipling") && StringUtils.containsIgnoreCase(livre.getTitre(), "livre") && StringUtils.containsIgnoreCase(livre.getTitre(), "jungle"); // Le livre correspond aux critères if(estTrouve) { livreTrouves.add(livre); } }
Amélioration de la recherche classique
Dans le code précédent, nous remarquons que nous avons une forte complexité au niveau du test sur les critères.
En effet, la logique métier est concentrée dans ce test de vérification des critères.
C'est pourquoi nous créons une méthode spécifiquement chargée d'effectuer ce test.
Nous obtenons ainsi le code suivant :
/** * Vérifier si le livre correspond aux critères de recherche. * @param livre Livre * @return Indique si le livre correspond aux critères. */ private boolean verifierCritere(Livre livre) { boolean estTrouve = StringUtils.containsIgnoreCase(livre.getAuteur(), "Kipling") && StringUtils.containsIgnoreCase(livre.getTitre(), "livre") && StringUtils.containsIgnoreCase(livre.getTitre(), "jungle"); return estTrouve; }
La boucle de recherche est simplifiée :
// Livres trouvés par la recherche List<Livre> livreTrouves = new ArrayList<Livre>(); // Boucle de recherche for(Livre livre : livres) { // Le livre correspond aux critères if(verifierCritere(livre)) { livreTrouves.add(livre); } }
Prédicat de recherche
La recherche a été simplifiée en externalisant dans une nouvelle méthode le test de vérification des critères sur un des éléments de la liste.
Les bibliothèques commons-collection et guava permettent d'effectuer cette séparation à l'aide de l'interface Predicate.
L'interface Predicate impose l'écriture d'une méthode prenant en paramètre un des éléments de la liste et qui se charge de retourner un booléen indiquant si l'élément correspond à ce que l'on cherche. Dans notre exemple, il s'agissait qu'un livre corresponde à un auteur et à un titre.
Cependant, la méthode principale conserve la logique de parcours des éléments de la liste et d'ajout des éléments trouvés dans la liste de résultats de la recherche. Les bibliothèques common-collections et guava permettent de ne plus avoir à écrire cette logique, tout simplement en fournissant à la méthode de recherche la liste des éléments et le prédicat.
Voyons deux exemples d'implémentation utilisant les deux bibliothèques.
Avec common-collections d'Apache
Nous utilisons l'interface Predicate :
public interface Predicate { boolean evaluate(Object object); }
Voici l'implémentation de notre prédicat :
Predicate predicat = new Predicate() { public boolean evaluate(Object o) { Livre livre = (Livre) o; boolean estTrouve = StringUtils.containsIgnoreCase(livre.getAuteur(), "Kipling") && StringUtils.containsIgnoreCase(livre.getTitre(), "livre") && StringUtils.containsIgnoreCase(livre.getTitre(), "jungle"); return estTrouve; } };
La méthode evaluate correspond à la méthode verifierCritere de notre code précédent.
La version officielle des commons-collection ne supporte pas les types génériques. Il existe cependant une version dérivée supportant les types génériques et qui sera indiquée à la fin de ce post.
Voici notre boucle de recherche qui utilise maintenant le prédicat :
// Livres trouvés par la recherche List<Livre> livreTrouves = new ArrayList<Livre>(); // Boucle de recherche for(Livre livre : livres) { // Test des critères if(predicat.evaluate(livre)) { // Le livre correspond aux critères livreTrouves.add(livre); } }
Cette boucle de recherche peut être remplacée par l'appel de la méthode select de la classe CollectionUtils :
// Livres trouvés par la recherche List<Livre> livreTrouves = new ArrayList<Livre>(); // Recherche de livres CollectionUtils.select(livres, predicate, livreResultats);
Avec guava de Google
Même chose que précédemment, nous utilisons l'interface Predicate, mais cette fois-ci celle de guava :
public interface Predicate<T> { boolean apply(T input); }
La bibliothèque guava supporte par défaut les types génériques ce qui simplifie l'écriture.
Voici l'implémentation de notre prédicat :
Predicate<Livre> predicate = new Predicate<Livre>() { public boolean apply(Livre livre) boolean estTrouve = StringUtils.containsIgnoreCase(livre.getAuteur(), "Kipling") && StringUtils.containsIgnoreCase(livre.getTitre(), "livre") && StringUtils.containsIgnoreCase(livre.getTitre(), "jungle"); return estTrouve; } };
Voici notre boucle de recherche qui utilise maintenant le prédicat :
// Livres trouvés par la recherche List<Livre> livreTrouves = new ArrayList<Livre>(); // Boucle de recherche for(Livre livre : livres) { // Test des critères if(predicat.apply(livre)) { // Le livre correspond aux critères livreTrouves.add(livre); } }
Cette boucle de recherche peut être remplacée par l'appel de la méthode filter de la classe Collections2 :
// Recherche de livres Collection<Livre> livreResultats = Collections2.filter(bibliotheque.getLivres(), predicate);
Conclusion
L'utilisation des prédicats pour la recherche permet de se concentrer sur l'intention : vérifier que les éléments trouvés correspondent bien aux critères de recherche.
Références
Common-collections d'Apache
Site
Dépendance Maven
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
Common-collections avec les types génériques
Site
Dépendance Maven
<dependency> <groupId>net.sourceforge.collections</groupId> <artifactId>collections-generic</artifactId> <version>4.01</version> </dependency>
Guava de Google
Site
Dépendance Maven
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>13.0.1</version> </dependency>