Modèle de conception d'interpréteur en Java

1. Vue d'ensemble

Dans ce didacticiel, nous présenterons l'un des modèles de conception comportementaux du GoF: l'interpréteur.

Dans un premier temps, nous donnerons un aperçu de son objectif et expliquerons le problème qu'il tente de résoudre.

Ensuite, nous examinerons le diagramme UML d'Interpreter et l'implémentation de l'exemple pratique.

2. Modèle de conception d'interprète

En bref, le modèle définit la grammaire d'une langue particulière d'une manière orientée objet qui peut être évaluée par l'interpréteur lui-même.

En gardant cela à l'esprit, techniquement, nous pourrions construire notre expression régulière personnalisée, un interpréteur DSL personnalisé ou nous pourrions analyser n'importe lequel des langages humains, créer des arbres de syntaxe abstraites, puis exécuter l'interprétation.

Ce ne sont que quelques-uns des cas d'utilisation potentiels, mais si nous réfléchissons un moment, nous pourrions en trouver encore plus d'utilisations, par exemple dans nos IDE, car ils interprètent continuellement le code que nous écrivons et nous fournissent des indices inestimables.

Le modèle d'interprétation doit généralement être utilisé lorsque la grammaire est relativement simple.

Sinon, il pourrait devenir difficile à maintenir.

3. Diagramme UML

Le diagramme ci-dessus montre deux entités principales: le contexte et l' expression .

Maintenant, toute langue doit être exprimée d'une manière ou d'une autre, et les mots (expressions) vont avoir une signification basée sur le contexte donné.

AbstractExpression définit une méthode abstraite qui prend le contextecomme paramètre. Grâce à cela, chaque expression affectera le contexte , changera son état et soit continuera l'interprétation, soit retournera le résultat lui-même.

Par conséquent, le contexte sera le détenteur de l'état global du traitement et il sera réutilisé pendant tout le processus d'interprétation.

Alors, quelle est la différence entre TerminalExpression et NonTerminalExpression ?

Une NonTerminalExpression peut avoir une ou plusieurs autres AbstractExpressions associées, par conséquent, elle peut être interprétée de manière récursive. En fin de compte, le processus d'interprétation doit se terminer par une TerminalExpression qui renverra le résultat.

Il convient de noter que NonTerminalExpression est un composite.

Enfin, le rôle du client est de créer ou d'utiliser une arborescence de syntaxe abstraite déjà créée , qui n'est rien de plus qu'une phrase définie dans le langage créé.

4. Mise en œuvre

Pour montrer le modèle en action, nous allons créer une syntaxe simple de type SQL orientée objet, qui sera ensuite interprétée et nous renverra le résultat.

Tout d'abord, nous allons définir les expressions Select, From et Where , créer une arborescence de syntaxe dans la classe du client et exécuter l'interprétation.

L' interface Expression aura la méthode d'interprétation:

List interpret(Context ctx);

Ensuite, nous définissons la première expression, la classe Select :

class Select implements Expression { private String column; private From from; // constructor @Override public List interpret(Context ctx) { ctx.setColumn(column); return from.interpret(ctx); } }

Il obtient le nom de la colonne à sélectionner et une autre expression concrète de type From en tant que paramètres dans le constructeur.

Notez que dans la méthode interprétée surchargée , elle définit l'état du contexte et transmet l'interprétation à une autre expression avec le contexte.

De cette façon, nous voyons que c'est une NonTerminalExpression.

Une autre expression est la classe From :

class From implements Expression { private String table; private Where where; // constructors @Override public List interpret(Context ctx) { ctx.setTable(table); if (where == null) { return ctx.search(); } return where.interpret(ctx); } }

Maintenant, en SQL, la clause where est facultative, donc cette classe est soit un terminal, soit une expression non-terminale.

Si l'utilisateur décide de ne pas utiliser de clause where, l' expression From se terminera par l' appel ctx.search () et retournera le résultat. Sinon, cela va être interprété davantage.

L' expression Where modifie à nouveau le contexte en définissant le filtre nécessaire et termine l'interprétation avec un appel de recherche:

class Where implements Expression { private Predicate filter; // constructor @Override public List interpret(Context ctx) { ctx.setFilter(filter); return ctx.search(); } }

Pour l'exemple, la classe Context contient les données qui imitent la table de base de données.

Notez qu'il comporte trois champs clés qui sont modifiés par chaque sous-classe d' expression et la méthode de recherche:

class Context { private static Map
    
      tables = new HashMap(); static { List list = new ArrayList(); list.add(new Row("John", "Doe")); list.add(new Row("Jan", "Kowalski")); list.add(new Row("Dominic", "Doom")); tables.put("people", list); } private String table; private String column; private Predicate whereFilter; // ... List search() { List result = tables.entrySet() .stream() .filter(entry -> entry.getKey().equalsIgnoreCase(table)) .flatMap(entry -> Stream.of(entry.getValue())) .flatMap(Collection::stream) .map(Row::toString) .flatMap(columnMapper) .filter(whereFilter) .collect(Collectors.toList()); clear(); return result; } }
    

Une fois la recherche terminée, le contexte s'efface de lui-même, de sorte que la colonne, la table et le filtre sont définis par défaut.

De cette façon, chaque interprétation n'affectera pas l'autre.

5. Test

À des fins de test, jetons un coup d'œil à la classe InterpreterDemo :

public class InterpreterDemo { public static void main(String[] args) { Expression query = new Select("name", new From("people")); Context ctx = new Context(); List result = query.interpret(ctx); System.out.println(result); Expression query2 = new Select("*", new From("people")); List result2 = query2.interpret(ctx); System.out.println(result2); Expression query3 = new Select("name", new From("people", new Where(name -> name.toLowerCase().startsWith("d")))); List result3 = query3.interpret(ctx); System.out.println(result3); } }

Tout d'abord, nous construisons un arbre de syntaxe avec les expressions créées, initialisons le contexte, puis exécutons l'interprétation. Le contexte est réutilisé, mais comme nous l'avons montré ci-dessus, il se nettoie après chaque appel de recherche.

En exécutant le programme, la sortie doit être la suivante:

[John, Jan, Dominic] [John Doe, Jan Kowalski, Dominic Doom] [Dominic]

6. Inconvénients

Lorsque la grammaire devient plus complexe, elle devient plus difficile à maintenir.

Cela peut être vu dans l'exemple présenté. Il serait raisonnablement facile d'ajouter une autre expression, comme Limit , mais ce ne sera pas trop facile à maintenir si nous décidons de continuer à l'étendre avec toutes les autres expressions.

7. Conclusion

Le modèle de conception d'interprète est idéal pour une interprétation grammaticale relativement simple , qui n'a pas besoin d'évoluer et de s'étendre beaucoup.

Dans l'exemple ci-dessus, nous avons montré qu'il est possible de construire une requête de type SQL de manière orientée objet à l'aide du modèle d'interpréteur.

Enfin, vous pouvez trouver cette utilisation du modèle dans JDK, en particulier dans java.util.Pattern , java.text.Format ou java.text.Normalizer .

Comme d'habitude, le code complet est disponible sur le projet Github.