Wednesday, July 13, 2022

Behavioral Design Patterns # Visitor

Visitor is a behavioral design pattern that lets you separate algorithms from the objects on which they operate.

Problem

Imagine that your team develops an app that works with geographic information structured as one colossal graph. Each node of the graph may represent a complex entity such as a city, but also more granular things like industries, sightseeing areas, etc. 

The nodes are connected with others if there’s a road between the real objects that they represent. Under the hood, each node type is represented by its own class, while each specific node is an object



At some point, you got a task to implement exporting the graph into XML format. At first, the job seemed pretty straightforward. You planned to add an export method to each node class and then leverage recursion to go over each node of the graph, executing the export method. 

The solution was simple and elegant: thanks to polymorphism, you weren’t coupling the code which called the export method to concrete classes of nodes.

Unfortunately, the system architect refused to allow you to alter existing node classes. He said that the code was already in production and he didn’t want to risk breaking it because of a potential bug in your changes.

Besides, he questioned whether it makes sense to have the XML export code within the node classes. The primary job of these classes was to work with geodata. The XML export behavior would look alien there.

There was another reason for the refusal. It was highly likely that after this feature was implemented, someone from the marketing department would ask you to provide the ability to export into a different format, or request some other weird stuff. 


This would force you to change those precious and fragile classes again.


Solution


The Visitor pattern suggests that you place the new behavior into a separate class called visitor, instead of trying to integrate it into existing classes. 

The original object that had to perform the behavior is now passed to one of the visitor’s methods as an argument, providing the method access to all necessary data contained within the object.

Now, what if that behavior can be executed over objects of different classes? For example, in our case with XML export, the actual implementation will probably be a little bit different across various node classes. 

Thus, the visitor class may define not one, but a set of methods, each of which could take arguments of different types, like this:.

class ExportVisitor implements Visitor is method doForCity(City c) { ... } method doForIndustry(Industry f) { ... } method doForSightSeeing(SightSeeing ss) { ... } // ...

But how exactly would we call these methods, especially when dealing with the whole graph? These methods have different signatures, so we can’t use polymorphism. 

To pick a proper visitor method that’s able to process a given object, we’d need to check its class. Doesn’t this sound like a nightmare?.

foreach (Node node in graph) if (node instanceof City) exportVisitor.doForCity((City) node) if (node instanceof Industry) exportVisitor.doForIndustry((Industry) node) // ... }

You might ask, why don’t we use method overloading? That’s when you give all methods the same name, even if they support different sets of parameters. Unfortunately, even assuming that our programming language supports it at all (as Java and C# do), it won’t help us. 

Since the exact class of a node object is unknown in advance, the overloading mechanism won’t be able to determine the correct method to execute. It’ll default to the method that takes an object of the base Node class.

However, the Visitor pattern addresses this problem. It uses a technique called Double Dispatch, which helps to execute the proper method on an object without cumbersome conditionals. 

Instead of letting the client select a proper version of the method to call, how about we delegate this choice to objects we’re passing to the visitor as an argument? Since the objects know their own classes, they’ll be able to pick a proper method on the visitor less awkwardly. They “accept” a visitor and tell it what visiting method should be executed.

// Client code foreach (Node node in graph) node.accept(exportVisitor) // City class City is method accept(Visitor v) is v.doForCity(this) // ... // Industry class Industry is method accept(Visitor v) is v.doForIndustry(this) // ..

I confess. We had to change the node classes after all. But at least the change is trivial and it lets us add further behaviors without altering the code once again.

Now, if we extract a common interface for all visitors, all existing nodes can work with any visitor you introduce into the app. If you find yourself introducing a new behavior related to nodes, all you have to do is implement a new visitor class


Structure



Pros and Con



Visitor Design Pattern Java Example

To implement visitor pattern, first of all we will create different type of items (Elements) to be used in shopping cart.
public interface ItemElement { public int accept(ShoppingCartVisitor visitor); }

Notice that accept method takes Visitor argument. We can have some other methods also specific for items but for simplicity I am not going into that much detail and focusing on visitor pattern only.

Let’s create some concrete classes for different types of items.
public class Book implements ItemElement { private int price; private String isbnNumber; public Book(int cost, String isbn){ this.price=cost; this.isbnNumber=isbn; } public int getPrice() { return price; } public String getIsbnNumber() { return isbnNumber; } @Override public int accept(ShoppingCartVisitor visitor) { return visitor.visit(this); } }

package com.journaldev.design.visitor; public class Fruit implements ItemElement { private int pricePerKg; private int weight; private String name; public Fruit(int priceKg, int wt, String nm){ this.pricePerKg=priceKg; this.weight=wt; this.name = nm; } public int getPricePerKg() { return pricePerKg; } public int getWeight() { return weight; } public String getName(){ return this.name; } @Override public int accept(ShoppingCartVisitor visitor) { return visitor.visit(this); } }

Notice the implementation of accept() method in concrete classes, its calling visit() method of Visitor and passing itself as argument.

We have visit() method for different type of items in Visitor interface that will be implemented by concrete visitor class.

public interface ShoppingCartVisitor { int visit(Book book); int visit(Fruit fruit); }

Now we will implement visitor interface and every item will have it’s own logic to calculate the cost.
public class ShoppingCartVisitorImpl implements ShoppingCartVisitor { @Override public int visit(Book book) { int cost=0; //apply 5$ discount if book price is greater than 50 if(book.getPrice() > 50){ cost = book.getPrice()-5; }else cost = book.getPrice(); System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost); return cost; } @Override public int visit(Fruit fruit) { int cost = fruit.getPricePerKg()*fruit.getWeight(); System.out.println(fruit.getName() + " cost = "+cost); return cost; } }

Lets see how we can use visitor pattern example in client applications
public class ShoppingCartClient { public static void main(String[] args) { ItemElement[] items = new ItemElement[]{new Book(20, "1234"),new Book(100, "5678"), new Fruit(10, 2, "Banana"), new Fruit(5, 5, "Apple")}; int total = calculatePrice(items); System.out.println("Total Cost = "+total); } private static int calculatePrice(ItemElement[] items) { ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl(); int sum=0; for(ItemElement item : items){ sum = sum + item.accept(visitor); } return sum; } }

When we run above visitor pattern client program, we get following output.

Book ISBN::1234 cost =20 Book ISBN::5678 cost =95 Banana cost = 20 Apple cost = 25 Total Cost = 160


You may also like

Kubernetes Microservices
Python AI/ML
Spring Framework Spring Boot
Core Java Java Coding Question
Maven AWS