[ Team LiB ] Previous Section Next Section

Tools for Building Object Hierarchies

In this section, we delve even deeper into object-oriented design issues. We have already seen how useful the hints are in method arguments introduced with PHP 5. Hints are important because we can be sure that when using them we will be working with an object of a particular type. We encountered this earlier in this hour:


public function update( Item $item ) {
  print "updating.. ";
  print $item->name;
}

The update() method knows that it has an Item object and can go ahead and work with the $name property that it knows will be accessible. So by constraining the type of the argument passed to the method, we ensure the interface of the object.

Abstract classes and interfaces are ways of doing a similar thing. Each ensures the availability of features for client code.

Abstract Classes

Abstract classes are deceptively simple but very useful. You must define an abstract class using the abstract keyword:


abstract class ItemUpdater {
}

The effect is that it is now impossible to directly instantiate an ItemUpdater object. Notice the following line:


$updater = new ItemUpdater();

It results in the following error:


Fatal error: Cannot instantiate abstract class itemupdater

An abstract class is a template for its children, rather than a functional class in its own right. Let us assume that all ItemUpdater objects should have update(), delete(), and retrieve() methods. We can enforce this rule within the abstract ItemUpdater() class by declaring abstract methods:


abstract class ItemUpdater {
  abstract public function update( Item $item );
  abstract public function retrieve( $identifier );
  abstract public function delete( Item $item );
}

Now, if we subclass ItemUpdater, we are required to take on the responsibilities the parent has laid down. Let's try dodging our duty:


class XmlItemUpdater extends ItemUpdater {
}

PHP will not let us create a concrete XMLItemUpdater class that extends ItemUpdater but does not implement its methods:



Fatal error: Class xmlitemupdater contains 3 abstract methods and must therefore be
graphics/ccc.gif declared abstract (itemupdater::update, [itemupdater::retrieve, itemupdater::delete, ...)

We could, if we want, defer the problem by declaring XmlItemUpdater abstract as well. Instead, let's provide a concrete subclass for ItemUpdater that reports when the update() method is invoked. We can also create two further subclasses, as shown in Listing 17.7, and use them to revisit our __destruct() example in Listing 17.5.

Listing 17.7 An Abstract Class and Concrete Implementations
 1: <?php
 2:
 3: class Item {
 4:   public $name = "item";
 5:   private $updater;
 6:
 7:   public function setUpdater( ItemUpdater $update ) {
 8:     $this->updater=$update;
 9:   }
10:   function __destruct() {
11:     if ( ! empty( $this->updater )) {
12:       $this->updater->update( $this );
13:     }
14:   }
15: }
16:
17: abstract class ItemUpdater {
18:   abstract public function update( Item $item );
19:   abstract public function retrieve( $identifier );
20:   abstract public function delete( Item $item );
21: }
22:
23: class ReportItemUpdater extends ItemUpdater {
24:
25:   public function update( Item $item ) {
26:     print get_class( $this )."::update(): $item->name<br />\n";
27:     return true;
28:   }
29:
30:   public function retrieve( $id ) {
31:     print get_class( $this )."::retrieve(): id $id<br />\n";
32:     return new Item();
33:   }
34:
35:   public function delete( Item $item ) {
36:     print get_class( $this )."::delete(): id $id<br />\n";
37:     return true;
38:   }
39: }
40:
41: class XmlItemUpdater extends ReportItemUpdater { }
42:
43: class MysqlItemUpdater extends ReportItemUpdater { }
44:
45: $item = new Item();
46: $item->setUpdater( new XmlItemUpdater );
47: unset( $item );
48: // prints "xmlitemupdater::update(): item<br />"
49:
50: $item = new Item();
51: $item->setUpdater( new MysqlItemUpdater );
52: unset( $item );
53: // prints "mysqlitemupdater::update(): item<br />"
54: ?>

For convenience, we re-present the Item class from Listing 17.4. The key features to note are the setUpdater() method on line 7 and the __destruct() method on line 10. setUpdater() requires an ItemUpdater object, which it stores in the private $updater property. The __destruct() method is automatically invoked before an Item object is destroyed and calls on the ItemUpdater object's update() method.

We define the abstract ItemUpdater on line 17 and a concrete subclass ReportItemUpdater on line 23. ReportItemUpdater does nothing but report on calls to its methods: implementations of update(), retrieve(), and delete(). We subclass ReportItemUpdater on lines 41 and 43, creating an empty XmlItemUpdater class and an empty MysqlUpdater class. Were this production code, we would of course implement both MysqlItemUpdater and XmlItemUpdater to write, delete, and retrieve Item objects. For this example, any method call through objects of these types will default to the ReportItemUpdater implementation, printing a report to the browser so that we can see what is going on.

On line 45, we instantiate an Item object and then pass an XmlItemUpdater object to its setUpdater() method. We destroy the Item object on line 47 by calling the unset() function. This step invokes its __destruct() method, thereby causing it to call XmlItemupdater::update(). This is confirmed by script output.

On line 50, we repeat the process with a MysqlItemUpdater object. The point of this exercise is to demonstrate the interchangeable nature of our ItemUpdater objects. The Item object does not know or care how it is being saved and retrieved. It knows only that it has been passed an ItemUpdater object for storage and use. The abstract ItemUpdater base class ensures that there will be an implemented update() method but leaves the implementation details up to its subclasses. This model, with variable functionality in different subclasses hidden behind a common ancestry and interface, is known as polymorphism.

So we can use argument hints in conjunction with an abstract class to ensure that a particular interface will be available in an object passed to a method.

We also use abstract classes to fix the definition of a method. Not only did we ensure the presence of a update() method when we defined ItemUpdater, but also we ensured that the update() method would always expect an Item object as its argument.

Unfortunately, PHP does not provide you with a way of defining a return type when you define an abstract. We can demand that all implementations of delete() must be passed an Item object, but we can't demand that all implementations of retrieve() return an Item object. There are still some things that we must take on trust.

Although our ItemUpdater class contained no implementation, a partial implementation is allowed in abstract classes. This is useful when all subclasses are likely to want to share the same implementation of a method. The best place to put it is usually in the base class. You will often see methods in abstract classes calling their own abstract methods:


abstract class OutputComponent {

  abstract function getComponentText();
  abstract function filterComponentText( $txt );
  abstract function writeComponentText();

  function doOutput() {
    $txt = $this->getComponentText();
    $txt = $this->filterComponentText( $txt );
    $this->writeComponentText( $txt );
  }
}

So in the preceding fragment, we define three abstract methods. In doOutput(), we work with the methods, leaving the details of implementation to subclasses. This neat trick (documented as the "template method" design pattern) illustrates once again the value of the interface in object-oriented programming. Different OutputComponent subclasses use different techniques to access text and apply different kinds of filters. Subclasses can generate output in different ways from one another. For all this potential for difference, the doOutput() method remains valid, ignoring the details hidden behind the interface. Template methods are often declared final, ensuring that all subclasses work in the same way.

Interfaces

We have talked a lot about a type's interface during this hour. To confuse matters, we are now going to discuss a language feature called an interface. An interface is similar in some ways to an abstract class. It allows you to define a set of methods that a related class is obligated to implement.

There are some key differences between abstract classes and interfaces. You declare interfaces with the interface keyword:


interface Serializable {
  function writeObject();
}

You are not allowed to add any implementation at all to an interface, only properties and method definitions. You do not have to declare your method definitions abstract; it is done for you implicitly.

Classes do not extend interfaces; they implement them. A class can extend another class and implement as many interfaces as you want. By implementing an interface, a class becomes that type, in addition to its type by inheritance:


class User extends Person implements Costable {
  // ...
}

So in the preceding example, the User class must implement any methods defined by the Costable interface. Any User objects will be of both type 'Person' and type 'Costable'. This means that we can use interfaces to aggregate objects that derive from different roots but share common facets. In Listing 17.8, we define an interface called Serializable and define two classes that implement it.

Listing 17.8 Defining and Using an Interface
 1: <?php
 2:
 3: interface Serializable {
 4:   public function writeObject();
 5: }
 6:
 7: class Point implements Serializable {
 8:   public $x;
 9:   public $y;
10:
11:   public function writeObject() {
12:     return serialize( $this );
13:   }
14: }
15:
16: class Item implements Serializable {
17:   public function writeObject() {
18:     return serialize( $this );
19:   }
20: }
21:
22: class Serializer {
23:   private $sArray = array();
24:
25:   function register( Serializable $obj ) {
26:     $this->sArray[] = $obj;
27:   }
28:
29:   function output() {
30:     foreach ( $this->sArray as $obj ) {
31:       print $obj->writeObject()."\n";
32:     }
33:   }
34: }
35:
36: $serializer = new Serializer();
37: $serializer->register( new Item() );
38: $serializer->register( new Point() );
39: print "<pre>";
40: $serializer->output();
41: print "</pre>";
42: ?>

We define our interface, Serializable, on line 3. We define a method writeObject() on line 4 that implementing classes must include. We set up two test classes, Point (line 7) and Item (line 16), both of which implement Serializable. In each case, the writeObject() method merely calls and returns PHP's serialize() function (lines 12 and 18).

We set up a demonstration class called Serializer on line 22. Serializer has a register() method that will only accept Serializable objects. Any object passed to register() is saved in the private $sObjects property. The output() method on line 29 loops through $sArray, calling writeObject() on each object in the array. We know that we can call writeObject() because the only mechanism we have provided for populating $sArray is the update() method.

On lines 36 through 41, we run Serializer through its paces, instantiating a Serializer object, registering Item and Point objects, and then calling Serializer::output(). Because they both implement Serializable, the Item and Point objects are recognized as type Serializable, and the update() method's type hinting causes no problems. The output() method prints the results to the browser:


O:4:"item":0:{}
O:5:"point":2:{s:1:"x";N;s:1:"y";N;}



    [ Team LiB ] Previous Section Next Section