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
|
||||
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
|
||||
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
|
||||
// constructor will be applied to subtypes without a problem
|
||||
this.__construct.apply( this, ( args || arguments ) );
|
||||
args = null;
|
||||
}
|
||||
|
||||
args = null;
|
||||
|
||||
// attach any instance properties/methods (done after
|
||||
// constructor to ensure they are not overridden)
|
||||
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/>.
|
||||
*/
|
||||
|
||||
var AbstractClass = require( __dirname + '/class_abstract' );
|
||||
|
||||
|
||||
function Trait()
|
||||
{
|
||||
|
@ -33,13 +35,34 @@ function Trait()
|
|||
|
||||
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()
|
||||
{
|
||||
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.__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;
|
||||
};
|
||||
|
@ -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
|
||||
*
|
||||
* 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 {Object} dfn definition object to merge into
|
||||
*
|
||||
* @return {Object} dfn
|
||||
*/
|
||||
Trait.mixin = function( trait, dfn )
|
||||
function mixin( trait, dfn )
|
||||
{
|
||||
var tdfn = trait.__dfn || {};
|
||||
for ( var f in tdfn )
|
||||
// the abstract class hidden within the trait
|
||||
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,
|
||||
// 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 ] )
|
||||
if ( !( Object.hasOwnProperty.call( pub, f ) ) )
|
||||
{
|
||||
// TODO: conflcit resolution
|
||||
throw Error( "Trait field `" + f + "' conflits" );
|
||||
}
|
||||
else if ( f.match( /\b__construct\b/ ) )
|
||||
{
|
||||
throw Error( "Traits may not define __construct" );
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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' ),
|
||||
wrappers = require( __dirname + '/MethodWrappers' ).standard,
|
||||
|
||||
Trait = require( __dirname + '/Trait' ),
|
||||
|
||||
class_builder = ClassBuilder(
|
||||
require( __dirname + '/MemberBuilder' )(
|
||||
MethodWrapperFactory( wrappers.wrapNew ),
|
||||
|
@ -383,7 +381,7 @@ function createUse( base, traits )
|
|||
// "mix" each trait into the provided definition object
|
||||
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 );
|
||||
|
|
|
@ -60,7 +60,7 @@ require( 'common' ).testCase(
|
|||
],
|
||||
[ 'bar',
|
||||
{ 'virtual bar': function() {} },
|
||||
{ 'override bar': function() {} },
|
||||
{ 'public bar': function() {} },
|
||||
],
|
||||
];
|
||||
},
|
||||
|
@ -200,9 +200,7 @@ require( 'common' ).testCase(
|
|||
{
|
||||
try
|
||||
{
|
||||
this.Class
|
||||
.use( this.Sut( { __construct: function() {} } ) )
|
||||
.extend( {} );
|
||||
this.Sut( { __construct: function() {} } );
|
||||
}
|
||||
catch ( e )
|
||||
{
|
||||
|
@ -222,9 +220,12 @@ require( 'common' ).testCase(
|
|||
*
|
||||
* 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 )
|
||||
{
|
||||
// TODO: not yet working with composition approach
|
||||
this.skip();
|
||||
|
||||
var fname = dfns[ 0 ];
|
||||
|
||||
// both traits define `foo'
|
||||
|
@ -256,41 +257,7 @@ require( 'common' ).testCase(
|
|||
},
|
||||
|
||||
|
||||
/**
|
||||
* 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()
|
||||
'Private class members are not accessible to used traits': function()
|
||||
{
|
||||
// TODO: this is not yet the case
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue