How To : Concevoir une API Fluent – Partie 2

Nous avons vu précédemment les requirements de notre POC pour mettre en pratique le concept de Fluent Interface sur une petite API de critères de recherche pour une requête SQL.

Structure d’un clause WHERE

Commençons par étudier la structure d’une clause WHERE en SQL.

criteria := criteria 'and' criteria
criteria := criteria 'or' criteria
criteria := fieldname operateur literal
criteria := fieldname is null
criteria := fieldname is not null

operateur := '='
operateur := '>'
operateur := '>='
...
avec fieldname : le nom d'un champ de la table
et literal une valeur numérique ou une chaine de caractères

Si on regarde bien notre clause WHERE, on voit que l’on peut toujours la découper de plus en plus finement pour obtenir un critère minimal de recherche (un « egale », ou un « is null », …). Chacun de ces critères sont collés au suivant soit par des AND soit par des OR. Les priorités de ces deux mots clés n’étant pas toujours clairs quand on regarde une requête complexe, j’ai pris la décision d’encapsuler chaque partie de mes critères des AND et des OR dans des parenthèses. Cela alourdi légèrement la requête générée mais facilite grandement la compréhension.

La classe de base : Criteria

Donc on va commencer par la classe principale de notre API : la classe Criteria.

Chaque critère pourra être chainé à un autre soit en faisant un and() soit un or().
Chaque critère sera responsable de générer la portion de SQL correspondant à lui même.

  1. package com.fluminis.criteria.internal.criteria;
  2.  
  3. public abstract class Criteria {
  4.  
  5.  public Criteria and(Criteria o2) {
  6.   return new AndCriteria(this, o2);
  7.  }
  8.  
  9.  public Criteria or(Criteria o2) {
  10.   return new OrCriteria(this, o2);
  11.  }
  12.  
  13.  public abstract String generateSQL();
  14. }

Gestion des AND et des OR

Un AND n’est autre qu’un Criteria qui fait l’union de deux autres Criteria avec un mot clé AND :
Comme mentionné plus haut, pour gérer facilement les priorités des opérateurs, on ajoute des parenthèses autours des deux critères.

  1. package com.fluminis.criteria.internal.criteria;
  2.  
  3. public class AndCriteria extends Criteria {
  4.  
  5.  private final static String AND = " AND ";
  6.  
  7.  private final Criteria o1;
  8.  private final Criteria o2;
  9.  
  10.  public AndCriteria(Criteria o1, Criteria o2) {
  11.   this.o1 = new Parenthesis(o1);
  12.   this.o2 = new Parenthesis(o2);
  13.  }
  14.  
  15.  @Override
  16.  public String generateSQL() {
  17.   StringBuilder sb = new StringBuilder(o1.generateSQL());
  18.   sb.append(AND).append(o2.generateSQL());
  19.   return sb.toString();
  20.  }
  21. }

(la classe OrCriteria est identique)

Et les parenthèses alors ?

La gestion des parenthèses est également facile. On peut se réprésenter cela comme un Criteria qui encapsule un autre Criteria.

  1. package com.fluminis.criteria.internal.criteria;
  2.  
  3. public class Parenthesis extends Criteria {
  4.  
  5.  private final static String OPEN_PARENTHESIS = "(";
  6.  private final static String CLOSE_PARENTHESIS = ")";
  7.  
  8.  private final Criteria o;
  9.  
  10.  public Parenthesis(Criteria o) {
  11.   this.o = o;
  12.  }
  13.  
  14.  @Override
  15.  public String generateSQL() {
  16.   StringBuilder sb = new StringBuilder(OPEN_PARENTHESIS);
  17.   sb.append(o.generateSQL()).append(CLOSE_PARENTHESIS);
  18.   return sb.toString();
  19.  }
  20. }

Les opérations booléennes

Bien maintenant, essayons de traiter les cas des opérations booléennes equals, greater, lesser, …
Si on décompose, il y a trois choses : le nom du champ, le signe de l’opération et la valeur.
On crée donc une classe abstraite pour mutualiser le comportement identique.

  1. package com.fluminis.criteria.internal.criteria;
  2.  
  3. public abstract class Operand extends Criteria {
  4.  
  5.  private final IField field;
  6.  private final Object value;
  7.  private final String operand;
  8.  
  9.  public Operand(IField field, String operand, Object value) {
  10.   this.field = field;
  11.   this.operand = operand;
  12.   this.value = value;
  13.  }
  14.  
  15.  @Override
  16.  public String generateSQL() {
  17.   StringBuilder sb = new StringBuilder(field.getName());
  18.   sb.append(operand).append(value);
  19.   return sb.toString();
  20.  }
  21. }
  22.  
  23. public class Equals extends Operand {
  24.  public Equals(IField field, Object value) {
  25.   super(field, "=", value);
  26.  }
  27. }
  28.  
  29. public class Greater extends Operand {
  30.  public Greater(IField field, Object value) {
  31.   super(field, ">", value);
  32.  }
  33. }

Le « is null » et « is not null » ne sont pas des Operand, mais de simples Criteria

  1. package com.fluminis.criteria.internal.criteria;
  2.  
  3. public class IsNull extends Criteria {
  4.  
  5.  private final IField field;
  6.  
  7.  public IsNull(IField field) {
  8.   this.field = field;
  9.  }
  10.  
  11.  @Override
  12.  public String generateSQL() {
  13.   StringBuilder sb = new StringBuilder(field.getName());
  14.   sb.append(" IS NULL");
  15.   return sb.toString();
  16.  }
  17. }

Comme vous l’aurez surement remarqué, nous n’utilisons pas des String pour le nom des champs mais des IField. Cela nous assure que le développeur ne sera pas tenté de manipuler des String à tout bout de champ.

  1. package com.fluminis.criteria.internal.criteria;
  2.  
  3. public interface IField {
  4.  String getName();
  5. }

Ok pour aujourd’hui. Nous verrons dans la prochaine partie comment gérer les types de données et comment réunir tout cela pour faire fonctionner notre API.
How To : Concevoir une API Fluent – Partie 3

Le commentaires sont fermés.