1
0
Fork 0

Class definition mixin now requires explicit extend

See the rather verbose docblocks in this diff for more information.
Additional rationale will be contained in the commits that follow.
perfodd
Mike Gerwitz 2014-03-08 23:52:47 -05:00
parent a7e381a31e
commit 696b8d05a6
4 changed files with 139 additions and 13 deletions

View File

@ -45,6 +45,8 @@ var util = require( __dirname + '/util' ),
)
;
var _nullf = function() { return null; }
/**
* This module may be invoked in order to provide a more natural looking class
@ -120,11 +122,25 @@ module.exports.implement = function( interfaces )
};
/**
* Mix a trait into a class
*
* The ultimate intent of this depends on the ultimate `extend' call---if it
* extends another class, then the traits will be mixed into that class;
* otherwise, the traits will be mixed into the base class. In either case,
* a final `extend' call is necessary to complete the definition. An attempt
* to instantiate the return value before invoking `extend' will result in
* an exception.
*
* @param {Array.<Function>} traits traits to mix in
*
* @return {Function} staging object for class definition
*/
module.exports.use = function( traits )
{
// consume traits onto an empty base
return createUse(
null,
_nullf,
Array.prototype.slice.call( arguments )
);
};
@ -304,7 +320,7 @@ function createStaging( cname )
use: function()
{
return createUse(
null,
_nullf,
Array.prototype.slice.call( arguments )
);
},
@ -373,27 +389,79 @@ function createImplement( base, ifaces, cname )
def
);
},
use: function()
{
var traits = Array.prototype.slice.call( arguments );
return createUse(
function() { return base; },
traits
);
},
};
}
function createUse( base, traits )
/**
* Create a staging object representing an eventual mixin
*
* This staging objects prepares a class definition for trait mixin. In
* particular, the returned staging object has the following features:
* - invoking it will, if mixing into an existing (non-base) class without
* subclassing, immediately complete the mixin and instantiate the
* generated class;
* - calling `use' has the effect of chaining mixins, stacking them atop
* of one-another; and
* - invoking `extend' will immediately complete the mixin, resulting in a
* subtype of the base.
*
* Mixins are performed lazily---the actual mixin will not take place until
* the final `extend' call, which may be implicit by invoking the staging
* object (performing instantiation).
*
* The third argument determines whether or not a final `extend' call must
* be explicit: in this case, any instantiation attempts will result in an
* exception being thrown.
*
* @param {function()} basef returns base from which to lazily
* extend
* @param {Array.<Function>} traits traits to mix in
* @param {boolean} nonbase extending from a non-base class
* (setting will permit instantiation
* with implicit extend)
*
* @return {Function} staging object for mixin
*/
function createUse( basef, traits, nonbase )
{
// invoking the partially applied class will immediately complete its
// definition and instantiate it with the provided constructor arguments
var partial = function()
{
return createMixedClass( base, traits )
// this argument will be set only in the case where an existing
// (non-base) class is extended, meaning that an explict Class or
// AbstractClass was not provided
if ( !( nonbase ) )
{
throw TypeError(
"Cannot instantiate incomplete class definition; did " +
"you forget to call `extend'?"
);
}
return createMixedClass( basef(), traits )
.apply( null, arguments );
};
// otherwise, its definition is deferred until additional context is
// given during the extend operation
partial.extend = function()
{
var args = Array.prototype.slice.call( arguments ),
dfn = args.pop(),
ext_base = args.pop();
ext_base = args.pop(),
base = basef();
// extend the mixed class, which ensures that all super references
// are properly resolved
@ -407,7 +475,14 @@ function createUse( base, traits )
// call simply to mix in another trait
partial.use = function()
{
return partial.extend( {} ).use.apply( null, arguments );
return createUse(
function()
{
return partial.extend( {} )
},
Array.prototype.slice.call( arguments ),
nonbase
);
};
return partial;
@ -609,8 +684,9 @@ function attachUse( func )
util.defineSecureProp( func, 'use', function()
{
return createUse(
func,
Array.prototype.slice.call( arguments )
function() { return func; },
Array.prototype.slice.call( arguments ),
true
);
} );
}

View File

@ -128,7 +128,7 @@ require( 'common' ).testCase(
{
// should invoke concrete foo; class definition should not fail,
// because foo is no longer abstract
Class.use( T )().foo();
Class.use( T ).extend( {} )().foo();
} );
this.assertOk( called );
@ -148,7 +148,7 @@ require( 'common' ).testCase(
T = this.Sut.implement( I ).extend( {} );
this.assertOk(
this.Class.isA( I, this.Class.use( T )() )
this.Class.isA( I, this.Class.use( T ).extend( {} )() )
);
},

View File

@ -25,6 +25,8 @@ require( 'common' ).testCase(
{
this.Sut = this.require( 'Trait' );
this.Class = this.require( 'class' );
this.Interface = this.require( 'interface' );
this.AbstractClass = this.require( 'class_abstract' );
// means of creating anonymous traits
this.ctor = [
@ -282,6 +284,51 @@ require( 'common' ).testCase(
},
/**
* When explicitly defining a class (that is, not mixing into an
* existing class definition), which involves the use of Class or
* AbstractClass, mixins must be terminated with a call to `extend'.
* This allows the system to make a final determination as to whether
* the resulting class is abstract.
*
* Contrast this with Type.use( T )( ... ), where Type is not the base
* class (Class) or AbstractClass.
*/
'Explicit class definitions must be terminated by an extend call':
function()
{
var _self = this,
Ta = this.Sut( { foo: function() {} } ),
Tb = this.Sut( { bar: function() {} } );
// does not complete with call to `extend'
this.assertThrows( function()
{
_self.Class.use( Ta )();
}, TypeError );
// nested uses; does not complete
this.assertThrows( function()
{
_self.Class.use( Ta ).use( Tb )();
}, TypeError );
// similar to above, with abstract; note that we're checking for
// TypeError here
this.assertThrows( function()
{
_self.AbstractClass.use( Ta )();
}, TypeError );
// does complete; OK
this.assertDoesNotThrow( function()
{
_self.Class.use( Ta ).extend( {} )();
_self.Class.use( Ta ).use( Tb ).extend( {} )();
} );
},
/**
* When a trait is mixed into a class, it acts as though it is part of
* that class. Therefore, it should stand to reason that, when a mixed

View File

@ -35,6 +35,9 @@ require( 'common' ).testCase(
* of the trait (so long as that is permitted). While this test exists
* to ensure consistency throughout the system, it may be helpful in
* situations where a trait is useful on its own.
*
* Note that we cannot simply use Class.use( T ), because this sets up a
* concrete class definition, not an immediate mixin.
*/
'Invoking partial class after mixin instantiates': function()
{
@ -49,7 +52,7 @@ require( 'common' ).testCase(
} );
// mixes T into an empty base class and instantiates
this.Class.use( T )().foo();
this.Class.extend( {} ).use( T )().foo();
this.assertOk( called );
},