A Simple Observer Pattern for Cocoa / iPhone Games

I'd like to outline a simple (superior?) method for implementing the observer pattern in Cocoa, and give an example for Cocoa / iPhone games. The observer pattern is a necessity for implementing the Model-View-Controller pattern.

Before we see the observer let's look at some code without the observer pattern. Here's a typical method from my game state  that adds a new enemy to the board or map:

//GameState.m
-(void)addEnemy:(MyCharClass*)enemy{

    //stuff to update the game state  
    [enemyList addObject:enemy];

    //inform the views of the change
    [miniMap addEnemy:enemy];
    [targetHints addEnemy:enemy];
    [gameView addEnemy:enemy];
}

This code is pretty awkward - it requires the GameState class to import the view headers (miniMap, targetHints, etc.) which is a violation of MVC. That makes it hard to change to a different view class, and impossible to add or remove views at runtime. Also, half the method is made up of "bookeeping" code to inform views about the model change, completely overshadowing the real purpose of the method. Let's rewrite it using the traditional Cocoa observer pattern, using NSNotificationCenter:

//GameState.m
-(void)addEnemy:(MyCharClass*)enemy{

    //stuff to update the game state  
    [enemyList addObject:enemy];

    //inform the views of the change
    [[NSNotificationCenter defaultCenter]
postNotificationName:@"addEnemy" object:self userInfo:enemy]; } //MiniMap.m //somewhere (init method?) you need to register for the notifications; //repeat this call for each message you want to receive! //TODO: could automate with an array of method names and NSSelectorFromString() [[NSNotificationCenter defaultCenter]
addObserver:self selector:@selector(addEnemy:) name:@"addEnemy" object:nil]; //inside the method addEnemy: MyCharClass *enemy= [notification userInfo]

This is a little better. The message sending takes fewer lines, although registering for messages takes more. At least we get the benefits of the observer pattern; any class can become an observer without changes to the GameState class. I don't like it though, because I need to write a lot of "bookeeping" code in the view classes to register for the events. I also need to pick names for my events; I'd make them match my method names, but I don't get the benefits of autocomplete and I feel like I'm creating a parallel system of messages alongside my methods. In short, there's a lot of tedium and bookeeping involved here.

Of course, methods in Cocoa are already messages. Wouldn't it be nice if we could use the system for calling methods that is already in place? Well, we can ... we just need to add two methods and one mutable array to the GameState Object. Here are the two methods:


//method to add an observer to the game state object
-(void)addObserver:(id)observer{
    //observers is an NSMutableArray
    [observers addObject: observer];
}

//forward a message from the game state object to each observer that wants it
//(object may be "nil" if you're forwarding a method with no parameters)
-(void)forward:(SEL)selector object:(id)object{
    //loop through the list of observers
    for(id observer in observers){
        //check if the observer repsonds to this method selector, and call the method
        if([observer respondsToSelector:selector])
            [observer performSelector:selector withObject:object];
    }
}

The "addObserver:" method should be self-explanatory - it adds an observer to the array of observers. Any object that wants to get messages should call [gameState addObserver:self]. The "forward:" method relies on some Objective-C runtime tricks. It takes a selector (this is a signature for a method) and an object parameter. Then it loops through the list of observers, checks if they can respond to that method, and then performs the method on that observer. If an observer doesn't implement this particular method then it gets skipped.

How do you call the forward: method? You can see that in the modified addEnemy: method:


//GameState.m
-(void)addEnemy:(CharClass*)enemy{

    //stuff to update the game state  
    [enemyList addObject:enemy];

    //inform the views of the change
    [self forward:_cmd object:enemy];
}

What do the observers have to do? Just implement the methods they're interested in. And look at that addEnemy method -- it's short and sweet now. That _cmd in there is a shorthand way to get the selector (method signature) for the current method. It saves you a from using the @selector directive and it makes it easy to copy and paste the forward: line into different method (you probably still have to change the object passed though).

That's all there is to it - a simple observer pattern for Cocoa in nine lines of code (or eleven, if you count the two lines to add the NSMutableArray.)

Advantages:

  • The GameState object can rebroadcast messages to all observers without linking to the headers.
  • Observers requires very little code. One call to addObserver: and they immediately start getting only the messages they need from the game state.
  • No parallel set of event names to keep in sync with the method names
  • Game state code becomes very clean; all the message forwarding is done in one or two lines in each method.
  • Fast prototyping - I can add new methods to the GameState object and implement them in the views later

Disadvantages:

  • It's not as flexible as the NSNotificationCenter. You can only send messages from the GameState method, not from any other place in your program.
  • Every "forward:" makes multiple calls to "respondsToSelector:"; one for each object in the array of observers. It's unlikely that the Cocoa's message overhead will impact your game's speed -- it's more likely that sound and drawing are using much more CPU -- but it's a consideration.
  • There's a danger that, in the future, a new method will be added to NSObject or UIView or CCNode that matches a name you used; in that case respondsToSelector: may start returning true instead of false, causing unexpected behavior in your observers.
  • Although autocomplete works, there's no warning if you mistype a method name in one of your observers. You'll just fail to get the message and you'll wonder what went wrong. You also don't get type checking on your parameters, buy you could fix that with a protocol for observers.

Extra credit:

  • You could override "forwardInvocation:" in the GameState object to forward *any* message to the observers without implementing the method in the GameState at all. This is great for fast prototyping; you can start adding messages willy-nilly and clean up the warnings later. You can call [gameState anyMethodYouWant] and not crash; the message will get passed to any observer that can handle it.
  • You could also add a counter to the "forward:" method, count the number of observers for this message, and log an warning if zero observers responded. That would help eliminate misspelled methods.

[Comments]