Observers and Dispatchers Revisited

So I've had requests to do some more explaining on the observer and dispatcher patterns, and what better way to do that than to make code for an example!

Let's assume that we have a simple database abstraction layer. For the purpose of debugging, we want to be able to display information about each query at the end of the page. To make an effective debugging system we need to keep track of the query used, how many rows it affected, and how much time it took to execute. So, how could we do this? Well, we could store the information in a singleton (registry) but that is more of a hack than anything. So why not dispatch observable events to some observers?

Before I get into the code, I'll briefly go over how things will work. First, we need an observer to collect observable events. This observer will be instanciated at the start of our program and will be available to our dispatcher. The dispatcher will take in observable events and pass them to our observer. Finally, at the end of the program, we will loop over all of the events collected with our observer (the observable events) and put out a nice debug display that makes use of all of that info.

So to start off, let's just create a dummy database abstraction layer. In the executeQuery function of our dummy database abstraction layer, we will notify observers of database query events. These events don't know if they are going to be picked up by observers or not. If this seems odd to you, think of it as if you are looking for a job and you are sending your resume out. You don't know if anyone will read your resume or not, and if you did, it would probably make the whole task unecessary.

//--------------------------------------------
// Our base class for the database abstraction layer.
// This class deals with setting up and managing the
// dispatcher that we are using.
//--------------------------------------------

class FADatabaseConnection
{
    //--------------------------------------------
    // The dispatcher that the database abstraction layer
    // will use to notify observers about events.
    //--------------------------------------------

    var $_dispatcher;

    //--------------------------------------------
    // Constructor, instanciate a new dispatcher to deal
    // with notifying any observers added to it about events.
    //--------------------------------------------

    function FADatabaseConnection()
    {
        $this->_dispatcher = &new FADispatcher();
    }

    //--------------------------------------------
    // Add observers to this dispatcher so that when this
    // dispatcher is notified about events, it can tell
    // the observers about them.
    //--------------------------------------------

    function addObserver(&$observer)
    {
        $this->_dispatcher->addObserver($observer);
    }

    //--------------------------------------------
    // Notify any observer in the dispatcher about
    // an event.
    //--------------------------------------------

    function notifyAll(&$event)
    {
        $this->_dispatcher->notifyAll($event);
    }

    //--------------------------------------------
    // The execute query method, this method will execute
    // a query on a database.
    //--------------------------------------------

    function executeQuery($sql = '')
    {
        assert(FALSE);
    }
}

So, everything in there should be self-explanatory, but... a dispatcher is used! Wtf is that!? A dispatcher (in this case FADispatcher) is used to collect observers and then notify them about incoming events. Here, I will paste the nicely commented code for the dispatcher and hopefully it will start to make more sense.

//--------------------------------------------
// A dispatcher. This class deals with managing
// observer classes and notifying these observers
// about events.
//--------------------------------------------

class FADispatcher 
{
    //--------------------------------------------
    // An array of the observers that this dispatcher
    // will notify about incoming events.
    //--------------------------------------------

    var $_observers = array();

    //--------------------------------------------
    // Add an observer to the above array so that when
    // the dispatcher is told to notifyAll (notify all
    // observers) then we will have an/some observers to
    // notify!
    //--------------------------------------------

    function addObserver(&$observer) 
    {
        //--------------------------------------------
        // Make sure that it's actually an observer being
        // passed in.
        //--------------------------------------------

        assert(is_a($observer, 'FAObserver'));

        //--------------------------------------------
        // Add an observer to the array of observers.
        //--------------------------------------------

        $this->_observers[] = &$observer;
    }

    //--------------------------------------------
    // Notify all of the observers in the _observers
    // array about $event.
    //--------------------------------------------

    function notifyAll(&$event) 
    {
        //--------------------------------------------
        // Make sure the even being passed is observable.
        //--------------------------------------------

        assert(is_a($event, 'FAObservableEvent'));

        $ret = FALSE;

        for($i = 0; $i < count($this->_observers); $i++)
        {
            //--------------------------------------------
            // Get one of the observers.
            //--------------------------------------------

            $observer = &$this->_observers[$i];

            //--------------------------------------------
            // If an observer has successfully been notified,
            // stop notifying observers.
            //--------------------------------------------

            if($ret = (bool)$observer->notify($event)) 
            {
                break;
            }
        }

        return $ret;
    }
}

The above FADispatcher class collects observers and tells them about events. Here's how it does it:

  • the dispatcher is instanciated
  • observers are added to it with the addObserver function
  • the dispatcher's notifyAll function is called with an "observable event" passed to it
  • the notify all function loops through all of the observers
  • it tells each observer about the event by passing the event through the observer's "notify" function
  • if the observer has been successfully notified, meaning the notify function returns true, then the dispatcher will stop trying to notify observers (it will stop looping through them)
  • Note: the notify function doesn't need to return true or false. It's simply an option if that's how you want things implemented.
So, the whole picture should slowly be coming together. I'll show you the last two base classes that we need, describe how everything fits together, then show you the final result in code!

//--------------------------------------------
// The base observer class. This class cannot
// be used directly.
//--------------------------------------------

class FAObserver 
{
    //--------------------------------------------
    // Notify this observer about an incoming event.
    //--------------------------------------------

    function notify(&$event) 
    {
        assert(FALSE);
    }
}
//--------------------------------------------
// The observable event class... Not much to say
// here.
//--------------------------------------------

class FAObservableEvent { }

Isn't this convenient: there's almost no code to look at! Well, here's how things will piece together... The observer that we will use will collect the events passed to it through its "notify" function. Also, we will have a function in that observer, "getEvents", that will return all of the events passed to it through its "notify" function.

Each of the events passed to the dispatcher and consequently the observer will have information about each query. That info will be stored in reference variables and will be accessible through some getter functions (getQuery, getRows, getTime).

Finally, at the end of our program, we will use the observer that we added to the FADatabaseConnection class's dispatcher and use the events passed to it (the info about each database query) to build a nice debug table for the queries executed on each page load. So, time to build some code. First, the observable event that will hold the query information...

//--------------------------------------------
// An observable database query event. This class holds
// information about a specific database query.
//--------------------------------------------

class DatabaseQueryEvent extends FAObservableEvent 
{
    var $_sql;
    var $_rows;
    var $_time;

    //--------------------------------------------
    // Constructor. The variables passed in are:
    // $sql - the SQL query
    // $rows - the number of rows that the sql query affected
    // $time - the number of microseconds that the query took
    //         to execute.
    //--------------------------------------------

    function DatabaseQueryEvent($sql, $rows = 0, $time = 0) 
    {
        $this->_sql = $sql;
        $this->_rows = $rows;
        $this->_time = $time;
    }

    //--------------------------------------------
    // Get the SQL of the query used.
    //--------------------------------------------

    function getQuery() 
    {
        return $this->_sql;
    }

    //--------------------------------------------
    // Get the number of rows that the query affected.
    //--------------------------------------------

    function getRows() 
    {
        return $this->_rows;
    }

    //--------------------------------------------
    // Get the number of microseconds that the query
    // took to execute.
    //--------------------------------------------

    function getTime()
    {
        return $this->_time;
    }
}

That code should be simple enough to understand.. nothing really goes on in it. If you're wondering how it is implemented, don't worry, that will come shortly. Now, on to the observer that we are going to use!

//--------------------------------------------
// The database observer class. This class collects
// database query events.
//--------------------------------------------

class DatabaseObserver extends FAObserver 
{
    //--------------------------------------------
    // An array of database query events.
    //--------------------------------------------

    var $_events = array();

    //--------------------------------------------
    // Notify this observer about an event. In this case
    // it will be a database query event. Push that event
    // onto the array of events.
    //--------------------------------------------

    function notify(&$event) 
    {
        $this->_events[] = $event;
    }

    //--------------------------------------------
    // Return the array of all of the database query
    // events.
    //--------------------------------------------

    function getEvents() 
    {
        return $this->_events;
    }
}

Heh, the database observer class is also simple and self-explanatory! Well, right now all of the core code has been laid out except for the final implementation of the database abstraction layer. In the DBA, everything should come together nicely :D

//--------------------------------------------
// This would normally be an abstraction layer for
// something like MySQL or PostgreSQL... For this example,
// we are dealing with the ever ambiguous SomeSQL!
//--------------------------------------------

class SomeSQL extends FADatabaseConnection
{
    //--------------------------------------------
    // Execute a database query
    //--------------------------------------------

    function executeQuery($sql)
    {
        //--------------------------------------------
        // Now, these two variables would normally take
        // their values from something like mysql_affected_rows
        // and by putting two microtime()'s around the execution
        // of the query and finding the difference. But, we won't
        // do that in this example.. mwahahaha.
        //--------------------------------------------

        $execution_time = microtime();
        $affected_rows = 0;

        //--------------------------------------------
        // Normally one would call the query method for a database here.
        //--------------------------------------------

        //--------------------------------------------
        // Aha! Instanciate a new database query event
        // (an observable event) and pass it to the notifyAll
        // function. The notifyAll function calls FADispatcher::notifyAll
        // which in turn calls FAObserver::notify! What a nice
        // chain of events.
        //--------------------------------------------

        $this->notifyAll(new DatabaseQueryEvent($sql, $affected_rows, $execution_time));
    }
}

Make sure to read the comments in that code, much is explained. By this point, all necessary code to make observers and dispatchers has been laid out. It's about time to finish everything off, but before I do that I will list out the chain of events of everything will happen.

  • DatabaseObserver is instanciated.
  • SomeSQL is instanciated, and in the constructor of its parent class, FADatabaseConnection, FADispatcher is instanciated. SomeSQL::_dispatcher is now an instance of FADispatcher.
  • The DatabaseObserver object is added to the SomeSQL object using DatabaseConnection::addObserver. SomeSQL now has a dispatcher and an observer to dispatch events to.
  • SomeSQL::executeQuery is called. In that function, FADatabaseConnection::notifyAll is called with a new instance of DatabaseQueryEvent passed to it.
  • FADatabaseConnection::notifyAll calls FADispatcher::notifyAll and passes the DatabaseQueryEvent event to it.
  • FADispatcher::notifyAll loops through the observers added to it. The only observer we added to that dispatcher is the DatabaseObserver. This being the case, DatabaseObserver::notify is called with the DatabaseQueryEvent event passed to it.
  • -- at this point, all the dispatching and observing is finished --
  • DatabaseObserver::getEvents is called and all of the database query events are looped over.
  • For each database query event, DatabaseQueryEvent::getQuery, DatabaseQueryEvent::getTime, and DatabaseQueryEvent::getRows is called.
  • Using the information from the calling of the mentioned three functions, we can build a nice table with database query debug information.

So, let's go finish off our program!

//--------------------------------------------
// Instantiate our observer. By the end of execution,
// this observer will hold and array of information
// for all of the database queries.
//--------------------------------------------

$observer = &new DatabaseObserver;

//--------------------------------------------
// Instanciate our SomeSQL database abstraction layer
// and add the above observer to the dispatcher that
// was implicitly instanciated when SomeSQL was.
//--------------------------------------------

$dba = &new SomeSQL;
$dba->addObserver($observer);

//--------------------------------------------
// Execute a dummy query just for the sake of this
// example.
//--------------------------------------------

$dba->executeQuery("SELECT moo FROM cow WHERE alive=1");

//--------------------------------------------
// Debug time! Let's loop over all of the query
// events stored in the observer and make a table
// with some debug information.
//--------------------------------------------

$html = "";
$html .= "<table border="1">";
$html .= "<tr><th>Query</th><th>Query Time</th><th>Affected Rows</th></tr>";

$i = 0;
foreach($observer->getEvents() as $event) {
    $html .= "\n\t<tr>";
    $html .= "\n\t\t<td>\n\t\t\t" . $event->getQuery() . "\n\t\t</td>";
    $html .= "\n\t\t<td>\n\t\t\t" . $event->getTime() . "\n\t\t</td>";
    $html .= "\n\t\t<td>\n\t\t\t" . $event->getRows() . "\n\t\t</td>";
    $html .= "\n\t</tr>";

    $i++;
}
$html .= "n</table>";

echo $html;

And what we get from running this program is...

QueryQuery TimeAffected Rows
SELECT moo FROM cow WHERE alive=1 0.16262100 1166746068 0
Note: the space in the number in the query time column is just what microtime() spits out because I didn't put any formatting on it.

Hopefully by now, you understand how observers and dispatchers work. As you might have noticed, a lot of code went into such a simple thing as database debugging. Obviously, if you want to do database debugging and only debugging, then working with observers and dispatchers is not the most efficient way to do it. However, if you are working in a coding environment where observers and dispatchers are already supported, usign them for database debugging might just be a nice way to more fully implement the tools available to you. If you want to download a working version of the above observers and dispatchers, click here. If you have any comments/questions, or need anything further clarified, just ask.


Comments


Comment