1. Introduction
Design patterns represent an important concept in software development and are used to solve recurring problems. One of these patterns is the Visitor pattern. We use it to separate the algorithm from the object structure on which it operates. This pattern particularly comes in handy when we want to add new functionality to existing classes without modifying their source code.
In this tutorial, we’ll discuss the Visitor pattern and how we can implement it in Kotlin.
2. Definition
By definition, the Visitor design pattern is a behavioral design pattern that allows us to add new operations to existing classes without modifying the structure. We use this pattern when we have a complex object structure and wish to perform different operations on that structure.
What’s more, this pattern defines two main components: the Visitor and the Visitable. The Visitor is responsible for defining an interface with visit methods for each Visitable object, while the Visitable is responsible for accepting the Visitor. This is generally done in an accept method.
2.1. Problem It Solves
The Visitor pattern aims to solve the issue of adding new operations to an object structure without altering its existing structure. Typically, incorporating new operations into a class hierarchy entails modifying the classes already in place. However, this approach may not always be feasible or practical.
Therefore, the visitor pattern introduces a visitor class that can traverse the existing object structure and carry out the desired operations. Ultimately, this makes the system more adaptable and extendable.
3. Implementation: Shopping Cart Application
To demonstrate how we can implement the Visitor pattern in Kotlin, let’s consider a simple Shopping Cart application. This application has just one role: calculating the total price of items in a shopping cart.
Concretely, this application has two classes: the Listing and Cart classes. The Listing class represents an item with a price in the cart, while the Cart class represents the shopping cart and contains all the listings.
As mentioned earlier, to implement this design pattern, we need to create a Visitor interface with a visit method for each class in the object hierarchy. In our case, we have two classes, so we need two visit methods.
3.1. Visitor Interface and Implementation
Let’s write our Visitor interface that defines two visit methods, one each for the Listing and Cart classes:
interface ShoppingCartVisitor {
fun visit(listing: Listing): Double
fun visit(cart: Cart): Double
}
This interface provides the visit methods for each class in our object structure that we intend to perform some operation on.
Additionally, we need a concrete visitor class that implements our Visitor interface:
class ShoppingCartVisitorImpl : ShoppingCartVisitor {
override fun visit(listing: Listing): Double {
return listing.price
}
override fun visit(cart: Cart): Double {
var totalPrice = 0.0
for (listing in cart.listings) {
totalPrice += listing.accept(this)
}
return totalPrice
}
}
In this code snippet, the ShoppingCartVisitorImpl class implements our ShoppingCartVisitor interface and provides a concrete implementation for the visit methods.
Visiting a Listing just returns its price, while visiting a Cart iterates over all listings in the cart, calling the accept() method on each one. We’ll implement the accept() methods for both objects next.
It’s worth noting that the visitor class is responsible for handling the complex task of adding up the prices of each listing in the cart while ensuring that no object in our object structure is aware of this operation.
3.2. Visitable Interface and Implementations
The Listing and Cart classes constitute our visitable classes. Since they must implement accept() methods, let’s define a Visitable interface:
interface Visitable {
fun accept(visitor: ShoppingCartVisitor): Double
}
Now, let’s define our Listing and Cart classes:
class Listing(val name: String, val price: Double): Visitable {
override fun accept(visitor: ShoppingCartVisitor): Double {
return visitor.visit(this)
}
}
class Cart: Visitable {
val listings = mutableListOf<Listing>()
fun addListing(listing: Listing) {
listings.add(listing)
}
fun removeListing(listing: Listing) {
listings.remove(listing)
}
override fun accept(visitor: ShoppingCartVisitor): Double {
return visitor.visit(this)
}
}
In the code above, the Listing and Cart classes are visitable classes, responsible for accepting the visitor. Therefore, they need to implement the appropriate accept() methods that will serve as an anchor for the visitor class.
Finally, the accept() methods will call the appropriate method on the visitor, which helps calculate the price of each listing and the total price of the contents in the cart.
3.3. Testing
Next, we need to test our application by using the visitor to calculate the total price of a shopping cart:
@Test
fun `test shopping cart visitor`() {
val cart = Cart()
cart.addListing(Listing("Listing 1", 10.0))
cart.addListing(Listing("Listing 2", 20.0))
cart.addListing(Listing("Listing 3", 30.0))
val visitor = ShoppingCartVisitorImpl()
val totalPrice = cart.accept(visitor)
assertEquals(60.0, totalPrice)
}
In the unit test above, we create a shopping cart with three listings. Then, we create a visitor and calculate the total price of the shopping cart using the accept() method. Finally, we assert that the total price is equal to the expected value.
4. Conclusion
In this article, we’ve discussed the Visitor pattern in Kotlin and how we can implement it using a simple Shopping Cart application. We saw that by using this pattern, we can add functionality to existing classes without modifying their structure or source code.