The Files:
The Class.js Library, although larger than it was when I started, still isn't big enough to call a Framework. Nor do I suspect that it ever will be. Famous last words, right? But seriously, since the only goal was to implement the ability to create fully functional encapsulating classes on top of JavaScript's prototype-oriented architecture, there's not much left to add beyond what I've already added, or at least, I don't think there's much more to add. Here's the line-up now:
-
- Class.js -
- This is the core component. It provides the ability to define Class objects in a fashion that doesn't dispense with common JavaScript notation, but is still in line with the Object Oriented paradigm in Java.
-
- WeakMap.js -
- This file implements a mostly ES6 compatible WeakMap shim with minimal leakage. The API for this implementation is identical to that of the native WeakMap. The only leakage is a single UUID for each object that needs to use a WeakMap (not each object in the map). This file is safe to include in any project since if native WeakMaps are found, then this implementation gets out of the way.
-
- Functor.js -
- This little object provides a dynamic alternative to Object.bind(), allowing a function to be called against a previously specified object.
-
- Enum.js -
- I wished for a long time that JavaScript supported enums. Then I figured out how to implement them for myself. This file gives us simple enumerations that are as close as possible to the equivalent constructs in other languages without giving up any of JavaScript's flexibility.
-
- Interface.js -
- Since single prototype-inheritance makes any attempt to do full-scale multiple inheritance impossible, I took a page from so-called modern OO languages and implemented the ability to define interfaces for Class.js objects. The fun part is that any object can be tested against an interface defined using Interface.js.
-
- Attribute.js -
- Sometimes you want to add some functionality a class without altering the class. Unlike in Java with it's various "@" annotations, attributes cannot be added directly to classes. Attributes are 1/2 of the mix-in implementation as they are meant to be added to objects after instantiation.
-
- EventHandler.js -
- This is just a simple implementation of an event manager that allows Class.js objects to act as event sources.
These 7 files represent the entire library. They are already written to be compatible with both node.js and HTML <script> tags. The first 4 files represent the core of the Class.js library. The other 3 are additional and can be left out if not needed.
Defining Classes:
var Foo =new Class ("Foo" , {/* Your });class definition here...*/
The first parameter to the Class function is the name of the Class. It's optional, but it can help in debugging as object that are instances of this Class will show up in most debuggers with the class name you specify here. The second parameter is the class definition object (CDO). This is where the fun really begins. But before we can get to that, there are a few helper functions and an enum that needs to be introduced that will help you define your classes to be exactly what you want them to be.
Class Definition Helpers:
The class definition helpers are what allows Class.js to give you full control over the encapsulation of your class members. First is the enum Class.ClassModes. ClassModes defines how this class is to be used with regards to inheritance. It has just 3 elements:
-
- Class.ClassModes.Default -
- This is the default, specifying that this class is open for all inheritance cases. This mode need not be specified.
-
- Class.ClassModes.Abstract -
- This mode restricts this class from being constructed except as part of a descendant class. Attempts to directly construct classes marked Abstract will cause a SyntaxError.
-
- Class.ClassModes.Final -
- This mode restricts this class from being extended by another class. Attempts to extend this class will cause a SyntaxError.
Java programmers will recognize these modes as the ones normally applied to Java classes for the same reasons. Of the helper functions, 5 of them represent the standard modes applied to class member elements in most OO languages. The last 2 are something I borrowed from .NET since they actually made sense in this context.
-
- Class.Private -
- Denotes the member as having private scope and only being available to other members of this class.
-
- Class.Protected -
- Denotes the member as having protected scope and being available to members of this class and its subclasses.
-
- Class.Public -
- Denotes the member as having public scope and being generally available.
-
- Class.Property -
- Denotes the member as requiring computation to compute and/or store its value while presenting itself as a mere member variable.
-
- Class.Static -
- Denotes the member as being singularly available to all instances of the class. This is also extended to the class itself if the member is also Public.
-
- Class.Final -
- Denotes the instance of the member as immutable, even though the properties of that instance may still be mutable.
-
- Class.Delegate -
- Denotes the member as being a function that will usually be called without the benefit of knowing the owning object. The owning object is attached to the member at construction time so that any calls to this member will always be made within the scope of the donating object instance.
-
- Class.Type -
- Denotes that this function, property, or member cannot return a value that is not of the specified type. Also limits the type of value that can be assigned to a member or property.
-
- Class.Abstract -
- Denotes that this member is function with no body defined. Attempts to call this function without sub-classing this class with cause an exception!
Each of these functions is to be treated like a wrapper around the value of the definition member. These wrappers can be nested as long as certain rules are followed. The following groups are mutually exclusive. Use of any 1 from the group for any member invalidates the use of any others in the same group on that member:
-
- Private, Protected, and Public -
- obviously, a member can't be tagged with multiple privilege levels. Privilege levels are exclusion rules. There's no way to both include and exclude an individual member from a set!
-
- Final and Property -
- Final makes it so a particular object instance will always be attached to a given member name. Since Property uses a function to decide the object value of a member, that member doesn't actually exist on that name. So it makes no sense to try to have a Final Property.
Reserved Class Definition Members
Before we can begin using our definition helpers, we need to be aware of the special members of a Class.js Definition:
-
- Mode:
- This is where you use one of the Class.ClassModes. There is no need to specify mode Default. Simply leave out the Mode: element and Default will be assumed.
-
- Extends:
- This is where you specify the base object for your class. This can either be a Class, a constructor function, or an object to use as the prototype. As with Java, there can be only 1.
-
- Implements:
- This is an array of one or more Interfaces as defined using Interface.js. The resulting Class definition has to meet the definition specified by all of the interfaces in this array or a TypeError will be thrown. If there are no Interfaces to be implemented, this element can be omitted.
-
- Mixins:
- This is an array of objects that will be directly merged into the Class as a part of its public privilege level.
-
- Events:
- This is an array of strings that specifies the names of the events that this class emits. This allows anyone to know what events can be listened for on this object just by examining the Events enumeration on the Class or Class instance. It also provides some assurance against mis-typed event names when used.
-
- Constructor:
- This is the user-defined constructor function that will be called once for each instance of the Class being defined just after the instance has been fully configured. This Constructor can have any privilege level, but it cannot use any of the modifiers.
-
- StaticConstructor:
- This is the user-defined static constructor function that will be called once for the entire Class just after the the class definition object has been fully processed.
Class Examples
Here are several examples of how declare a Class. These are by no means examples of every possibility. You can do just about as much with a Class.js class as you can with a Java, C#, C++, or any other OO language class. While there are no templates or generics like in C++ and Java/C# respectively, there's no need for that either. JavaScript's var keyword already gives you everything and more that you'd expect to find with generics. JavaScripts eval() function already gives you everything and more that you'd expect to find with templates. So I hope you can see why I'm not particularly feeling like I missed a feature here. In these examples, I'm assuming the use of node.js.
This first class just defines a rudimentary base class for Automobiles. The var mapping at the top of the file is just there to reduce some typing. Why type "Class." all the time if you don't need to?
/* Filename: Automobile.js */ var EventHandler = require("EventHandler" );var Class = require(" );Class "var Private =Class .Private ;var Protected =Class .Protected ;var Public =Class .Public ;var Property =Class .Property ;var Static =Class .Static ;var Final =Class .Final ;var Delegate =Class .Delegate ;var Mode =Class .ClassMode;var Automobile =new Class ("Automobile" , {//Define automobile as just a base class Mode :Mode .Abstract ,//Extend EventHandler to notify others about events on an instance. Extends : EventHandler,//Declare your events to make the names more readily available. Events : ["moving" ,"stopped" ]// _make:Private membersPrivate (null ), _model:Private (null ), _year:Private (null ), _wheels:Private (null ), _manualTransmission:Private (true ), _miles:Private (0 ), driving:Private (null ), mps:Private (0 ),//miles per second onDrivingInterval:Private (Delegate (function () {this ._miles +=this .mps; })),// setMake:Protected membersProtected (function setMake(value) {this ._make = value; }), setModel:Protected (function setModel(value) {this ._model = value; }), setYear:Protected (function setYear(value) {this ._year = value; }), setWheels:Protected (function setWheels(value) {this ._wheels = value; }), setManualTransmission:Protected (function setManualTransmission(value) {this ._manualTransmission = value; }),//User-defined Constructor Constructor :Public (function (wheels) {this ._wheels = wheels; }),// make:Public membersPublic (Property ({ get:function getMake() {return this ._make; } })), model:Public (Property ({ get:function getModel() {return this ._model; } })), year:Public (Property ({ get:function getYear() {return this ._year; } })), wheels:Public (Property ({ get:function getWheels() {return this ._wheels; } })), isManual:Public (Property ({ get:function getIsManual() {return this ._manualTransmission; } })), odometer:Public (Property ({ get:function getOdometer() {return this ._miles; } })), isDriving:Public (Property ({ get:function getIsDriving() {return (this .driving !=null ); } })), drive:Public (function drive(speed) {if (!this .isDriving) {this .mps = speed /3600 ;this .driving = setInterval(this .onDrivingInterval,1000 ); fireEvent(this .Events .moving, { sender:this .Self }); }else throw new Exception("Already driving!" ); }), park:Public (function park() {if (this .isDriving) {this .mps =0 ; clearInterval(this .driving);this .driving =null ; fireEvent(this .Events .stopped, { sender:this .Self }); }else throw new Exception("Already parked!" ); }) });if (module &&extends && (module.extends ===extends )) module.extends = Automobile;
Just this class alone should give you a pretty good idea of how to use Class.js. It's fairly easy to see how features from more conventional OO languages have been mapped into JavaScript notation. Although it may look somewhat similar, please don't get this confused with the weak version of class support in the ES6 Recommendation. Class.js goes much further into the OO paradigm than that. Since Automobile is an Abstract Class, there's no way to instantiate it, so you really can't test it by itself. So lets add another class on top of it and show how inheritance works.
/* Filename: Motorcycle.js */
var Automobile = require("Automobile" );
var Enum = require("Enum " );
var Class = require("Class " );
var Private = Class .Private ;
var Protected = Class .Protected ;
var Public = Class .Public ;
var Property = Class .Property ;
var Static = Class .Static ;
var Final = Class .Final ;
var Delegate = Class .Delegate ;
var Mode = Class .ClassMode;
//Yes, motorcycles are a kind of automobile.
var Motorcycle = new Class ("Motorcycle" , {
Extends : Automobile ,
//Private members
currentStunt: Private (null ),
stuntStartMile: Private (0 ),
lastStuntMiles: Private (0 ),
onAccident: Private (function onAccident(e) {
alert("An accident occurred because you tried to park while doing a " +
this .currentStunt.name + "!!!" );
}),
//User-defined Constructor
Constructor : Public (function (make, model, year) {
this .Super (2 ); //Motorcycles are generally 2 -wheel vehicles
this .setMake(make);
this .setModel(model);
this .setYear(year);
}),
//Public members
Stunts: Public (new Enum ("None" , ["None" , "Jump" , "Wheelie" , "Stoppie" , "HandStand" , "Surf" ])),
stuntMiles: Public (Property ({
get: function getStuntMiles() {
var retval = this .lastStuntMiles;
if (this .currentStunt != this .Stunts.None)
retval = this .miles - this .stuntStartMile;
return retval;
}
})),
performStunt: Public (function performStunt(stunt) {
if (this .isDriving) {
if (this .Stunts.isMember(stunt)) {
this .currentStunt = stunt;
if (stunt == this .Stunts.None) {
this .removeEventListener(this .Events .stopped, this .onAccident);
this .lastStuntMiles = this .miles - this .stuntStartMile;
this .stuntStartMile = 0 ;
}
else {
this .stuntStartMile = this .miles;
this .addEventListener(this .Events .stopped, this .onAccident);
}
}
else
throw new Exception("Cannot perform an unknown stunt!" );
}
else
throw new Exception("Cannot perform a stunt if the Motorcycle isn't moving!" );
})
});
if (module && extends && (module.extends ===extends ))
module.extends = Motorcycle;
I'll dig into some of the details of what's going on in these 2 classes in my next post.
No comments:
Post a Comment