Interpreter mode of design mode

Posted by d~l on Mon, 17 Jan 2022 00:12:03 +0100

summary

As shown in the figure above, a software is designed for addition and subtraction calculation. Our first idea is to use tool classes to provide corresponding addition and subtraction tools and methods.

//Used to add two integers
public static int add(int a,int b){
    return a + b;
}

//Used to add two integers
public static int add(int a,int b,int c){
    return a + b + c;
}

//Used to add n integers
public static int add(Integer ... arr) {
    int sum = 0;
    for (Integer i : arr) {
        sum += i;
    }
    return sum;
}

The above form is relatively simple and limited. If the form changes very much, it does not meet the requirements, because there are infinite combinations of addition and subtraction operators and values. For example, 1 + 2 + 3 + 4 + 5, 1 + 2 + 3-4, etc.

Obviously, there is a need for a translation recognition machine that can parse the legal operation sequence composed of numbers and + - symbols. If operators and numbers are regarded as nodes, they can be read and analyzed node by node, which is the thinking of interpreter mode.

Definition: given a language, define its grammatical representation, and define an interpreter that uses the identifier to interpret sentences in the language.

In the interpreter mode, we need to extract rules from the problems to be solved and abstract them into a "language". For example, the operation rule of addition and subtraction is: a legal sequence composed of numerical values and + - symbols, "1 + 3-2" is the sentence of this language.

The interpreter is to parse the meaning of the statement. But how to describe the rules?

Grammar (grammar) Rules: grammar is a formal rule used to describe the grammatical structure of a language.

expression ::= value | plus | minus
plus ::= expression '+' expression   
minus ::= expression '-' expression  
value ::= integer

Note: the symbol ":: =" here means "defined as", and the vertical bar | indicates one of the left and right or. The character itself is inside the quotation mark, and the syntax is outside the quotation mark.

The above rules are described as follows: an expression can be a value, plus or minus operation, and plus and minus are composed of expression combination operators. The type of value is integer.

Abstract Syntax tree: in computer science, abstract Syntax tree (AST), or Syntax tree for short, is an abstract representation of the syntax structure of source code. It represents the syntax structure of the programming language in the form of a tree, and each node on the tree represents a structure in the source code. Use a tree to represent sentences that conform to grammatical rules.

structure

The Interpreter pattern contains the following main roles.

  1. Abstract Expression role: defines the interface of the interpreter and specifies the interpretation operation of the interpreter, mainly including the interpretation method interpret().
  2. Terminator Expression role: it is a subclass of abstract Expression, which is used to implement the operations related to terminators in grammar. Each terminator in grammar has a specific terminator Expression corresponding to it.
  3. Non terminal expression role: it is also a subclass of abstract expression, which is used to realize the operations related to non terminal in grammar. Each rule in grammar corresponds to a non terminal expression.
  4. Context role: it usually contains the data or public functions required by each interpreter. It is generally used to transfer the data shared by all interpreters. Subsequent interpreters can obtain these values from here.
  5. Client (Client): the main task is to translate the sentences or expressions that need analysis into the abstract syntax tree described by the interpreter object, then invoke the interpreter's interpretation method, and of course, can indirectly access the interpreter's interpretation method through the role of the environment.

The structure diagram of interpreter mode is shown in the figure below:

The key to the implementation of interpreter mode is to define grammar rules, design terminator classes and non terminator classes, draw structure diagrams, and build syntax trees if necessary. The code structure is as follows:

//Abstract expression class
interface AbstractExpression {
    public void interpret(String info);    //Interpretation method
}

//Terminator expression class
class TerminalExpression implements AbstractExpression {
    public void interpret(String info) {
        //Processing of terminator expressions
    }
}

//Non terminator expression class
class NonterminalExpression implements AbstractExpression {
    private AbstractExpression exp1;
    private AbstractExpression exp2;

    public void interpret(String info) {
        //Processing of non terminator expressions
    }
}

//Environment class
class Context {
    private AbstractExpression exp;

    public Context() {
        //Data initialization
    }

    public void operation(String info) {
        //Call the interpretation method of the related expression class
    }
}

Case realization

Example: design and implement the software of addition and subtraction method.

The UML class diagram is designed as follows:

The implementation codes are as follows:

  • AbstractExpression.java
/**
 * @author lcl100
 * @create 2021-07-18 12:06
 * @desc Abstract expression role
 */
public abstract class AbstractExpression {
    /**
     * Interpretation expression
     * @param context
     * @return
     */
    abstract int interpret(Context context);
}
  • Value.java
/**
 * @author lcl100
 * @create 2021-07-18 12:07
 * @desc Terminator expression role
 */
public class Value extends AbstractExpression {
    private int value;

    public Value(int value) {
        this.value = value;
    }

    @Override
    int interpret(Context context) {
        return value;
    }

    @Override
    public String toString() {
        return Integer.toString(value);
    }
}
  • Variable.java
/**
 * @author lcl100
 * @create 2021-07-18 12:08
 * @desc Terminator expression
 */
public class Variable extends AbstractExpression {
    private String name;

    public Variable(String name) {
        this.name = name;
    }

    @Override
    int interpret(Context context) {
        return context.getValue(this);
    }

    @Override
    public String toString() {
        return name;
    }
}
  • Minus.java
/**
 * @author lcl100
 * @create 2021-07-18 12:11
 * @desc Nonterminal expression, role, subtraction expression
 */
public class Minus extends AbstractExpression{
    private AbstractExpression left;
    private AbstractExpression right;

    public Minus(AbstractExpression left, AbstractExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    int interpret(Context context) {
        return left.interpret(context)-right.interpret(context);
    }

    @Override
    public String toString() {
        return "("+left.toString()+"-"+right.toString()+")";
    }
}
  • Plus.java
/**
 * @author lcl100
 * @create 2021-07-18 12:11
 * @desc Nonterminal expression, role, additive expression
 */
public class Plus extends AbstractExpression{
    private AbstractExpression left;
    private AbstractExpression right;

    public Plus(AbstractExpression left, AbstractExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    int interpret(Context context) {
        return left.interpret(context)+right.interpret(context);
    }

    @Override
    public String toString() {
        return "("+left.toString()+"+"+right.toString()+")";
    }
}
  • Context.java
/**
 * @author lcl100
 * @create 2021-07-18 12:13
 * @desc Environment class
 */
public class Context {
    /**
     * Store the mapping relationship between the expression and its corresponding value, for example: a=1, b=2
     */
    private Map<Variable, Integer> map = new HashMap<>();

    /**
     * Assign a value to the specified expression
     * @param var Specify expression
     * @param value Value to be assigned
     */
    public void assign(Variable var, Integer value) {
        map.put(var, value);
    }

    public int getValue(Variable var) {
        return map.get(var);
    }
}
  • Client.java
/**
 * @author lcl100
 * @create 2021-07-18 12:
 * @desc Test class
 */
public class Client {
    public static void main(String[] args) {
        Context context = new Context();

        // Create variable expression
        Variable a = new Variable("a");
        Variable b = new Variable("b");
        Variable c = new Variable("c");
        Variable d = new Variable("d");
        Variable e = new Variable("e");
        // Assign values to variable expressions
        context.assign(a, 1);
        context.assign(b, 2);
        context.assign(c, 3);
        context.assign(d, 4);
        context.assign(e, 5);

        AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);
        System.out.println(expression + "=" + expression.interpret(context));
    }
}

Advantages and disadvantages

Interpreter mode is a kind of behavioral mode, and its main advantages are as follows:

  1. Good scalability. Because classes are used to represent the grammar rules of the language in the interpreter mode, the grammar can be changed or extended through mechanisms such as inheritance.
  2. Easy to implement. Each expression node class in the syntax tree is similar, so it is easier to implement its grammar.
  3. It is more convenient to add a new interpretation expression. If you need to add a new interpretation expression, you only need to add a new terminator expression or non terminator expression class accordingly. The original expression class code does not need to be modified and complies with the "opening and closing principle".

The main disadvantages of interpreter mode are as follows:

  1. Low execution efficiency. Interpreter mode usually uses a large number of loops and recursive calls. When the sentences to be interpreted are complex, its running speed is very slow, and the debugging process of the code is also troublesome.
  2. Will cause class expansion. Each rule in the Interpreter pattern needs to define at least one class. When there are many grammar rules, the number of classes will increase sharply, making it difficult for the system to manage and maintain.
  3. There are few applicable scenarios. In software development, there are very few application examples that need to define language grammar, so this model is rarely used.

Applicable scenarios:

  • When the grammar of the language is relatively simple and execution efficiency is not the key problem.

  • When the problem occurs repeatedly and can be expressed in a simple language.

  • When a language needs interpretation and execution, and the sentences in the language can be represented as an abstract syntax tree.

Topics: Design Pattern