The visitor pattern

A start-up company decided to create a music/song library application using Salesforce public sites and communities. Users can browse through thousands of songs, add them to the cart, and purchase them. There are many types of music genres available on websites, such as pop, rock, electronic, and so on. Prices are based on music genres; thus, during a checkout, the development team needs to maintain a list of different songs sold by genres and calculate the price accordingly. For the sake of simplicity, we will consider only three types of music in our example.

Developers decided that all the songs will share some common properties, such as a title and album name, and thus it makes sense to use inheritance. They developed the following abstract class that must be extended by all types of song/music:

public abstract class Music { 
   protected String title;  
    protected String album; 
     
    public abstract String getMusicDetail();  
} 
 

The getMusicDetail() method, which is declared as an abstract, assumes that different operations need to be performed, depending on different types of music.

The following three classes have been created by developers for different types of music:

public class RockMusic extends Music{ 
 
    public  RockMusic(String title,  String album){ 
        this.title = title;  
        this.album = album; 
    } 
     
    public override String getMusicDetail(){  
        return 'Rock Music : Title - '+title+' ,album - '+album;  
    } 
} 
public class PopMusic extends Music{ 
 
    public PopMusic(String title,  String album){ 
        this.title = title;  
        this.album = album; 
    } 
     
    public override String getMusicDetail(){  
        return 'Pop Music : Title - '+title+' ,album - '+album;  
    } 
} 
 
public class ElectronicMusic extends Music{ 
 
    public ElectronicMusic(String title,  String album){ 
        this.title = title;  
        this.album = album; 
    } 
     
    public override String getMusicDetail(){  
        return 'Electronic Music : Title - '+title+ 
                ' ,album - '+album;  
    } 
} 
 

As shown in the preceding code, all the classes extend the Music abstract class and implement their own version of the getMusicDetail methods.

Consider the following sample calculator class, showing the correct method being invoked:

public class MusicPriceCalculator { 
 
    public static void calculatePrice(PopMusic m){ 
        System.debug('Calculate price of Pop Music') ; 
    }   
     
    public static void calculatePrice(RockMusic m){ 
        System.debug('Calculate price of Rock Music') ; 
    }  
     
    public static void calculatePrice(ElectronicMusic m){ 
        System.debug('Calculate price of Electronic Music') ; 
    }  
} 

Now, assume that end users have added a few songs to their cart. The following code snippet shows the calculation of the price:

List<Music> lstMusic = new List<Music>(); 
 
lstMusic.add(new RockMusic('Go Johnny go', 
                'Chuck Berry Is on Top')); 
 
lstMusic.add(new PopMusic('When Doves Cry', 
                'The Very Best Of Prince')); 
 
lstMusic.add(new ElectronicMusic('Strobe', 
                'For Lack of a Better Name')); 
   
for(Music m : lstMusic){ 
    if(m instanceof PopMusic) 
    { 
        MusicPriceCalculator.calculatePrice((PopMusic)m) ; 
    }else if(m instanceof RockMusic){ 
        MusicPriceCalculator.calculatePrice((RockMusic)m) ; 
    } else if(m instanceof ElectronicMusic){ 
        MusicPriceCalculator.calculatePrice((ElectronicMusic)m) ; 
    }      
} 

Running the preceding anonymous code will generate the following output:

Calculate price of Pop Music 
Calculate price of Rock Music 
Calculate price of Electronic Music  

Assume that a new music category has been added, PopRockMusic ,which is a child of PopMusic, so it becomes a grandchild of the Music class:

  public virtual class PopRockMusic extends PopMusic{ 
    protected String title;  
    protected String album; 
      
    public PopRockMusic(String title,  String album){ 
        super(title,album); 
        this.title = title;  
        this.album = album; 
    } 
     
    public virtual override String getMusicDetail(){   
        return 'Pop Rock Music : Title - '+title+' ,album - '+album;  
    } 
} 

The updated MusicPriceCalculator will be as follows:

 public class MusicPriceCalculator { 
 
    public static void calculatePrice(PopMusic m){ 
        System.debug('Calculate price of Pop Music') ; 
    }   
     
    public static void calculatePrice(RockMusic m){ 
        System.debug('Calculate price of Rock Music') ; 
    }  
     
    public static void calculatePrice(ElectronicMusic m){ 
        System.debug('Calculate price of Electronic Music') ; 
    } 
  
    public static void calculatePrice(PopRockMusic m){ 
        System.debug('Calculate price of PopRock Music') ; 
    } 
 
} 

Now, try to run the following anonymous code:

List<Music> lstMusic = new List<Music>(); 
 
lstMusic.add(new PopMusic('Pop Song', 
                'Pop Album')); 
 
lstMusic.add(new PopRockMusic('PopRock Song', 
                'PopRock Album')); 
 
  
for(Music m : lstMusic){ 
    if(m instanceof PopMusic) 
    { 
        MusicPriceCalculator.calculatePrice((PopMusic)m) ; 
    }else if(m instanceof RockMusic){ 
        MusicPriceCalculator.calculatePrice((RockMusic)m) ; 
    } else if(m instanceof ElectronicMusic){ 
        MusicPriceCalculator.calculatePrice((ElectronicMusic)m) ; 
    }  else if(m instanceof PopRockMusic){ 
        MusicPriceCalculator.calculatePrice((PopRockMusic)m) ; 
    }     
} 

The expected output is:

Calculate price of Pop Music 
Calculate price of PopRock Music 

However, the actual output is:

Calculate price of Pop Music 
Calculate price of Pop Music //unexpected result 

As we can see in the preceding output, both the music objects qualify as instances of the PopMusic class. Hence, both the times the overloaded calculatePrice  method was called for the PopMusic input parameter.

The problems with the preceding code are as follows:

  • Explicit type casting (from music to child classes) could result in an unexpected behavior (the double dispatch problem)
  • If any new type of music is added to the application, then type casting needs to be checked and changed accordingly throughout the application

Before proceeding to the solution of the preceding problems, let's discuss dispatching in OOPs.

Single dispatch

Single dispatch is also known as virtual methods and we have seen it multiple times in a real-life scenario.

The following classes show simple inheritance in Apex:

public virtual class Car { 
    
    public virtual void detail(){ 
        System.debug('Its Car'); 
    } 
} 
 
public class Honda extends Car{  
    
    public override void detail(){ 
        System.debug('Its Honda'); 
    } 
} 

Let's try to run the following code as anonymous apex in developer console

Car obj = new Honda(); 
obj.detail(); 

It will print Its Honda, which means that at runtime, it will decide which method needs to be executed. Even though the type of object is declared as Car, but at runtime Apex is able to determine it solely on the basis of its actual type and thus it's known as single-dispatch.

Double-dispatch

Double dispatch is a mechanism in programming where a call to various methods of a concrete class is decided on the basis of two types of objects involved.

In addition to the preceding Car and Honda classes, let's add the following class for insurance:

public virtual class Insurance { 
     
    public virtual void insure(Car c){ 
        System.debug('This is insurance for Car'); 
    }    
     
    public virtual void insure(Honda c){ 
        System.debug('This is insurance for Honda'); 
    } 
} 
 
public class PaintInsurance extends Insurance {     
     
    public override void insure(Car c){ 
        System.debug('This is Paint insurance for Car'); 
    }     
     
    public override void insure(Honda c){ 
        System.debug('This is Paint insurance for Honda'); 
    } 
} 

Now, try to run this code in an anonymous window of the developer console:

Insurance ins = new PaintInsurance (); 
Car obj = new Honda(); 
ins.insure(obj); 

The expected output will be as follows:

"This is Paint insurance for Honda"  

However, the actual output is as follows:

"This is Paint insurance for Car" 

Note that in the preceding code, a method is chosen solely on the basis of the type of the ins(insurance) object and not on the basis of the obj(car) type. This happens because Apex and other programming languages, such as Java and C#, support single-dispatch but not double-dispatch.

The visitor pattern can be used to implement the double dispatch behavior to ensure that the system generates the desired output.

Note

The visitor pattern is used to separate the algorithm from the object structure on which it operates.

The advantage of this pattern is to add new operations to an object without modifying its structure. This pattern follows the open/close principle of design patterns, which represents O from SOLID, as discussed in the previous chapters.

In short, this pattern allows you to add functionalities to a family of related classes without modifying the class itself.

Let's come back to our music application example and see how we can resolve this problem using the visitor pattern.

Guidelines to implementing the visitor pattern

We need to follow these two points for the visitor pattern:

  1. The visitor class should implement the visit() method, which will be called for every element of a data structure. In our case, we should implement the visit() method, which will be called by the PopMusic, PopRockMusic, RockMusic, and ElectronicMusic classes.
  2. All classes on which an operation (the price calculation) needs to be performed should provide the accept() method to accept the visitor and pass itself as an argument to the visitor class.

Let's work on replacing MusicPriceCalculator with the visitor class. We will create an  IMusicVisitor interface, which needs to be implemented by all visitor classes:

public interface IMusicVisitor { 
   void visit(RockMusic music); 
   void visit(PopMusic music); 
   void visit(ElectronicMusic music); 
   void visit(PopRockMusic music); 
} 

As we can see in the preceding interface, a concrete class needs to implement the visit method for all types of elements of a data structure.

The following code is a concrete class implementation of the IMusicVisitor interface, which calculates the price on the basis of the genre type:

public class MusicPriceVisitor  implements IMusicVisitor{     
     
    public Double finalPrice = 0; 
     
    public MusicPriceVisitor(){ 
    } 
     
    public void visit(RockMusic music){ 
        System.debug('In Rock Music Algorithm'); 
        finalPrice += 2; 
    } 
    public void visit(PopMusic music){ 
        System.debug('In Pop Music Algorithm'); 
        finalPrice += 3; 
    } 
    public void visit(ElectronicMusic music){ 
        System.debug('In Electronic Music Algorithm'); 
        finalPrice += 4; 
    }  
    public void visit(PopRockMusic music){ 
        System.debug('In PopRock Music Algorithm'); 
        finalPrice += 2; 
    }  
}

So far, we have completed the first part of the visitor pattern, which specifies that an external class needs to implement the visit method for all the required types to be supported.

Now, it's time to move to the second point, which states that all the types that need to perform an operation (invoke the algorithm) must provide the accept method, and therefore our Music abstract class will change to the following code:

public abstract class Music { 
   protected String title;  
    protected String album; 
     
    public abstract String getMusicDetail();  
    public abstract void accept(IMusicVisitor visitor);  
} 

In the same way, all concrete classes are refactored to introduce the accept method to support the visitor pattern:

public class RockMusic extends Music{  
 
    public  RockMusic(String title,  String album){ 
        this.title = title;  
        this.album = album; 
    } 
     
    public override String getMusicDetail(){  
        return 'Rock Music : Title - '+title+' ,album - '+album;  
    } 
     
    public override void accept(IMusicVisitor visitor){ 
        visitor.visit(this); 
    } 
} 
  
public class PopMusic extends Music{ 
 
    public PopMusic(String title,  String album){ 
        this.title = title;   
        this.album = album; 
    } 
     
    public override String getMusicDetail(){  
        return 'Pop Music : Title - '+title+' ,album - '+album;  
    } 
     
    public override void accept(IMusicVisitor visitor){ 
        visitor.visit(this); 
    } 
} 
  
public class ElectronicMusic extends Music{ 
 
    public ElectronicMusic(String title,  String album){ 
        this.title = title;   
        this.album = album; 
    } 
     
    public override String getMusicDetail(){  
        return 'Electronic Music : Title - '+title+' ,album - '+album;  
    } 
     
    public override void accept(IMusicVisitor visitor){ 
        visitor.visit(this); 
    } 
} 
 
public virtual class PopRockMusic extends PopMusic{ 
    protected String title;  
    protected String album; 
      
    public PopRockMusic(String title,  String album){ 
        super(title,album); 
        this.title = title;  
        this.album = album; 
    } 
     
    public virtual override String getMusicDetail(){   
        return 'Pop Rock Music : Title - '+title+' ,album - '+album;  
    } 
     
    public virtual override void accept(IMusicVisitor visitor){ 
        visitor.visit(this); 
    } 
} 
 

Now, we can solve the previously discussed double dispatch problem using the following code snippet:

List<Music> lstMusic = new List<Music>(); 
lstMusic.add(new RockMusic('Go Johnny go','Chuck Berry Is on Top')); 
lstMusic.add(new PopMusic('When Doves Cry','The Very Best Of Prince')); 
lstMusic.add(new ElectronicMusic('Strobe','For Lack of a Better Name')); 
lstMusic.add(new PopRockMusic('Sweet Home Alabama','Second Helping')); 
 
MusicPriceVisitor visitor = new MusicPriceVisitor(); 
 
for(Music m : lstMusic) 
{ 
    m.accept(visitor); 
} 
System.debug('Final Amount - '+visitor.finalPrice); 

The output is as follows:

In Rock Music Algorithm 
In Pop Music Algorithm 
In Electronic Music Algorithm 
In PopRock Music Algorithm 
Amount - 11.0 

In the preceding output, we can see that the PopRockMusic method (algorithm) is correctly invoked and solved the double dispatch problem.

If in future, developers want to add a new set of algorithms, let's say to calculate tax or discounts, we simply need to create a new visitor class by implementing the IMusicVisitor interface. Concrete classes already have the accept method that allows new visitor classes to use them.

The following class diagram shows the complete implementation of our example:

Guidelines to implementing the visitor pattern

Points to consider

  • Sometimes, the visitor class may require to access private members of the actual class, and you may be forced to change variables access to public.
  • The order of iteration is not confirmed. As we can see, the accept() method is called on the basis of the existing collection of music. If you need to maintain the order of iteration like the first RockMusic process and then PopMusic, then this is not a suitable design pattern in this scenario.
  • If the data structure is not confirmed, then this pattern is of no use. Slight changes in the data structure may result in rewriting the visitor class.
  • If a new visitor (new music type) is added, then we need to update the visitor class to introduce operations on the new member.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset