BladeRunner object-oriented JS
The Base Library allows you to code your application in an easy, Java-like way. This section explains how.
Traditionally, JavaScript has been seen and used as a light-weight language, not really suited for enterprise scale application development. The Caplin approach to JavaScript coding for developing applications within BladeRunner, has therefore been influenced a lot by Java. We’ve provided a structure that allows developers to code in a Java-like way, with concepts such as interfaces and class-based inheritance. If you were only ever using JavaScript in its more conventional, light-weight role, all this additional structure would probably seem rather over-engineered, but for enterprise-scale JavaScript it offers considerable benefits.
Java-Based Approach
One of the conventions adopted from Java is that each class or interface should be defined in a separate file, and the files should be placed within a directory structure that matches the namespace of the class or interface. As long as the namespacing rules are obeyed, BladeRunner’s JS Bundler will be able to locate and 'bundle' all the necessary code into the application at run-time.
Classes and Namespacing
Classes have to be declared using their full namespace. For example: if you want to declare a class called HelloWorld in the novox package, you should first create a file called HelloWorld.js under the src/novox directory, within your desired location. The desired location could be an aspect, blade or suchlike. Your HelloWorld.js file might contain something like this:
novox.HelloWorld = function()
{
};
novox.HelloWorld.prototype.sayHi = function()
{
alert('Hello World!!');
};
General Structure
The overall structure of JavaScript in BladeRunner can be summarised as follows. We’ll go into more detail on each of these further down, but it might help to give you a 'starter for ten', as it were!
-
Interfaces. An interface is a class with a number of methods defined for it, but its methods cannot be called directly; if an interface’s method is called directly, it will throw an exception. In essence, an interface is a template for other classes that will implement it and its methods. Interfaces are created using the
caplin.core.Utility.interfaceMethod()
method. -
Classes that Implement Interfaces. You can create any number of classes that implement an interface (i.e. that use the interface as a template), by using the
caplin.implement()
method. In order to be properly usable, a class that implements an interface must also explicitly implement all its methods before it calls them. If you call an interface’s method without implementing it first, will just throw an exception – as it would if you called it directly.-
Abstract Classes. If you create a class that implements an interface, but doesn’t implement all of its methods, it is incomplete, and cannot be used on its own. You might want to create such a class though, in order to define shared methods for a set of classes that have certain things in common, but which differ in other ways. You would create these additional classes by extending your abstract class.
-
Concrete Classes. A concrete class implements an interface, and all its methods, either by implementing it directly, or by extending an abstract class, and declaring the methods that the abstract class omits. If it extends an abstract class, then by default, it will inherit the implementations of the interface methods that were declared in the abstract class.
-
-
Extending Classes. A class can be extended using the
caplin.extend()
construct. The new class will inherit all the default methods declared by the class that it extended from, although it can modify them if necessary. -
Static Classes. A static class is declared without using the
prototype
element of the declaration. It is not extensible, cannot be instantiated and must be named in full, every time it is called.
Interfaces
Declaring an interface is essentially the same as declaring a class. With interfaces though, it’s important to provide feedback when an arbitrary method has not been implemented. You can do that by using caplin.core.Utility.interfaceMethod()
, within the body of each of your interface’s methods.
Using Animal
as an example interface, your definition could look something like this:
novox.Animal = function()
{
};
novox.Animal.prototype.speak = function()
{
caplin.core.Utility.interfaceMethod("novox.Animal" , "speak");
};
novox.Animal.prototype.getGender = function()
{
caplin.core.Utility.interfaceMethod("novox.Animal" , "getGender");
};
novox.Animal.prototype.getLegCount = function()
{
caplin.core.Utility.interfaceMethod("novox.Animal" , "getLegCount");
};
If another class implements the Animal
interface, it must implement all its methods as well, or it will not be able to invoke them.
For example: if a class implements Animal
, but does not provide an implementation for the method speak()
, any invocation to speak will cause an exception with the message "Unexpected exception (This is an interface method (novox.Animal.speak))" being thrown to the console.
Implementing an Interface
You can implement an interface using the caplin.implement()
method. As you might expect, you can use this invocation to implement as many interfaces as you wish. If we wanted to declare a Quadruped
class that implements the Animal
interface, we could do it like this:
novox.Quadruped = function(sGender)
{
this.m_sGender = sGender;
};
caplin.implement(novox.Quadruped, novox.Animal);
novox.Quadruped.prototype.getLegCount = function()
{
return 4;
};
novox.Quadruped.prototype.getGender = function()
{
return this.m_sGender;
};
Note that it’s important to implement the interface before any method declarations for it to work properly. It’s also important to provide the actual JavaScript constructor functions to caplin.implement()
rather than the function names as strings. You may also notice the Quadruped
class hasn’t implemented the speak()
method, which effectively makes this an abstract class.
Extending a Class
As Quadruped
is an abstract class, we are going to need to extend it to provide a concrete implementation. Extending a class is very similar to implementing an interface; you do it by using the caplin.extend()
construct. You should follow the same rules as for caplin.implement()
when using this construct, declaring the class extension before you declare any of its methods.
Going back to our example, let’s extend Quadruped
to give us two concrete classes of Quadruped
: Cat
and Dog
.
novox.Dog = function(sGender)
{
};
caplin.extend(novox.Dog, novox.Quadruped);
novox.Dog.prototype.speak = function()
{
alert("Woof!");
};
novox.Cat = function(sGender)
{
};
caplin.extend(novox.Cat, novox.Quadruped);
novox.Cat.prototype.speak = function()
{
alert("Meow!");
}
By extending the Quadruped
class and implementing the remaining method from the Animal
interface, each of these new classes is complete and we could create instances of them.
Calling Super-Methods
Using the JavaScript methods call()
and apply()
allows us to call super-constructors and methods whilst retaining the correct "this" pointer.
Ideally, our Dog
and Cat
constructors would both pass on their genders to the Quadruped
super-constructor, so we could add a line to each constructor so that this happens:
novox.Dog = function(sGender)
{
novox.Quadruped.apply(this, arguments);
};
...
novox.Cat = function(sGender)
{
novox.Quadruped.apply(this, arguments);
};
If we added any additional arguments at a later date, using the arguments
keyword ensures that they would all be passed back to the Quadruped
super-constructor without having to explicitly add them to the apply()
method in each constructor.
Overriding Super-methods
By default, our cats and dogs all have four legs, a feature that they inherit – not unreasonably – from the Quadruped
class. If we wanted to allow for the occasional three-legged dog we might encounter however, the following code would allow us to do this, whilst still being able to call the super-method, allowing normal dogs to retain their traditional allocation of legs.
novox.Dog.prototype.getLegCount = function()
{
if (this.is3LeggedDog())
{
return 3;
}
return novox.Quadruped.prototype.getLegCount.call(this);
};
Static Classes and Enums
It may be useful to provide static classes and enums within your application, to complement particular classes and interfaces. You can write static classes and enums by omitting the prototype
from property and method declarations. For example: we may want to provide an AnimalGender
enum to be used in conjunction with the Animal
interface:
novox.AnimalGender = function()
{
};
novox.AnimalGender.MALE = "male";
novox.AnimalGender.FEMALE = "female";