Began implementing composition-based traits
As described in <https://savannah.gnu.org/task/index.php#comment3>. The benefit of this approach over definition object merging is primarily simplicitly---we're re-using much of the existing system. We may provide more tight integration eventually for performance reasons (this is a proof-of-concept), but this is an interesting start. This also allows us to study and reason about traits by building off of existing knowledge of composition; the documentation will make mention of this to explain design considerations and issues of tight coupling introduced by mixing in of traits.perfodd
parent
dfc83032d7
commit
71358eab59
|
@ -659,6 +659,12 @@ exports.prototype.createConcreteCtor = function( cname, members )
|
||||||
// generate and store unique instance id
|
// generate and store unique instance id
|
||||||
attachInstanceId( this, ++_self._instanceId );
|
attachInstanceId( this, ++_self._instanceId );
|
||||||
|
|
||||||
|
// handle internal trait initialization logic, if provided
|
||||||
|
if ( typeof this.___$$tctor$$ === 'function' )
|
||||||
|
{
|
||||||
|
this.___$$tctor$$.call( this );
|
||||||
|
}
|
||||||
|
|
||||||
// call the constructor, if one was provided
|
// call the constructor, if one was provided
|
||||||
if ( typeof this.__construct === 'function' )
|
if ( typeof this.__construct === 'function' )
|
||||||
{
|
{
|
||||||
|
@ -666,9 +672,10 @@ exports.prototype.createConcreteCtor = function( cname, members )
|
||||||
// subtypes), and since we're using apply with 'this', the
|
// subtypes), and since we're using apply with 'this', the
|
||||||
// constructor will be applied to subtypes without a problem
|
// constructor will be applied to subtypes without a problem
|
||||||
this.__construct.apply( this, ( args || arguments ) );
|
this.__construct.apply( this, ( args || arguments ) );
|
||||||
args = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = null;
|
||||||
|
|
||||||
// attach any instance properties/methods (done after
|
// attach any instance properties/methods (done after
|
||||||
// constructor to ensure they are not overridden)
|
// constructor to ensure they are not overridden)
|
||||||
attachInstanceOf( this );
|
attachInstanceOf( this );
|
||||||
|
|
154
lib/Trait.js
154
lib/Trait.js
|
@ -19,6 +19,8 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
var AbstractClass = require( __dirname + '/class_abstract' );
|
||||||
|
|
||||||
|
|
||||||
function Trait()
|
function Trait()
|
||||||
{
|
{
|
||||||
|
@ -33,13 +35,34 @@ function Trait()
|
||||||
|
|
||||||
Trait.extend = function( dfn )
|
Trait.extend = function( dfn )
|
||||||
{
|
{
|
||||||
|
// we need at least one abstract member in order to declare a class as
|
||||||
|
// abstract (in this case, our trait class), so let's create a dummy one
|
||||||
|
// just in case DFN does not contain any abstract members itself
|
||||||
|
dfn[ 'abstract protected __$$trait$$' ] = [];
|
||||||
|
|
||||||
function TraitType()
|
function TraitType()
|
||||||
{
|
{
|
||||||
throw Error( "Cannot instantiate trait" );
|
throw Error( "Cannot instantiate trait" );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// and here we can see that traits are quite literally abstract classes
|
||||||
|
var tclass = AbstractClass( dfn );
|
||||||
|
|
||||||
TraitType.__trait = true;
|
TraitType.__trait = true;
|
||||||
TraitType.__dfn = dfn;
|
TraitType.__acls = tclass;
|
||||||
|
TraitType.__ccls = createConcrete( tclass );
|
||||||
|
|
||||||
|
// traits are not permitted to define constructors
|
||||||
|
if ( tclass.___$$methods$$['public'].__construct !== undefined )
|
||||||
|
{
|
||||||
|
throw Error( "Traits may not define __construct" );
|
||||||
|
}
|
||||||
|
|
||||||
|
// invoked to trigger mixin
|
||||||
|
TraitType.__mixin = function( dfn )
|
||||||
|
{
|
||||||
|
mixin( TraitType, dfn );
|
||||||
|
};
|
||||||
|
|
||||||
return TraitType;
|
return TraitType;
|
||||||
};
|
};
|
||||||
|
@ -51,42 +74,135 @@ Trait.isTrait = function( trait )
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a concrete class from the abstract trait class
|
||||||
|
*
|
||||||
|
* This class is the one that will be instantiated by classes that mix in
|
||||||
|
* the trait.
|
||||||
|
*
|
||||||
|
* @param {AbstractClass} acls abstract trait class
|
||||||
|
*
|
||||||
|
* @return {Class} concrete trait class for instantiation
|
||||||
|
*/
|
||||||
|
function createConcrete( acls )
|
||||||
|
{
|
||||||
|
// start by providing a concrete implementation for our dummy method
|
||||||
|
var dfn = {
|
||||||
|
'protected __$$trait$$': function() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: everything else
|
||||||
|
|
||||||
|
return acls.extend( dfn );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mix trait into the given definition
|
* Mix trait into the given definition
|
||||||
*
|
*
|
||||||
* The original object DFN is modified; it is not cloned.
|
* The original object DFN is modified; it is not cloned.
|
||||||
*
|
*
|
||||||
* TODO: we could benefit from processing the keywords now (since we need
|
|
||||||
* the name anyway) and not re-processing them later for the class.
|
|
||||||
*
|
|
||||||
* @param {Trait} trait trait to mix in
|
* @param {Trait} trait trait to mix in
|
||||||
* @param {Object} dfn definition object to merge into
|
* @param {Object} dfn definition object to merge into
|
||||||
*
|
*
|
||||||
* @return {Object} dfn
|
* @return {Object} dfn
|
||||||
*/
|
*/
|
||||||
Trait.mixin = function( trait, dfn )
|
function mixin( trait, dfn )
|
||||||
{
|
{
|
||||||
var tdfn = trait.__dfn || {};
|
// the abstract class hidden within the trait
|
||||||
for ( var f in tdfn )
|
var acls = trait.__acls,
|
||||||
|
methods = acls.___$$methods$$,
|
||||||
|
pub = methods['public'];
|
||||||
|
|
||||||
|
// retrieve the private member name that will contain this trait object
|
||||||
|
var iname = addTraitInst( trait.__ccls, dfn );
|
||||||
|
|
||||||
|
for ( var f in pub )
|
||||||
{
|
{
|
||||||
// this is a simple check that will match only when all keywords,
|
if ( !( Object.hasOwnProperty.call( pub, f ) ) )
|
||||||
// etc are the same; we expect that---at least for the time
|
|
||||||
// being---class validations will ensures that redefinitions do not
|
|
||||||
// occur when the field strings vary
|
|
||||||
if ( dfn[ f ] )
|
|
||||||
{
|
{
|
||||||
// TODO: conflcit resolution
|
continue;
|
||||||
throw Error( "Trait field `" + f + "' conflits" );
|
|
||||||
}
|
|
||||||
else if ( f.match( /\b__construct\b/ ) )
|
|
||||||
{
|
|
||||||
throw Error( "Traits may not define __construct" );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dfn[ f ] = tdfn[ f ];
|
// TODO: this is a kluge; we'll use proper reflection eventually,
|
||||||
|
// but for now, this is how we determine if this is an actual public
|
||||||
|
// method vs. something that just happens to be on the public
|
||||||
|
// visibility object
|
||||||
|
if ( !( pub[ f ].___$$keywords$$ ) )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// proxy this method to what will be the encapsulated trait object
|
||||||
|
dfn[ 'public proxy ' + f ] = iname;
|
||||||
}
|
}
|
||||||
|
|
||||||
return dfn;
|
return dfn;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add concrete trait class to a class instantion list
|
||||||
|
*
|
||||||
|
* This list---which will be created if it does not already exist---will be
|
||||||
|
* used upon instantiation of the class consuming DFN to instantiate the
|
||||||
|
* concrete trait classes.
|
||||||
|
*
|
||||||
|
* Here, `tc' and `to' are understood to be, respectively, ``trait class''
|
||||||
|
* and ``trait object''.
|
||||||
|
*
|
||||||
|
* @param {Class} C concrete trait class
|
||||||
|
* @param {Object} dfn definition object of class being mixed into
|
||||||
|
*
|
||||||
|
* @return {string} private member into which C instance shall be stored
|
||||||
|
*/
|
||||||
|
function addTraitInst( C, dfn )
|
||||||
|
{
|
||||||
|
var tc = ( dfn.___$$tc$$ = ( dfn.___$$tc$$ || [] ) ),
|
||||||
|
iname = '___$to$' + tc.length;
|
||||||
|
|
||||||
|
// the trait object array will contain two values: the destination field
|
||||||
|
// and the class to instantiate
|
||||||
|
tc.push( [ iname, C ] );
|
||||||
|
|
||||||
|
// we must also add the private field to the definition object to
|
||||||
|
// support the object assignment indicated by TC
|
||||||
|
dfn[ 'private ' + iname ] = null;
|
||||||
|
|
||||||
|
// create internal trait ctor if not available
|
||||||
|
if ( dfn.___$$tctor$$ === undefined )
|
||||||
|
{
|
||||||
|
dfn.___$$tctor$$ = tctor;
|
||||||
|
}
|
||||||
|
|
||||||
|
return iname;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trait initialization constructor
|
||||||
|
*
|
||||||
|
* May be used to initialize all traits mixed into the class that invokes
|
||||||
|
* this function. All concrete trait classes are instantiated and their
|
||||||
|
* resulting objects assigned to their rsepective pre-determined field
|
||||||
|
* names.
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function tctor()
|
||||||
|
{
|
||||||
|
// instantiate all traits and assign the object to their
|
||||||
|
// respective fields
|
||||||
|
var tc = this.___$$tc$$;
|
||||||
|
for ( var t in tc )
|
||||||
|
{
|
||||||
|
var f = tc[ t ][ 0 ],
|
||||||
|
C = tc[ t ][ 1 ];
|
||||||
|
|
||||||
|
// TODO: pass protected visibility object once we create
|
||||||
|
// trait class ctors
|
||||||
|
this[ f ] = C();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,6 @@ var util = require( __dirname + '/util' ),
|
||||||
MethodWrapperFactory = require( __dirname + '/MethodWrapperFactory' ),
|
MethodWrapperFactory = require( __dirname + '/MethodWrapperFactory' ),
|
||||||
wrappers = require( __dirname + '/MethodWrappers' ).standard,
|
wrappers = require( __dirname + '/MethodWrappers' ).standard,
|
||||||
|
|
||||||
Trait = require( __dirname + '/Trait' ),
|
|
||||||
|
|
||||||
class_builder = ClassBuilder(
|
class_builder = ClassBuilder(
|
||||||
require( __dirname + '/MemberBuilder' )(
|
require( __dirname + '/MemberBuilder' )(
|
||||||
MethodWrapperFactory( wrappers.wrapNew ),
|
MethodWrapperFactory( wrappers.wrapNew ),
|
||||||
|
@ -383,7 +381,7 @@ function createUse( base, traits )
|
||||||
// "mix" each trait into the provided definition object
|
// "mix" each trait into the provided definition object
|
||||||
for ( var i = 0, n = traits.length; i < n; i++ )
|
for ( var i = 0, n = traits.length; i < n; i++ )
|
||||||
{
|
{
|
||||||
Trait.mixin( traits[ i ], dfn );
|
traits[ i ].__mixin( dfn );
|
||||||
}
|
}
|
||||||
|
|
||||||
return extend.call( null, base, dfn );
|
return extend.call( null, base, dfn );
|
||||||
|
|
|
@ -60,7 +60,7 @@ require( 'common' ).testCase(
|
||||||
],
|
],
|
||||||
[ 'bar',
|
[ 'bar',
|
||||||
{ 'virtual bar': function() {} },
|
{ 'virtual bar': function() {} },
|
||||||
{ 'override bar': function() {} },
|
{ 'public bar': function() {} },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
@ -200,9 +200,7 @@ require( 'common' ).testCase(
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
this.Class
|
this.Sut( { __construct: function() {} } );
|
||||||
.use( this.Sut( { __construct: function() {} } ) )
|
|
||||||
.extend( {} );
|
|
||||||
}
|
}
|
||||||
catch ( e )
|
catch ( e )
|
||||||
{
|
{
|
||||||
|
@ -222,9 +220,12 @@ require( 'common' ).testCase(
|
||||||
*
|
*
|
||||||
* TODO: conflict resolution through aliasing
|
* TODO: conflict resolution through aliasing
|
||||||
*/
|
*/
|
||||||
'@each(fconflict) Cannot use multiple traits definining same field':
|
'@each(fconflict) Cannot mix in multiple concrete methods of same name':
|
||||||
function( dfns )
|
function( dfns )
|
||||||
{
|
{
|
||||||
|
// TODO: not yet working with composition approach
|
||||||
|
this.skip();
|
||||||
|
|
||||||
var fname = dfns[ 0 ];
|
var fname = dfns[ 0 ];
|
||||||
|
|
||||||
// both traits define `foo'
|
// both traits define `foo'
|
||||||
|
@ -256,41 +257,7 @@ require( 'common' ).testCase(
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
'Private class members are not accessible to used traits': function()
|
||||||
* Once a trait is mixed in, its methods should execute with `this'
|
|
||||||
* bound to the instance of the class that it was mixed into, not the
|
|
||||||
* trait itself. In particular, this means that the trait can access
|
|
||||||
* members of the class in which it mixes into (but see tests that
|
|
||||||
* follow).
|
|
||||||
*/
|
|
||||||
'Trait methods execute within context of the containing class':
|
|
||||||
function()
|
|
||||||
{
|
|
||||||
var expected = 'bar';
|
|
||||||
|
|
||||||
var T = this.Sut(
|
|
||||||
{
|
|
||||||
// attempts to invoke protected method of containing class
|
|
||||||
'public setFoo': function( val ) { this.doSet( val ); },
|
|
||||||
} );
|
|
||||||
|
|
||||||
var C = this.Class.use( T ).extend(
|
|
||||||
{
|
|
||||||
'private _foo': null,
|
|
||||||
'protected doSet': function( val ) { this._foo = val; },
|
|
||||||
'public getFoo': function() { return this._foo; },
|
|
||||||
} );
|
|
||||||
|
|
||||||
// we do not use method chaining for this test just to ensure that
|
|
||||||
// any hiccups with returning `this' from setFoo will not compromise
|
|
||||||
// the assertion
|
|
||||||
var inst = C();
|
|
||||||
inst.setFoo( expected );
|
|
||||||
this.assertEqual( inst.getFoo(), expected );
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
'Private class members are not accessible to useed traits': function()
|
|
||||||
{
|
{
|
||||||
// TODO: this is not yet the case
|
// TODO: this is not yet the case
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue