Visitor design pattern will be usefull in applications where we do not know all possible use cases during design time . There might be request for new features from time to time, and in order to implement them, some refactoring has to be done. In these cases this pattern helps us to add new operations to existing object structures without modifying them.
This pattern allows us to keep our structures separately and then use the visitor design pattern to add functionality on top of out existing structure. Visitor pattern is also usefull in cases where we have a object structure where we have a base class which has all the operations defined and only a few of them are implemented by the concrete classes here we can create visitors that will add the functionality when required instead of defining all the operations in the base class.
The visitor design pattern is usefull when we have large object hierarchies, where adding a new functionality will involve a lot of code re factoring.
Below is the generic class diagram for visitor pattern
Lets take an example
We have a system which produced the dynamic line management data and system data for all the communication providers like airtel,idea and vadofone. We have a upstream applications which produces this data and puts all the data of dynamic line management inside one folder and the system data is another folder structure. The file naming convention will have the required information to distinguish the communication provider for example if a file belongs to airtel the file name convention will be Airtel_(.*).tar.gz. So now the requirement is to send these data into the appropriate communication provider. As we are expecting new communication providers to register into this service and new data like call details data would be required in future we need to make sure we should be able to add these features without making any changes into the existing code.
To solve this problem lets use visitor design pattern
Below is the class diagram we want to achieve finally
Lets code the required interfaces first
trait Visitor { def sendFiles(vadafone: Vadafone); def sendFiles(airtel: Airtel); def sendFiles(idea: Idea); }
trait CommunicationProvider { def accept(visitor: Visitor) }
Lets add the concrete visitor implementation DynamicLineDataVisitor and the SystemDataVisitor
import scala.collection.mutable.ListBuffer class DynamicLineDataVisitor(var list:ListBuffer[java.io.File]) extends Visitor { def sendFiles(vadafone: Vadafone)= { val pattern=vadafone.pattern list.foreach{ file => if(pattern.matcher(file.getName()).matches()) { println("sending file into vadofone") } } } def sendFiles(airtel: Airtel)={ val pattern=airtel.pattern list.foreach{ file => if(pattern.matcher(file.getName()).matches()) { println("sending file into airtel") } } } def sendFiles(idea: Idea)={ val pattern=idea.pattern list.foreach{ file => if(pattern.matcher(file.getName()).matches()) { println("sending file into idea") } } } }
import scala.collection.mutable.ListBuffer class SystemDataVisitor(var list: ListBuffer[java.io.File]) extends Visitor { def sendFiles(vadafone: Vadafone) = { val pattern = vadafone.pattern list.foreach { file => if (pattern.matcher(file.getName()).matches()) { println("sending file into vadofone") } } } def sendFiles(airtel: Airtel) = { val pattern = airtel.pattern list.foreach { file => if (pattern.matcher(file.getName()).matches()) { println("sending file into airtel") } } } def sendFiles(idea: Idea) = { val pattern = idea.pattern list.foreach { file => if (pattern.matcher(file.getName()).matches()) { println("sending file into idea") } } }
Lets add the concrete communication provider implementation
class Airtel extends CommunicationProvider { val pattern:java.util.regex.Pattern = java.util.regex.Pattern.compile("Airtel_(.*).tar.gz"); def accept(visitor: Visitor) = { visitor.sendFiles(this) } }
class Vadafone extends CommunicationProvider { val pattern: java.util.regex.Pattern = java.util.regex.Pattern.compile("Vadafone_(.*).tar.gz"); def accept(visitor: Visitor) = { visitor.sendFiles(this) } }
class Idea extends CommunicationProvider { val pattern:java.util.regex.Pattern = java.util.regex.Pattern.compile("Idea_(.*).tar.gz"); def accept(visitor: Visitor) = { visitor.sendFiles(this) } }
Finally lets code the test class
object Test extends App { import java.io.File import scala.collection.mutable.ListBuffer val file1: File = new File("Airtel_(.*).tar.gz") val file2: File = new File("Vadafone_(.*).tar.gz") val file3: File = new File("Idea_(.*).tar.gz") val listOfFiles = ListBuffer[File](); listOfFiles += file1 listOfFiles += file2 listOfFiles += file3 val dynamicLineDataVisitor = new DynamicLineDataVisitor(listOfFiles); val systemDataVisitor = new SystemDataVisitor(listOfFiles); val airtel = new Airtel(); val vadofone = new Vadafone(); val idea = new Idea(); airtel.accept(dynamicLineDataVisitor); vadofone.accept(systemDataVisitor); idea.accept(dynamicLineDataVisitor); }
As we can in the above example we are able to completely decouple the data and the communication providers. So in case in future i want to add a new vendor say aircel i just need to extend the CommunicationProvider interface and implement the requirement methods . Same way if i have to start sending call details information which will be a new data i just need to add a new visitor for call details and implement the required methods from the visitor interface without modifying any existing classes.