Design Patterns Software Engineering

Design Patterns – Visitor

Pinterest LinkedIn Tumblr

The code is available on GitHub

The Visitor Pattern decouples an operation from an object or a hierarchy of objects. Instead of adding the operation directly to the object, you create a separate Visitor that contains the operation. This allows you to have a hierarchy of objects and attach to it new functionalities without changing the hierarchy itself or any of the objects. To achieve this, you create a new Visitor that can traverse the entire hierarchy and perform the operation based on the object it visits.

When to use

Use the Visitor Pattern when you want to add a new operation for an existing object that doesn’t belong to that object.

Sometimes you need to add a new operation to an existing object that doesn’t fit well with what the object already does. Normally, you would add the operation as a method to the object, but if the operation does not belong to the object, it would result in multiple responsibilities, so it should be separated. In this case, you can create a new Visitor that handles the operation and decouples it from the object.

Use the Visitor Pattern when you want to add various operations to an object or an object hierarchy without altering the objects.

If you need to add multiple operations to an object or a hierarchy of objects (Composite), you would normally add it to base classes and override the operation in each object and implement it accordingly. However, sometimes it is not possible to alter the hierarchy of objects. The objects may exist in an external module or a module that we cannot modify. Alternatively, you can model the operations as separate Visitor objects that contain different implementations of the operations for different types of objects in the hierarchy. This way, any future changes to the operations can be isolated to a single Visitor class.

Add operation using Polymorphism
You can add an operation to a hierarchy of objects by creating a new method in each object of the hierarchy. However this may not be always possible.
Add operation using Visitor

Use the Visitor Pattern when you want your design to be easily extended every time a new operation for a hierarchy of objects is added.

In Object-Oriented Programming, methods should be encapsulated inside the related objects along with their state. When a new method is added to a superclass, all the subclasses should implement it accordingly. However, this approach has two main issues. First, the method may not be suitable for the objects, so it shouldn’t be encapsulated in their classes. Second and most importantly, every time a new operation is added, many different objects must be updated to provide the appropriate implementation. The Visitor Pattern inverts this logic by isolating the operation to a new dimension that can be expanded independently from the object hierarchy itself. All implementations of the same operation are grouped inside a visitor class. When a new operation is added, only a new visitor class needs to be created.

EntityOperation1()Operation2()Operation3()New Operation
Entity1[Operation 1][Operation 2][Operation 3][New operation]
Entity2[Operation 1][Operation 2][Operation 3][New operation]
Entity3[Operation 1][Operation 2][Operation 3][New operation]
NewEntity[Operation 1][Operation 2][Operation 3][New operation]
Polymorphic Design – Without Visitor – Each row is a separate entity. Every time a new operation is added we must update all entities, thus many components.

In the table above, it is easy to add a new Entity but very difficult to update the design each time a new operation is added. The visitor inverts the previous table, by having operations at each row instead of entities.

OperationsEntity1Entity2Entity3New Entity
Operation1()[Operation 1 for entity 1][Operation 1 for entity 2][Operation 1 for entity 3][Operation 1 for new entity]
Operation2()[Operation 2 for entity 1][Operation 2 for entity 2][Operation 2 for entity 3][Operation 2 for new entity]
Operation3()[Operation 3 for entity 1][Operation 3 for entity 2][Operation 3 for entity 3][Operation 3 for new entity]
AddNewOperation[New operation for entity 1][New operation for entity 2][New operation for entity 3][New operation for new entity]
Visitor Design – Each row is a visitor implementing an operation for each entity type. Adding a new operation is as easy as creating a new Visitor.

By having visitors as separate entities, you organize your design in a way that makes it easier to add new operations without the need to update multiple components each time.

How to implement

First, we need to define the object or the hierarchy of objects we want to perform an operation on.

To begin, you need to identify the objects or groups of objects on which you want to perform various operations. For example, in an application about recipes, we’ll have a collection of Ingredient objects such as Butter, Salt, Flour, and Sugar that form a recipe. Below, is a brief overview of the object hierarchy:

class Recipe
{ 
   public List<Ingredient> Ingredients { get; set; }
}

class Ingredient
{
   public string Name { get; set; }
   public double Quantity { get; set; }
}

class Butter : Ingredient
{
   public double FatContent { get; }
}

class Salt : Ingredient
{
   public bool IsIodized { get; set; }
}

class Flour : Ingredient
{
   public string FlourType { get; set; }
}

class Sugar : Ingredient
{
   public double SweetnessLevel { get; set; }
}

Those objects may have its own properties and behavior.

Next, we create a new IVisitor interface that defines visit methods for each object type in the hierarchy.

The IVisitor component could be an interface or an abstract class. This interface allows you to define different methods for implementing the operation accordingly for each object type.

Additionally, the visit methods can be overloaded (with the same name, e.g. Visit(Recipe recipe), Visit(Butter butter) etc. ) or separated with a distinctive name that describes the object type to visit ( e.g. VisitRecipe(Recipe recipe), VisitButter(Butter butter) etc. ). In our case, we’ll define visit methods for Recipe, Butter, Salt, Flour, and Sugar objects as shown below:

interface IVisitor
{
   void VisitRecipe(Recipe recipe);
   void VisitButter(Butter butter);
   void VisitSalt(Salt salt);
   void VisitFlour(Flour flour);
   void VisitSugar(Sugar sugar);
}

Next, we create a new Visitor object for each operation we want to implement.

In this step, we model each operation as a separate Visitor. These Visitor objects will implement the methods defined in the IVisitor interface. For instance, let’s say we want to calculate the total calories of a recipe. We can create a CalorieCalculator visitor object like this:

class CalorieCalculator : IVisitor
{
   public double TotalCalories { get; private set; }

   public CalorieCalculator()
   {
      TotalCalories = 0;
   }

   public void VisitRecipe(Recipe recipe)
   {
      foreach (var ingredient in recipe.Ingredients)
      {
         ingredient.Accept(this);
      }
   }

   public void VisitButter(Butter butter)
   {
      // Calculate calories based on fat content and quantity
      double calories = butter.FatContent * butter.Quantity;
      TotalCalories += calories;
   }

   public void VisitSalt(Salt salt)
   {
      // No calories for salt
   }

   public void VisitFlour(Flour flour)
   {
      // Calculate calories based on flour type and quantity
      double calories = 0;

      switch (flour.FlourType)
      {
         case "All-Purpose":
            calories = 3.64 * flour.Quantity; // Assuming 3.64 calories per 1 gram
            break;
         case "Whole Wheat":
            calories = 3.39 * flour.Quantity; // Assuming 3.39 calories per 1 gram
            break;
            // Add more cases for other flour types if needed
      }

      TotalCalories += calories;
   }

   public void VisitSugar(Sugar sugar)
   {
      // Calculate calories based on sweetness level and quantity
      double calories = 4.0 * sugar.SweetnessLevel * sugar.Quantity; // Assuming 4 calories per gram of sugar
      TotalCalories += calories;
   }
}

The visitor can have void in each of its visit methods and the final result can be stored in an instance variable, or it can have a return type for each of its methods without the need of such a variable.

Finally, we need to define a new IVisitable interface, which contains an Accept(IVisitor visitor) method that all objects in the hierarchy must implement.

This interface allows visitors to access the object’s state. We’ll add the Accept method to both the Ingredient and Recipe classes (as they are the base classes) and implement it in each object.

interface IVisitable
{
   void Accept(IVisitor visitor);
}

class Recipe : IVisitable
{ 
   public List<Ingredient> Ingredients { get; set; }

   public void Accept(IVisitor visitor)
   {
      visitor.VisitRecipe(this);
   }
}

abstract class Ingredient : IVisitable
{
   public string Name { get; set; }
   public double Quantity { get; set; }

   public abstract void Accept(IVisitor visitor);
}

class Butter : Ingredient
{
   public double FatContent { get; }

   public override void Accept(IVisitor visitor)
   {
      visitor.VisitButter(this);
   }
}

class Salt : Ingredient
{
   public bool IsIodized { get; set; }

   public override void Accept(IVisitor visitor)
   {
      visitor.VisitSalt(this);
   }
}

class Flour : Ingredient
{
   public string FlourType { get; set; }

   public override void Accept(IVisitor visitor)
   {
      visitor.VisitFlour(this);
   }
}

class Sugar : Ingredient
{
   public double SweetnessLevel { get; set; }

   public override void Accept(IVisitor visitor)
   {
      visitor.VisitSugar(this);
   }
}

By following these steps, you’ve successfully implemented the Visitor Design Pattern.

The code of the Recipe example is available on GitHub

Visitor Pattern – Class diagram

Below we present the class diagram of the Visitor Pattern. The key separate the operations from the objects is to define it as a abstract component by its own (the IVisitor interface).

Real World Example – Power Fx

Power Fx is an easy-to-use general purpose programming language that is based on formulas similar to spreadsheets. As an expression language, Power Fx utilizes the Visitor Pattern in order to evaluate expressions.

In Power Fx, there are 4 main components involved:

  • RecalcEngine – Is the interpreter of Power Fx expressions. You can use this component to evaluate expressions and perform calculations.
  • CheckResult – It represents the result of the expression parsing process. It contains the parsed expression tree and an instance of the IExpressionEvaluator.
  • IExpressionEvaluator – It represents the parsed expression and contains a visitor that evaluates the expression tree.
  • EvalVisitor – This is the actual visitor that implements the evaluation logic for different types of expressions.

Inside the RecalcEngine, there is an EvalAsync method that evaluates the expression. It receives the expression text, parser options and other configurations. It performs the necessary checks and creates an evaluator using the GetEvaluator method from the CheckResult.

public async Task<FormulaValue> EvalAsync(string expressionText,
 CancellationToken cancellationToken,
 ParserOptions options = null,
 ReadOnlySymbolTable symbolTable = null,
 RuntimeConfig runtimeConfig = null)
{
   var parameterSymbols = runtimeConfig?.Values?.SymbolTable;
   var symbolsAll = ReadOnlySymbolTable.Compose(parameterSymbols, symbolTable);

   options ??= this.GetDefaultParserOptionsCopy();

   var check = Check(expressionText, options, symbolsAll);
   check.ThrowOnErrors();

   var stackMarker = new StackDepthCounter(Config.MaxCallDepth);
   var eval = check.GetEvaluator(stackMarker);

   var result = await eval.EvalAsync(cancellationToken, runtimeConfig).ConfigureAwait(false);
   return result;
}

The GetEvaluator method extracts the necessary information from the CheckResult and creates a new instance of the ParsedExpression, which implements the IExpressionEvaluator interface. It sets up the necessary context for the evaluator, such as global values and symbols.

internal static IExpressionEvaluator GetEvaluator(this CheckResult result, StackDepthCounter stackMarker)
{
   ReadOnlySymbolValues globals = null;

   if (result.Engine is RecalcEngine recalcEngine)
   {
      // Pull global values from the engine. 
      globals = recalcEngine._symbolValues;
   }

   var irResult = result.ApplyIR();
   result.ThrowOnErrors();

   // Uses the TopNode as the root node of the composite for the visitor to iterate.
   var expr = new ParsedExpression(irResult.TopNode, irResult.RuleScopeSymbol, stackMarker, result.ParserCultureInfo)
   {
      _globals = globals,
      _allSymbols = result.Symbols,
      _parameterSymbolTable = result.Parameters,
      _additionalFunctions = result.Engine.Config.AdditionalFunctions
   };

   return expr;
}

Then, inside the ParsedExpression, the EvalAsync method is called to start the evaluation process. It creates an instance of the EvalVisitor that uses the visitor pattern to traverse the expression tree. The Accept method is called on the root node passing the visitor as parameter, which then recursively calls the appropriate Visit method in the EvalVisitor based on the type of the node.

public async Task<FormulaValue> EvalAsync(CancellationToken cancellationToken, IRuntimeConfig runtimeConfig = null)
{
   // code omitted..

   // Here a new Visitor is created.
   var evalVisitor = new EvalVisitor(runtimeConfig2, cancellationToken);

   try
   {
      // We begin the evaluation by calling Accept on the root node.
      var newValue = await _irnode.Accept(evalVisitor, new EvalVisitorContext(SymbolContext.New(), _stackMarker)).ConfigureAwait(false);
      return newValue;
   }
   catch (CustomFunctionErrorException customError)
   {
      var error = new ErrorValue(_irnode.IRContext, customError.ExpressionError);
      return error;
   }
   catch (MaxCallDepthException maxCallDepthException)
   {
      return maxCallDepthException.ToErrorValue(_irnode.IRContext);
   }
}

Finally, inside the EvalVisitor we can find the various Visit method for each of the expression type it evaluates.

As the following code shows, the EvalVisitor has Visit methods for different types of literals, such as text, numbers, decimals, booleans, and colors. Each Visit method handles the evaluation logic for that specific type of expression.

internal class EvalVisitor : IRNodeVisitor<ValueTask<FormulaValue>, EvalVisitorContext>
{
   // code omitted.
   public override async ValueTask<FormulaValue> Visit(TextLiteralNode node, EvalVisitorContext context)
   {
      return new StringValue(node.IRContext, node.LiteralValue);
   }

   public override async ValueTask<FormulaValue> Visit(NumberLiteralNode node, EvalVisitorContext context)
   {
      return new NumberValue(node.IRContext, node.LiteralValue);
   }

   public override async ValueTask<FormulaValue> Visit(DecimalLiteralNode node, EvalVisitorContext context)
   {
      return new DecimalValue(node.IRContext, node.LiteralValue);
   }

   public override async ValueTask<FormulaValue> Visit(BooleanLiteralNode node, EvalVisitorContext context)
   {
      return new BooleanValue(node.IRContext, node.LiteralValue);
   }

   public override async ValueTask<FormulaValue> Visit(ColorLiteralNode node, EvalVisitorContext context)
   {
      return new ColorValue(node.IRContext, node.LiteralValue);
   }

   public override async ValueTask<FormulaValue> Visit(RecordNode node, EvalVisitorContext context)
   {
      var fields = new List<NamedValue>();

      foreach (var field in node.Fields)
      {
         CheckCancel();

         var name = field.Key;
         var value = field.Value;

         var rhsValue = await value.Accept(this, context).ConfigureAwait(false);
         fields.Add(new NamedValue(name.Value, rhsValue));
      }

      return new InMemoryRecordValue(node.IRContext, fields);
   }
   // code omitted.
}

This is how the Visitor Pattern can be used in a real-world scenario to implement an expression language.

Example – Interpreter

In this section, our goal is to create an interpreter that can evaluate simple mathematical expressions and convert them into string representations. We will utilize the Visitor Pattern in order to design a flexible and extensible solution for interpreting and manipulating expressions.

Expressions

First, we create an abstract class called Expression, which models every expression we want to evaluate. This class includes an Accept method that takes an IVisitor object.

public abstract class Expression
{
   public abstract object Accept(IVisitor visitor);
}

We will support 4 different types of expressions:

  • Literal – This expression encapsulates a single value, such as numbers, strings and Booleans.
  • Unary – It performs logical operations like logical negation or negating a number.
  • Binary – It carries out operations between two expressions, returning a result. It supports various arithmetic and comparison operators, such as addition, subtraction, multiplication, division, and comparison operators like greater and less than.
  • Logical – It performs logical operations, such as logical AND or OR, between two expressions, yielding a Boolean result.

The implementation of each expression includes its state but not the evaluation of the expression itself. Evaluation will be handled by the Visitor. Below we present the implementations of each expression type:

public class Binary : Expression
{
   public Expression Left { get; }
   public Expression Right { get; }

   public Operator Operator { get; }

   public Binary(Expression left, Operator @operator, Expression right)
   {
      Left = left;
      Operator = @operator;
      Right = right;
   }

   public override object Accept(IVisitor visitor)
      => visitor.Visit(this);
}

public class Literal : Expression
{
   public object Value { get; private set; }

   public Literal(object value)
   {
      Value = value;
   }

   public override object Accept(IVisitor visitor)
      => return visitor.Visit(this);
}

public class Logical : Expression
{
   public Expression Left { get; }
   public Operator Operator { get; }
   public Expression Right { get; }

   public Logical(Expression left, Operator @operator, Expression right)
   {
      Left = left;
      Operator = @operator;
      Right = right;
   }

   public override object Accept(IVisitor visitor)
      => visitor.Visit(this);
}

public class Unary : Expression
{
   public Expression Expression { get; }
   public Operator Operator { get; }

   public Unary(Expression expression, Operator @operator)
   {
      Expression = expression;
      Operator = @operator;
   }

   public override object Accept(IVisitor visitor)
      => visitor.Visit(this);
}

The Interpreter Visitor

With our expressions in place, we can construct a hierarchy representing a specific mathematical expression. We can then feed this expression hierarchy to the visitor (Interpreter) to evaluate it. The Interpreter implements all the visit methods required to process each expression type.

To begin, we define the IVisitor interface, which contains 4 visit methods, one for each expression type:

public interface IVisitor
{
   object Visit(Literal expr);
   object Visit(Unary expr);
   object Visit(Binary expr);
   object Visit(Logical expr);
}

Below we show the Interpreter implementing the IVisitor interface:

public class Interpreter : IVisitor
{
   public object Visit(Literal expr)
   {
      return expr.Value;
   }

   public object Visit(Binary expr)
   {
      // Evaluate the expressions recursively.
      var left = expr.Left.Accept(this);
      var right = expr.Right.Accept(this);

      switch (expr.Operator)
      {
         case BANG_EQUAL: return left != right;
         case EQUAL_EQUAL: return left == right;
         case GREATER:
            return (int)left > (int)right;
         case GREATER_EQUAL:
            return (int)left >= (int)right;
         case LESS:
            return (int)left < (int)right;
         case LESS_EQUAL:
            return (int)left <= (int)right;
         case MINUS:
            return (int)left - (int)right;
         case PLUS:
            if (left is int leftAsInt && right is int rightAsInt)
            {
               return leftAsInt + rightAsInt;
            }

            if (left is string leftStr && right is string rightStr)
            {
               return leftStr + rightStr;
            }
            break;
         case SLASH:
            return (int)left / (int)right;
         case STAR:
            return (int)left * (int)right;
      }

      throw new InvalidOperationException($"Invalid operator {expr.Operator} for Binary expression.");
   }

   public object Visit(Unary expr)
   {

      // Evaluate the expression recursively.
      var value = expr.Expression.Accept(this);

      switch (expr.Operator) {
         case BANG:
            if (value == null) return false;
            if (value is bool valueAsBool)
               return valueAsBool;
            return true;
         case MINUS:
            return -(int)value;
      }

      throw new InvalidOperationException($"Invalid operator {expr.Operator} for Unary expression.");
   }

   public object Visit(Logical expr)
   {
      if (expr.Operator != OR && expr.Operator != AND)
         throw new InvalidOperationException($"Invalid operator {expr.Operator} for Logical expression.");

      // Evaluate the expression recursively.
      var left = expr.Left.Accept(this);

      if (expr.Operator == OR) {
         if (IsTruthy(left)) return left;
      } else
      {
         if (!IsTruthy(left)) return left;
      }

      // Evaluate the expression recursively.
      return expr.Right.Accept(this);
   }

   private bool IsTruthy(object obj)
   {
      if (obj == null) return false;
      if (obj is bool objAsBool) return objAsBool;
      return true;
   }
}

The Interpreter visitor defines the logic for evaluating each expression type. For the Literal expression, it simply returns the value it holds.

The Unary expression has an operator and an expression. The operator can be logical negation or negation of numbers.

The Binary expression performs operations such as addition, subtraction, and comparison.

Lastly, the Logical expression deals with logical AND and OR operations between two expressions.

Usage example

In this section, we provide examples demonstrating how we can create mathematical expressions and evaluate them using the Interpreter visitor.

// "a string"
res = interpreter.Visit(new Literal("a string"));
Console.WriteLine(res); // Output: "a string"

// 1 > 10
res = interpreter.Visit(new Binary(new Literal(1), Operator.GREATER, new Literal(10)));
Console.WriteLine(res); // Output: false

// 10 - ( -5 / 5 ) * 3
var division = new Binary(new Unary(new Literal(5), Operator.MINUS), 
   Operator.SLASH, new Literal(5));
var multiplication = new Binary(division, Operator.STAR, new Literal(3));
diff = new Binary(new Literal(10), Operator.MINUS, multiplication);
res = interpreter.Visit(diff); // Output: 13

// true AND 1 >= 0 AND 2 + 4 < 10
var firstLogical = new Logical(new Literal(true), Operator.AND, 
   new Binary(new Literal(1), Operator.GREATER_EQUAL, new Literal(0)));
var secondLogical = new Logical(firstLogical, Operator.AND, 
   new Binary(new Binary(new Literal(2), Operator.PLUS, new Literal(4)), 
      Operator.LESS, new Literal(10)));
res = interpreter.Visit(secondLogical); // result: true

Generic Visitor

In this section, we create a new visitor capable of converting expressions into a string representation of mathematical expressions. Up until now, our visitor interface has object as return type in each of its visit method. In order to support another type of visitor that returns a string, we’ll make the visitor interface generic, allowing subclasses to specify their own return type.

To enable the visitor to return strings instead of objects, we refactor the existing visitor interface, IVisitor, to support generics. The modified interface looks as follows:

public interface IVisitor<T>
{
   T Visit(Literal expr);
   T Visit(Unary expr);
   T Visit(Binary expr);
   T Visit(Logical expr);
   T Visit(Group expr);
}

By making the interface generic, we can specify the desired return type for each visit method. Additionally, we introduce a new visit method for the Group expression, which represents parentheses in a mathematical expression.

Updating Expression and its Subclasses

We need to modify the Accept method in the abstract Expression class. This method will now accept the new generic visitor. The updated code for the Expression and its subclasses are shown below:

public abstract class Expression
{
   public abstract T Accept<T>(IVisitor<T> visitor);
}

public class Binary : Expression
{
   public Expression Left { get; }
   public Expression Right { get; }

   public Operator Operator { get; }

   public Binary(Expression left, Operator @operator, Expression right)
   {
      Left = left;
      Operator = @operator;
      Right = right;
   }

   public override T Accept<T>(IVisitor<T> visitor)
      => visitor.Visit(this);
}

public class Literal : Expression
{
   public object Value { get; private set; }

   public Literal(object value)
   {
      Value = value;
   }

   public override T Accept<T>(IVisitor<T> visitor)
      => visitor.Visit(this);
}

public class Logical : Expression
{
   public Expression Left { get; }
   public Operator Operator { get; }
   public Expression Right { get; }

   public Logical(Expression left, Operator @operator, Expression right)
   {
      Left = left;
      Operator = @operator;
      Right = right;
   }

   public override T Accept<T>(IVisitor<T> visitor)
      => visitor.Visit(this);
}

public class Unary : Expression
{
   public Expression Expression { get; }
   public Operator Operator { get; }

   public Unary(Expression expression, Operator @operator)
   {
      Expression = expression;
      Operator = @operator;
   }

   public override T Accept<T>(IVisitor<T> visitor)
      => visitor.Visit(this);
}

Finally, we create a new Group expression that represents the parenthesis ‘(‘ and ‘)’ a mathematical expression could have:

public class Group : Expression
{
   public Expression Inner { get; }

   public Group(Expression inner)
   {
      Inner = inner;
   }

   public override T Accept<T>(IVisitor<T> visitor)
      => visitor.Visit(this);
}

Stringify expressions with a PrintVisitor

To demonstrate the stringification of expressions, we implement a new visitor called PrintVisitor. This visitor traverses the expression tree and generates a string representation of the mathematical expression. The PrintVisitor class will have string as the generic argument and is defined as follows:

public class PrintVisitor : IVisitor<string>
{
   public string Visit(Literal expr)
      => expr.Value.ToString();

   public string Visit(Unary expr)
   {
      string @operator = expr.Operator == BANG ? "!" : "-";
      return $"{@operator}{expr.Expression.Accept(this)}";
   }

   public string Visit(Binary expr)
   {
      string left = expr.Left.Accept(this);
      string @operator = string.Empty;
      switch (expr.Operator)
      {
         case BANG_EQUAL: @operator = "!="; break;
         case EQUAL_EQUAL: @operator = "=="; break;
         case GREATER: @operator = ">"; break;
         case GREATER_EQUAL: @operator = ">="; break;
         case LESS: @operator = "<"; break;
         case LESS_EQUAL: @operator = "<="; break;
         case MINUS: @operator = "-"; break;
         case PLUS: @operator = "+"; break;
         case SLASH: @operator = "/"; break;
         case STAR: @operator = "*"; break;
      }
      string right = expr.Right.Accept(this);
      return $"{left} {@operator} {right}";
   }

   public string Visit(Logical expr)
   {
      string left = expr.Left.Accept(this);
      string @operator = string.Empty;
      if (expr.Operator == OR)
         @operator = "OR";
      else
         @operator = "AND";

      string right = expr.Right.Accept(this);

      return $"{left} {@operator} {right}";
   }

   public string Visit(Group expr)
      => $"( {expr.Inner.Accept(this)} )";
}

The PrintVisitor is similar to the Interpreter but does not evaluate any expression. Instead, it visits each expression and generates the appropriate string representation.

Usage Example

In this section we demonstrate 2 different Visitors that visit the Expression hierarchy and produce two different results. The Interpreter visits each expression type and evaluates it into a value. Similarly, the PrintVisitor visits each expression and returns its string representation.

Following, is an example that make use of the Interpreter and PrintVisitor:

// true AND 1 >= 0 AND 2 + 4 < 10
var firstLogical = new Logical(new Literal(true), Operator.AND, 
   new Binary(new Literal(1), Operator.GREATER_EQUAL, new Literal(0)));
var secondLogical = new Logical(firstLogical, Operator.AND, 
   new Binary(new Binary(new Literal(2), Operator.PLUS, new Literal(4)),
      Operator.LESS, new Literal(10)));
var res = new PrintVisitor().Visit(secondLogical); // Output: True AND 1 >= 0 AND 2 + 4 < 10
var evaled = new Interpreter().Visit(secondLogical); // Output: true

// 10 - ( -5 / 5 ) * 3
var division = new Group(new Binary(new Unary(new Literal(5), Operator.MINUS),
    Operator.SLASH, new Literal(5)));
var multiplication = new Binary(division, Operator.STAR, new Literal(3));
var diff = new Binary(new Literal(10), Operator.MINUS, multiplication);
res = new PrintVisitor().Visit(diff); // Output: 10 - ( -5 / 5 ) * 3
evaled = new Interpreter().Visit(diff); // Output: 13

Using Dependency Injection

Dependency injection is a powerful technique that enables flexible and loosely coupled code. In this section, we’ll explore how to use dependency injection for implementing the Visitor Pattern.

C# with Autofac

In this section we explore how we can register our visitors using Autofac in C#. Below we show the registration of the generic IVisitor interface with two distinct implementations. As we can see we can resolve them by using the concrete type of the generic argument in the IVisitor<> interface.

The code with dependency injection using Autofac in C# is available on GitHub

var builder = new ContainerBuilder();
builder.RegisterType<Interpreter>()
	.As(typeof(IVisitor<object>));
builder.RegisterType<PrintVisitor>()
	.As(typeof(IVisitor<string>));

var container = builder.Build();

var printVisitor = container.Resolve<IVisitor<string>>();
var interpreter = container.Resolve<IVisitor<object>>();

Further Reading

Looking to dive deeper into design patterns and take your programming skills to the next level? Check out the following highly recommended books:

clean code cover

Clean Code

Author: Robert C. Martin
Publication: August 1, 2008

head first design patterns

Head First – Design Patterns

Authors: Eric Freeman, Elisabeth Robson
Publication: January 12, 2021

* This site contains product affiliate links. We may receive a commission if you make a purchase after clicking on one of these links.

Write A Comment

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.