TIBET Logo

TIBET OO & Traits

OO

wins

  • Simplify your code thanks to unmatched inheritance and composition.
  • Design more efficiently by leveraging full type and instance inheritance.
  • Use MVC and MVVC patterns easily with built-in Change notification.
  • Eliminate overhead due to compilers, transpilers, source maps, etc.
  • Support metaprogramming through runtime reflection and MOP features.

contents

concepts

cookbook

Type Definition
Trait Definition
Behavior Definition
Attribute Definition
Calling super (callNextMethod())
Initializers
Overrides

code


concepts

Ask 100 programmers what object-orientation is and you're likely to get 100 distinct answers. It's an unfortunate reality of our industry that terms are often twisted or co-opted until they start to lose their clarity and their value. Still, most developers seem to define OO as encompassing three key features: encapsulation, messaging, and polymorphism. We believe inheritance's contributions to reuse make it worth including as well.

Because we want to be clear about what we mean when we discuss OO we've added a few paragraphs in each section that follows to define each term as we use it. You can disagree. Just be aware TIBET uses these definitions.

Encapsulation

One could argue that without encapsulation you can't possibly have objects. After all, a defining characteristic of an object is the integration of state and behavior -- particularly integration such that the state can't be corrupted.

Encapsulation is what protects programmers from corrupting data by manipulating it directly rather than protecting it via API. You'd never do that, right? ;-)

Without a lot of syntactic gymnastics JavaScript doesn't support true encapsulation.

To support encapsulation through a more convention-based approach, TIBET implements a standard set()/get() method pair that offers a lot of power without some of the limitations of native setter/getter functions.

Let's say we want to access the lastname of a new instance of Person:

inst = Person.construct();
lname = inst.get('lastname');

When the get() method is invoked, TIBET first looks for a method of the name getBlah where Blah is the title case version of whatever you've queried for.

In our prior example, TIBET would attempt to find a getLastname() method to invoke. If found, that method is run. If not, the get() call resorts to calling a lower-level routine ($get()) to access the data.

The set() call operates in precisely the inverse fashion, with the additional feature that set() will signal a state change notification if the new value differs from the old value.

The nice thing about this approach is:

  • consumers don't have to know where the data is to access it,
  • you don't have to write hundreds of setBlah/getBlah methods, and
  • consumers of types can use a late-bound and specializable API.

The best part of the TIBET approach is the impact on maintainability.

As you implement smarter setters and getters in your application they automatically get used, without you having to go back and update the code that calls them.

If all this sounds similar to the behavior of the setter and getter functionality which has found its way into modern browsers via ECMAScript Edition 5, it should, however TIBET eliminates many of the limitations on ECMA getter/setter implementations.

See JavaScript: The TIBET Parts for a more detailed discussion of using set and get rather than direct property access.

Messaging

The operations a particular object can perform to get work done are typically referred to as methods. Methods associated directly with a type are known as type methods or, in some languages, static methods. Methods associated with individual instances are known as instance methods. Certain languages (VB, Self, JavaScript, etc.) also allow individual instances to have methods specific to the instance. We call these local methods.

Invoking a particular method on an object is referred to as messaging the object.

Messaging is perhaps the central feature of OO in that its use directly supports the other key features. If you're not using messages you're unlikely to be leveraging polymorphism and you're probably violating encapsulation. TIBET is heavily "message oriented".

Polymorphism

One of the more misunderstood elements of OO has to be polymorphism which is often conflated with inheritance. For our purposes the definition of polymorphism is simple:

The provision of a single interface to entities of different types.

As defined, polymorphism just means that different objects of random types can respond to a message and that their response is potentially specific to their type.

In terms of how polymorphism is achieved, whether by subtyping, composition, or some other means, we try to avoid putting any constraints on the development model. For what it's worth TIBET supports pretty much all variants from inheritance to composition.

In TIBET you can provide method implementations in a number of ways:

  • Inheritance - Perhaps the most common approach in TIBET, but not required.
  • Traits - Traits are a mechanism used to provide a form of multiple inheritance. They avoid many of the problems with multiple inheritance by providing mechanisms for explict conflict resolution of same-named methods.
  • Local Methods - Directly programming one or more objects to respond.

The key point is TIBET gives you a variety of ways to factor and reuse functionality without the limitations found in many other JavaScript OO implementations.

Inheritance

Inheritance is the ability of descendants to reuse behavior and attributes defined by their ancestors. In programming terms, the descendants are referred to as subclasses or subtypes, while ancestors are referred to as superclasses or supertypes.

TIBET uses the term type rather than class specifically to avoid confusion with the reserved class keyword in JavaScript. TIBET types are not JavaScript classes, nor is it likely they ever will be given that such a change would actually remove powerful functionality.

When descendants can inherit from multiple parents that's known as multiple inheritance. This approach is supported in TIBET through the use of traits which allows our core single-inheritance tree to be augmented via composition. We believe the result is the most powerful system for factoring and reusing functionality in JavaScript applications.

cookbook

Type Definition

In TIBET, types are a dynamic entity. They are created dynamically and can be created on-the-fly. While they leverage the prototypal inheritance of JavaScript 'under the covers', the API they present imposes a 'classical inheritance' approach for ease-of-use.

Define A Subtype:

To define a subtype of an existing type use the defineSubtype method:

<SupertypeRef>.defineSubtype('<SubtypeName>');

The root of the object hierarchy in TIBET applications (except for instances of built-ins like Array and Date) is the TP.lang.Object type. Let's create a subtype of it:

TP.lang.Object.defineSubtype('MyType');

By default a new subtype receives the namespace of the supertype so in the example above our resulting type's full name is TP.lang.MyType. That's probably not what we want in most cases, but we can provide a namespace to change that as shown below.

Define A Namespaced Subtype:

To ensure new types get namespaces explicitly we can provide the namespace and separate it from the type name using either dots (.) or colons (:).

// Use a dot ('.') for a JavaScript namespace.
<SupertypeRef>.defineSubtype('<NamespaceName>.<SubtypeName>');

// OR

// Use a colon (':') for an XML namespace.
<SupertypeRef>.defineSubtype('<NamespaceName>:<SubtypeName>');

There are two namespace roots in the TIBET namespace hierarchy: TP and APP.

All TIBET library code has been placed in the TP. namespace while application code is intended to target the APP. namespace. (New project code follows this convention).

It is strongly recommended you provide at least one nested namespace level below the APP level rather than placing new types and logic directly on APP.

Assuming we want our code in a corp namespace we can create a new type as:

TP.lang.Object.defineSubtype('APP.corp.MyType');

If you already have a supertype in corp you might use the simpler syntax which relies on the new subtype inheriting the namespace roots from the supertype:

APP.corp.MyType.defineSubtype('MySubType');

In both cases the result is a new type in the APP.corp namespace.

Define a Custom Tag Type:

In TIBET, 'custom tags' are modeled via types. For syntactic consistency you can use a colon (:) when defining these subtypes, as in:

TP.core.Element.defineSubtype('mycorp:header');

Our new type can be referenced using either standard JavaScript dot syntax, or by using a string with the colon:

var newHeadInst = mycorp.header.construct();

// OR

var newHeadInst = "mycorp:header".construct();

If your tag relies on an unknown namespace prefix you also need to define a URL and prefix for it via code similar to:

TP.w3.Xmlns.registerNSInfo('http://www.mycorp.com', TP.hc('prefix', 'mycorp'));

Note that TIBET's application template does this for you for a single 'starter' namespace corresponding to your app name when you perform a tibet clone operation, but you will need to do it for additional namespaces you wish to define.

Trait Definition

Define a Trait

TIBET's multiple inheritance / composition system is an implementation of a traits-based model - inspiration for which can be found in the traits.js open source library.

TIBET's implemetation of traits is seamlessly integrated with the rest of TIBET's OO infrastructure so that defining a trait is identical to defining any other type in TIBET.

One "hint" you can provide that a type is a trait is to define the type as "abstract". You do that via the isAbstract type method:

APP.corp.MyType.isAbstract(true);

That's it. Your type will now throw an exception if you attempt to construct a new instance, but all of its attributes and methods are available to be mixed in.

Mix in a Trait

Unlike single inheritance via defineSubtype to leverage a trait in one of your types you need to mix in that trait, adding its properties to one or more targets.

Use the addTraits method to mix in one or more traits. The example below is taken directly from the TIBET logging subsystem:

TP.log.Logger.addTraits(TP.log.Leveled);
TP.log.Logger.addTraits(TP.log.Filtered);

As the sample above shows, adding traits is a simple process. But what about conflicts? How does TIBET resolve those? The answer is "it tries, but when you don't like TIBET's choice, you do".

Resolving Traits

TIBET's trait implementation uses the well-known, robust C3 resolution algorithm to determine which trait'ed behavior or property should "win" in the case of conflicts. In most cases this algorithm is sufficient, but when you want more control over the resolution process TIBET gives you that capability via the resolveTrait and resolveTraits methods.

// Logger's inherit from their ancestor chain so we need to preserve getters.
TP.log.Logger.Inst.resolveTraits(
    TP.ac('getFilters', 'getLevel', 'getParent'),
    TP.log.Logger);

TP.log.Logger.Inst.resolveTrait('getName', TP.log.Nestable);

The examples above are again drawn from TIBET's logging subsystem which makes use of traits for a number of features.

The first segment ensures that TP.log.Logger instances retain their getFilters, getLevel, and getParent functionality in the face of overlays from traited types.

The second segment tells the system that TP.log.Logger instances should use the getName function from the TP.log.Nestable trait rather than some other source.

Note that the above two resolveTraits methods are happening on the .Inst track, but traits work and can be resolved in exactly the same way on the .Type track for 'type-side' multiple inheritance / traiting.

Behavior Definition

TIBET has a very structured way of adding behavior to types using what Douglas Crockford refers to as method methods.

TIBET's method-based approach provides a robust way of managing the inevitable complexity that comes with managing 'type', 'instance' and 'local' behavior.

Define A Type Method (inherited by subtypes):

Type behavior in TIBET is inheritable by subtypes, something that's not supported in languages with 'statics' like Java, C++, ES6 JavaScript, etc.

To define an inheritable type method use the defineMethod call and target the .Type object of your type:

//  The `.Type` part here is key:
<TypeRef>.Type.defineMethod('<methodName>', function() {
    //  Type method functionality
    //  'this.' references in this method refer to the *type*.
});

A concrete example might resemble:

APP.corp.MyType.Type.defineMethod('getStuff', function() {
    //  As a type method the `this` reference is the Type.
    return this.get('something');
});

We can use this new getStuff method by messaging the type directly:

var stuff = APP.corp.MyType.getStuff();

Because this method is inherited (we defined the method on the .Type object of our APP.corp.MyType), subtypes can also use this method:

APP.corp.MyType.defineSubtype('MySubType');

//  This works - true 'type-level' inheritance
var stuff = APP.corp.MySubType.getStuff();

Define An Instance Method (inherited by subtype instances):

Defining an instance method is similar to defining a type method, however we target the .Inst property of the type rather than the .Type property:

//  The `.Inst` part here is key:
<TypeRef>.Inst.defineMethod('<methodName>', function() {
    //  Inst method functionality
    //  'this.' references in this method refer to *instances* of the type.
});

Let's create an instance method on our type, APP.corp.MyType:

APP.corp.MyType.Inst.defineMethod('getThings', function() {
    //  As an inst method the `this` reference is the instance.
    return this.get('something');
});

We can use this method by messaging instances of the type:

var newInst = APP.corp.MyType.construct();
var things = newInst.getThings();

Because this method is inherited, subtypes of this type can also use this method:

//  Define a subtype of APP.corp.MyType
APP.corp.MyType.defineSubtype('MySubType');

//  Create an instance and message it.
var newInst = APP.corp.MySubType.construct();
var things = newInst.getThings();

Define A Local Method (not inherited):

A 'local' method is a method defined on a single object, be it a type or instance. This is possible in JavaScript since all objects can contain their own unique properties.

To define a local method leave off the .Type or .Inst specializer and just use defineMethod directly on the type or instance in question:

<objectRef>.defineMethod('<methodName>', function() {
    //  method functionality only this object will have.
});

For example, we can create a method on our type that is not inherited:

APP.corp.MyType.defineMethod('getLocalStuff', function() {
    //  Stuff that only this object can provide.
});

Our local method can then be invoked as follows:

APP.corp.MyType.getLocalStuff();

Note that using local methods effectively 'overrides' any methods that the object inherited. This is standard JavaScript behavior:

//  Assume an instance of 'APP.corp.MyType' as defined earlier.
var newInst = APP.corp.MyType.construct();

//  Call the 'getThings' instance method
var things = newInst.getThings();

//  Now define a local method on 'newInst'
newInst.defineMethod('getThings', function() {
    return 'only local things';
});

//  Now 'things' will have the result of the *local* 'getThings method:
things = newInst.getThings();

Note that TIBET will correctly invoke the overridden instance method when "calling super" from within a local method. See Calling "super" for more info.

Attribute Definition

Having defined your type structure and the behavior of the various types and their instances, you’ll want to define attributes next. As you might imagine, the methods for attribute definition follow a similar pattern to those for method definition.

Define A Type Attribute (inherited by subtypes)

As with methods, defining a type attribute is done by messaging the .Type property of a type. The attribute name is the first parameter followed by either a property descriptor or a default value.

Attributes are initialized to null by default which help distinguish properties that are known but not yet initialized from those which are unknown (aka undefined).

When the default value is an object rather than a non-mutable value such as a string or number you should provide it as the 'value' property of the property descriptor.

//  The `.Type` part here is key:
<TypeRef>.Type.defineAttribute('<attributeName>', defaultValue);

Let's see a couple of concrete examples of type attributes on APP.corp.MyType:

//  Define a simple type attribute which will initialize to `null`.
APP.corp.MyType.Type.defineAttribute('typeStuff');

//  Define a type attribute that's default value is `false`.
APP.corp.MyType.Type.defineAttribute('fluffy', false);

//  Define a type attribute with an object value as a default.
APP.corp.MyType.Type.defineAttribute('info', {value: {infostuff: 'str'}});

Using the approaches above each unique subtype will inherit the default value but will have its own value if the attribute is set. In other words, using this approach preserves the copy-on-write semantics from standard JavaScript.

Define An Instance Attribute (inherited by subtype instances):

Defining a instance attribute follows the same pattern as a type attribute, but targets the .Inst property of the desired type:

//  The `.Inst` part here is key:
<TypeRef>.Inst.defineAttribute('<attributeName>', defaultValue);

Let's create a simple instance attribute on APP.corp.MyType:

//  The value of this instance attribute will default to `null`:
APP.corp.MyType.Inst.defineAttribute('instanceStuff');

Setting the runtime value of an instance attribute is a simple set operation:

var newInst = APP.corp.MyType.construct();
newInst.set('instanceStuff', 'This is some instance stuff');

One thing to note with regards to default instance values is that JavaScript's copy-on-write semantics don't hold true for reference types (Array and Object in particular).

To define an Array or Dictionary as a default value for an instance you don't normally use defineAttribute's second parameter, instead you set the value in the init method for instances of the type (discussed a bit later).

Define A Local Attribute (not inherited):

As with methods, if you want a local attribute leave off the .Type or .Inst qualifier and message the target object directly via defineAttribute:

<aRef>.defineAttribute('<attributeName>', defaultValue);

Local attributes affect only the object they're defined on and they can be used with (almost) any object in a running TIBET system.

Calling "super": callNextMethod()

In an OO system a common requirement is invoking an inherited implementation of a method in a subtype's override of that method. This operation is commonly referred to as "calling super", a reference to invoking the "supertype" version of a method.

In TIBET to 'call up' an inheritance chain you use the .callNextMethod() method.

TIBET's .callNextMethod() name is specifically not called callSuper* for a variety of reasons, perhaps the most relevant being that with multiple inheritance via traits the "next method" may not follow the strict supertype chain. In addition, thanks to local methods (i.e. methods directly placed on the instance of the object) the next method may actually be on that object's local type, not on a supertype.

All you need to know is that, regardless of whether you're in a local method, an instance method, a type method, or a traited method, callNextMethod will find and invoke the proper "next method" and ensure it's bound to the invoking object.

In the common example below we implement an init method for our sample subtype and start off by invoking the next method up the chain, ensuring that our init doesn't skip any functionality from prior init implementations.

APP.corp.MySubType.Inst.defineMethod('init', function() {
    //  Almost always do this first in an `init` override:
    this.callNextMethod();

    //  Do subtype stuff...

    //  Return the instance object to return from `construct`:
    return this;
});

When invoking callNextMethod any arguments to the current method are automatically provided to the next method invoked. You only need to provide arguments when you need to alter the parameter list being passed.

Initializers

In TIBET you can initialize both instances and types. In both cases the object being initialized is initialized only once.

Type initialization happens after all types have been loaded but before the application starts running. This "type initialization phase" is part of the TIBET Loader's sequencing which ensures that all code is loaded before any type initialization is run, helping to eliminate cyclic dependencies.

Instance initialization happens after the new object is created via alloc but before the instance is returned for use from the construct method.

Define A Type Initializer:

Define a one-time initializer for a type by defining an initialize type method:

<TypeRef>.Type.defineMethod('initialize', function() {

    // DO NOT CALL callNextMethod here!

    // Perform one-time type configuration...
    ...

    //  Return value isn't used so no need to return the type.
    return;
});

Type initializers must not call their supertype initializer. TIBET invokes each initializer once per type so invoking them via each subtype is neither necessary nor appropriate.

Define An Instance Initializer:

You can control instance initialization by creating an init instance method:

<TypeRef>.Inst.defineMethod('init', function() {
    // Do this first, or right after inbound parameter cleanup.
    this.callNextMethod();

    // Custom initialization work here...
    ...

    // Return an alternative object if you need to here.
    return this;
});

All instance initializers should invoke .callNextMethod() to ensure 'top-down' (i.e. supertype all the way down to subtype) initialization occurs properly.

Instance initializers should return the object which will serve as the new instance. Note that this actually doesn't have to be the this reference, although it typically is.

Overrides

Using inheritance implies you'll often need to alter how a subtype responds to messages. With TIBET you can easily override supertype methods and invoke them via .callNextMethod() as needed in your new implementation.

Override A Supertype Method:

The first step to overriding is simply to define the method in a subtype:

<TypeRef>.Inst.defineMethod('doX', function() {
    // Do this to invoke the supertype verison...
    this.callNextMethod();

    // Add your custom stuff here...
});

If you need to change the parameters being passed to the supertype call use this variant:

<TypeRef>.Inst.defineMethod('doX', function() {
    // Adjust parameters as needed...

    // Call super with explicit parameters.
    this.callNextMethod(a, b, c, ...);

    // More custom stuff here...
});

Override A Constructor (advanced):

While it's not common to override the actual constructor function for a type you can do it. The default construct() method invokes an allocation function ($alloc) to get an instance, then invokes the init() method of that instance.

Normally you can simply override init() and you're all set. But what if you want your type to manage allocation differently? Perhaps you want to manage a singleton, or count instance creations, implement a factory, etc.

In those cases you can override construct(), which effectively puts you ahead of the invocation of the underlying new invocation which typically happens in the allocator.

<TypeRef>.Type.defineMethod('construct', function() {
    //  Manage object creation here. usually via a
    //  callNextMethod(), but possibly this.$alloc()
    var inst = ....

    //  Be sure to invoke 'init' and return its value.
    return inst.init();
});

code

~lib/src/tibet/kernel/TIBETInheritance.js contains the core inheritance logic.

~lib/src/tibet/kernel/TIBETPrimitivesPre.js contains most of TIBET's method methods.