< Day Day Up > |
In our earlier order-form example, we learned that a class can inherit from another class while also belonging to a separate datatype defined by an interface. Our earlier OrderUI class inherited from MovieClip but also belonged to the OrderListener datatype because it implemented the OrderListener interface. That architectural pattern is one of the more common and powerful uses of interfaces ”one in which a class belongs to multiple datatypes without inheriting from multiple classes. Let's take a closer look at this pattern with a new example (source code available at: http:// moock .org/eas2/examples). Suppose we're creating an application that stores objects on disk, either via a server-side script or a SharedObject instance. Each stored object is responsible for providing a method, serialize( ) , that returns a string representation of the object. The string representation can be used to reconstitute the object from scratch. For example, a Rectangle class with width and height properties might provide a serialize( ) method that returns the string "width= w height= h " (where w and h are the values of the width and height properties). Later, the string "width= w height= h " is retrieved and used to create a new Rectangle instance of the original size . To keep things simple for this example, we'll presume that every object must store only property names and values and that no property values are, themselves , objects that would need serialization. When the time comes to save the state of our application, the StorageManager class performs the following tasks :
In order to guarantee that every stored object can be serialized (i.e., converted to a string), the StorageManager class rejects any instances of classes that do not belong to the Serializable datatype. The Serializable datatype is defined by the interface Serializable , which contains a single method, serialize( ) , as follows : interface Serializable { function serialize( ):String; } Classes that support serialization as described are said to "implement Serializable ." To handle generic serialization cases, in which property names and values are retrieved and converted to strings, we create a class, Serializer , which implements Serializable . The Serializer class has the following methods :
Here's the class listing for Serializer : class Serializer implements Serializable { private var serializationProps:Array; private var serializationObj:Serializable; private var recordSeparator:String; public function Serializer ( ) { setSerializationObj(this); } public function setSerializationProps (props:Array):Void { serializationProps = props; } public function setSerializationObj (obj:Serializable):Void { serializationObj = obj; } public function setRecordSeparator (rs:String):Void { recordSeparator = rs; } public function serialize ( ):String { var s:String = ""; for (var i:Number = serializationProps.length; --i >= 0; ) { s += serializationProps[i] + "=" + String(serializationObj[serializationProps[i]]); if (i > 0) { s += recordSeparator; } } return s; } } To use the Serializer class's serialization services, a class can simply extend Serializer . By extending Serializer directly, the extending class inherits both the Serializable interface and the Serializer class's implementation of that interface.
For example, the following code shows a Point class that defines x and y properties, which need to be serialized. The Point class extends Serializer , allowing it to use Serializer 's services directly. // The Point class class Point extends Serializer { private var x:Number; private var y:Number; public function Point (x:Number, y:Number) { super( ); setRecordSeparator(","); setSerializationProps(["x", "y"]); this.x = x; this.y = y; } } Code that wishes to save a Point instance to disk simply calls serialize( ) on that instance, as follows: var p:Point = new Point(5, 6); trace(p.serialize( )); // Displays: y=6,x=5 Note that the Point class does not implement Serializable directly. It extends Serializer , which in turn implements Serializable . The Point class does not extend any other class, so it's free to extend Serializer . However, if a class wants to use Serializer but already extends another class, it must use composition instead of inheritance. That is, rather than extending Serializer , the class implements Serializable directly, stores a Serializer object in an instance property, and forwards serialize( ) method calls to that object. For example, here's a Rectangle class that extends a Shape class but uses Serializer via composition (refer specifically to the sections in bold): // The Shape superclass class Shape { private var target:MovieClip; private var depth:Number; public function Shape (t:MovieClip, d:Number) { target = t; depth = d; } } // The Rectangle subclass implements Serializable directly class Rectangle extends Shape implements Serializable { private var width:Number = 0; private var height:Number = 0; private var serializer:Serializer; public function Rectangle (t:MovieClip, d:Number) { super(t, d); // Here is where the composition takes place serializer = new Serializer( ); serializer.setRecordSeparator(""); serializer.setSerializationProps(["height", "width"]); serializer.setSerializationObj(this); } public function setSize (w:Number, h:Number):Void { width = w; height = h; } public function getArea ( ):Number { return width * height; } public function draw ( ):Void { var container_mc:MovieClip = target.createEmptyMovieClip("rect" + depth, depth); container_mc.clear( ); container_mc.lineStyle(1, 0x000000); container_mc.moveTo(0, 0); container_mc.beginFill(0xFFFFFF, 100); container_mc.lineTo(width, 0); container_mc.lineTo(width, height); container_mc.lineTo(0, height); container_mc.lineTo(0, 0); container_mc.endFill( ); } public function serialize ( ):String { // Here is where the Rectangle class forwards the serialize( ) // invocation to the Serializer instance stored in serializer return serializer.serialize( ); } } As with the Point class, code that wishes to store a Rectangle instance simply invokes serialize( ) on that instance. Through composition, the invocation is forwarded to the Serializer instance stored by the Rectangle class. Here is an example of its use: var r:Rectangle = new Rectangle(this, 1); r.setSize(10, 20); trace(r.serialize( )); // Displays: width=10height=20 If a class would rather implement its own custom serialize( ) method instead of using the generic one provided by Serializer , then the class should simply implement the Serializable interface directly, providing the serialize( ) method definition and body itself. Separating the Serializable datatype's interface from its implementation allows any class to flexibly choose from among the following options when providing an implementation for the serialize( ) method:
If the class does not already extend another class, it can extend Serializer (this option means the least work). If the class already extends another class, it can still use Serializer via composition (this option is the most flexible). Finally, if the class needs its own special serialization routine, it can implement Serializable directly (this option means the most work but may be required by the situation at hand). This flexibility led Sun to formally recommend that, in a Java application, any class that is expected to be subclassed should be an implementation of an interface. As such, it can be subclassed directly, or it can be used via composition by a class that inherits from another class. Sun's recommendation is also sensible for large-scale ActionScript applications. Figure 8-1 shows the generic structure of a datatype whose implementation can be used via either inheritance or composition. Figure 8-1. Multiple datatype inheritance via interfacesFigure 8-2 shows the structure of the specific Serializable , Point , and Rectangle example. Figure 8-2. Multiple datatype inheritance Serializable example |
< Day Day Up > |