Visitor Design Pattern Notes

Visitor classes are commonly used to perform operations on a set of objects without modifying the structure of their underlying classes.

Visitor Design Pattern allows you to add new operations(behaviours) via Visitor classes to existing classes without modifying their structure. Instead of adding the operation within the existing class, you create a visitor class named as operation you want to have in existing class, so that the operation is externalised from a set of objects without modifying the structure of their underlying classes.

Visitor Design Pattern is more about adding new operations to existing classes via Visitor classes without modifying their structure, rather than adding new fields or methods directly to the classes themselves.

Consider a system where you have a hierarchy of classes representing different shapes, like Circle, Rectangle, and Triangle. Each of these shapes implements Visitable interface(or its equivalent) to allow visitor classes to operate on them.

In the context of Visitor Design Pattern, the name of Visitor class reflects the operation or behaviour it implements for the objects it visits. The operation is externalised from the objects themselves and encapsulated within the visitor class.

For instance:

PrintDetailsVisitor might be a visitor that prints details about each object it visits.
CalculatePriceVisitor could be a visitor that calculates the price or cost associated with each object it visits.
ApplyDiscountVisitor might be a visitor that applies a discount to each object it visits.

The core idea is to separate the operation from the class structure, allowing you to add new operations without modifying the classes of the objects you're operating upon. Each new operation would typically be represented by a new visitor class.

The typical scenario where the Visitor Design Pattern shines is when you have a stable class hierarchy (like different types of products or shapes) and you want to perform various operations on those classes (like calculating taxes, discounts, or drawing the shapes) without changing the code within the classes themselves.

Here's a breakdown:

Adding New Operations: If you want to add new operations or behaviours that should be applied to a group of classes, you can use the Visitor Design Pattern. You define a visitor interface with a visit method for each class in the original hierarchy and then implement specific visitors for different operations.
Not Modifying Existing Classes: Visitor Design Pattern allows you to add new operations via Visitor classes without modifying existing classes.
– Not Ideal for Adding New Classes: If you frequently add new classes within the hierarchy, Visitor Design Pattern becomes less suitable, as you'll have to modify all existing visitor classes to handle the new class. Assume that following code is our initial design, there are two classes named Book and Fruit and one visitor interface named ProductVisitor which contains two visit methods with Book and Fruit parameters. I created two visitor classes named PrintDetailsVisitor and CalculatePriceVisitor which implements ProductVisitor interface. But, by adding new classes in class hierarchy(in this case, Book and Fruit), we have to add a new visit method in ProductVisitor interface.

I will just add one class in hierarch and i had to modify ProductVisitor interface by adding new visit(electronic: Electronic): void; as a result, two visitor classes which implement this interface have to be modified. More visitor classes mean more modification with every new class in hierarch.

Use case 1

// =========================================================
// DOMAIN LAYER
// The heart of the system, where the main business logic resides.
// Entities, value objects, and domain events are typically found here.
// =========================================================

// =============== INTERFACE DEFINITIONS ===================
// Abstract contracts that shape the interactions within the domain.

interface Product {
  accept(visitor: ProductVisitor): void;
}

interface ProductVisitor {
  visit(book: Book): void;
  visit(fruit: Fruit): void;
}

// ================== PRODUCT AGGREGATES ====================
// Aggregates ensure that all operations on the contained entities and 
// value objects maintain consistency and invariants.

class Book implements Product {
  title: string;
  author: string;
  isbn: string;

  constructor(
    title: string,
    author: string,
    isbn: string,
  ) {
    this.title = title;
    this.author = author;
    this.isbn = isbn;
  }

  accept(visitor: ProductVisitor): void {
    visitor.visit(this);
  }
}

class Fruit implements Product {
  name: string;
  expiryDate: Date;

  constructor(
    name: string,
    expiryDate: Date,
  ) {
    this.name = name;
    this.expiryDate = expiryDate;
  }

  accept(visitor: ProductVisitor): void {
    visitor.visit(this);
  }
}

// ================== DOMAIN SERVICES ========================
// Domain services hold specific operations, which don't naturally fit 
// within an entity or value object.

class PrintDetailsVisitor implements ProductVisitor {
  visit(book: Book): void;
  visit(fruit: Fruit): void;
  visit(product: Book | Fruit): void {
    if (product instanceof Book) {
      this.printBookDetails(product);
    } else if (product instanceof Fruit) {
      this.printFruitDetails(product);
    }
  }

  private printBookDetails(book: Book){
    // Print something
  }

  private printFruitDetails(fruit: Fruit){
    // Print something
  }
}

class CalculatePriceVisitor implements ProductVisitor {
  visit(book: Book): void;
  visit(fruit: Fruit): void;
  visit(product: Book | Fruit): void {
    if (product instanceof Book) {
      this.calculateBookPrice(product);
    } else if (product instanceof Fruit) {
      this.calculateFruitPrice(product);
    }
  }

  private calculateBookPrice(book: Book){
    // calculate book price
  }

  private calculateFruitPrice(fruit: Fruit){
    // calculate fruit price
  }
}

Use case 1: Cost of adding new class in hierarch

By adding Electronic class in product class hierarchy will affect ProductVisitor, PrintDetailsVisitor, and CalculatePriceVisitor so if there are 10 visitor classes, then ProductVisitor interface and 10 visitor classes need to be changed.

// =========================================================
// DOMAIN LAYER
// The heart of the system, where the main business logic resides.
// Entities, value objects, and domain events are typically found here.
// =========================================================

// =============== INTERFACE DEFINITIONS ===================
// Abstract contracts that shape the interactions within the domain.

interface Product {
  accept(visitor: ProductVisitor): void;
}

interface ProductVisitor {
  visit(book: Book): void;
  visit(fruit: Fruit): void;
  visit(electronic: Electronic): void; // NEW
}

// ================== PRODUCT AGGREGATES ====================
// Aggregates ensure that all operations on the contained entities and 
// value objects maintain consistency and invariants.

// ... (Other product components like Book, Fruit, etc.)

// NEW: Added a new class to the product hierarchy for electronics
class Electronic implements Product {
  name: string;
  price: number;
  model: string;

  constructor(name: string, price: number, model: string) {
    this.name = name;
    this.price = price;
    this.model = model;
  }

  accept(visitor: ProductVisitor): void {
    visitor.visit(this);
  }
}

// ================== DOMAIN SERVICES ========================
// Domain services hold specific operations, which don't naturally fit 
// within an entity or value object.

// ... (Other domain services like PrintDetailsVisitor, CalculatePriceVisitor, etc.)

class PrintDetailsVisitor implements ProductVisitor {
  visit(book: Book): void;
  visit(fruit: Fruit): void;
  visit(electronic: Electronic): void // NEW
  visit(product: Book | Fruit | Electronic): void { // NEW
    if (product instanceof Book) {
      this.printBookDetails(product);
    } else if (product instanceof Fruit) {
      this.printFruitDetails(product);
    } else if (product instanceof Electronic) { // NEW
      this.printElectronicDetails(product);
    }
  }

  private printBookDetails(book: Book){
    // Print something
  }

  private printFruitDetails(fruit: Fruit){
    // Print something
  }

  // NEW
  private printElectronicDetails(electronic: Electronic){
    // Print something
  }
}

class CalculatePriceVisitor implements ProductVisitor {
  visit(book: Book): void;
  visit(fruit: Fruit): void;
  visit(electronic: Electronic): void // NEW
  visit(product: Book | Fruit | Electronic): void { // NEW
    if (product instanceof Book) {
      this.calculateBookPrice(product);
    } else if (product instanceof Fruit) {
      this.calculateFruitPrice(product);
    } else if (product instanceof Electronic) { // NEW
      this.calculateElectronicPrice(product);
    }
  }

  private calculateBookPrice(book: Book){
    // calculate book price
  }

  private calculateFruitPrice(fruit: Fruit){
    // calculate fruit price
  }

  // NEW
  private calculateElectronicPrice(electronic: Electronic){
    // calculate electronic price
  }
}

Use case 2

Interfaces

interface VisitableShape {
  accept<T>(visitor: ShapeVisitor<T>): T;
}
interface ShapeVisitor<T> {
  visit(circle: Circle): T;
  visit(rectangle: Rectangle): T;
  visit(square: Square): T;
}

A set of Objects

Notice that Shape classes don't have any method(behaviour), instead, it has accept method to send its reference by calling visitor.visit(this). So, visitor class name determine operation need to be done on shape.

class Circle implements VisitableShape {
  constructor(public radius: number) {}

  accept<T>(visitor: ShapeVisitor<T>): T {
    return visitor.visit(this);
  }
}
class Rectangle implements VisitableShape {
  constructor(public width: number, public height: number) {}

  accept<T>(visitor: ShapeVisitor<T>): T {
    return visitor.visit(this);
  }
}
class Square extends Rectangle {
  constructor(public sideLength: number) {
    super(sideLength, sideLength);
  }
}

Visitors

There are 8 visitor classes as below, in terms of Visitor Design Pattern, instead of adding behaviours in Shape classes, each behaviour(method) is externalised by one Visitor class. For example, instead of adding calculateArea method in Shape class, create AreaCalculatorVisitor class and

AreaCalculatorVisitor = calculateArea
PerimeterCalculatorVisitor = calculatePerimeter
DiagonalDiameterCalculatorVisitor = calculateDiagonalDiameter
CentroidCalculatorVisitor = calculateCenter
ScalingCalculatorVisitor = calculateScaling or resize or scale etc
ShapeDuplicatorVisitor = duplicateShape or clone etc
RotationCalculatorVisitor = calculateRotation or rotate etc
AreaComparerVisitor = compareArea

class AreaCalculatorVisitor implements ShapeVisitor<number> {
  visit(circle: Circle): number;
  visit(rectangle: Rectangle): number;
  visit(square: Square): number;
  visit(shape: Circle | Rectangle | Square): number {
    if (shape instanceof Circle) {
      return this.calculateCircleArea(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectangleArea(shape);
    }
  }

  private calculateCircleArea(circle: Circle) {
    return Math.PI * Math.pow(circle.radius, 2);
  }

  private calculateRectangleArea(rectangle: Rectangle) {
    return rectangle.width * rectangle.height;
  }
}
class PerimeterCalculatorVisitor implements ShapeVisitor<number> {
  visit(circle: Circle): number;
  visit(rectangle: Rectangle): number;
  visit(square: Square): number;
  visit(shape: Circle | Rectangle | Square): number {
    if (shape instanceof Circle) {
      return this.calculateCirclePerimeter(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectanglePerimeter(shape);
    }
  }

  private calculateCirclePerimeter(circle: Circle) {
    return 2 * Math.PI * circle.radius;
  }

  private calculateRectanglePerimeter(rectangle: Rectangle) {
    return 2 * (rectangle.width + rectangle.height);
  }
}
class DiagonalDiameterCalculatorVisitor implements ShapeVisitor<number> {
  visit(circle: Circle): number;
  visit(rectangle: Rectangle): number;
  visit(square: Square): number;
  visit(shape: Circle | Rectangle | Square): number {
    if (shape instanceof Circle) {
      return this.calculateCircleDiagonalDiameter(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectangleDiagonalDiameter(shape);
    }
  }

  private calculateCircleDiagonalDiameter(circle: Circle): number {
    return 2 * circle.radius;
  }

  private calculateRectangleDiagonalDiameter(rectangle: Rectangle): number {
    return Math.sqrt(
      rectangle.width * rectangle.width + rectangle.height * rectangle.height
    );
  }
}
class Point {
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

class CentroidCalculatorVisitor implements ShapeVisitor<Point> {
  visit(circle: Circle): Point;
  visit(rectangle: Rectangle): Point;
  visit(square: Square): Point;
  visit(shape: Circle | Rectangle | Square): Point {
    if (shape instanceof Circle) {
      return this.calculateCircleCenter(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectangleCenter(shape);
    }
  }

  private calculateCircleCenter(circle: Circle): Point {
    return new Point(0, 0); // Assuming the circle is centered at the origin
  }

  private calculateRectangleCenter(rectangle: Rectangle): Point {
    return new Point(rectangle.width / 2, rectangle.height / 2); // Centroid is at half its width and half its height
  }
}
class ScalingCalculatorVisitor implements ShapeVisitor<void> {
  private scaleFactor: number;

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

  visit(circle: Circle): void;
  visit(rectangle: Rectangle): void;
  visit(square: Square): void;
  visit(shape: Circle | Rectangle | Square): void {
    if (shape instanceof Circle) {
      this.calculateCircleScaling(shape);
    } else if (shape instanceof Rectangle) {
      this.calculateRectangleScaling(shape);
    }
  }

  private calculateCircleScaling(circle: Circle): void {
    circle.radius *= this.scaleFactor;
  }

  private calculateRectangleScaling(rectangle: Rectangle): void {
    rectangle.width *= this.scaleFactor;
    rectangle.height *= this.scaleFactor;
  }
}
class ShapeDuplicatorVisitor implements ShapeVisitor<VisitableShape> {
  visit(circle: Circle): VisitableShape;
  visit(rectangle: Rectangle): VisitableShape;
  visit(square: Square): VisitableShape;
  visit(shape: Circle | Rectangle | Square): VisitableShape {
    if (shape instanceof Circle) {
      this.duplicateCircle(shape);
    } else if (shape instanceof Rectangle) {
      this.duplicateRectangle(shape);
    }

    return undefined;
  }

  private duplicateCircle(circle: Circle): VisitableShape {
    return new Circle(circle.radius);
  }

  private duplicateRectangle(rectangle: Rectangle): VisitableShape {
    return new Rectangle(rectangle.width, rectangle.height);
  }
}
class RotationCalculatorVisitor implements ShapeVisitor<void> {
  visit(circle: Circle): void;
  visit(rectangle: Rectangle): void;
  visit(square: Square): void;
  visit(shape: Circle | Rectangle | Square): void {
    if (shape instanceof Circle) {
      this.calculateCircleRotation(shape);
    } else if (shape instanceof Rectangle) {
      this.calculateRectangleRotation(shape);
    }
  }

  private calculateCircleRotation(circle: Circle): void {
    // For the Circle, rotation doesn't affect its dimensions.
    // No changes as rotating a circle has no visual effect on its properties.
  }

  private calculateRectangleRotation(rectangle: Rectangle): void {
    // For the Rectangle, we'll swap its width and height.
    const temp = rectangle.width;
    rectangle.width = rectangle.height;
    rectangle.height = temp;
  }
}
enum AreaComparison {
  Larger,
  Equal,
  Smaller,
}

class AreaComparerVisitor implements ShapeVisitor<AreaComparison> {
  private areaCalculator = new AreaCalculatorVisitor();

  constructor(private secondShape: VisitableShape) {}

  visit(circle: Circle): AreaComparison;
  visit(rectangle: Rectangle): AreaComparison;
  visit(square: Square): AreaComparison;
  visit(shape: Circle | Rectangle | Square): AreaComparison {
    if (shape instanceof Circle) {
      return this.compareCircle(shape);
    } else if (shape instanceof Rectangle) {
      this.compareRectangle(shape);
    }
  }

  private compareCircle(circle: Circle): AreaComparison {
    return this.compareAreas(circle, this.secondShape);
  }

  private compareRectangle(rectangle: Rectangle): AreaComparison {
    return this.compareAreas(rectangle, this.secondShape);
  }

  private compareAreas(
    firstShape: VisitableShape,
    secondShape: VisitableShape
  ): AreaComparison {
    const firstShapeArea = firstShape.accept(this.areaCalculator);
    const secondShapeArea = secondShape.accept(this.areaCalculator);

    if (firstShapeArea > secondShapeArea) {
      return AreaComparison.Larger;
    } else if (firstShapeArea < secondShapeArea) {
      return AreaComparison.Smaller;
    } else {
      return AreaComparison.Equal;
    }
  }
}

Application

function printShapeDetails(shapes: VisitableShape[]) {
  const perimeterCalculator = new PerimeterCalculatorVisitor();
  const areaCalculator = new AreaCalculatorVisitor();
  const diagonalDiameterCalculator = new DiagonalDiameterCalculatorVisitor();
  const centroidCalculator = new CentroidCalculatorVisitor();
  const scalingCalculator = new ScalingCalculatorVisitor(2);
  const shapeDuplicator = new ShapeDuplicatorVisitor();
  const rotationCalculator = new RotationCalculatorVisitor();

  const circle: VisitableShape = new Circle(5);
  const areaComparer = new AreaComparerVisitor(circle);

  for (const shape of shapes) {
    const name = `${shape.constructor.name}`;

    const shapePerimeter = shape.accept(perimeterCalculator);
    const shapeArea = shape.accept(areaCalculator);
    const shapeDiagonalDiameter = shape.accept(diagonalDiameterCalculator);
    const shapeCenter = shape.accept(centroidCalculator);
    const duplicatedShape = shape.accept(shapeDuplicator);

    console.log(`Perimeter of ${name}: ${shapePerimeter}`);
    console.log(`Area of ${name}: ${shapeArea}`);
    console.log(`Diagonal Diameter of ${name}: ${shapeDiagonalDiameter}`);
    console.log(`Center of ${name}: ${shapeCenter}`);
    console.log(
      `Duplicated shape is created: ${JSON.stringify(duplicatedShape)}`
    );

    shape.accept(scalingCalculator);
    console.log(`Resized dimensions of ${name}: ${JSON.stringify(shape)}`);

    shape.accept(rotationCalculator);
    console.log(`Rotated shape: ${JSON.stringify(shape)}`);

    const comparisonResult = shape.accept(areaComparer);
    console.log(`Area comparison result: ${comparisonResult}`);
  }
}
const circle: VisitableShape = new Circle(5);
const rectangle: VisitableShape = new Rectangle(4, 6);
const square: VisitableShape = new Square(4);

const shapes: VisitableShape[] = [circle, rectangle, square];

printShapeDetails(shapes);

Use case 2: Cost of adding new class in hierarchy

Triangle class is added in hierarchy and 8 Visitor classes and 1 Visitor interface are updated. So, adding new class will affect all visitor classes.

class Triangle implements VisitableShape {
    constructor(public base: number, public height: number) {}

    accept<T>(visitor: ShapeVisitor<T>): T {
        return visitor.visit(this);
    }
}
interface ShapeVisitor<T> {
  visit(circle: Circle): T;
  visit(rectangle: Rectangle): T;
  visit(square: Square): T;
  visit(triangle: Triangle): T;
}
class AreaCalculatorVisitor implements ShapeVisitor<number> {
  visit(circle: Circle): number;
  visit(rectangle: Rectangle): number;
  visit(square: Square): number;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      return this.calculateCircleArea(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectangleArea(shape);
    } else if (shape instanceof Triangle) {
      return this.calculateTriangleArea(shape);
    }
  }

  private calculateCircleArea(circle: Circle) {
    return Math.PI * Math.pow(circle.radius, 2);
  }

  private calculateRectangleArea(rectangle: Rectangle) {
    return rectangle.width * rectangle.height;
  }

  private calculateTriangleArea(triangle: Triangle) {
    return 0.5 * triangle.base * triangle.height;
  }
}
class PerimeterCalculatorVisitor implements ShapeVisitor<number> {
  visit(circle: Circle): number;
  visit(rectangle: Rectangle): number;
  visit(square: Square): number;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      return this.calculateCirclePerimeter(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectanglePerimeter(shape);
    } else if (shape instanceof Triangle) {
      return this.calculateTrianglePerimeter(shape);
    }
  }

  private calculateCirclePerimeter(circle: Circle) {
    return 2 * Math.PI * circle.radius;
  }

  private calculateRectanglePerimeter(rectangle: Rectangle) {
    return 2 * (rectangle.width + rectangle.height);
  }

  private calculateTrianglePerimeter(triangle: Triangle) {
    // For a right triangle: perimeter = base + height + hypotenuse
    const hypotenuse = Math.sqrt(triangle.base**2 + triangle.height**2);
    return triangle.base + triangle.height + hypotenuse;
  }
}
class DiagonalDiameterCalculatorVisitor implements ShapeVisitor<number> {
  visit(circle: Circle): number;
  visit(rectangle: Rectangle): number;
  visit(square: Square): number;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      return this.calculateCircleDiagonalDiameter(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectangleDiagonalDiameter(shape);
    } else if (shape instanceof Triangle) {
      return this.calculateTriangleDiagonalDiameter(shape);
    }
  }

  private calculateCircleDiagonalDiameter(circle: Circle): number {
    return 2 * circle.radius;
  }

  private calculateRectangleDiagonalDiameter(rectangle: Rectangle): number {
    return Math.sqrt(
      rectangle.width * rectangle.width + rectangle.height * rectangle.height
    );
  }

  private calculateTriangleDiagonalDiameter(triangle: Triangle): number {
    // For a right triangle, the hypotenuse is the longest side.
    return Math.sqrt(triangle.base**2 + triangle.height**2);
  }
}
class Point {
  readonly x: number;
  readonly y: number;

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

class CentroidCalculatorVisitor implements ShapeVisitor<Point> {
  visit(circle: Circle): Point;
  visit(rectangle: Rectangle): Point;
  visit(square: Square): Point;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      return this.calculateCircleCenter(shape);
    } else if (shape instanceof Rectangle) {
      return this.calculateRectangleCenter(shape);
    } else if (shape instanceof Triangle) {
      return this.calculateTriangleCenter(shape);
    }
  }

  private calculateCircleCenter(circle: Circle): Point {
    return new Point(0, 0); // Assuming the circle is centered at the origin
  }

  private calculateRectangleCenter(rectangle: Rectangle): Point {
    return new Point(rectangle.width / 2, rectangle.height / 2); // Centroid is at half its width and half its height
  }

  private calculateTriangleCenter(triangle: Triangle): Point {
    // For a right triangle with base along the x-axis and height along y-axis:
    return { x: triangle.base / 3, y: triangle.height / 3 };
  }
}
class ScalingCalculatorVisitor implements ShapeVisitor<void> {
  private scaleFactor: number;

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

  visit(circle: Circle): void;
  visit(rectangle: Rectangle): void;
  visit(square: Square): void;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      this.calculateCircleScaling(shape);
    } else if (shape instanceof Rectangle) {
      this.calculateRectangleScaling(shape);
    } else if (shape instanceof Triangle) {
      return this.calculateTriangleScaling(shape);
    }
  }

  private calculateCircleScaling(circle: Circle): void {
    circle.radius *= this.scaleFactor;
  }

  private calculateRectangleScaling(rectangle: Rectangle): void {
    rectangle.width *= this.scaleFactor;
    rectangle.height *= this.scaleFactor;
  }

  private calculateTriangleScaling(triangle: Triangle): void {
    const scaledBase = triangle.base * this.scaleFactor;
    const scaledHeight = triangle.height * this.scaleFactor;

    triangle.base = scaledBase;
    triangle.height = scaledHeight;
  }
}
class ShapeDuplicatorVisitor implements ShapeVisitor<VisitableShape> {
  visit(circle: Circle): VisitableShape;
  visit(rectangle: Rectangle): VisitableShape;
  visit(square: Square): VisitableShape;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      this.duplicateCircle(shape);
    } else if (shape instanceof Rectangle) {
      this.duplicateRectangle(shape);
    } else if (shape instanceof Triangle) {
      this.duplicateTriangle(shape);
    }

    return undefined;
  }

  private duplicateCircle(circle: Circle): VisitableShape {
    return new Circle(circle.radius);
  }

  private duplicateRectangle(rectangle: Rectangle): VisitableShape {
    return new Rectangle(rectangle.width, rectangle.height);
  }

  private duplicateTriangle(triangle: Triangle): VisitableShape {
    return new Triangle(triangle.base, triangle.height);
  }
}
class RotationCalculatorVisitor implements ShapeVisitor<void> {
  visit(circle: Circle): void;
  visit(rectangle: Rectangle): void;
  visit(square: Square): void;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      this.calculateCircleRotation(shape);
    } else if (shape instanceof Rectangle) {
      this.calculateRectangleRotation(shape);
    } else if (shape instanceof Triangle) {
      this.calculateTriangleRotation(shape);
    }
  }

  private calculateCircleRotation(circle: Circle): void {
    // For the Circle, rotation doesn't affect its dimensions.
    // No changes as rotating a circle has no visual effect on its properties.
  }

  private calculateRectangleRotation(rectangle: Rectangle): void {
    // For the Rectangle, we'll swap its width and height.
    const temp = rectangle.width;
    rectangle.width = rectangle.height;
    rectangle.height = temp;
  }

  private calculateTriangleRotation(triangle: Triangle): void {
    // For simplicity, we'll rotate the right-angle vertex of the triangle
    const rotatedVertex = this.rotatePoint({ x: triangle.base, y: triangle.height });
    const newBase = rotatedVertex.x;
    const newHeight = rotatedVertex.y;

    triangle.base = newBase;
    triangle.height = newHeight;
  }
}
enum AreaComparison {
  Larger,
  Equal,
  Smaller,
}

class AreaComparerVisitor implements ShapeVisitor<AreaComparison> {
  private areaCalculator = new AreaCalculatorVisitor();

  constructor(private secondShape: VisitableShape) {}

  visit(circle: Circle): AreaComparison;
  visit(rectangle: Rectangle): AreaComparison;
  visit(square: Square): AreaComparison;
  visit(triangle: Triangle): number;
  visit(shape: Circle | Rectangle | Square | Triangle): number {
    if (shape instanceof Circle) {
      return this.compareCircle(shape);
    } else if (shape instanceof Rectangle) {
      this.compareRectangle(shape);
    } else if (shape instanceof Triangle) {
      this.compareTriangle(shape);
    }
  }

  private compareCircle(circle: Circle): AreaComparison {
    return this.compareAreas(circle, this.secondShape);
  }

  private compareRectangle(rectangle: Rectangle): AreaComparison {
    return this.compareAreas(rectangle, this.secondShape);
  }

  private compareTriangle(triangle: Triangle): AreaComparison {
    return this.compareAreas(triangle, this.secondShape);
  }

  private compareAreas(
    firstShape: VisitableShape,
    secondShape: VisitableShape
  ): AreaComparison {
    const firstShapeArea = firstShape.accept(this.areaCalculator);
    const secondShapeArea = secondShape.accept(this.areaCalculator);

    if (firstShapeArea > secondShapeArea) {
      return AreaComparison.Larger;
    } else if (firstShapeArea < secondShapeArea) {
      return AreaComparison.Smaller;
    } else {
      return AreaComparison.Equal;
    }
  }
}
const circle: VisitableShape = new Circle(5);
const rectangle: VisitableShape = new Rectangle(4, 6);
const square: VisitableShape = new Square(4);
const triangle: VisitableShape = new Triangle();

const shapes: VisitableShape[] = [circle, rectangle, square, triangle];

printShapeDetails(shapes);