Saturday, August 26, 2006

Observers and Subjects

The most well-known design pattern for decoupling object instances or classes from one another and still have a mechanism for the observer to know, when a particular event occurs on a subject, is the Observer-Pattern. The implementation is typically done either using abstract classes or using interfaces.

While the prior has the advantage that the abstract classes may provide a reusable behaviour for observer registration and notification it limits an observer to stay in the observer role. It's not possible for an observer of one subject, to act as a subject itself (unless, you are relying on implicit coding/naming conventions rather than on an explicit mechanism). While this may not be a problem with languages allowing multiple inhertiance, it is for languages that support single inheritance only.

The latter (inteface-based) approach, releaves you of this issue at the cost of implementing the required methods on every class that wants to make use of the pattern. While this may be inconvenient it is a valid means to resolve and works most of the time.

With dynamic languages such as PHP or JavaScript there is yet another way to approach the issue using a mediating class that organizes the eventhandling between abitrary objects (or classes or basically any entity you may think of). Let's show an example for the usage of this approach:

class Event { ... }


class Subject {

 public function doSomething() {
  //do something and throw the event
  Event::raise($this,'onDoSomething');
 }

}


class Observer {

 public function onEvent(Event $event) {
  //react to the event
  echo get_class($event->getSourceObject());
 }

}

$subject = new Subject();
$observer = new Observer();
Event::register($subject,'onDoSomething',$observer,'onEvent');
$subject->doSomething(); //prints 'Subject'

What you are now probably interested in the most is the implementation of the "Event"-class. As you have seen there are two static methods on it:

1) register() allows to register abitrary methods or functions to be registered as callbacks for a particular event - 'onDoSomething' in this case - of objects or classes (the latter is not yet supported by the following implementation)

2) raise() allows to indicate the occurrence of a particular event on an object or class (the latter is not yet supported by the following implementation). All previously registered callback methods (you may as well call them the observers) will be executed using a single argument: an "Event"-object instance.

This object may be used to query for particular aspects of the event happening using one of the following methods.

1) getSourceObject() returns the subject the event occurred on if existing

2) getName() returns the name of the event so that the observer may decide how to handle different types of events, if this is not already distinguished using different callback methods.

3) getTargetObject() returns a reference to the observer if it is an object instance

4) getTargetMethod() returns the name of the callback method

5) getParameters() return an array of additional parameters that the subject may hand over to raise().

A possible implementation of the "Event"-class may look shown below. Please note that this version currently only supports object-to-object event handling:

class Event {

    protected $targetObject;
    protected $targetMethod;
    protected $eventName;
    protected $sourceObject;
    protected $parameters;

    protected function __construct($targetObject,
                                   $targetMethod,
                                   $eventName,
                                   $sourceObject,
                                   array $parameters = array()) {
        $this->targetObject = $targetObject;
        $this->targetMethod = $targetMethod;
        $this->eventName = $eventName;
        $this->sourceObject = $sourceObject;
        $this->parameters = $parameters;
    }

    public function getSourceObject() {
        return $this->sourceObject;
    }

    public function getName() {
        return $this->eventName;
    }

    public function getTargetMethod() {
        return $this->targetMethod;
    }

    public function getTargetObject() {
        return $this->targetObject;
    }

    public function getParameters() {
        return $this->parameters;
    }

    public static function raise($onObject,
                                 $event,
                                 array $parameters = array()) {
        foreach($onObject->__eventRegistry[$event] as $target) {
            if($target[0]) {
                call_user_func_array($target,
                                     array(
                                         new Event(
                                             $target[0],
                                             $target[1],
                                             $event,
                                             $onObject,
                                             $parameters)
                                     )
                );
            } else {
                call_user_func_array($target[1],
                                     array(
                                         new Event(
                                             NULL,
                                             $target[1],
                                             $event,
                                             $onObject,
                                             $parameters)
                                     )
                );
        }
    }

    public static function register($sourceObject,
                                    $forEvent,
                                    $targetObject,
                                    $targetMethod) {
        $sourceObject->__eventRegistry[$forEvent][] = array($targetObject,$targetMethod);
    }
}

This implementation should be session-save as well, as the dynamically built "__eventRegistry" should be serialized to and restored from the session automatically. For class based event registries this may be a little more tricky to implement.

But once the "strict" keywork is incorporated into PHP, this approach will not work on classes anymore that are marked strict.

0 comments: