Interpreter Design Pattern

In this article I’ll introduce the interpreter design pattern and give you an example how you can use it.

About the pattern

As you may guess this pattern is about interpretation. Just like an interpreter who translates texts / speeches of different languages to a language which you can understand. Or like me who translates high-level programming patterns into an understandable tutorials with examples.

Let’s see what the Gang of Four (GoF) tells us about the pattern:

“Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.”

This pattern should be used when you have a really simple language which can be represented as an Abstract Syntax Tree (or AST for short).

Example

Today’s example will be a book search service with query expression. This example is very basic but I hope you’ll understand the concepts of this design pattern.

First of all let’s create the basic “entity” Book which will be used throughout this example:

import java.text.MessageFormat;

/**
 * Simple entity / DTO for this example.
 *
 * @author GHajba
 *
 */
public class Book {

    private final String title;
    private final String author;

    public Book(String title, String author) {
        this.title = title;
        this.author = author;
    }

    public String getAuthor() {
        return this.author;
    }

    public String getTitel() {
        return this.title;
    }

    @Override
    public String toString() {
        return MessageFormat.format("''{0}'' by {1}", this.title, this.author);
    }
}

Now we need an interpreter context to get the pattern implementation started:

import java.util.List;

/**
 * The context of the interpreter.
 *
 * @author GHajba
 *
 */
public class InterpreterContext {

    private final LeanPubWebService webService;

    /**
     * Constructor. Here we initialize our mock web service.
     *
     * @param endpoint
     *            the endpoint of the web service
     */
    public InterpreterContext(String endpoint) {
        this.webService = new LeanPubWebService(endpoint);
    }

    /**
     * @return all books from the web service
     */
    public List<Book> getAllBooks() {
        return this.webService.getAllBooks();
    }
}

Then we create a base class to have common functionality of expressions. Later this class can be extended to have multiple expressions:

/**
 * This is the base class for all expressions.
 *
 * @author GHajba
 *
 */
public abstract class AbstractExpression {

    /**
     * This function interprets the given context to fit our purposes.
     *
     * It does not need to have always a String as a return value -- you can adapt the functionality as you need.
     *
     * @param context
     *            the context of the interpreter
     * @return an interpreted result
     */
    public abstract String interpret(InterpreterContext context);
}

Naturally this solution can be created as an interface too. Currently it does not matter. However if you have common functionality in your sub-classes then it is always a good idea to extract it to a single point of this base class.

Now let’s create a simple implementation of this extension:

import java.util.List;

/**
 * Implementation of the {@link AbstractExpression}. This implementation interprets our book author search expression.
 *
 * @author GHajba
 *
 */
public class BookAuthorExpression extends AbstractExpression {

    private final String searchString;

    public BookAuthorExpression(String searchString) {
        this.searchString = searchString;
    }

    @Override
    public String interpret(InterpreterContext context) {
        final List<Book> books = context.getAllBooks();
        final StringBuilder result = new StringBuilder();
        for (final Book book : books) {
            if (book.getAuthor().equalsIgnoreCase(this.searchString)) {
                result.append(book.toString());
                result.append("\n");
            }
        }
        return result.toString();
    }
}

As you can see, this implementation knows about the attributes of a book and knows it has to filter the list of the books by the name of the author which is provided through the searchString parameter. Through the context it loads all the books from the web service and filters them by the name of the author.

In a real-world example you would send a specific search query to the web service where you already let the remote endpoint filter out all the books by the author (imagine we get 1_000_000 books back and we only need 3 of them — what a waste of I/O and time!). This won’t matter for the pattern because in this case the BookAuthorExpression will know how to send the information to the web service to get only those books where the author is the same as in the search String.

OK we are set-up. The following classes are the environment to enable running the application. The functionality (as you can see) is in this case a mock but for this example there is no need to have a live web service to fetch books.

First of all we need our web service endpoint:

import java.util.Arrays;
import java.util.List;

/**
 * A mock web service, actually does nothing useful. In a real world example you would abstract the functionality.
 *
 * @author GHajba
 *
 */
public class LeanPubWebService {

    public LeanPubWebService(String endpoint) {
        // we should initialize our web service here with the endpoint string
    }

    /**
     * @return list of all books in the web service
     */
    public List<Book> getAllBooks() {
        return Arrays.asList(
                new Book("Website Scraping with Python", "Gabor Laszlo Hajba"),
                new Book("Java 8 in Action", "Raul-Gabriel Urma"),
                new Book("Python 3 in Anger", "Gabor Laszlo Hajba"),
                new Book("XML Processing and Website Scraping with Java", "Gabor Laszlo Hajba"));
    }
}

This is clearly a mock service: there is no initialization to have this example as tiny as I can, and the list of books is very static and self-centric.

The next step is to have the main entry point of the application with our client to search for books by a given author:

/**
 * Our client and the main entry point of the application.
 *
 * @author GHajba
 *
 */
public class LeanPubClient {

    private final InterpreterContext context;

    public LeanPubClient(InterpreterContext context) {
        this.context = context;
    }

    /**
     * Interprets a string input of the form books by author '<string>'.
     *
     * As you can see, the expression is in the following format <searchType> by <searchAttribute> '<value>' where
     * <searchType> is the type of the search, in this particular case books; <searchAttribute> is the attribute where
     * to filter, in this example it is author; and <value> is the value of the filter criteria, in this example the
     * author's name.
     */
    public String interpret(String expression) {

        AbstractExpression exp = null;

        final String[] stringParts = expression.split(" ");
        final String searchType = stringParts[0];
        final String searchAttribute = stringParts[2];

        final String query = expression.substring(expression.indexOf("'") + 1, expression.lastIndexOf("'"));

        if (searchType.equals("books")) {
            if (searchAttribute.equals("author")) {
                exp = new BookAuthorExpression(query);
            }
        }
        if (exp != null) {
            return exp.interpret(this.context);
        }

        return "--";
    }

    public static void main(String... args) {
        final InterpreterContext context = new InterpreterContext("http://api.leanpub.com/");
        final LeanPubClient client = new LeanPubClient(context);

        final String result = client.interpret("books by author 'Gabor Laszlo Hajba'");
        System.out.println(result);
    }
}

Actually the API does not work. I could have enter any string for the constructor parameter it would not matter. As you can see the client parses the search expression and looks for an appropriate expression-handler to interpret the given string and get the results.

Conclusion

The interpreter pattern interprets you a custom language syntax and does the required functionality expressed with this language. However there are downsides too: too complex language structures can blow-up the number of your classes; and this pattern does no parsing, so you need a parser for complex language structures to provide it for your interpreter.

Leave A Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.