Thursday, January 28, 2010

The Visitor Pattern

The visitor pattern is one of those patterns that seems to be quite perplexing in the beginning, but is quite useful as well. It took me the longest to understand this pattern as compared to others, mainly because of the drawbacks that this pattern has and due to the various ways in which it can be implemented. In this post, I will try to keep things simple and implement it in a way that I feel, will make it easier to understand.

Let's first see where this pattern can be used.
Suppose that you have a class that for which you have already defined a number of methods. But you observe that similar methods exist in other classes as well, but with subtle differences. Usually, this can be implemented by means of inheritance wherein you keep the common functionality in the superclass and override the necessary methods only in cases where you need to.

Now suppose you already have a set if similar classes. But due to some business requirement, you need to add a certain set of functionality to all those classes. What would you do in the normal approach? Add a method to each of those classes? Ok, you would. But suppose, such requirements keep coming and these requirements are not intended to be permanent. For example, suppose you need a given functionality in the current release of your product, but somehow, you don't feel the need to have that functionality to become the core part of your classes because in the next release, they might become irrelevant.

In my opinion, the Visitor pattern lets you create a fall back mechanism in your classes for exactly the above mentioned scenario. Following are the steps that you can follow :


  1. Declare your class to implement a Visitable interface and implement a method in your classses that accepts a Visitor as an argument.
  2. Create concrete Visitor class that knows about group of classes for which it has to provide the new functionality. It does this by declaring overloaded methods, having the same name but taking in arguments that correspond to the classes for which you want to add the functionality.
  3. In the visitor method of your class, dispatch control to the appropriate method of your visitor class by passing 'this' as the argument. Or you can also pass control to the visitor class, which determines which method needs to be called based upon the type of argument it receives


Lets see an example.

Scenario :
I have a game which has a player. A player can carry 2 weapons. A player can accumulate points in a game. I have 2 types of weapons - ShotGun and a MagicWand. Every weapon has a primary and a secondary attack mechanism which is implemented by each weapon in a weapon specific way. A Shotgun's secondary attack is can be enabled or disabled at any point in the game by setting a boolean property on the Shotgun. A MagicWand has a power level which can be increased/decreased as the game progresses.

Now suppose that you have made all the remaining components of the game. And the game is functioning. Now, all of a sudden, you wake up one morning and decide to add three new features to your game:

  • The shotgun's secondary attack is enabled when the player reaches level 2 or higher
  • The MagicWand's power level is increased by 2 points at the beginning of each level.
  • The player is to be given level clearance bonus points at each level.


So, how do you implement it??

Approach 1 : The normal approach
Create a method called upgrade(int level) in your Player and Weapon class. Override the upgrade in the subclasses to upgrade the specific weapons. The problem with this approach is that for each such new business requirement, you will end up expanding the functionality of your core classes. However the inherent simplicity of this technique will play a vital role in choosing this technique. I believe that one can prefer this technique over the Visitor Pattern Based approach especially if the classes will require the functionality as a core functionality. But if thats not the case, then you certainly don't want to clutter your classes with business logic.

Approach 2 : Helper classes
This approach is pretty obvious. Create a helper class that does all the dirty work for you. You can implement it in any way you find suitable. I am not going to elaborate on this because the possibilities are far too many to be discussed here. So, that's left completely up to you.

Approach 3 : Visitor Pattern
This is by far the most complicated approach of the three approaches, but all the more extensible. In the following examples, observe carefully how I try to use the Visitor Pattern to implement the business requirement of my scenario. There are two core interfaces. One is the Visitor interface, that declares a single method called visit(Visitable visitable). The other is the Visitable interface that declares an accept(Visitor visitor) method. The classes of my domain, namely Player and Weapon, implement this interface. When a Visitable object is visited by a visitor, it accepts it and gives control to the visitor to execute the new functionality for that object by passing itself as an argument to the visit method of the visitor. My concrete visitor class is the LevelUpgradeVsitor that will provide the functionality to upgrade the objects passed to it.
Enough with that talking. Lets see some code now babay..

Visitor.java

public interface Visitor {
    public void visit(Visitable visitable);
}


Visitable.java

public interface Visitable {
    public void accept(Visitor visitor);
}

Weapon.java

public interface Weapon extends Visitable{
    public void primaryAttack();
    public void secondaryAttack();
}

Player.java

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author ryan
 */
public class Player implements Visitable{
    private List <Weapon> weapons;
    private int points;

    public Player()
    {
        weapons=new ArrayList <Weapon>();
    }

    public List <Weapon> getWeapons() {
        return weapons;
    }

    public void setWeapons(List <Weapon> weapons) {
        this.weapons = weapons;
    }

    public boolean addWeapon(Weapon w)
    {
        weapons.add(w);
        return true;
    }

    public int getPoints() {
        return points;
    }

    public void setPoints(int points) {
        this.points = points;
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
        
        for(Weapon w: weapons)
        {
            w.accept(visitor);
        }
    }

}

ShotGun.java

public class ShotGun implements Weapon{

    private boolean dualShotEnabled;

    public ShotGun()
    {
        dualShotEnabled=false;
    }

    public void setDualShotEnabled(boolean dualShotEnabled) {
        this.dualShotEnabled = dualShotEnabled;
    }

    public boolean isDualShotEnabled() {
        return dualShotEnabled;
    }
    

    public void primaryAttack() {
        System.out.println("ShotGun : SingleShot");
    }

    public void secondaryAttack() {
        if(dualShotEnabled)
        {
            System.out.println("ShotGun : DualShot");
        }
    }

    public void accept(Visitor visitor) {
        
        visitor.visit(this);
    }

}


MagicWand.java

public class MagicWand implements Weapon{
    private int powerLevel;

    public void setPowerLevel(int powerLevel) {
        this.powerLevel = powerLevel;
    }

    public int getPowerLevel() {
        return powerLevel;
    }
    

    public void primaryAttack() {
        System.out.println("Magic Wand(Power level : "+powerLevel+") : Primary Attack.");
    }

    public void secondaryAttack() {
        System.out.println("Magic Wand(Power level : "+powerLevel+") : Secondary Attack.");
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

LevelUpgradeVisitor.java -- The implementation of the Visitor Pattern. You can have many such visitors based upon different business requirements.

public class LevelUpgradeVisitor implements Visitor{

    private int level;

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }
    
    public void visit(Visitable visitable) {
        
        if(visitable instanceof Player)
        {
            upgrade((Player)visitable);
            System.out.println("Player Upgraded");
        }
        else
        {
            
            if(visitable instanceof ShotGun)
            {
                upgrade((ShotGun)visitable);
                System.out.println("ShotGun Upgraded");
            }
            else
            {
                
                if(visitable instanceof MagicWand)
                {
                    upgrade((MagicWand)visitable);
                    System.out.println("MagicWand Upgraded");
                }
            }
        }
    }

    public void upgrade(Player p)
    {
        int bonus=level*100;
        System.out.println("Adding Level Bonus For Player : "+ bonus);
        p.setPoints(p.getPoints()+bonus);
    }

    public void upgrade(ShotGun s)
    {
        if(level>=2)
        {
            s.setDualShotEnabled(true);
        }
    }

    public void upgrade(MagicWand m)
    {
        if(level>=2)
        {
            m.setPowerLevel(m.getPowerLevel()+2);
        }
    }
}

Game.java

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author ryan
 */
public class Game implements Visitable{
    private Player player1;
    private int level;
    
    public Game() {}


    public Player getPlayer1() {
        return player1;
    }

    public int getLevel() {
        return level;
    }

    public void setLevel(int level) {
        this.level = level;
    }

    
    public void setPlayer1(Player player1) {
        this.player1 = player1;
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
        player1.accept(visitor);
    }

}


Main.java

public class Main {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {

        Game game=new Game();
        LevelUpgradeVisitor luv = new LevelUpgradeVisitor();

        Player p=new Player();
        ShotGun sg=new ShotGun();
        MagicWand mw= new MagicWand();
        

        //Initialize the Player's values
        p.setPoints(10);
        p.addWeapon(sg);
        p.addWeapon(mw);

     
        //Lets Initialize the game.
        game.setPlayer1(p);
        game.setLevel(1);

        
        
        System.out.println("Before Level Upgrade");
        System.out.println("Initial Level "+game.getLevel());
        System.out.println("Initial Player Points : " + game.getPlayer1().getPoints());
        game.getPlayer1().getWeapons().get(0).primaryAttack();
        game.getPlayer1().getWeapons().get(0).secondaryAttack();
        game.getPlayer1().getWeapons().get(1).primaryAttack();
        game.getPlayer1().getWeapons().get(1).secondaryAttack();



        //Upgrade Level
        game.setLevel(2);
        luv.setLevel(game.getLevel());
        game.accept(luv);


        System.out.println("\n\nAfter Level Upgrade");
        System.out.println("New Level "+game.getLevel());
        System.out.println("New Player Points : " + game.getPlayer1().getPoints());
        game.getPlayer1().getWeapons().get(0).primaryAttack();
        game.getPlayer1().getWeapons().get(0).secondaryAttack();
        game.getPlayer1().getWeapons().get(1).primaryAttack();
        game.getPlayer1().getWeapons().get(1).secondaryAttack();


    }

}



Please not that in in order to keep things simple, I have used a simple if-else structure to determine the appropriate method to be invoked. This could also have been done using reflection in fewer lines of code. But  I thought it would unnecessarily complicate things for an introductory article as our motive here is to learn about the pattern rather that learn about the pros and cons of using reflection to implement this pattern. If you have better ideas to implement this scenario using this patter, please comment. I'd love to learn a new way to do stuff. But I sincerely hope that the example is lucid enough to help you understand the objective of this pattern. Cheers!


Signing Off
Ryan

1 comment:

Anonymous said...

ha, I will test my thought, your post give me some good ideas, it's really awesome, thanks.

- Norman