Visitor pattern

Origin

This Visitor pattern is based on the Visitor pattern as described in [Gam+, pages 331..344]. The implementation in Delphi’s Object Pascal language originates from White Ants.

Intent

‘Represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates’ [Gam+, page 331].

Motivation

The motivation is an adjusted version of the motivation as described in [Gam+, page 331..332].

Consider the implementation of an OO CASE-tool which represents models using classes and members. Inside this CASE-tool there are lots of operations on members such as: drawing members in lists, generating source code for members and creating help entries for members.

Most of these actions will need to treat members that represent fields differently from members that represent methods or properties. Hence there will be one class for fields, another for methods, and so on. The set of member types, which is dependent on the target language,

 

doesn’t change much.

This diagram shows part of the member hierarchy. The problem here is that distributing all these operations across the various member classes leads to a system that’s hard to understand, maintain and change. It will be confusing to have drawing behaviour code mixed with code generation or help file generation code. Moreover adding a new operation usually will have to be implemented in all off the member classes, which will spread the related code around and requires recompiling all of these classes. It would be better if each new operation could be added separately, and the member classes were independent of the operations that apply to them.

We can have both by packaging related operations from each class in a separate object, called a visitor, and passing it to members of a class’s member list as it’s traversed. When an member ‘accepts’ the visitor, it sends a request to the visitor that encodes the member’s class. It also includes the member as an argument. The visitor will then execute the operation for that member.

For example, a code generator that didn’t use visitors might generate source code for a member by calling that member’s TMember.WriteInterfaceCode(Output: TStream); abstract method. Each member would implement WriteInterfaceCode by writing appropriate code to the output. If the generator created code using visitors, then it would create a TInterfaceCodeVisitor object and call the AcceptVisitor method on the member list with that visitor object as argument. Each member would implement AcceptVisitor by calling back on the visitor: a field calls the VisitField method on the visitor, a method calls VisitMethod. What used to be the WriteInterfaceCode operation in class TField, is now the VisitField method call on TInterfaceCodeVisitor.

To make visitors work for more than just interface code generation, we need an abstract parent class TMemberVisitor for all visitors of a member list. TMemberVisitor must declare a method for each member class. An application that needs to generate HTML style output for members, will define a new subclass of TMemberVisitor and will no longer need to add application specific code to the member classes. The visitor pattern encapsulates the operations.

With the Visitor pattern, you define two class hierarchies: one for the elements being operated on (the TMember hierarchy) and one for the visitors that define operations on the elements (the TMemberVisitor hierarchy). You create a new operation by adding a subclass to the visitor class hierarchy. As long as we don’t have to add new member types, we can simply add new functionality by defining new TMemberVisitor subclasses.

Refer to [Gam+] for applicability and more examples of this highly interesting pattern.

Implementation

The following code demonstrates the implementation of the visitor pattern applied to the TMember example described above.

type
  TMember = class (TObject)
  public
    procedure AcceptMemberVisitor(Visitor: TMemberVisitor); virtual;
  end;

  TField = class (TMember)
  public
    procedure AcceptMemberVisitor(Visitor: TMemberVisitor); override;
  end;

  TMethod = class (TMember)
  public
    procedure AcceptMemberVisitor(Visitor: TMemberVisitor); override;
  end;

  TProperty = class (TMember)
  public
    procedure AcceptMemberVisitor(Visitor: TMemberVisitor); override;
  end;

  TMemberVisitor = class (TObject)
  public
    procedure VisitField(Instance: TField); virtual;
    procedure VisitMember(Instance: TMember); virtual;
    procedure VisitMethod(Instance: TMethod); virtual;
    procedure VisitProperty(Instance: TProperty); virtual;
  end;

implementation

{ TMember }
procedure TMember.AcceptMemberVisitor(Visitor: TMemberVisitor);
begin
  Visitor.VisitMember(Self);
end;

{ TField }
procedure TField.AcceptMemberVisitor(Visitor: TMemberVisitor);
begin
  Visitor.VisitField(Self);
end;

{ TMethod }
procedure TMethod.AcceptMemberVisitor(Visitor: TMemberVisitor);
begin
  Visitor.VisitMethod(Self);
end;

{ TProperty }
procedure TProperty.AcceptMemberVisitor(Visitor: TMemberVisitor);
begin
  Visitor.VisitProperty(Self);
end;

{ TMemberVisitor }
procedure TMemberVisitor.VisitField(Instance: TField);
begin
end;

procedure TMemberVisitor.VisitMember(Instance: TMember);
begin
end;

procedure TMemberVisitor.VisitMethod(Instance: TMethod);
begin
end;

procedure TMemberVisitor.VisitProperty(Instance: TProperty);
begin
end;

In this implementation notice: The AcceptMemberVisitor methods in TMember, TField, TMethod and TProperty. These methods are inserted by the pattern and make up the first half of the pattern. These methods are fully implemented. The VisitMember, VisitField etc. methods in the TMemberVisitor class. These methods make up the second half of the pattern. Since TMemberVisitor is an abstract class, these methods are implemented by doing nothing. The useful implementations must come from descendant visitor classes. You might want to add code like:

procedure TMemberVisitor.VisitField(Instance: TField);
begin
  VisitMember(Instance);
end;

To demonstrate the use of this pattern in the above example, the implementation for a simple code generator just generating the member interface is listed below.

In this example notice how: The dedicated visitor implementing the member code generation is defined in the implementation, since it is only needed in this unit. The visitor has a context defining property Output: TTextStream, which must be provided before the visitor can actually handle any VisitXXX methods. A DrawingVisitor would typically need a context containing a canvas to draw on and rectangle to draw within. This context is passed by the generator to the visitor before traversing the member list. All member code generation related code is neatly situated in one class.

To really understand the visitor pattern, you might implement this example, and step through the double dispatch mechanism: accept/visit.

unit CodeGenerators;

interface

uses Classes, TextStreams;

type

  TCodeGenerator = class (TObject)
  public
    procedure Generate(Members: TList; Output: TTextStream);
  end;

implementation

uses Members;

type
  TCodeGenerationVisitor = class (TMemberVisitor)
  private
    FOutput: TTextStream;
  public
    procedure VisitField(Instance: TField); override;
    procedure VisitMethod(Instance: TMethod); override;
    procedure VisitProperty(Instance: TProperty); override;
    property Output: TTextStream read FOutput write FOutput;
  end;
 

{ TCodeGenerationVisitor }
procedure TCodeGenerationVisitor.VisitField(Instance: TField);
begin
  Output.WriteLnFmt('  %s: %s;', [Instance.Name, Instance.DataName]);
end;

procedure TCodeGenerationVisitor.VisitMethod(Instance: TMethod);
var
  MKStr, DTStr: string;
begin
  case Instance.MethodKind of
    mkConstructor: MKStr := 'constructor';
    mkDestructor: MKStr := 'destructor';
    mkProcedure: MKStr := 'procedure';
    mkFuntion: MKStr := 'function';
  end;
  if Instance.MethodKind = mkFunction then
    DTStr := ': ' + Instance.DataName
  else
    DTStr := '';
  { for sure this is not complete, but is demonstrates that methods get generated }
  Output.WriteLnFmt('  %s %s%s%s;'
                    [MKStr, Instance.Name, Instance.Parameters, DTStr]);
end;

procedure TCodeGenerationVisitor.VisitProperty(Instance: TProperty);
begin
  Output.WriteLnFmt('  property %s: %s read %s write %s;',
                    [Instance.Name, Instance.DataName,
                     Instance.ReadSpecifier, Instance.WriteSpecifier]);
end;

{ TCodeGenerator }
procedure TCodeGenerator.Generate(Members: TList; Output: TTextStream);
var
  I: Integer;
begin
  { write the class definition }
  Output.WriteLine('TSample = class (TObject)');
 
  { now add the member's interfaces using a code visitor }
  Visitor := TCodeGenerationVisitor.Create;
  try
    { provide context to visitor, so that it can handle VisitXXX methods }
    for I := 0 to Members.Count - 1 do
      { here the miracle happens: Accept will invoke VisitField, VisitMethod etc.
        to be called on the visitor }
      TMember(Members[I]).AcceptMemberVisitor(Visitor);
  finally
    Visitor.Free;
  end;
 
  { write the end of the class's interface definition }
  Output.WriteLine('end;');
end;