Saturday, July 3, 2010

The Observer Pattern

In today's post, I am going to discuss about the observer pattern. This is one of the most widely used patterns in most J2EE applications. I say this because of the inherent simplicity of this pattern and the the fact that it is highly possible that you are already implementing this pattern without actually being aware of it! This is something that certainly cannot be said about other patterns like the Visitor or the Factory pattern.

As I always do, this time as well, i am going to elaborate the usage of this pattern in a fictional - game like situation.

Lets imagine we are playing a horror game. Yea, this time i pick a horror game instead of the good ol FPS!

First, lets begin with a somewhat formal introduction to the pattern.

As the name suggests, the Observer pattern is basically used to associate an object A with a group of other objects that observe any changes in the properties of the object A, and perform some very critical, earth shattering tasks, based upon the change made to the properties of the object A.

Well, its like another way of talking about the event - event listener, model of programming.

There are primarily three major concerns in the Observer pattern.
1) The object being observed - The Observable object.
2) The objects that observe the observable object - The Observer objects.
3) A way to associate and invoke the Observable object with the Observers - The Delegate function.

To be concise - Observer Objects Observe Observable Objects.
That's 5 O's in one sentence!

There two are styles of implementing the Observer pattern - The Push model and The Pull model. These two models differ in the way in which the Observers and the Delegate function are designed. The observable object remains the same. I will elaborate both of these models later in the post and also discuss the pros and cons of both the techniques.

However, now its time to play some game.

The protagonist of our horror game features a very bold player named ScoobyDoo.
So, we create a basic player class with three instance variables :
name, xCoordnate, yCoordinate. Here is the class definition.

public class Player {
    
    private String name;
    private Integer xCooridinate;
    private Integer yCooridinate;

    public Integer getxCooridinate() {
        return xCooridinate;
    }

    public Integer getyCooridinate() {
        return yCooridinate;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void moveForward(Integer steps){
        xCooridinate+=steps;
    }

    public void moveBackward(Integer steps){
        xCooridinate-=steps;
    }

    public void jump(Integer height){
        yCooridinate+=height;
    }

    public void fallTo(Integer height){
        yCooridinate=height;
    }
}

I have also added a function that allows the player to move forward and jump(i.e. move upward) and vice versa. These will be used to manipulate the xCoordinate and yCoordinate properties of our our Player.

Now, I don't think a game can be made scary without proper lighting and sound effects. But what makes a game scary is the appropriate timing of these effects.

Lets assume a scene where ScoobyDoo enters an empty room. As of now, the lights are bright and shining. The xCoordinate and yCoordinate of the player are initialized to 0 when the player enters the room.

Now what we want is that when ScoobyDoo is right in the middle of the room, i.e. approximately 10 steps into the room, the lights should suddenly begin to flicker.


Not only that, but we also want a scary sound to start playing in case ScoobDoo tries to act smart and tries to jump up to run out of the room.

Yea, it might sound as if I am being so mean by trying to scare the shit out of this dog, but hey, i am not to blame. I bet this dog would get scared of the sound of his own fart as well!

Okey, back to work.

What we need is a way to observe the coordinates of ScoobyDoo in the room and take appropriate actions when his coordinates cross a certain threshold.

So, here, the Observable object is the Player.

Before we begin we need to define a few interfaces that will be used to generically use and handle Observers and Observables.

Lets see the definition of these interface first.

public interface Observable {
    public void notifyObservers();
    public void addObserver(Observer o);
    public void removeObserver(Observer o);
}


public interface Observer {
    public void update(Object o);
}


Lets create Observers.

public class SoundEffectController implements Observer{

    private boolean isSoundPlaying = false;

    public void playSound(){
        System.out.println("Play an eerie sound");
        isSoundPlaying = true;
    }

    public void update(Object o) {
        if(o instanceof Integer){
            Integer yCoordiante = (Integer)o;
            if(yCoordiante>0 && isSoundPlaying== false){
                playSound();
            }
        }
    }

}



public class LightEffectController  implements Observer{

    private boolean isLightFlickering = false;

    public void flickerLights(){
        System.out.println("Flicker the lights");
        isLightFlickering = true;
    }

    public void update(Object o) {
        if(o instanceof Integer){
            Integer xCoordiante = (Integer)o;
            if(xCoordiante>10 && isLightFlickering==false){
                flickerLights();
            }
        }
    }

}


As you see, there are two observers. The SoundEffectController, and the LightEffectController.

The SoundEffectController is class that manages the sound effects of the room. The light effect controller is a class that manages the lighting effects of the room.

Now, what we need is a way to associate these observers with the events with which they are interested- the change in the xCoordinate and yCoordinate of the Player. This is where the delegate function comes into the picture.

The delegate functions's primary purpose is to pass on control to all the interested Observers upon the occurrence of any event in the Observable object. This is the core function in the observer pattern. It is this function that allows you to have 2 flavors of the Observer pattern. We will first see the Push Model of the observer pattern.

The delegate function is defined in the Observable object. In order to make the delegate function do useful things, the methods that cause a change in the properties of the object, need to invoke the delegate function. Also, the Observable class needs to maintain a list of observers.

In the Push model, the delegate function iterates over each of these observers and passes the relevant information to the Observer. The following code demonstrates the new implementation of the player class using the push model.
And now, here is the new definition of the Player class which now implements the Observable interface.

public class Player implements Observable{
    
    private String name;
    private Integer xCooridinate;
    private Integer yCooridinate;

    private List observers ;

    public Player(String name) {
        this.name = name;
        this.observers=new ArrayList();
    }


    public Integer getxCooridinate() {
        return xCooridinate;
    }

    public Integer getyCooridinate() {
        return yCooridinate;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void moveForward(Integer steps){
        xCooridinate+=steps;
        notifyObservers();
    }

    public void moveBackward(Integer steps){
        xCooridinate-=steps;
        notifyObservers();
    }

    public void jump(Integer height){
        yCooridinate+=height;
        notifyObservers();
    }

    public void fallTo(Integer height){
        yCooridinate=height;
        notifyObservers();
    }

    //This is the core function that delegates the change in the state of the
    //Observable object to the observers
    public void notifyObservers() {
        for(Observer o:observers){
            if(o instanceof SoundEffectController){
                o.update(yCooridinate);
            }
            else{
                if(o instanceof LightEffectController){
                    o.update(xCooridinate);
                }
            }
        }
    }

    public void addObserver(Observer o) {
        this.observers.add(o);
    }

    public void removeObserver(Observer o) {
        this.observers.remove(o);
    }
}


Here is the Main class.

public class Main {
    public static void main(String[] args){
        Player myDog = new Player("ScoobyDoo");

        myDog.addObserver(new SoundEffectController());
        myDog.addObserver(new LightEffectController());

        myDog.moveForward(2);
        System.out.println("Nothing yet.");
        myDog.moveForward(3);
        System.out.println("Nothing yet.");

        myDog.moveForward(7);
        
        myDog.jump(3);
        
    }
}


On running the main program, you get the following output.

Nothing yet.
Nothing yet.
Flicker the lights
Play an eerie sound


As is seen in the above code, it is clearly evident that the delegate function - notifyObservers() is invoking a particular function on an Observer to notify it of the changes in state. On close examination, you would notice that the delegate function is not as flexible as we want it to be. This is because, the delegate needs to be aware of the data that each observer is interested in listening to, thereby making the delegate function a tad more complex than it should be. As time progresses and more observers want to be notified of the changes in the Observable object, the delegate function becomes more convoluted as it needs to be aware of the requirements of each type of observer. For example, there may be 'n' number of observers interested in the change in the x coordinate of the player, while there may be 'm' number of observers that may be interested in the change of 'y' coordinate of the player. There might also be 'l' number of observers that may be interested in both the x and y coordinate of the player. The problem certainly becomes harder to solve as the variety and number of observers increases with time.


This is where the pull model comes to the rescue. In the pull model, the delegate function sends the Observable object itself to the Observers. It then becomes the observer's responsibility to extract the the information in which they are interested, from the Observable object. In our case, this can be implemented as follows.

Change the code in notifyObservers() in the Player class to the following


public void notifyObservers() {
        for(Observer o:observers){
                o.update(this);
        }
    }


Change the update function in SoundEffectControler to the following

public void update(Object o) {
        if(o instanceof Player){
            Player p = (Player)o;
            Integer yCoordiante = p.getyCooridinate();
            if(yCoordiante>0 && isSoundPlaying== false){
                playSound();
            }
        }
    }


Change the update function in LightEffectController to the following

public void update(Object o) {
        if(o instanceof Player){
            Player p = (Player)o;
            Integer xCoordiante = p.getxCooridinate();
            if(xCoordiante>10 && isLightFlickering==false){
                flickerLights();
            }
        }
    }


On running the main program, you get the following output.

Nothing yet.
Nothing yet.
Flicker the lights
Play an eerie sound


As you can see from the above code, the complexity has now shifted from the delegate function to the Observer classes. It is now the Observer's responsibility to take extract the information and also perform typecasting to identify the object with which it is being notified.

When you decide to implement the observer pattern, you keep in mind a few things. The push model works great in areas where you have several components that build up a system, and some of these components act as observers to many different types of Observable objects. In this case, you would prefer to provide only a specific portion of information to the observer instead of the entire Observable object. This would greatly increase the decoupling of your Observer components from the Observable components thereby giving you the chance to use the same observer across multiple objects.

The pull model works great if your Observable object is already in place and you are designing components around this Observable object. If there are many observers, and each observer depends of different combinations of the properties of the Observable object. In this case, since the components are being designed with only a particular Observable class in mind, shifting the complexity to the Observer classes seems like a good idea. Each observer would extract the relevant information from a Observable perform necessary actions.

As is the case with all design pattern, the way you choose to implement it depends on multiple factors. Understanding how a particular implementation will impact your solution should be the key concern in deciding upon a particular implementation.


Happy Programming :)
Signing Off
Ryan

No comments: