From 71358eab5940564ea3009ae7df95e411277bc4f2 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Thu, 23 Jan 2014 00:34:15 -0500 Subject: [PATCH] Began implementing composition-based traits As described in . 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. --- lib/ClassBuilder.js | 9 +- lib/Trait.js | 154 ++++++++++++++++++++++++++++++----- lib/class.js | 4 +- test/Trait/DefinitionTest.js | 47 ++--------- 4 files changed, 151 insertions(+), 63 deletions(-) diff --git a/lib/ClassBuilder.js b/lib/ClassBuilder.js index 8b4db46..8947f11 100644 --- a/lib/ClassBuilder.js +++ b/lib/ClassBuilder.js @@ -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 ); diff --git a/lib/Trait.js b/lib/Trait.js index 292065f..29604d3 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -19,6 +19,8 @@ * along with this program. If not, see . */ +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(); + } }; diff --git a/lib/class.js b/lib/class.js index 1d676ef..0a0cdfa 100644 --- a/lib/class.js +++ b/lib/class.js @@ -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 ); diff --git a/test/Trait/DefinitionTest.js b/test/Trait/DefinitionTest.js index bc9facf..8c1dcc9 100644 --- a/test/Trait/DefinitionTest.js +++ b/test/Trait/DefinitionTest.js @@ -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 },