Applying the visitor pattern to our code
Now that we know what the visitor pattern is, let's take a look at how we are going to apply it to our code:
- First, we are going to create the IVisitor and IVisitable interfaces as follows:
interface IVisitor {
Visit(token : ParseElement, markdownDocument : IMarkdownDocument) : void;
}
interface IVisitable {
Accept(visitor : IVisitor, token : ParseElement, markdownDocument : IMarkdownDocument) : void;
}
- When our code reaches the point where Visit is called, we are going to use the TagTypeToHtml class to add the relevant opening HTML tag, the line of text, and then the matching closing HTML tag to our MarkdownDocument. As this is common to each of our tag types, we can implement a base class that encapsulates this behavior, as follows:
abstract class VisitorBase implements IVisitor {
constructor (private readonly tagType : TagType, private readonly TagTypeToHtml : TagTypeToHtml) {}
Visit(token: ParseElement, markdownDocument: IMarkdownDocument): void {
markdownDocument.Add(this.TagTypeToHtml.OpeningTag(this.tagType), token.CurrentLine,
this.TagTypeToHtml.ClosingTag(this.tagType));
}
}
- Next, we need to add the concrete visitor implementations. This is as simple as creating the following classes:
class Header1Visitor extends VisitorBase {
constructor() {
super(TagType.Header1, new TagTypeToHtml());
}
}
class Header2Visitor extends VisitorBase {
constructor() {
super(TagType.Header2, new TagTypeToHtml());
}
}
class Header3Visitor extends VisitorBase {
constructor() {
super(TagType.Header3, new TagTypeToHtml());
}
}
class ParagraphVisitor extends VisitorBase {
constructor() {
super(TagType.Paragraph, new TagTypeToHtml());
}
}
class HorizontalRuleVisitor extends VisitorBase {
constructor() {
super(TagType.HorizontalRule, new TagTypeToHtml());
}
}
At first, this code may seem like overkill, but it serves a purpose. If we take Header1Visitor, for instance, we have a class that has the single responsibility of taking the current line and adding it to our markdown document wrapped in H1 tags. We could litter our code with classes that were responsible for checking whether the line started with #, and then remove the # from the start, prior to adding the H1 tags and the current line. However, that makes the code harder to test and more likely to break, especially if we want to change the behavior. Also, the more tags we add, the more fragile this code will become.
The other side of the visitor pattern code is the IVisitable implementation. For our current code, we know that we want to visit the relevant visitor whenever we call Accept. What this means to our code is that we can have a single visitable class that implements our IVisitable interface. This is shown in the following code:
class Visitable implements IVisitable {
Accept(visitor: IVisitor, token: ParseElement, markdownDocument: IMarkdownDocument): void {
visitor.Visit(token, markdownDocument);
}
}