OOJS Tutorial Using Class.js: Part 2

Last time, I introduced 2 example classes: Motorcycle, and its Abstract superclass Automobile. Those of you who are familiar with classic OO languages probably recognize almost everything that's going on here, but I'm going to explain it anyway for those who don't get it yet.

A Brief Intro On JavaScript Object-Oriented Programming

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript

I seriously hope you didn't think I was going to rehash that wheel here. That subject has been beaten to death on the internet. The link I provided isn't necessarily the best explanation, but it's good enough to give those of you who are unfamiliar with the subject a foundation for understanding what I'm about to explain.

Under The Hood Of Class.js

The problem with creating real, encapsulating classes in JavaScript is the fact that everything declared on an object is essentially public. The closest thing to private members on an object is a non-enumerable member. Using function closures can remedy that. The private data can be left off of the object and relegated to the closure. This is the technique used by many other OO JavaScript frameworks. However, it's not enough. Protected members are more complicated. They need to be protected from the user of the object like private members, but still be available to objects that use the current one as a prototype. If you want to see how I solved that riddle, just look at the source for Class.js. Good luck though. It still occasionally confuses me, and I wrote it!

The long and short of it is that a Class instance is composed of 4 objects: the public scope, the private scope, the protected scope, and the static scope. The main object for the class instance is the private scope. It contains all of the instance elements, as well as redirection properties for the static elements. Only the code in non-static member functions can access this scope. To do so, just use the 'this' keyword.

The next most important scope is the static scope. The elements in the static scope are accessed either via 'this' in any member function, or via the Class instance constructor if the element is static and public. Static member functions do not have access to the private scope as the 'this' element is the static scope object. To provide for public static elements, redirectors are put directly on the Class instance constructor (not the constructor's prototype), allowing for the familiar <ClassName>.<Element> notation for public static members.

After that is the protected scope object. This object is nothing more than an object full of redirector properties that reference anything public or protected. All of the properties on this object are redirected back to the private scope. This is the inheritable object that is used when creating instances of a subclass.

The final object is the public scope. This is the object returned when using new <ClassName>() Only the public and public static elements are available on this object. The only exceptions to this are the StaticConstructor and Constructor. These two methods are not placed on the public interface since they are not intended to be called directly. The StaticConstructor is never made available after it is called at the end of processing the Class Definition Object. The Constructor, on the other hand, is made available to the static scope. This is to allow users of Class.js to implement the singleton pattern.

Inside a Class

When in a method of a Class, you are operating either in static or private scope, depending on whether or not the method was declared Static. In either case, everything you need to use in the Class can be accessed with 'this', but there's a catch. Since all the inherited members of a subclass are granted to the private scope of the subclass by making that protected scope the prototype of the private scope, a call into an inherited member will have a different 'this' than the one used to call the inherited member. Here's an example using the Automobile and Motorcycle classes from my previous post:

var myBike = new Motorcycle("Honda", "CRF250R", 2015);
myBike.drive(45);  //Average 45 mph

//Drive around for 1 hour.
setTimeout(function doneDriving() { myBike.park(); }, 3600000);

function checkMileage() {
   console.log("Current mileage: " + myBike.odometer + "miles");
   if (myBike.isDriving)
      setTimeout(checkMileage, 300000);
}

//Check my mileage every 5 minutes
setTimeout(checkMileage, 300000);

Suppose we were to put a breakpoint in Automobile.drive() and peek at it's 'this'. It would not contain any methods from Class Motorcycle, and (this instanceof Motorcycle) would return false. This is correct behavior considering that parent classes aren't supposed to be aware of their subclasses. This also leaves us in a little bit of a bind.

There are cases in normal OO programming where it is necessary to pass around instances of an object. Passing around the 'this' in this case would result in many surprising and hard to figure out errors. To make this scenario possible, every Class instance has a 'Self' on it's private scope. That 'Self' always references the public scope of the instantiated Class. So, if you ever need to pass the Class instance to another function outside the Class, then you would pass 'this.Self' instead of simply 'this'.

Callback Methods and Delegates

Much like dealing with 'this' and 'this.Self', care has to be taken when creating member functions that will be used as callback functions. The problem is fairly straight forward. Since a function variable only refers to the the function and not any object that the function may be a part of, there is no way for the method calling the callback function to reference the correct object, if any.

JavaScript already has a solution for this: Object.bind(). For most normal cases, Object.bind() is perfectly ok. However, when using Class.js, Object.bind() is problematic. If a callback member function is created in a base class and bound to that class, then when a derived class needs to pass that callback function to some other process, the other process would only ever succeed in reaching the base class. Effectively, the information regarding the derived class would be lost.

To alleviate this problem, Class.js offers the Delegate Helper for creating callback methods directly on the Class Definition Object, and 'this.Delegate()' for creating callback method instances on-the-fly inside a Class method. With Delegate methods, the difference is that Class.js defines the owning context as the context at the time of use. This means that the process that calls the callback method is guaranteed to make the call against the correct scope, namely, the one that issued the delegate.

'Automobile.onDrivingInterval()'  is an example of how to set up a delegate on the Class Definition Object. You should use this method of defining delegates if the function in question is only or primarily used as a callback method. For all other cases, 'this.Delegate()' is the preferred approach. Here's a couple of examples of the exact same setup from Class Automobile but using 'this.Delegate()'.

Example 1: by function definition
var Automobile = new Class("Automobile", {
   ...
   
   onDrivingInterval: Private(function() {
      this._miles += this.mps;
   }),

   ...
   
   drive: Public(function drive(speed) {
      if (!this.isDriving) {
         this.mps = speed / 3600;
         this.driving = setInterval(this.Delegate(this.onDrivingInterval), 1000);
         fireEvent(this.Events.moving, { sender: this.Self });
      }
      else
         throw new Exception("Already driving!");
   }),
});
Any member function, whether or direct or inherited, can be used in this fashion simply by referring to the Class member.

Example 2: by function reference
var Automobile = new Class("Automobile", {
   ...
   
   onDrivingInterval: Private(function() {
      this._miles += this.mps;
   }),

   ...
   
   drive: Public(function drive(speed) {
      if (!this.isDriving) {
         this.mps = speed / 3600;
         this.driving = setInterval(this.Delegate("onDrivingInterval"), 1000);
         fireEvent(this.Events.moving, { sender: this.Self });
      }
      else
         throw new Exception("Already driving!");
   }),
});
Member functions can also be used as delegates by referring to the string name of the member function. This allows delegates to be set up for members discovered during enumeration.

Example 3: by function name
var Automobile = new Class("Automobile", {
   ...
   
   drive: Public(function drive(speed) {
      if (!this.isDriving) {
         this.mps = speed / 3600;
         this.driving = setInterval(this.Delegate(function() { this._miles += this.mps; }), 1000);
         fireEvent(this.Events.moving, { sender: this.Self });
      }
      else
         throw new Exception("Already driving!");
   }),
});
It doesn't even have to be a member function. As long as it is a function that you want to be called within the scope of the current Class instance, Delegate() will do the job for you.

Next time, I'll dig into the event handling used by Class.js.


No comments:

Post a Comment