Visitor Design Pattern in Kotlin

1. Definition

The Visitor Design Pattern is a way of separating an algorithm from an object structure it operates on. It promotes adding further operations to existing object structures without having to modify them.

2. Problem Statement

Imagine you have a collection of items, and you want to perform a variety of unrelated operations on these items. If you keep adding these operations to the items, they will become cluttered and violate the Single Responsibility Principle.

3. Solution

The Visitor Pattern suggests that you place the new operation in a separate class called Visitor, instead of adding methods to the objects. Then, you can pass this visitor object to the elements as they "accept" the visitor; the visitor will then interact with the elements and perform the operation.

4. Real-World Use Cases

1. A computer system's components report (CPU, Memory, Disk, etc.) and you want various operations like display, export, and error-check without altering the components.

2. An e-commerce cart where you can apply multiple discount strategies.

3. Syntax tree representation of programming code where operations like pretty printing, error checking, and optimization can be added.

5. Implementation Steps

1. Define a 'Visitable' interface that declares an 'accept' method.

2. Concrete elements implement this Visitable interface.

3. Define a 'Visitor' interface for each type of operation.

4. Implement concrete visitor classes for each operation.

5. The elements will use the visitor to perform the operation.

6. Implementation in Kotlin

// Step 1: Visitable interface
interface Visitable {
    fun accept(visitor: Visitor)
}
// Step 2: Concrete elements
class Book(val price: Double, val isbn: String) : Visitable {
    override fun accept(visitor: Visitor) {
        visitor.visit(this)
    }
}
class Fruit(val price: Double, val weight: Double, val name: String) : Visitable {
    override fun accept(visitor: Visitor) {
        visitor.visit(this)
    }
}
// Step 3: Visitor interface
interface Visitor {
    fun visit(book: Book)
    fun visit(fruit: Fruit)
}
// Step 4: Concrete visitor
class ShoppingCartVisitor : Visitor {
    var totalCost = 0.0
    override fun visit(book: Book) {
        totalCost += book.price
    }
    override fun visit(fruit: Fruit) {
        totalCost += fruit.price * fruit.weight
    }
}
// Testing the implementation
fun main() {
    val items: Array<Visitable> = arrayOf(Book(20.0, "1234"), Fruit(2.0, 2.5, "Apple"))
    val visitor = ShoppingCartVisitor()
    for (item in items) {
        item.accept(visitor)
    }
    println("Total Cost = ${visitor.totalCost}")
}

Output:

Total Cost = 25.0

Explanation:

1. We defined two types of items, Book and Fruit. Both these classes implement the Visitable interface, which means they can accept visitors.

2. The Visitor interface provides a way to visit different types of items. The concrete ShoppingCartVisitor implements this interface to calculate the total cost of the cart.

3. In our main function, we created an array of items and a shopping cart visitor. We then passed this visitor to each item.

4. As each item accepts the visitor, the appropriate visit method of the visitor is called, and the operation (in this case, price calculation) is performed.

7. When to use?

Use the Visitor Pattern when:

1. You need to add operations on elements of an object structure without modifying the objects themselves.

2. A set of operations must be performed on elements of different types of a complex object structure.

3. The object structure is unlikely to be changed but is very likely to have new operations which have to be added.


Comments