Gang of Four Behavioural pattern: Interpreter
Behavioural Pattern
A way to include language elements in a program. Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
The interpreter pattern has a InterpreterContext which the interpreted language can interact with.
To get the code for this example:
git clone https://github.com/spotadev/gangoffour.git
In src/main/java navigate to this package:
com.javaspeak.designpatterns.go4.behavioural.interpreter
You can run the code from the main method of:
InterpreterApplication
This example uses "Object Query Language (oql)" as its interpreted language and the InterpreterContext is used to register Java objects with. The idea is the once a java object is registered in the InterpreterContext that you can use oql to update the java object with data or to select data from.
In the example we are using oql to insert some data into Person and retrieve some data. We print the data retrieved to System.out.
The mechanics of the Interpreter pattern in this example work as follows:
The interpreter first of all separates the oql into statements. It does this by looking for a ";" separator between the statements. It then executes the statements one by one.
Note that the Interpreter uses Expression objects to help it parse data and call the right commands. In our example we have a InsertExpression and SelectExpression which both extends the abstract class AbstractExpression.
If a oql statement starts with "insert" the interpreter knows to use the InsertExpression to perform the parsing and call setFieldValue(..) method on the InterpreterContext.
If however the oql statement starts with "select" the interpreter knows to use the SelectStatement to perform the parsing and call getFieldValue(..) method on the InterpreterContext and add the selected values to the valuesToReturn list.
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
import java.util.List;
/**
* Text book description:
* <ul>
* Interpreter: A way to include language elements in a program. Given a language, define a
* representation for its grammar along with an interpreter that uses the representation to
* interpret sentences in the language.
* </ul>
* The interpreter pattern has a InterpreterContext which the interpreted language can interact
* with. This example uses "Object Query Language (oql)" as its interpreted language and the
* InterpreterContext is used to register Java objects with it. The idea is that once a java
* object is registered in the InterpreterContext, you can use oql to update the java object with
* data or to select data from it.
* <p>
* In the example we are using oql to insert some data into Person and retrieve some data. We
* print the data retrieved to System.out.
* <p>
* The mechanics of the Interpreter pattern in this example work as follows:
* <p>
* The interpreter first of all separates the oql into statements. It does this by looking for
* a ";" separator between the statements. It then executes the statements one by one.
* <p>
* Note that the Interpreter uses Expression objects to help it parse data and call the right
* commands. In our example we have a InsertExpression and SelectExpression which both extends
* the abstract class AbstractExpression.
* <p>
* If a oql statement starts with "insert" the interpreter knows to use the InsertExpression
* to perform the parsing and call setFieldValue(..) method on the InterpreterContext.
* <p>
* If however the oql statement starts with "select" the interpreter knows to use the
* SelectStatement to perform the parsing and call getFieldValue(..) method on the
* InterpreterContext and add the selected values to the valuesToReturn list.
* <p>
* @author John Dickerson - 21 Feb 2020
*/
public class InterpreterApplication {
public void runExample() throws Exception {
InterpreterContext interpreterContext = new InterpreterContext();
// register an object with the interpreter which we wish to insert data into or select
// data from
interpreterContext.registerObject( new Person() );
Interpreter interpreter = new Interpreter( interpreterContext );
// Build up an insert statement and select statement. Separate the statements by ";"
StringBuilder sb = new StringBuilder();
sb.append( "insert into " ).append( Person.class.getName() );
sb.append( " firstName=John lastName=Dickerson height=180;" );
sb.append( "select height, firstName, lastName from " );
sb.append( Person.class.getName() ).append( ";" );
// Object Query Language
String oql = sb.toString();
// Call the interpreter to execute the oql and return values.
List<Value> values = interpreter.interpret( oql );
// Print the values retrieved to System.Out
for ( Value value : values ) {
System.out.print( value + " " );
}
}
public static void main( String[] args ) throws Exception {
InterpreterApplication application = new InterpreterApplication();
application.runExample();
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
/**
* Value is a wrapper that wraps values of different types and provides helper methods to
* retrieve the types in different formats. Note that attempting to retrieve a value in the wrong
* format could result in an error.
*
* @author John Dickerson - 21 Feb 2020
*/
public class Value {
private Object object;
/**
* Constructor
*
* @param object
*/
public Value( Object object ) {
this.object = object;
}
/**
* If the object is instance of String return it, else call toString() method on it. This
* method should not cause a ClassCastException if used incorrectly.
*
* @return String value
*/
public String getString() {
if ( object instanceof String ) {
return ( String )object;
}
else {
return object.toString();
}
}
/**
* This method will cast the object to Integer or attempt to convert the value to an Integer.
*
* @return Integer
*/
public Integer getInteger() {
if ( object instanceof Integer ) {
return ( Integer )object;
}
else {
// Note that if the object cannot be represented as an Integer this could throw an error
return Integer.parseInt( object.toString() );
}
}
/**
* Returns the object with no conversion
*
* @return object
*/
public Object getObject() {
return object;
}
public String toString() {
if ( object instanceof String ) {
return getString();
}
else if ( object instanceof Integer ) {
return getInteger().toString();
}
else if ( object != null ) {
return object.toString();
}
else {
return null;
}
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* In the interpreter pattern, Expressions interact with the InterpreterContext to retrieve
* information from it or update it.
* <p>
* Expressions are used to parse code snippets of interpreted language. In this example we have
* InsertExpression to parse "Object Query Language" insert statements and we have a
* SelectExpression to parse select statements.
* <p>
* The InsertExpression updates the fields of registered objects in the InterpreterContext using
* methods provided by the InterpreterContext.
* <p>
* Similarly the SelectExpression selects fields from the registered objects in the
* InterpreterContext using methods provided by the InterpreterContext.
* <p>
* Note that the InterpreterContext holds registered objects in a map and uses reflection to update
* or retrieve the fields of the registered objects. You will note that in this example we have
* only registered one object called Person and its fields are private. Using reflection we are
* able to access private fields; infact there are no public getters or setters in the registered
* Person object.
*
* @author John Dickerson - 21 Feb 2020
*/
public class InterpreterContext {
// Map to hold registered Java objects
private Map<String, Object> objects = new HashMap<String, Object>();
/**
* Adds or updates an object in the map
*
* @param object
* The object to add or update
*/
public void registerObject( Object object ) {
objects.put( object.getClass().getName(), object );
}
/**
* Retrieve object by className
*
* @param className
* The className of the object to retrieve
*
* @return registered Object
*/
public Object getObject( String className ) {
return objects.get( className );
}
/**
* Constructor
*/
public InterpreterContext() {
}
/**
* Uses reflection to retrieve the value of a field from an object in the Map
*
* @param className
* The class name of the object in the map
*
* @param fieldName
* The fieldName we wish to retrieve the value for
*
* @return the Field Value
* @throws NoSuchFieldException
* @throws SecurityException
* @throws IllegalAccessException
*/
public Object getFieldValue( String className, String fieldName )
throws NoSuchFieldException, SecurityException, IllegalAccessException {
Object instance = objects.get( className );
// throw an error if there is no registered object of that className in
// the map
if ( instance == null ) {
throw new ObjectNotRegisteredException();
}
// retrieve the Field
Field field = instance.getClass().getDeclaredField( fieldName );
// allow us to access a private method
field.setAccessible( true );
// return the field value
return field.get( instance );
}
/**
* Uses reflection to set a field value of an object in the map. Note that
* the implementation is very sparse - it only supports String and Integer.
*
* @param className class name of the object which has the field we wish to
* update
*
* @param fieldName name of field we wish to update
*
* @param fieldValue value we wil update the field with
*
* @throws NoSuchFieldException
* @throws SecurityException
* @throws IllegalAccessException
*/
public void setFieldValue(
String className, String fieldName, String fieldValue )
throws NoSuchFieldException, SecurityException,
IllegalAccessException {
Object instance = objects.get( className );
// throw an error if there is no registered object of that className in
// the map
if ( instance == null ) {
throw new ObjectNotRegisteredException();
}
// retrieve field we wish to update
Field field = instance.getClass().getDeclaredField( fieldName );
field.setAccessible( true );
// If the field is an Integer convert the String passed in to a Integer
if ( field.getType().equals( Integer.class ) ) {
field.set( instance, Integer.parseInt( fieldValue ) );
}
else {
field.set( instance, fieldValue );
}
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
import java.util.ArrayList;
import java.util.List;
/**
* The interpreter class is responsible for holding a reference to the InterpreterContext and for
* determining which Expressions to use for parsing the interpreted language. The interpreter
* parses "Object Query Language (oql)" into statements and then executes the statements. Different
* Expression objects are used for parsing different kinds of statements.
* <p>
* For example a InsertExpression is used for parsing oql insert statements and a SelectExpression
* is used for parsing oql select statements.
* <p>
* In out example the InterpreterContext registers Java objects and allows us to update their
* values and select values from them by making method calls. The Expression objects,
* InsertExpression and SelectExpression make calls to the InterpeterContext to update the
* registered objects with new values or to retrieve values.
*
* @author John Dickerson - 21 Feb 2020
*/
public class Interpreter {
// Context holding registered objects
private InterpreterContext interpreterContext;
/**
* Constructor to pass in the context
*
* @param interpreterContext
*/
public Interpreter( InterpreterContext interpreterContext ) {
this.interpreterContext = interpreterContext;
}
/**
* Performs the following steps:
* <ul>
* <li>
* Parses "Object Query Language (oql)" into statements by looking
* for ";" separators.
* </li>
* <li>
* Checks the first word of each statement to determine which Expression to use to
* parse the statement. If a "select" is present uses a SelectExpression. If a
* "insert" is present uses a InsertExpression.
* </li>
* <li>
* interprets the Expression. The Expression in turn parses the oql and makes calls
* to the InterpreterContext to either insert data into the registered objects or
* retrieve data from them.
* </li>
* </ul>
*
* @param oql
* The language to interpret
* @return
* List of Expressions
* @throws Exception
*/
public List<Value> interpret( String oql ) throws Exception {
// separate statements by ";"
String[] statements = oql.split( ";" );
// create a list of values to return. Note that multile select
// statements can be called but only one list of values is returned.
List<Value> valuesToReturn = new ArrayList<Value>();
// For each statement choose a Expression to do the work
for ( String statement : statements ) {
statement = statement.trim();
Expression expression;
// if the statements start with "select" use a SelectExpression to
// parse statement. If starts with "insert" use a InsertExpression
// to parse statement
if ( statement.toLowerCase().startsWith( "select" ) ) {
expression = new SelectExpression( statement );
valuesToReturn.addAll(
expression.interpret( interpreterContext ) );
}
else if ( statement.toLowerCase().startsWith( "insert" ) ) {
expression = new InsertExpression( statement );
expression.interpret( interpreterContext );
}
else {
// The statements starts with an unexpected word so throw
// an error
throw new Exception( "Incorrect syntax in : " + statement );
}
}
return valuesToReturn;
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
import java.util.List;
import java.util.StringTokenizer;
/**
* Used for parsing insert "Object Query Language (oql)" statements.
*
* @author John Dickerson - 21 Feb 2020
*/
public class InsertExpression implements Expression {
private String insertStatement;
/**
* Insert statement is passed through the consructor
*
* @param insertStatement
* Insert statement to be parsed and executed
*/
public InsertExpression( String insertStatement ) {
this.insertStatement = insertStatement;
}
@Override
public List<Value> interpret( InterpreterContext context ) throws Exception {
// tokenize statement using spaces and commas as seperators
StringTokenizer wordTokenizer = new StringTokenizer( insertStatement, ", " );
// the first token is "insert"
wordTokenizer.nextToken();
// the second token is "into"
wordTokenizer.nextToken();
// the third token is the className of the object we wish to insert into
String className = wordTokenizer.nextToken();
String valuePair;
StringTokenizer pairTokenizer;
String fieldName;
String fieldValue;
// we assume that all remaining tokens have the form:
// fieldName=fieldValue. We also assume that fieldValue do not have
// spaces
while ( wordTokenizer.hasMoreTokens() &&
( valuePair = wordTokenizer.nextToken() ) != null ) {
pairTokenizer = new StringTokenizer( valuePair, "=" );
fieldName = pairTokenizer.nextToken();
fieldValue = pairTokenizer.nextToken();
context.setFieldValue( className, fieldName, fieldValue );
}
// insert statements do not return values so we return null
return null;
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
/**
* Used for parsing select "Object Query Language (oql)" statements.
*
* @author John Dickerson - 21 Feb 2020
*/
public class SelectExpression implements Expression {
// select statement to parse
private String selectStatement;
// select statement is passed through constructor
public SelectExpression( String selectStatement ) {
this.selectStatement = selectStatement;
}
@Override
public List<Value> interpret( InterpreterContext context ) throws Exception {
// tokenize statement using spaces and commas as seperators
StringTokenizer wordTokenizer = new StringTokenizer( selectStatement, ", " );
// the first token is "select"
wordTokenizer.nextToken();
String tokenString;
// creates a list to hold the fieldNames
List<String> fieldNames =
new ArrayList<String>( wordTokenizer.countTokens() );
// add all tokens before the toek "from" to the fieldNames list
while ( wordTokenizer.hasMoreTokens() &&
!( tokenString = wordTokenizer.nextToken() ).equals( "from" ) ) {
fieldNames.add( tokenString );
}
// the next token is the className
String className = wordTokenizer.nextToken();
// initialize the list of field values to return
List<Value> fieldValues = new ArrayList<Value>( fieldNames.size() );
Value value;
for ( String fieldName : fieldNames ) {
// make a call to the InterpreterContext to retrieve the field value
// for the field in the specified object registered with the
// InterpreterContext
value = new Value( context.getFieldValue( className, fieldName ) );
// add the field value to the list of Values to return.
fieldValues.add( value );
}
// return the field values
return fieldValues;
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
/**
* Thrown if we attempt to retrieve an object from a map in the InterpreterContext when the object
* is not there.
*
* @author John Dickerson - 21 Feb 2020
*/
public class ObjectNotRegisteredException extends RuntimeException {
private static final long serialVersionUID = -2331430364438294926L;
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
/**
* This is test class we are using to test our interpreted "Object Query Language" on. Note that
* as our InterpreterContext uses reflection to access private instance fields we do not even need
* setters and getters. We have consequently left setters and getters out of the code.
*
* @author John Dickerson - 21 Feb 2020
*/
public class Person {
@SuppressWarnings( "unused" )
private String firstName;
@SuppressWarnings( "unused" )
private String lastName;
@SuppressWarnings( "unused" )
private Integer height;
public Person() {
}
}
package com.javaspeak.designpatterns.go4.behavioural.interpreter;
import java.util.List;
/**
* Typically an interpreted language encapsulates the parsing into different expressions. In
* this example all Expressions need to extend AbstractExpression. In this example we have an
* InsertExpression and a SelectExpression.
* <p>
* The interpreter parses the "Object Query Language (oql) into statements and then checks which
* expression to use to parse each statement. For example the InsertExpression is used for parsing
* oql which is inserting data into objects and the SelectExpression is used for parsing oql which
* is used for retrieving data from objects.
*
* @author John Dickerson - 21 Feb 2020
*/
public interface Expression {
/**
* Classes implementing AbstractExpression should pass the oql to parse through their
* constructor
*
* @param context
* The InterpreterContext which holds a registry of objects.
*
* @return return List of Value or null.
* The InsertExpression returns null while the SelectExpression returns a list of Value.
*
* @throws Exception if oql is not well formed.
*/
public List<Value> interpret(
InterpreterContext context ) throws Exception;
}
Back: Gang of Four
Page Author: JD