Wednesday, January 7, 2009

Custom Observer Pattern

Sometimes my articles are a balance of both Magic and programming but this one is 99% programming, so you have my blessing to skip this one if your eyes start to glaze over and you think that coding is utterly boring/confusing. (In truth I find that coding is utterly boring/confusing sometimes, lol.)

The observer pattern is very common and it consists of two objects: the observer itself and the object that is being observed. For brevity object A will be the object that we are observing. A brief overview of the observer pattern is that the observer registers itself to A typically through a method like A.addObserver(observer) and when A changes, it notifies all of its observers.

Some of the problems in MTG Forge that occur like the phase upkeep mistakes and drawing too many cards to begin with is because of problems with the observer pattern. Let me give an example to show the difference between the typical observer pattern and my custom observer.

Let's say that objects X,Y,Z are observing A. When A changes X,Y, Z are all notified. The problem is when X, Y, Z is notified and one of those objects change A, so A has to notify X, Y, Z again. For example let us presume that Y changes A. The objects are notified in this order:

X
Y - changes A
Z
X
Y
Z

So you might see my problem, in my way of viewing things X, Y, Z are notified too many times. I only want each object to be notified once when a change occurs:

X
Y - changes A
X
Y
Z
The Java code is almost trivial once I realized what exactly that I wanted the observer pattern to do. This custom observer solves my problem of "observers being called too many times." In essence I am actually trying to remove the recursion that happens when Y changes A.


//shouldContinue is an class (instance) variable

public void updateObservers()
{
shouldContinue = true;
notifyObservers();
shouldContinue = false;
}

//if updateObservers() is called while
//looping through all observers
//the loop stops and then starts over
private void notifyObservers()
{
Observer ob;
for(int i = 0; i < list.size(); i++)
{
ob = (Observer) list.get(i);

if(shouldContinue)
ob.notify(this);
}
}

6 comments:

  1. I'm not sure to have understood what you still needed to do.
    In your example, you described the wrong and correct behavior.
    From what I can understand from your code, it already assures you to have the correct behavior happen
    (so X Y, then X Y Z, skipping Z once)

    So what do you mean with still trying to avoid "recursion"?

    ReplyDelete
  2. a I right that the original behavior was not
    xyzxyz but
    xyxyzz
    I think so because observer y causes a change and so updates all observers again. the original update loop continues after the update that y invoked.

    your solution causes the old loop to break after the update. though there's not much a difference, i would save the counter and reset when an update occurs:

    private int counter;
    public void updateObservers() {
    if(counter == -1) notifyObservers();
    //not 0, because the loop will
    //increment it!
    else counter = -1;
    }
    private void notifyObservers() {
    Observer ob;
    for(counter = 0; counter < list.size(); counter++) {
    ob = (Observer) list.get(i);
    }
    counter = -1;
    }

    this version has the problem that multiple changes would break it. but it can be done with an iterator:
    private Iterator it;
    public void updateObservers() {
    if(it == null) notifyObservers();
    else it = list.iterator();
    }
    private void notifyObservers() {
    Observer ob;
    for(it = list.iterator(); it.hasNext();) {
    ob = (Observer) it.next();
    }
    it = null;
    }

    ReplyDelete
  3. i forgot the point of this: your code can still cause the false behavior if updateObservers is called multiple times in one observer's observe method, simmilar to my first variant

    second, your variant produces a high stack if multiple changes occur during the updates somewhen. at least in my opinion, this is not elegant and may have side effects if you use attributes.
    in my code, updateObservers() is always only called once, so you can understand better what's going on.

    ReplyDelete
  4. Interesting. We use pydispatch as our analogue to an "observer" method. Instead of telling everything that something is "observing" it, we tell the dispatcher that we're "connecting" to specific events, and then whenever those events are sent, everything connected to them is notified (it's slightly more complicated than that, but not much more so).

    For example, triggered abilities have a Trigger object which has an Event object (for example, AttackerDeclaredEvent), which tells the triggered ability what event triggers it (in combination with a condition function that evaluates the arguments of the event that was sent). Once the event is fired and it satisfies the condition function, the ability goes on the stack.

    ReplyDelete
  5. fyi there's a bug in the program as it currently stands. for some reason, cards that deal damage to you during upkeep (like bitterblossom and juzam djinn) also make you draw cards.

    obviously, i'm not complaining about the benefits or anything. . .but it is a bug. =)

    ReplyDelete
  6. This is one of those posts that makes sense to me and probably doesn't make sense to anyone else. Basically I think this solves some of my errors.

    Basically I'm trying to convert "observing" into an event. Events tend to be important like a button click while "observing" since to register too many tiny events.

    In version 2 the computer skips all of it's phases (the computer will actually do something once it gets programmed). Since the computer skips the phases, none of the other observers "see" the computer phase with my custom observer. If I used the regular observer, they would "see" the computer's phases.

    I could put an IF statement checking for the computer's phases but it seems better if those observers don't have to hassle with it.

    Like I always say, its just my thoughts, albeit how random they seem.

    ReplyDelete

Note: Only a member of this blog may post a comment.