Modèle de conception d’interprète en Java

Modèle de conception d'interprète en Java

1. Vue d'ensemble

Dans ce didacticiel, nous allons présenter l’un des modèles de conception comportementaux du GoF, l’interprète.

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

Nous verrons ensuite le diagramme UML d’Interpreter et la mise en œuvre de l’exemple concret.

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

En bref, le patterndefines the grammar of a particular language 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 créer notre expression régulière personnalisée, un interpréteur DSL personnalisé ou nous pourrions analyser n'importe lequel des langages humains,build abstract syntax trees and then run the interpretation.

Ce ne sont que quelques-uns des cas d'utilisation potentiels, mais si nous réfléchissons pendant 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 ainsi des indices inestimables.

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

Sinon, cela pourrait devenir difficile à maintenir.

3. Diagramme UML

image

Le diagramme ci-dessus montre deux entités principales: lesContext et lesExpression.

Maintenant, toute langue doit être exprimée d’une manière ou d’une autre, et les mots (expressions) vont avoir une signification en fonction du contexte donné.

AbstractExpression définit une méthode abstraite qui prend le contexte  comme paramètre. Grâce à cela,each expression will affect the context, change son état et soit continue l'interprétation, soit renvoie 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 lesTerminalExpression etNonTerminalExpression?

UnNonTerminalExpression peut avoir un ou plusieurs autresAbstractExpressions associés, donc il peut être interprété récursivement. En fin de compte,the process of interpretation has to finish with a TerminalExpression that will return the result.

Il convient de noter queNonTerminalExpression est uncomposite.

Enfin, le rôle du client est de créer ou d'utiliser unabstract syntax tree déjà créé, qui n'est rien de plus qu'unsentence defined in the created language.

4. la mise en oeuvre

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 expressionsSelect, From, etWhere, créer une arborescence de syntaxe dans la classe du client et exécuter l'interprétation.

L'interfaceExpression aura la méthode d'interprétation:

List interpret(Context ctx);

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

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 un autreExpression concret de typeFrom comme paramètres dans le constructeur.

Notez que dans la méthodeinterpret() substituée, elle définit l'état du contexte et transmet l'interprétation à une autre expression avec le contexte.

De cette façon, on voit que c'est unNonTerminalExpression.

Une autre expression est la classeFrom:

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);
    }
}

Or, en SQL, la clause where est facultative. Par conséquent, cette classe est une expression terminale ou non terminale.

Si l’utilisateur décide de ne pas utiliser de clause where, l’expressionFrom sera terminée par l’appelctx.search() et retournera le résultat. Sinon, cela va être interprété davantage.

La sexpressionWhere 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 classeContext contient les données qui imitent la table de la base de données.

Notez qu'il a trois champs clés qui sont modifiés par chaque sous-classe deExpression 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 lui-même. Par conséquent, la colonne, la table et le filtre sont définis sur les valeurs par défaut.

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

5. Essai

À des fins de test, examinons la classeInterpreterDemo :

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 et exécutons ensuite 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, le résultat devrait être le suivant:

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

6. Inconvénients

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

Cela se voit dans l'exemple présenté. Il serait assez facile d’ajouter une autre expression, commeLimit, 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 de l'interpréteur est excellentfor relatively simple grammar interpretation, qui n'a pas besoin d'évoluer et de s'étendre beaucoup.

Dans l'exemple ci-dessus, nous avons montré qu'il était possible de créer 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 de modèle dans JDK, en particulier, dansjava.util.Pattern,java.text.Format oujava.text.Normalizer.

Comme d'habitude, le code complet est disponible surthe Github project.