OO & Traits
Wins
- Simplify your code with exceptional inheritance and composition support.
- 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
- Define A Type Method (inherited by subtypes)
- Define An Instance Method (inherited by subtype instances)
- Define A Local Method (not inherited)
Attribute Definition
- Define A Type Attribute (inherited by subtypes)
- Define An Instance Attribute (inherited by subtype instances)
- Define A Local Attribute (not inherited)
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 as it's 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 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.
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.
Multiple inheritance 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 in JavaScript: The Good Parts 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.
When the default value is an access path, TIBET will map that attribute to use
that access path when get()
or set()
is used with that attribute.
// The `.Type` part here is key:
<TypeRef>.Type.defineAttribute('<attributeName>', defaultValue|accessPath);
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. Because
// the value is an object and is mutable, we supply it as the 'value'
// property of the descriptor.
APP.corp.MyType.Type.defineAttribute('info', null, {value: {infostuff: 'str'}});
// Define a type attribute with a path value as a default. TP.tpc() is
// shorthand for constructing a 'TIBET access path'. In this case,
// accessing the 'info' attribute will retrieve the value of 'typeStuff'.
APP.corp.MyType.Type.defineAttribute('info', TP.tpc('typeStuff));
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|accessPath);
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|accessPath);
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
.