This Visitor pattern is based on the Visitor pattern as described in [Gam+, pages 331..344]. The implementation in Delphis Object Pascal language originates from White Ants.
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].
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,
doesnt 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 thats 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 classs member list as its traversed. When an member accepts the visitor, it sends a request to the visitor that encodes the members 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 didnt use visitors might generate source code for a member by calling that members 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 dont 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.
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;