Behavioral Interpreter Design Pattern
we're going to look at the interpreter design pattern. The interpreter pattern is a behavior pattern that you used to represent the grammar of a language. A lot of tools use this pattern when parsing various aspects of grammar.
Let's look at some of the concepts considered when choosing this pattern.
the concept surrounding why you would choose the interpreter pattern or that it represents grammar. This could be music notation or mathematical equations, or even another language. Compilers will use the interpreter pattern too often.
This goes hand in hand with representing the grammar, but we can then use it to interpret a sentence. This enables us to map out a domain-specific language. If you've ever used SQL or an XML parser, this is the exact thing that is.
This pattern was designed to do define a language that can be interpreted to do things you'll often see an interpreter used when defining an abstract syntax tree. To examples of this in the Java API. The Java Util pattern, the pattern classes used to represent a compiled regular expression, is an incredibly powerful way to search through strings. Another representation is the java class format is an abstract-based class that is used to represent locale-sensitive content such as dates, numbers, and strings. Let's look at some of the design considerations when choosing this pattern.
the design of the interpreter is quite a bit different than most of the other patterns that we've looked at. There is an abstract-based class or an abstract expression that declares an interface for executing an operation. That operation is an interpreted method. Expressions are then broken into terminal expressions, which represent a leaf of a tree or an expression that does not contain other expressions. If it does contain other expressions then it's a nonterminal expression. Nonterminal expressions represent compound expressions and continue. So we called itself a recursive tree until it finally represents a terminal expression or multiple subexpressions.
The pieces of the UML diagram are the context, abstract expression, terminal expression, non-terminal expression, and a client.
Example: Pattern
the pattern class used in conjunction with regular expressions is a good example of the interpreter pattern being used in the Java API. We created sentences and establish grammar for that sentence. From there, we're going to interpret the sentence and display what we parsed using the pattern. Let's look at this in life code now.
Code :
package interpreterpattern;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class InterpreterJavaAPIDemo {
public static void main(String[] args) {
String input = "Lions , and tigers , and bears! oh , my";
Pattern p = Pattern.compile("(Lions , and tigers , and bears! oh , my)");
Matcher m = p.matcher(input);
while (m.find()) {
System.out.println("Found a " + m.group() + ".");
}
}
}
Output :
Lions Bears is true
Demo Structure for real-time project implementation:
package interpreterpattern;
public class InterpreterDemo {
static Expression buildInterpreterTree() {
Expression terminal1 = new TerminalExpression("Lions");
Expression terminal2 = new TerminalExpression("Tigers");
Expression terminal3 = new TerminalExpression("Bears");
Expression alternation1 = new AndExpression(terminal2, terminal3);
Expression alternation2 = new OrExpression(terminal1, alternation1);
return new AndExpression(terminal3, alternation2);
}
public static void main(String[] args) {
// String context = "Lions";
// String context = "Tigers";
// String context = "Tigers";
//String context = "Lions Tigers";
String context = "Lions Bears";
Expression define = buildInterpreterTree();
System.out.println(context + " is " + define.interpret(context));
}
}
package interpreterpattern;
public interface Expression {
boolean interpret(String context);
}
package interpreterpattern;
public class OrExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public OrExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) || expr2.interpret(context);
}
}
package interpreterpattern;
import java.util.StringTokenizer;
public class TerminalExpression implements Expression {
private String data;
public TerminalExpression(String data) {
this.data = data;
}
@Override
public boolean interpret(String str) {
StringTokenizer st = new StringTokenizer(str);
while (st.hasMoreTokens()) {
String test = st.nextToken();
if (test.equals(data)) {
return true;
}
}
return false;
}
}
package interpreterpattern;
public class AndExpression implements Expression {
private Expression expr1 = null;
private Expression expr2 = null;
public AndExpression(Expression expr1, Expression expr2) {
this.expr1 = expr1;
this.expr2 = expr2;
}
@Override
public boolean interpret(String context) {
return expr1.interpret(context) && expr2.interpret(context);
}
}
Output :
Lions Bears is true
Pitfalls
Now that we've created our own interpreter, let's look at some of the pitfalls of it. If the grammar becomes very complex, it can be difficult to maintain, as you noticed in our rule example that we had you could see very quickly that if I started adding these different answers or combinations, it could become a little bit interesting to try and debug and walkthrough. So complexity can be an issue there. There is at least one class per rule, so every time we create one of our new expressions were creating another class. Complex rules will require multiple classes to define them. That's where the difficulty of maintenance can come into play. The use of other patterns might help with your specific implementation of a complex interpreter, and adding a new variant requires us to change every variant of that class. The interpreter is a little unique compared to some of the other patterns that we have looked at because it is fairly specific to the problem that we're trying to solve to better understand when we should use this or a different pattern. Let's contrast it with the visitor pattern.
Contrast to Other Patterns
To contrast the interpreter pattern. Let's compare it with the visitor. The interpreter and the visitor are very similar in structure, but a different focus on implementation. The interpreter has access to properties because it contains the object. Functions are defined as methods, and since we extend implement the base interface, each interpret function is contained within a method. One drawback is that adding new functionality changes every variant recall the demo that we coated together when we were building the expression tree and the complexity that we could get by calm compounding those expressions together, the visitor is actually very similar to the interpreter, with some slight variations. Instead of having access to the properties we need, we have to implement the observer observable functionality to gain access to those properties similar to the interpreter. Functionality is found in one place, but it is in the visitors and not in the expression objects that were building and just like the interpreter. Adding a new variant requires changing every visitor. The focus is more about whether you're adding more expressions or grammar rules or adding new visitors to interact with, and that is the main focus on choosing one over the other.
Thanks.