Implementing Lazy Instantiation

Lazy instantiation refers to the ability of the Collection class to defer creation of its members until such time as they are needed. In the registrar's application discussed at the beginning of the chapter, a Student object has multiple Course objects associated with it. When you want to use a Student object to display the name of a student, you do not need any of the information about that student's courses. However, for the sake of a consistent interface, the course objects should be available as member variables of the student object.

To keep things simple when displaying a list of courses for a given student, the software interface should allow you to write code like this:

   <?php      $objStudent = StudentFactory::getStudent(12345); //12345 is the student ID      print "Name: " . $objStudent->name . "<br>\n";      print "Courses: <br>\n";      foreach($objStudent->courses as $objCourse) {        print $objCourse->coursecode."-". $objCourse->name . "<br>\n";      }    ?> 

For now, it's enough to know that the StudentFactory is a class with a static method getStudent that returns a new student object, given the ID as a parameter. It does all the heavy lifting in the database. Factory classes are discussed in detail in Chapter 9.

You should assume that the Student class constructor takes care of populating the list of courses. But if you just have a listing of students by name, as in the next example, you shouldn't have to incur the overhead of fetching the unwanted courses. Again, assume the StudentFactory is a class that creates student objects by interrogating the database for us. In this case, we retrieve a collection of students by searching by their last names:

   <?php      $colStudents = StudentFactory::getByLastName("Smith");      print "<h1>Students With the Last Name 'Smith'</h1>";      foreach($colStudents as $objStudent) {        print $objStudent->name . "<br>\n";    }    ?> 

In this case, getting the course information would be totally unnecessary because you're merely displaying the name. But how can you keep the simple interface of the Student class without forcing the database activity? You could add methods such as $objStudent->loadCourses() that would populate the collection and have to be called before interacting with the courses collection, but that isn't terribly intuitive and clutters the interface. You could do something like CourseFactory::getCoursesForStudent($objStudent), which would return a collection of courses for a given student, but again, it's not obvious to someone who just joined your development team that such a function needs to be called to get the Course objects for a given student. There is a better way, and it involves using callbacks.

Callbacks

If you've ever done any JavaScript programming and assigned some activity to happen in the onclick event for some object, you've used a callback. A callback is a nifty programming trick for which you tell the application to perform a function when some event happens. That event is out of your control you don't necessarily know when it will happen. You tell the computer to take care of performing that function if and when the event takes place. Many times in JavaScript an onSubmit event handler is created for a form that allows client-side data validation to happen if the user tries to submit that form. You don't know when the user is going to submit the form, or even whether the user will do it at all. After the event handler is specified, the JavaScript engine takes care of the rest. You can also use this technique in server-side application development.

When you're designing the Student class for the registrar's application, you don't know whether or when the course collection will be accessed by the code that uses the class. Because fairly significant overhead is associated with getting the Course objects, this is a great time to take advantage of a callback. You need to be able to tell the course collection, "If someone tries to talk to you, you need to populate yourself first."

However, because the Collection class is a generic member of the reusable toolkit, you don't want to hard code the name of a function inside the class (this obviously limits the reusability of the class), and you don't want to have to create a new subclass of Collection every time you want to use it (because this is creating more work for you, not less). Instead, you should be able to supply the name of a procedural function, or supply a reference to an object and specify the method on that object that you want to call to populate the collection. To do so, you need to understand a special built-in PHP function.

Using call_user_func

When you want to call a function, you usually invoke the literal name of the function. However, PHP also allows you to call functions (and methods on objects) using string variables. The following is perfectly valid PHP:

   <?php      $myFunc = "pow";      print $myFunc(4, 2); //prints 16, or pow(4, 2)    ?> 

PHP evaluates the variable name and executes the function with that name. This works for compiled-in core functions as well as user-defined functions. You can perform the same trick with a method of an object.

   <?php      $myMethod = 'sayHello';      $obj = new Person();      $obj->$myMethod();    ?> 

Assuming that you have a class called Person with a method called sayHello(), this would work just fine (you can also invoke static methods that way that is, Person::$myMethod()).

The only problem with this trick is that it's not at all obvious that $obj->$myMethod() is calling $obj->sayHello(). This would be even more confusing if the value of $myMethod was passed in as a parameter to some wrapper function. PHP provides a built-in method for doing the same thing, in a manner a little less confusing and far more transparent.

The function call_user_func() takes one mandatory parameter, to define the function to be called, and zero or more additional parameters to be passed as parameters to the user-defined function. The function definition is as follows:

   mixed call_user_func (callback function [, mixed parameter [, mixed ...]]) 

The value of the callback function parameter takes one of three forms:

Here are a few examples of using this function:

   <?php    class Bar {      private $_foo;      public function __construct($fooVal) {        $this->_foo = $fooVal;      }      public function printFoo() {        print $this->_foo;      }      public static function sayHello($name) {        print "Hello there, $name!";      }    }    //procedural function - not part of the Bar class    function printCount($start, $end) {      for($x = $start; $x <= $end; $x++) {        print "$x ";      }    }    //prints12345678910    call_user_func('printCount', 1, 10);                   /* ex. 1 */    //calls $objBar->printFoo()    $objBar = new Bar('elephant');    call_user_func(array($objBar, 'printFoo'));            /* ex. 2 */    //calls Bar::sayHello('Steve')    call_user_func(array('Bar', 'sayHello'), 'Steve');     /* ex. 3 */    //This throws a fatal error "Using $this when not    //in object context" because the function call    //is Bar::printFoo, which is not a static method    call_user_func(array('Bar', 'printFoo'));              /* ex. 4 */    ?> 

The first example of calling call_user_func is straightforward. You pass the name of the procedural function as a string and supply the two parameters it expects. In the second example, you pass an array as the first parameter. Because the first element of that array is an object, the method printFoo() is invoked on that object. In the third and fourth example, the first element in the array is a string. Therefore, PHP attempts to statically invoke the method on the specified class. If the method doesn't exist or relies on nonstatic class variables (that is, $this), you will encounter a fatal runtime error. This is what happens in the last line when you try to statically invoke printFoo().

Important 

Terminology Reminder: Static methods are those that do not rely on nonstatic class member variables and can be invoked without first instantiating an object of that class. The invocation syntax is ClassName::methodName(). The keyword $this is not available to statically invoked methods.

Implementing a Callback

You can use call_user_func to create a callback in one of your classes. By creating a method that allows the same types of parameters as call_user_func, you can allow code outside your class to specify a function or method to be invoked when certain events in your class happen.

To create a callback in one of your classes, take the following steps:

  1. Determine which events will be raised by your class.

  2. For each of those events, create an on[EventName] method that takes two parameters:

  3. For each event, create a private member variable in your class that will store those parameters as a string (if no object or class name was provided), or as an array (if the second parameter had a value).

  4. At each point in the class where these callbacks should be triggered, query the private member variable created in step 3 to see whether it has been set. If so, call call_user_func(), passing in the value of that variable.

The following example shows how to create an onspeak() event in a class called Dog. Create a function called onspeak() that takes a function name as a string and, optionally, an object or class name. Construct the string or array that should be passed to call_user_func(), and test the value to make sure that it's callable. If it is, store the value in a private member variable for later use. Have a look at the bark() method to see how this value is used.

   <?php    class Dog {      private $_onspeak;      public function __construct($name) {        $this->_name = $name;      }      public function bark() {        if(isset($this->_onspeak)) {          if(! call_user_func($this->_onspeak)) {            return false;          }        }        print "Woof, woof!";      }      public function onspeak($functionName, $objOrClass = null) {        if($objOrClass) {          $callback = array($objOrClass, $functionName);        } else {          $callback = $functionName;        }        //make sure this stuff is valid        if(!is_callable($callback, false, $callableName)) {          throw new Exception("$callableName is not callable " .                              "as a parameter to onspeak");          return false;        }        $this->_onspeak = $callback;      }    }   //end class Dog    //procedural function    function isEveryoneAwake() {      if(time() < strtotime("today 8:30am") ||           time() > strtotime("today 10:30pm")) {        return false;      } else {        return true;      }    }    $objDog = new Dog('Fido');    $objDog->onspeak('isEveryoneAwake');    $objDog->bark(); //polite dog    $objDog2 = new Dog('Cujo');    $objDog2->bark(); //always barks!    //Throws exception when onspeak is called.    $objDog3 = new Dog('Lassie');    $objDog3->onspeak('nonExistentFunction', 'NonExistentClass');    $objDog3->bark();    ?> 

The isEveryoneAwake() function checks to see that the current time is between 8:30 a.m. and 10:30 p.m. If so, return true, allowing the dog to bark. If not, return false, keeping the dog silent. If only real dogs could be programmed with callbacks! This approach works only for $objDog because $objDog2 did not have the callback set.

Note the use of the is_callable() function in onspeak(). This is another built-in PHP function, as is call_user_func(). It's basically a sanity check on the parameters passed to the onspeak() method. When you pass in the parameters to $objDog3->onspeak(), the nonexistent function and class will cause an exception to be thrown.

The setLoadCallback Method in the Collection Class

Now that you've seen how callbacks can work, you need to understand how to put them to work in the Collection class to give it one of its more important advantages lazy instantiation.

Consider the Student class from the registrar's application. You want to be able to keep the very simple interface you saw at the beginning of the chapter, in which the courses are a member variable of the Student object. But you shouldn't have to suffer the overhead of fetching the course list every time you fetch a student object. Instead, you need to enable a callback on the Collection class that will tell it to call some function before allowing any external code to interact with it.

Each call to addItem(), getItem(), length(), exists(), and keys() should first make sure that the collection has been loaded and, if not, invoke any callback functions that might be specified. The function specified in the callback is responsible for populating the collection. You will also need some sort of flag to indicate whether the callback has been invoked. This flag will prevent unnecessarily repeated attempts to load a collection that has already been loaded because that could have a detrimental impact on performance.

The code that follows shows the complete Collection class with the modifications necessary to implement the lazy instantiation capabilities of this class (shown in gray screen).

   <?php    class Collection {      private $_members = array();    //collection members      private $_onload;               //holder for callback function      private $_isLoaded = false;     //flag that indicates whether the callback                                      //has been invoked      public function addItem($obj, $key = null) {        $this->_checkCallback();      //_checkCallback is defined a little later        if($key) {          if(isset($this->_members[$key])) {            throw new KeyInUseException("Key \"$key\" already in use!");          } else {             $this->_members[$key] = $obj;          }        } else {           $this->_members[] = $obj;        }      }      public function removeItem($key) {        $this->_checkCallback();        if(isset($this->_members[$key])) {          unset($this->_members[$key]);        } else {           throw new KeyInvalidException("Invalid key \"$key\"!");        }      }      public function getItem($key) {        $this->_checkCallback();        if(isset($this->_members[$key])) {           return $this->_members[$key];        } else {           throw new KeyInvalidException("Invalid key \"$key\"!");        }      }      public function keys() {        $this->_checkCallback();        return array_keys($this->_members);      }      public function length() {        $this->_checkCallback();        return sizeof($this->_members);      }      public function exists($key) {        $this->_checkCallback();        return (isset($this->_members[$key]));      }      /**       * Use this method to define a function to be       * invoked prior to accessing the collection.       * The function should take a collection as a       * its sole parameter.       */      public function setLoadCallback($functionName, $objOrClass = null) {        if($objOrClass) {          $callback = array($objOrClass, $functionName);        } else {          $callback = $functionName;        }        //make sure the function/method is valid        if(!is_callable($callback, false, $callableName)) {          throw new Exception("$callableName is not callable " .                              "as a parameter to onload");          return false;        }        $this->_onload = $callback;      }      /**       * Check to see if a callback has been defined and if so,       * whether or not it has already been called. If not,       * invoke the callback function.       */      private function _checkCallback() {        if(isset($this->_onload) && !$this->_isLoaded) {          $this->_isLoaded = true;          call_user_func($this->_onload, $this);        }      }    }    ?> 

The setLoadCallback() function allows you to supply the name of a function and, optionally, the name of a class or an object variable that will be invoked to populate this collection. If a callback is supplied to the collection, the $_onload private member variable is populated with the string or array to be passed to call_user_func. Note that just as in the example using the Dog class previously, there's a bit of sanity checking to make sure that the callback is callable.

All the other methods of the Collection class have had the line $this->_checkLoadCallback() added as the first line in the function declaration. Adding this line forces the collection to check for the existence of an as-yet uninvoked callback and will cause that function to be called. Before calling the callback function, we set a flag to make sure that we call that function only once. Note the check for $this->_isLoaded. Any function used as a load callback with the Collection class should expect exactly one parameter, which will be of type Collection. A reference to the current collection (using the $this variable) is passed to the callback function with the line:

   call_user_func($this->_onload, $this) 

This allows the callback function to operate directly on the collection object that caused the function to be invoked. The example that follows shows how this would work in a real situation.

Create a class called NightClub. This class has a name property and a collection of Singer objects. Sometimes we just need to display the name of the club. Sometimes we want to do something with the collection of singers. As a result, we're using the setLoadCallback() method of the collection object to ensure that we don't execute any unnecessary code.

Here are the NightClub and Singer classes. Name the file club_singer.php:

   <?php    require_once("class.Collection.php");    class Singer {      public $name;      public function __construct($name) {        $this->name = $name;      }    }    class NightClub {      public $name;      public $singers;      public function __construct($name) {        $this->name = $name;        $this->singers = new Collection();        $this->singers->setLoadCallback('_loadSingers', $this);      }      private function _loadSingers(Collection $col) {        print "(We're loading the singers!)<br>\n";        //these would normally come from a database        $col->addItem(new Singer('Frank Sinatra'));        $col->addItem(new Singer('Dean Martin'));        $col->addItem(new Singer('Sammy Davis, Jr.'));        }      }    }    ?> 

Note the print statement in the _loadSingers() method. Having this here is just for demonstration purposes. Run the following code (you can add it to the bottom of the previous code, if you wish), and you'll see that the print statement does not appear on the screen.

   <?php      $objNightClub = new NightClub('The Sands');      print "Welcome to " . $objNightClub->name . ".<br>\n";    ?> 

Now try it again, this time doing something with the $singers collection.

   <?php      $objNightClub = new NightClub('The Sands');      print "Welcome to " . $objNightClub->name . ".<br>\n";      print "We have " . $objNightClub->singers->length() . " singers " .            "for your listening pleasure this evening.";    ?> 

Running this should give the following output:

   Welcome to The Sands.    (We're loading the singers!)    We have 3 singers for your listening pleasure this evening. 

As you can see, this makes using the singers collection really easy. If you were to create the NightClub class, you could hand that class off to a more junior member of your software development team who could then use that class to display the screen shown in the example without any knowledge of how or when that collection was loaded. And you can be sure that the application isn't taking up any more system resources than are absolutely necessary.

In the next section, we return to the registrar's office example and show how to put all this to good use in a real-world application.



Professional PHP5 (Programmer to Programmer Series)
Professional PHP5 (Programmer to Programmer Series)
ISBN: N/A
EAN: N/A
Year: 2003
Pages: 182
BUY ON AMAZON

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net