Design Pattern: Visitor Pattern in TypeScript

Visitor pattern allows detailed implementation of methods in concrete classes, and also ensures a common interface for visiting different implementations.

This article demonstrates Visitor pattern implementations in TypeScript. Check the following examples.

Implementation

Here is a simple example of the Visitor pattern implementation in TypeScript-

interface ElementInterface {
    visit(visitor: VisitorInterface): void;
}

class Element1 implements ElementInterface {
    constructor(private elementContent: string) {

    }

    get content(): string{
        return this.elementContent;
    }

    visit(visitor: VisitorInterface): void {
        visitor.addContent(this);
    }
}

class Element2 implements ElementInterface {
    constructor(private elementContent: string, private addMergin: boolean = false) {

    }

    get content(): string{
        return this.elementContent;
    }

    get margin(): boolean{
        return this.addMergin;
    }

    visit(visitor: VisitorInterface): void {
        visitor.addContentWithMargin(this);
    }
}


interface VisitorInterface {
    addContent(element: Element1): void;
    addContentWithMargin(element: Element2): void;
}

class Visitor implements VisitorInterface {
    public output: string = '';

    addContent(element: Element1): void {
        this.output += element.content;
    }

    addContentWithMargin(element: Element2): void {
        if (element.margin) {
            this.output += '[margin]' + element.content + '[/margin]';
        } else {
            this.output += element.content;
        }
    }    
}


// list of elements
const elements: ElementInterface[] = [
    new Element1("Element1: first element"),
    new Element2("Element2: second element without margin"),
    new Element2("Element2: third element with margin", true),
    new Element1("Element1: last element"),
];

const visitior = new Visitor();

for (let element of elements) {
    element.visit(visitior)
}

console.log(visitior.output)

Output will be as below-

Element1: first element
Element2: second element without margin
[margin]Element2: third element with margin[/margin]
Element1: last element

Examples

Here are a few examples of Visitor pattern-

Example #1: Hosting Cost Calculator

Let’s consider an example of a hosting service, and calculate the total cost of the hosting service used using the visitor pattern.

Service Interface

// service.ts

import HostingCalculatorVisitor from "./hosting-calculator-visitor";

interface Service {
    accept(hostingCalculatorVisitor: HostingCalculatorVisitor): number;
}

export default Service;

Compute Service Class

// compute-service.ts

import HostingCalculatorVisitor from "./hosting-calculator-visitor";
import Service from "./service";


class ComputeService implements Service {

    private readonly price: number = 10.50;
    private quantity: number;

    constructor(quantity: number) {
        this.quantity = quantity;
    }

    getPrice(): number {
        return this.price;
    }

    getQuantity(): number {
        return this.quantity;
    }

    accept(hostingCalculatorVisitor: HostingCalculatorVisitor): number {
        return hostingCalculatorVisitor.visit(this);
    }
}

export default ComputeService;

Database Service Class

// database-service.ts

import HostingCalculatorVisitor from "./hosting-calculator-visitor";
import Service from "./service";


class DatabaseService implements Service {

    private readonly price: number = 100.00;
    private readonly backPrice: number = 30.00;
    private quantity: number;
    private backupEnabled: boolean = false;

    constructor(quantity: number, backupEnabled: boolean = false) {
        this.quantity = quantity;
        this.backupEnabled = backupEnabled;
    }

    getPrice(): number {
        return this.price;
    }

    getQuantity(): number {
        return this.quantity;
    }

    getBackPrice(): number {
        return this.backPrice;
    }

    isBackupEnabled(): boolean {
        return this.backupEnabled;
    }

    accept(hostingCalculatorVisitor: HostingCalculatorVisitor): number {
        return hostingCalculatorVisitor.visit(this);
    }
}

export default DatabaseService;

File Storage Service Class

// file-storage-service.ts

import HostingCalculatorVisitor from "./hosting-calculator-visitor";
import Service from "./service";


class FileStorageService implements Service {

    private readonly pricePerGB: number = 1.70;
    private quantity: number;

    constructor(quantity: number) {
        this.quantity = quantity;
    }

    getPricePerGB(): number {
        return this.pricePerGB;
    }

    getQuantity(): number {
        return this.quantity;
    }

    accept(hostingCalculatorVisitor: HostingCalculatorVisitor): number {
        return hostingCalculatorVisitor.visit(this);
    }
}

export default FileStorageService;

Serverless Service Class

// serverless-service.ts

import HostingCalculatorVisitor from "./hosting-calculator-visitor";
import Service from "./service";

class ServerlessService implements Service {

    private readonly hourlyPrice: number = 0.32;
    private totalHours: number;

    constructor(totalHours: number) {
        this.totalHours = totalHours;
    }

    getHourlyPrice(): number {
        return this.hourlyPrice;
    }

    getTotalHours(): number {
        return this.totalHours;
    }

    accept( hostingCalculatorVisitor: HostingCalculatorVisitor): number {
        return hostingCalculatorVisitor.visit(this);
    }
}

export default ServerlessService;

Container Service Class

// container-service.ts

import HostingCalculatorVisitor from "./hosting-calculator-visitor";
import Service from "./service";

class ContainerService implements Service {

    private readonly price: number = 5.60;
    private quantity: number;

    constructor(quantity: number) {
        this.quantity = quantity;
    }

    getPrice(): number {
        return this.price;
    }

    getQuantity(): number {
        return this.quantity;
    }

    accept(hostingCalculatorVisitor: HostingCalculatorVisitor): number {
        return hostingCalculatorVisitor.visit(this);
    }
}

export default ContainerService;

Visitor Interface

// hosting-calculator-visitor.ts

import Service from "./service";

interface HostingCalculatorVisitor {
    visit(service: Service): number;
}

export default HostingCalculatorVisitor;

Visitor Interface Implementation

// hosting-calculator-visitor-impl.ts

import ComputeService from "./compute-service";
import ContainerService from "./container-service";
import DatabaseService from "./database-service";
import FileStorageService from "./file-storage-service";
import HostingCalculatorVisitor from "./hosting-calculator-visitor";
import ServerlessService from "./serverless-service";
import Service from "./service";


class HostingCalculatorVisitorImpl implements HostingCalculatorVisitor {
    visit(service: Service): number {
        if (service instanceof ComputeService) {
            return service.getPrice() * service.getQuantity();
        }

        if (service instanceof ContainerService) {
            return service.getPrice() * service.getQuantity();
        }

        if (service instanceof DatabaseService) {

            const serviceCost = service.getPrice() * service.getQuantity();
            let backupCost = 0;

            if (service.isBackupEnabled()) {
                backupCost = service.getBackPrice() * service.getQuantity();
            }

            return serviceCost + backupCost;
        }
        if (service instanceof FileStorageService) {
            return service.getPricePerGB() * service.getQuantity();
        }

        if (service instanceof ServerlessService) {
            return service.getHourlyPrice() * service.getHourlyPrice();
        }

        return 0;
    }
}

export default HostingCalculatorVisitorImpl;

Demo

// demo.ts

import ComputeService from "./compute-service";
import ContainerService from "./container-service";
import DatabaseService from "./database-service";
import FileStorageService from "./file-storage-service";
import HostingCalculatorVisitorImpl from "./hosting-calculator-visitor-impl";
import ServerlessService from "./serverless-service";
import Service from "./service";

// function for calculating price
function calculateHostingCost(services: Service[]): number {
    const hostingCalculatorVisitorImpl = new HostingCalculatorVisitorImpl();

    let totalCost = 0;

    for (let service of services) {
        totalCost += service.accept(hostingCalculatorVisitorImpl);
    }

    return totalCost;
}

// list of used service
const usedServices: Service[] = [
    new ComputeService(3),
    new DatabaseService(3, true),
    new FileStorageService(120),
    new ServerlessService(720),
    new ContainerService(2),
];

const totalCost = calculateHostingCost(usedServices);

console.log("Total cost of hosting is: " + totalCost);

Output

Total cost of hosting is: 636.8024

Example #1: UI Elements

Here is another example of visitor pattern. We are printing some UI elements(dummy) here, to demonstrate how can this pattern be used to easily print UI elements.

UI Element Interface

// ui-element.ts

import Visitor from "./visitor";

interface UIElement {
    appendElement(vistor: Visitor): void;
}

export default UIElement;

Text Element Class

// text-element.ts

import UIElement from "./ui-element";
import Visitor from "./visitor";

class TextElement implements UIElement {
    constructor(private text: string) {

    }

    getText(): string {
        return this.text
    }

    appendElement(visitor: Visitor): void {
        visitor.appendContent(this);
    }
}

export default TextElement;

Wrap Element Class

// wrap-element.ts

import UIElement from "./ui-element";
import Visitor from "./visitor";

class WrapElement implements UIElement {
    constructor(private text: string, private wrapper: string) {

    }

    getText(): string {
        return this.text
    }

    getWrapper(): string {
        return this.wrapper;
    }

    appendElement(visitor: Visitor): void {
        visitor.appendContentWithWrapper(this);
    }
}

export default WrapElement;

Head Element Class

// head-element.ts

import UIElement from "./ui-element";
import Visitor from "./visitor";

class HeadElement implements UIElement {
    private readonly wrapper: string = 'h1';

    constructor(private text: string) {

    }

    getText(): string {
        return this.text
    }

    getWrapper(): string {
        return this.wrapper;
    }

    appendElement(visitor: Visitor): void {
        visitor.appendContentWithWrapper(this);
    }
}

export default HeadElement;

List Element Class

// list-element.ts

import UIElement from "./ui-element";
import Visitor from "./visitor";

class ListElement implements UIElement {
    constructor(private lines: string[]) {

    }

    getListItems(): string[] {
        return this.lines
    }

    appendElement(vistor: Visitor): void {
        vistor.appendList(this);
    }
}

export default ListElement;

Visitor Interface

// visitor.ts

import ListElement from "./list-element";
import HeadElement from "./head-element";
import TextElement from "./text-element";
import WrapElement from "./wrap-element";

interface Visitor {
    appendContent(textElement: TextElement): void;
    appendContentWithWrapper(wrapElement: WrapElement | HeadElement): void;
    appendList(listElement: ListElement): void;
}

export default Visitor;

Visitor Interface Implementation

// element-visitor.ts

import HeadElement from "./head-element";
import ListElement from "./list-element";
import TextElement from "./text-element";
import Visitor from "./visitor";
import WrapElement from "./wrap-element";

class ElementVisitor implements Visitor {
    public output: string = '';

    appendContent(textElement: TextElement): void {
        this.output += textElement.getText();
    }

    appendContentWithWrapper(wrapElement: WrapElement | HeadElement): void {
        this.output += `[${wrapElement.getWrapper()}] ${wrapElement.getText()} [/${wrapElement.getWrapper()}]`;
    }

    appendList(listElement: ListElement): void {
        this.output += '[ul]';

        for (let listItem of listElement.getListItems()) {
            this.output += `[li] ${listItem} [/li]`;
        }

        this.output += '[/ul]';
    }
}

export default ElementVisitor;

Demo

// demo.ts

import ElementVisitor from './element-visitor';
import HeadElement from './head-element';
import ListElement from './list-element';
import TextElement from './text-element';
import UIElement from './ui-element';
import WrapElement from './wrap-element';

// set the list of elements we want print
const uiElements: UIElement[] = [
    new HeadElement('My Heading'),
    new TextElement('First line of text'),
    new ListElement(['abc', 'def', 'ghi', 'jkl']),
    new WrapElement('Content wrapped with div', 'div'),
    new TextElement('Last line of text'),
];

const visitor = new ElementVisitor();

for (let element of uiElements) {
    element.appendElement(visitor);
}

// let's check the output
console.log(visitor.output);

Output

[h1] My Heading [/h1]

First line of text

[ul]
    [li] abc [/li]
    [li] def [/li]
    [li] ghi [/li]
    [li] jkl [/li]
[/ul]

[div] Content wrapped with div [/div]

Last line of text

Source Code

Use the following link to get the source code:

Other Code Implementations

Use the following links to check Visitor pattern implementation in other programming languages.

Leave a Comment


The reCAPTCHA verification period has expired. Please reload the page.