6.8 The Theory of Inheritance

 <  Day Day Up  >  

So far this chapter has focused mainly on the practical details of using inheritance in ActionScript 2.0. But the theory of why and when to use inheritance in OOP runs much deeper than the technical implementation. Before we conclude, let's consider some basic theoretical principles, bearing in mind that a few pages is hardly enough room to do the topic justice . For a much more thorough consideration of inheritance theory, see Using Inheritance Well (http://archive. eiffel .com/doc/manuals/technology/oosc/inheritance-design/page.html), an online excerpt from Bertrand Meyer's illuminating work Object-Oriented Software Construction (Prentice Hall).

6.8.1 Why Inheritance?

Superficially, the obvious benefit of inheritance is code reuse. Inheritance lets us separate a core feature set from customized versions of that feature set. Code for the core is stored in a superclass while code for the customizations is kept neatly in a subclass. Furthermore, more than one subclass can extend the superclass, allowing multiple customized versions of a particular feature set to exist simultaneously . If the implementation of a feature in the superclass changes, all subclasses automatically inherit the change.

But inheritance also lets us express the architecture of an application in hierarchical terms that mirror the real world and the human psyche. For example, in the real world, we consider plants different from animals, but we categorize both as living things. We consider cars different from planes, but we see both as vehicles. Correspondingly, in a human resources application, we might have an Employee superclass with Manager , CEO , and Worker subclasses. Or, in a banking application, we might create a BankAccount superclass with CheckingAccount and SavingsAccount subclasses. These are canonical examples of one variety of inheritance sometimes called subtype inheritance , in which the application's class hierarchy is designed to model a real-world situation (a.k.a. the domain or problem domain ).

However, while the Employee and BankAccount examples make attractive demonstrations of inheritance, not all inheritance reflects the real world. In fact, overemphasizing real-world modeling can lead to miscomprehension of inheritance and its subsequent misuse. For example, given a Person class, we might be tempted to create Female and Male subclasses. These are logical categories in the real world, but if the application using those classes were, say, a school's reporting system, we'd be forced to create MaleStudent and FemaleStudent classes just to preserve the real-world hierarchy. In our program, male students do not define any operations differently from female students and, therefore, should be used identically. Hence, the real-world hierarchy in this case conflicts with our application's hierarchy. If we need gender information, perhaps simply for statistics, we're better off creating a single Student class and simply adding a gender property to the Person class. As tempting as it may be, we should avoid creating inheritance structures based solely on the real world rather than the needs of your software.

Finally, in addition to code reuse and logical hierarchy, inheritance allows instances of different subtypes to be used where a single type is expected. Known as polymorphism , this important benefit warrants a discussion all its own.

6.8.2 Polymorphism and Dynamic Binding

Polymorphism is a feature of all truly object-oriented languages, wherein an instance of a subclass can be used anywhere an instance of its superclass is expected. The word polymorphism itself means literally "many forms" ”each single object can be treated as an instance of its own class or as an instance of any of its superclasses.

Polymorphism's partner is dynamic binding , which guarantees that a method invoked on an object will trigger the behavior defined by that object's actual class (no matter what the type of the data container in which the object resides).

Don't confuse dynamic binding, which occurs at runtime, with static type checking, which occurs at compile time. Dynamic binding takes into consideration the class of the instance stored in a variable, whereas static type checking does the opposite ”it ignores the datatype of the data and considers only the declared datatype of the variable.


Let's see an example at work, and then we'll reconcile this new information with what we learned earlier in Chapter 3 regarding type checking and type casting.

The canonical example of polymorphism and dynamic binding is a graphics application that displays shapes . The application defines a Shape class with an unimplemented draw( ) method:

 class Shape {   public function draw ( ):Void {     // No implementation. In other languages,   draw( )   would be      // declared with the   abstract   attribute, which syntactically     // forces subclasses of   Shape   to provide an implementation.   } } 

The Shape class has several subclasses ”a Circle class, a Rectangle class, and a Triangle class, each of which provides its own definition for the draw( ) method:

 class Circle extends Shape {   public function draw ( ):Void {     // Code to draw a   Circle   on screen, not shown...    } } class Rectangle extends Shape {   public function draw ( ):Void {     // Code to draw a   Rectangle   on screen, not shown...    } } class Triangle extends Shape {   public function draw ( ):Void {     // Code to draw a   Triangle   on screen, not shown...    } } 

The application stores many different Circle , Rectangle , and Triangle instances in an array named shapes . The shapes array could be created by the user or generated internally. For this example, we'll populate it with 10 random shapes:

 var rand:Number; var shapes:Array = new Array( ); for (var i:Number = 0; i < 10; i++) {   // Retrieve a random integer from 0 to 2   rand = Math.floor(Math.random( ) * 3);     if (rand == 0) {     shapes[i] = new Circle( );   } else if (rand == 1) {     shapes[i] = new Rectangle( );   } else if (rand == 2) {     shapes[i] = new Triangle( );   } } 

When it comes time to update the screen, the application runs through its shapes array, invoking draw( ) on each element without knowing (or caring) whether the element contains a Circle , Rectangle , or Triangle instance:

 for (var i:Number = 0; i < shapes.length; i++) {   shapes[i].draw( ); } 

In the preceding loop, dynamic binding is the runtime process by which each invocation of draw( ) is associated with the appropriate implementation of that method. That is, if the instance is a Circle , the interpreter invokes Circle.draw( ) ; if it's a Rectangle , the interpreter invokes Rectangle.draw( ) ; and if it's a Triangle , the interpreter invokes Triangle.draw( ) . Importantly, the class of each instance in shapes is not known at compile time. The random shapes array is generated at runtime, so the appropriate version of draw( ) to invoke can be determined only at runtime. Hence, dynamic binding is often called late binding : the method call is bound to a particular implementation "late" (i.e., at runtime).

You might ask how this dynamic binding example differs from the code example under "Casting" in Chapter 3, reproduced here for your convenience:

 var ship:EnemyShip = theEnemyManager.getClosestShip( ); if (ship instanceof Bomber) {   Bomber(ship).bomb( );                 // Cast to   Bomber   } else if (ship instanceof Cruiser) {   Cruiser(ship).evade( );               // Cast to   Cruiser   } else if (ship instanceof Fighter) {   Fighter(ship).callReinforcements( );  // Cast to   Fighter   Fighter(ship).fire( );                // Cast to   Fighter   } 

The preceding code checks ship 's datatype using the instanceof operator and then casts it to the Bomber , Cruiser , or Fighter class before invoking a method, such as bomb( ) , evade( ) , or fire( ) . So why doesn't our shapes example check the class of each array element or perform any type casting?

Well, for one thing, the compiler doesn't perform type checking on array elements accessed with the [] operator. So let's take the array access out of the equation and simplify the situation as follows :

 var someShape:Shape = new Circle( ); someShape.draw( );      // Invokes   Circle.draw( )   not   Shape.draw( )   

In this case, the compiler checks the datatype of the someShape variable, namely Shape , and confirms that the Shape class defines draw( ) . However, at runtime, thanks to dynamic binding, the call to shape.draw( ) invokes Circle.draw( ) instead of Shape.draw( ) . (The draw( ) method happens to be declared on the Circle class, but if it weren't, Circle would still inherit draw( ) from the Shape class.)

In contrast, the bomb( ) , evade( ) , and fire( ) methods are not declared in the EnemyShip class. Those methods are declared only in the Bomber , Cruiser , and Fighter subclasses, so our EnemyShip example needs to perform casting to prevent compiler errors and manual type checking (using instanceof ) to prevent runtime errors. So our Shape example is simplified by the fact that the superclass defines a method named draw( ) , which is common to all its subclasses.

Dynamic binding means that the runtime interpreter ignores the datatype of the container and instead considers the class of the data in the container. In this case, the interpreter ignores the Shape.draw( ) method in the superclass and instead uses the overriding subclass version, Circle.draw( ) , because shape holds a Circle instance despite being declared with the Shape datatype.


To bring the discussion full circle, let's revisit our Ball and Basketball example, also from Chapter 3:

 var ball1:Ball = new Basketball( );  // Legal, so far... if (ball1 instanceof Basketball) {  // Include manual runtime type checking   Basketball(ball1).inflate( );      // Downcast prevents compiler error! } 

Here we see again the advantage of late binding. The manual downcast to the Basketball type is purely for the compiler's benefit. However, dynamic binding ensures that the Basketball . inflate( ) method is invoked because the interpreter recognizes that ball1 stores an instance of type Basketball even though the variable's datatype is Ball . What if we store a Ball instance instead of a Basketball instance in ball1 ? See the following code snippet (changes shown in bold):

 var ball1:Ball = new  Ball( )  ;        // Store a   Ball   instance if (ball1 instanceof Basketball) {  // Include manual type checking   Basketball(ball1).inflate( );      // Cast to   Basketball   type } 

In this case, the manual runtime type checking (the if statement) prevents the interpreter from executing Basketball(ball1).inflate( ) . See that! Manual type checking works! But let's remove the type checking and try again:

 var ball1:Ball = new Ball( );        // Store a   Ball   instance Basketball(ball1).inflate( );        // This cast is a "lie" 

Here, the cast to the Basketball type is a "lie." The attempt to invoke inflate( ) fails at runtime because the cast to Basketball fails and, hence, returns null . The interpreter can't invoke inflate( ) (or any other method) on null .

The key benefit of dynamic binding and polymorphism is containment of changes to code. Polymorphism lets one part of an application remain fixed even when another changes. For example, let's consider how we'd handle the random list of shapes if polymorphism didn't exist. First, we'd have to use unique names for each version of draw( ) :

 class Circle extends Shape {   public function drawCircle ( ):Void {     // Code to draw a Circle on screen, not shown...   } } class Rectangle extends Shape {   public function drawRectangle ( ):Void {     // Code to draw a Rectangle on screen, not shown...   } } class Triangle extends Shape {   public function drawTriangle ( ):Void {     // Code to draw a Triangle on screen, not shown...   } } 

Then we'd have to check the class of each shape element manually and invoke the appropriate draw method, as follows:

 for (var i:Number = 0; i < shapes.length; i++) {   if (shapes[i] instanceof Circle) {     shapes[i].drawCircle( );   } else if (shapes[i] instanceof Rectangle) {     shapes[i].drawRectangle ( );   } else if (shapes[i] instanceof Triangle) {     shapes[i].drawTriangle( );   } } 

That's already more work. But imagine what would happen if we added 20 new kinds of shapes. For each one, we'd have to update and recompile the code in the preceding examples. In a polymorphic world, we don't have to touch the code that invokes draw( ) on each Shape instance. As long as each Shape subclass supplies its own valid definition for draw( ) , our application will "just work" without other changes.

Polymorphism not only lets programmers collaborate more easily, but it allows them to use and expand on a code library without requiring access to the library's source code. Some argue that polymorphism is OOP's greatest contribution to computer science.

6.8.3 Inheritance Versus Composition

In this chapter, we focused most of our attention on one type of interobject relationship: inheritance. But inheritance isn't the only game in town. Composition , an alternative form of interobject relationship, often rivals inheritance as an OOP design technique. In composition, one class (the front end class ) stores an instance of another class (the back end class ) in an instance property. The front end class delegates work to the back end class by invoking methods on that instance. Here's the basic approach, shown in code:

 // The back end class is analogous to the superclass in inheritance. class BackEnd {   public function doSomething ( ) {   } } // The front end class is analogous to the subclass in inheritance. class FrontEnd {   // An instance of the back end class is stored in   // a private instance property, in this case called   be   .   private var be:BackEnd;   // The constructor creates the instance of the back end class.   public function FrontEnd ( ) {     be = new BackEnd( );   }   // This method delegates work to   BackEnd.doSomething( )   .   public function doSomething ( ) {     be.doSomething( );   } } 

Notice that the FrontEnd class does not extend the BackEnd class. Composition does not require or use its own special syntax, as inheritance does. Furthermore, the front end class may use a subset of the methods of the back end class, or it may use all of them, or it may add its own unrelated methods. The method names in the front end class might match those exactly in the back end class, or they might be completely different. The front end class can constrain, extend, or redefine the back end class's features, just like a subclass in inheritance, as briefly outlined earlier in Example 6-4.

Example 6-3 showed how, using inheritance, a Square class could constrain the behavior of a Rectangle class. Example 6-5 shows how that same class relationship can be implemented with composition instead of inheritance. In Example 6-5, notice that the Rectangle class is unchanged. But this time, the Square class does not extend Rectangle . Instead, it defines a property, r , that contains a Rectangle instance. All operations on r are filtered through Square 's public methods. The Square class forwards, or delegates , method calls to r .

Example 6-5. An example composition relationship
 // The   Rectangle   class is unchanged from Example 6-3. class Rectangle {   private var w:Number = 0;   private var h:Number = 0;   public function Rectangle (width:Number, height:Number) {     setSize(width, height);   }   public function setSize (newW:Number, newH:Number):Void {     w = newW;     h = newH;   }   public function getArea ( ):Number {     return w * h;   } } // Here's the new   Square   class.  // Compare with the version under "Invoking an Overridden Instance Method." class Square {   private var r:Rectangle;   public function Square (side:Number) {     r = new Rectangle(side, side);   }   // Note that we use our earlier version of   Square.setSize( )   , which defines    // two parameters,   newW   and   newH   . We stick to the original implementation    // for the sake of direct comparison, rather than implementing a more    // elegant version, which would define a single   sideLength   parameter only.   public function setSize (newW:Number, newH:Number):Void {     if (newW == newH) {       r.setSize(newW, newH);     }   }   public function getArea ( ):Number {     return r.getArea( );   } } 

6.8.3.1 Is-A, Has-A, and Uses-A

In OOP parlance, an inheritance relationship is known colloquially as an " Is-A" relationship because, from a datatype perspective, the subclass can be seen literally as being an instance of the superclass (i.e., the subclass can be used wherever the superclass is expected). In our earlier polymorphic example, a Circle "Is-A" Shape because the Circle class inherits from the Shape class and can be used anywhere a Shape is used.

A composition relationship is known as a "Has-A" relationship because the front end class stores an instance of the back end class (e.g., a ChessBoard "Has-A" Tile ). The "Has-A" relationship should not be confused with the "Uses-A" relationship, in which a class instantiates an object of another class but does not store it in an instance property. In a "Uses-A" relationship, the class uses the object and throws it away. For example, a Circle might store its numeric color in a property, col ("Has-A"), but then use a Color object temporarily to actually set that color on screen ("Uses-A").

In Example 6-5, our Square class "Has-A" Rectangle instance and adds restrictions to it that effectively turn it into a Square . In the case of Square and Rectangle , the "Is-A" relationship seems natural, but the "Has-A" relationship can also be used. Which begs the question: which relationship is best?

6.8.3.2 When to use composition over inheritance

Example 6-5 raises a serious design question. How do you choose between composition and inheritance? In general, it's fairly easy to spot a situation in which inheritance is inappropriate. An AlertDialog instance in an application "has an" OK button, but an AlertDialog instance, itself, "isn't an" OK button. However, spotting a situation in which composition is inappropriate is trickier, because any time you can use inheritance to establish the relationship between two classes, you could use composition instead. If both techniques work in the same situation, how can you tell which is the best option?

If you're new to OOP, you may be surprised to hear that composition is often favored over inheritance as an application design strategy. In fact, some of the best-known OOP design theoreticians explicitly advocate composition over inheritance (see Design Patterns , published by Addison-Wesley). Hence, conventional wisdom tells us to at least consider composition as an option even when inheritance seems obvious. That said, here are some general guidelines to consider when deciding whether to use inheritance or composition:

  • If a parent class needs to be used where a child class is expected (i.e., if you want to take advantage of polymorphism), consider using inheritance.

  • If a class just needs the services of another class, you consider a composition relationship.

  • If a class you're designing behaves very much like an existing class, consider an inheritance relationship.

For more advice on choosing between composition and inheritance, read Bill Venner's excellent JavaWorld article, archived at his site: http://www.artima.com/designtechniques/compoinh.html. Mr. Venner offers compelling evidence that, generally speaking:

  • Changing code that uses composition has fewer consequences than changing code that uses inheritance.

  • Code based on inheritance generally executes faster than code based on composition. (This is potentially important in ActionScript, which executes far more slowly than Java.)

In ActionScript, the composition versus inheritance debate most frequently appears when using the MovieClip class. The debate is over whether one should subclass the MovieClip class or store a MovieClip instance as a property of another class. Both approaches are acceptable, though MovieClip inheritance is more complicated and requires use of a Library symbol in a .fla file. For an example of MovieClip composition, see Chapter 5, in which the ImageViewer class uses a MovieClip to display an image on screen. For an example of MovieClip inheritance, see Chapter 13. When working with the MovieClip class, the preceding guidelines can help you determine whether to use inheritance or composition.

6.8.3.3 Using composition to shed an old habit

Because ActionScript 1.0 leniently allowed properties to be added to any object of any class, some Flash developers grew accustomed to forming marriages of convenience between completely unrelated objects. For example, in order to load the coordinates for a "circle" movie clip, some developers would marry an XML instance with a MovieClip instance, as follows:

 // *** ActionScript 1.0 code *** // Create an   XML   instance. var coords = new XML( ); // Store a reference to   circle_mc   directly on the  //   coords   instance, in a property named   mc   . coords.mc = circle_mc; // When the coordinates data loads, use the   XML   instance's //   mc   property to transfer the loaded data to the   circle_mc   movie clip. coords.onLoad = function ( ):Void {   this.mc._x = parseInt(this.firstChild...);  // XML node access not shown   this.mc._y = parseInt(this.firstChild...);  // XML node access not shown }; // Load the coordinates data. coords.load("circleCoords.xml"); 

We can correctly argue that the preceding ActionScript 1.0 practice contributes to:

  • Unreliable, ungeneralized application architecture

  • Spaghetti code that is a tangle of special cases

  • Code that is difficult to follow, extend, and change

However, in ActionScript 2.0, there's a more practical problem: the previous code won't compile. In ActionScript 2.0, it's illegal to add properties to a class unless the class is dynamic , which the XML class is not. The rules of ActionScript 2.0 are designed to help programmers avoid such pitfalls.

An impulsive response to the situation would be: "Fine, what's the easiest way I can circumvent ActionScript 2.0's strictness and add my property to the XML instance?" That kind of thinking might lead to subclassing XML and adding the mc property to the subclass as follows:

 class CoordsXML extends XML{   private var mc:MovieClip;   public function CoordsXML (mc:MovieClip, URL:String) {     this.mc = mc;     load(URL);   }   public function onLoad(success:Boolean):Void {     mc._x = parseInt(firstChild...);  // XML node access not shown     mc._y = parseInt(firstChild...);  // XML node access not shown   } } 

Extending XML as shown technically works, but here we must return to our inheritance versus composition question. Is the relationship we need simply a case of using the XML object to load data (composition) or do we really need a new kind of XML object (inheritance)? It's not very natural to claim that an XML subclass that arbitrarily stores a reference to a movie clip really "Is-A" kind of XML . We don't need a new breed of XML here; we merely need to use XML to load some data. Hence, the situation calls for more of a "Has-A," or even simply a "Uses-A," relationship. After all, we're trying to represent a circle here. The means of transfer (the XML ) and the means of display (the MovieClip ) are mere implementation details. That is, if we want a circle on screen, then we should make a Circle class. The Circle class should deal with loading data and providing a draw-to-screen method. How that actually happens is left up to the Circle class.

In the next example, the Circle class uses an XML instance to load data and a MovieClip instance to draw to the screen. These two instances are stored as Circle properties, as follows:

 class Circle {   var coordsLoader:XML;   var mc:MovieClip;   public function Circle (target:MovieClip,                            symbolID:String,                            name:String,                           depth:Number) {     mc = target.attachMovie(symbolID, name, depth);     coordsLoader = new XML( );   }   public function loadCoords (URL:String):Void {     var clip:MovieClip = this.mc;     coordsLoader.onLoad = function (success:Boolean):Void {       if (success) {         clip._x = parseInt(this.firstChild...); // XML node access not shown         clip._y = parseInt(this.firstChild...); // XML node access not shown       } else {         // Handle a load error         trace("Could not load coords from file: " + URL);       }     };     coordsLoader.load(URL);   } } 

With access to both instances, the Circle class can happily transfer loaded coordinates from the XML file to the movie clip. Not only is this composition relationship more natural, but it lets us easily change the implementation details later without breaking any code that uses the Circle class. For example, we could change the XML instance to a LoadVars instance without affecting any external code that invokes Circle.loadCoords( ) .

Note that a Circle instance doesn't really have to store the XML instance in the coordsLoader property. Instead, the Circle.loadCoords( ) method could store the XML instance in a local variable (in other words, we could define a "Uses-A" relationship rather than a "Has-A" relationship). The latter technique works but has a higher performance cost because the XML instance has to be constructed anew every time loadCoords( ) is invoked.

 <  Day Day Up  >  


Essential ActionScript 2.0
Essential ActionScript 2.0
ISBN: 0596006527
EAN: 2147483647
Year: 2004
Pages: 177
Authors: Colin Moock

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