How to work with the Interpreter design pattern

Take advantage of the Interpreter design pattern to represent your application’s business rules in terms of domain specific languages

How to work with the Interpreter design pattern
Thinkstock

Design patterns are proven solutions to common software design problems. They provide the correct and tested solution for what is often encountered while designing software applications. The interpreter pattern is such a solution.

The Interpreter design pattern defined

The Interpreter design pattern lets you define your application's business rules as classes. You can take advantage of this design pattern to build domain-specific languages. The Interpreter design pattern is a behavioral pattern, specifying how the instances of classes should communicate; behavioral design patterns deal with object collaboration and delegation of responsibilities among objects.

The Interpreter design pattern is similar to the Composite design pattern. The Composite design pattern works like an Interpreter design pattern when the resulting hierarchical structure represents grammar: You have an interpreter that accepts a set of rules and interprets them. In essence, this pattern evaluates language grammar and expressions.

Typical use cases for the Interpreter design pattern

The Interpreter design pattern isn’t as popular as other patterns are, and it is often overlooked due to its complexity. However, it is a powerful design pattern with some distinct use cases. A typical use case of the Interpreter design pattern is to represent a simple grammar as an abstract syntax tree like structure. Another use case is to produce various types of output of your application's data; that is, generating reports—maybe in different formats. You can also take advantage of the Interpreter design pattern to implement SQL parsers and symbol processing engines.

The participants in the Interpreter design pattern

The participants in a typical implementation of the Interpreter design pattern include the following:

  • Context: This is the class that contains information available to the interpreter.
  • AbstractExpression: This is typically an interface that defines an operation.
  • TerminalExpression: This acts as the main interpreter and implements an operation ssociated with the terminal symbols as defined in the grammar.
  • NonTerminalExpression: This is typically used to implement the logical operators (AND, OR, etc.) between terminal and nonterminal expressions
  • Client: This is the consumer that builds a syntax tree or is provided with a syntax tree that represents a sentence in the language defined by the grammar. The client typically initiates the interpret operations.

Implementing the Interpreter design pattern

To implement a structural representation of the Interpreter design pattern in C#, consider the following abstract class named ExpressionBase. Note that this is just a structural implementation of the Interpreter design pattern.

public abstract class ExpressionBase
    {
        public abstract void Interpret(Context context);
    }

The TerminalExpression class extends the ExpressionBase abstract base class and implements the Interpret method:

public class TerminalExpression : ExpressionBase
    {
        public override void Interpret(Context context)
        {
            Console.WriteLine("TerminalExpression for {0}.", context.Output);
        }
    }

The Interpret method accepts an instance of the Context class as a parameter.

public class Context
    {
        public string Output { get; set; }
        public Context(string output)
        {
            Output = output;
        }
    }

Here's the Client class that instantiates the Context and the TerminalExpression classes and invokes the Interpret method on the TerminalExpression class by passing the instance of the Context class.

public class Client
    {
        public void Interpret()
        {
            Context context = new Context("IDG Context");
            TerminalExpression terminalExpressionObj = new TerminalExpression();          
            terminalExpressionObj.Interpret(context);
        }
    }

Here’s the complete code:

public class Context
    {
        public string Output { get; set; }
        public Context(string output)
        {
            Output = output;
        }
    }
    public abstract class ExpressionBase
    {
        public abstract void Interpret(Context context);
    }
    public class TerminalExpression : ExpressionBase
    {
        public override void Interpret(Context context)
        {
            Console.WriteLine("TerminalExpression for {0}.", context.Output);
        }
    }
    public class Client
    {
        public void Interpret()
        {
            Context context = new Context("IDG Context");
            TerminalExpression terminalExpressionObj = new TerminalExpression();          
            terminalExpressionObj.Interpret(context);
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Client client = new Client();
            client.Interpret();
            Console.Read();
        }
    }

Potential downsides of the Interpreter design pattern

One potential downside to using this pattern is its implementation complexity. Thus, you should have extensive error checks in place before you release the application to production.

Also, as the grammar becomes complicated over time (maybe due to the updates in the language), maintaining the code base might become difficult. Plan accordingly!

Copyright © 2018 IDG Communications, Inc.