| |
|
||||
|
Extends - A Custom Keyword Now that we know what we know, it's time to decide what we want. We will take all these new concepts we have learned, and try make them available in a robust, predictable, easy-as-possible-to-use system. After repeatedly failing to accomplish anything close to this, we will settle for a few new keywords that are a bit clunky, but get the job done. So what would our dream requirements of this system be?
That looks daunting, and it is, but we have already covered most of these features. Now we would just like to automate them. If you are not interested, or not awake enough to digest how these routines work, you will be happy to know that you can skip this section an still follow the rest of this book, and still crete fully functioning object-oriented programs. The first thing we must have is a way of linking one class to another, creating a subclass (child) / superclass (parent) relation. This is reasonably easy, all we need is a custom method that we pass two objects - the parent class name object and the child class name object. Then within this method we link the child to the parent by modifying its prototype's __proto__ property. This seems easy enough. We also have to think of a name for this custom method. Rather than pull one out of our butt, we can probably look to other OO systems for guidance. There are many ways to approach inheritance, two common ways are what is known 'restriction inheritance' and 'extension inheritance'. Restriction inheritance defines everything in the top, and then kind of carves away, restricting or hiding features as you work down to the instance (so a square is-a rectangle that restricts itself to one measured side). Extension inheritance is the opposite, where the top classes include only the most general features, and more specific features are added as you work down toward the instance. Generally in Actionscript, classes are adding features as they are subclassed, so they are 'extending' or specializing parent class. Java uses the word extends in the form 'SubClass extends SuperClass' which would make perfect sense for us, however for reasons we will get into, we are limited to passing these two classes as arguments to our custom method. So we can say something like, extends( SuperClass, SubClass ) Not very intuitive syntax unfortunatly, but this may end up a 'fact of life'. We certainly could switch the arguments around, and say extends( SubClass, SuperClass ), though that doesn't help much either. We will stick with the higher class first in our system ( extends(Pet, Dog) ), recognizing it is simply a random choice. Happily the whole method will be suprisingly simple (at least until we are told the terrible truth!). /** Extends ------------------------ */ extends = function( superClass, subClass ) { // link to parent subClass.prototype.__proto__ = superClass.prototype; } This may seem like a lot of smoke for nothing, it is pretty much as easy to write that line after every class as it is to call the extends method, but there are a few things to remember. First, this part only creates the inheritance, it does not handle the running of the constructors. Secondly, well, it doesn't fully work, even doing just that. One of the problem is a simple fix, and the other one deep and mysterious. Let's start with dessert - the easy fix. The scenario: Jane uses your extends method a lot, she likes it. One day she uses it from a movie she loaded into _level0. The extends method has been destroyed, the program doesn't work, Jane drops her stick, the stick does not beat the dog, the dog does not bite the pig, and the pig does not jump over the fence. So we can conclude from this that _root is probably not a safe place to put a custom function. We have a few options though, here are the pros and cons of different ones: Security Through Obscurity: We can put custom functions in _level44497.
All Things to Everyone: We can put it in the Object.prototype.
The Sweedish Function Extender: We can put it in the Function.prototype then.
The Object of the Game: We can hang it in the Object.namespace then.
So we are going to settle on hanging it off the Object, it's really not that bad once you get used to it. So after all that huffing and puffing, we can now add a single word to our extends method (whew, this could take a while at this rate!): /** Extends ------------------------ */ Object.extends = function( superClass, subClass ) { // link to parent subClass.prototype.__proto__ = superClass.prototype; } Remember this means that we now have to always use the syntax Object.extends instead of just extends (or _level0.extends), so it is a good thing we have established this before writing 5000 lines of code.
The second major problem with this extends method is much more of a monster. Let's fall back to 'tricky code mode' - flesh out the problem, define the problem as clearly as we can with a sentence and a few lines of code, and then attempt a solution. Properties and methods can belong to either a class or an instance. If they belong to the class, they go in the prototype. You will sometimes want to set these properties using methods in the same prototype (or from a higher class's prototype). This creates two problems. The first is that the property has no way to (relatively) access a method in its own prototype, or in higher prototypes - only instances can do this (using the word 'this'). The second is that because properties are normally (and correctly) defined first, they can not access methods that have not been defined yet - forward referencing is not allowed. If we define methods first, then methods called would not have access to other properties in teh prototype (again because they may not have been defined yet), which is something you also need. So we actually have two problems -no access to methods when initally setting prototype properties, and forward referencing. This may sound like trivial uses of the prototype, but it isn't. Without getting these things working, the only safe solution is to put all properties (non-methods) into the instances. This is a common recommendation in both Javascript and Actionscript books (even the Flash manual). However, by doing this you end up with a program that is somewhat organized at design time, and very flat at run time. It obliviates many of the advantages of using OO, which is kind of like taking the burden and leaving the pleasure (undressing alone in a restaurant?). This is why in the end, most ECMA language programs look pretty flat - chaos sets in as scale -especially levels- increase in this system. Inheritance is supposed to tame this chaos, not cause it! Let's flesh out the problem a bit better... For a prototype property to access a method in the same prototype (or higher prototypes) when initializing, it must hardcode the path. Remember, this is before instances have been created, it is while setting up the class. Here are the few lines of code: Cat.prototype.petType = "cat"; Cat.prototype.defaultState = ???.setState("sleep"); Cat.prototype.setState = function( state ) { if( state == "sleep" ) return "The " + ???.petType + " is sleeping."; if( state == "eat" ) return "The " + ???.petType + " is eating."; } If you just hardcode the method using Cat.prototype.setState( ), it might work (though not in this case - can you see why not?), however now you have a hardcoded reference to a location. If you ever change the class name, you have at best some code hunting to do, or worse, a couple hours of debugging. You might argue that the Cat class is already hardcoded on the left side of the expression (Cat.prototype.xxx), so what is the big deal... Sure, it is worse to have a hardcoded reference on the right, but let's ignore that. In fact all hard coded references are bad, so rather than treat this as a justification, we'll just add the hardcoded reference on the left to our list of problems. After all, they make it much harder to rename the class, and shuffling things around while designing your structures often includes renaming them. That all being said, we could probably live with this - if it solved the whole problem. Things get very messy (and unsolvable) though, when one of these called methods needs to access a property from its prototype (or a higher prototype), like the return values do. Here if we say return Cat.prototype.petType it will work, but what will happen if the petType property ever gets overridden? What if a subclass or the instance says something like xxx.petType = "siamese cat"? The setState method will return "The cat is eating", instead of "The siamese cat is eating", when called from an instance of SiameseCat. Giant problem, and there is no workaround (if you declare prototypes this way). The second 'forward referencing' problem is also happening in the code above. As mentioned earlier, swf files do not allow forward referencing. They are not compiled, so they have no idea what is coming next (and 'what is coming next' is often not even loaded). So the setState method is called before it exists, which is obviously not going to get you far. You may have already decided on the names of your fututre children, but that doesn't mean they will come for supper if you call them. It's that kind of thing, err, yeah. So what about just putting the property definitions after the methods? Well close, but now the methods can not access the properties that have not yet been defined. How about splitting them up, or just keeping track of them mentally? Quagmire. So in a sentence the problems are:
What about a solution then? The forward referencing issue may seem like a chicken and egg problem - the methods must be defined first, the properties must be defined first - but it isn't exactly that. If we look closely, the methods only need to be defined first, not run, the properties need to run first. Second tiny crucial bit-o-trivia: the prototype is an object like any other object, not a class. We know objects are essentially instances created from a template. So what we can do is set up a template to define the prototype, much like we have been doing for other instances. This way all the properties and methods will be defined first, before ever being called. If we define this template inside the prototype, calling it will set the this keyword to be equal to the class's prototype. So all of these properties and methods will be copied into the prototype. The giant benefit of this is that the this keyword will be equal to the prototype while the properties and methods are being setup. This means that properties can access methods, and methods can access properties. To insure that methods are defined before properties call them, we can simply split them into two routines, methods and properties, and run the methods first. Will this mean that methods can not access properties now? No, because as we pointed out before, methods aren't being run at this point, just being defined. So even though the method definition is calling a property that does not yet exist, it will exist by the time a method is called. What about instances? If methods refer to other properties in the prototype, will that not mean that when we create an instance of this class, it will be directly referencing the prototype? No again, because the meaning of the word 'this' changes when accessed from an instance. When called from the class prototype, it refers to the prototype, when invoking a class method from an instance 'this' will refer to the instance. Sweetness. Further nice things are now happening as an indirect result of this. Your code is suddenly more organised and readable. All class properties are defined together, and the all methods are defined together. The hardcoding of the class name has all but disappeared - once for the class, once for the props/methods, and then once to create inheritance (and we can even reduce that if we like). There is no pollution, not even the two property and method definitions, because once they run we can (and have to) delete them. What about those people who are just used to setting properties and methods directly on the prototype? Maybe they are really used to doing things this way, or have old code they want to use, or maybe they are just to stubborn to change? Not a problem, because you do not have to use these methods if you don't need the benefits .All they so is set up the prototype, so if that is done directly, it will work exactly the same as it always did (eg. not that well, but if thats what you like...). Other than the guys that wanted the extra three inches, everyone gets what they want. Here is how a class defintion might look, in this full fledged extender: // Class B = function( ){ } // Properties B.prototype.classProperties = function ( ) { this.prop1 = 55; this.prop2 = this.double( this.prop1 ); } // Methods B.prototype.classMethods = function ( ) { this.double = function ( ) {return this.prop1 * 2;} } // make B a subclass of A Object.extends( A, B ); So what would our custom 'extends' method look like then? Try to follow through the code below, ignoring the customKeyword part for now. // Extends ------------------------ Object.extends = function( superClass, subClass )
{
// link to customKeyword if this is top level
// if higher levels are inserted later will still work
if( (superClass.prototype.__proto__ == Object.prototype)
&& (superClass.prototype <> Object.customKeyword.prototype) )
{
superClass.prototype.__proto__ = Object.customKeyword.prototype;
// set the superClass's prototype if it hasn't been set already
if( typeof(superClass.prototype.classMethods) != undefined )
{
superClass.prototype.classMethods();
delete superClass.prototype.classMethods;
}
if( typeof(superClass.prototype.classProperties) != undefined )
{
superClass.prototype.classProperties();
delete superClass.prototype.classProperties;
}
}
// link to parent
subClass.prototype.__proto__ = superClass.prototype;
// Set the prototype of the subclass - these methods should be deleted
// when they are finished, to avoid pollution
// and keep them from running twice
if( typeof(subClass.prototype.classMethods) != undefined )
{
subClass.prototype.classMethods();
delete subClass.prototype.classMethods;
}
if( typeof(subClass.prototype.classProperties) != undefined )
{
subClass.prototype.classProperties();
delete subClass.prototype.classProperties;
}
}
Well, cetainly
time for a caffine fix after that one. Wait about fifteen minutes for
it to take hold, then lets tackle the constructors passing arguments up
the chain. It is probably harder code, but there are no major hidden slap-in-the-face
things like in extends, so in the end it is the easy part. Almost home.
|