Implemented and abstract with mixins properly handled
Classes will now properly be recognized as concrete or abstract when mixing in any number of traits, optionally in conjunction with interfaces.perfodd
parent
696b8d05a6
commit
255a60e425
22
lib/class.js
22
lib/class.js
|
@ -346,7 +346,7 @@ function createImplement( base, ifaces, cname )
|
||||||
{
|
{
|
||||||
// Defer processing until after extend(). This also ensures that implement()
|
// Defer processing until after extend(). This also ensures that implement()
|
||||||
// returns nothing usable.
|
// returns nothing usable.
|
||||||
return {
|
var partial = {
|
||||||
extend: function()
|
extend: function()
|
||||||
{
|
{
|
||||||
var args = Array.prototype.slice.call( arguments ),
|
var args = Array.prototype.slice.call( arguments ),
|
||||||
|
@ -390,15 +390,25 @@ function createImplement( base, ifaces, cname )
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: this is a naive implementation that works, but could be
|
||||||
|
// much more performant (it creates a subtype before mixing in)
|
||||||
use: function()
|
use: function()
|
||||||
{
|
{
|
||||||
var traits = Array.prototype.slice.call( arguments );
|
var traits = Array.prototype.slice.call( arguments );
|
||||||
return createUse(
|
return createUse(
|
||||||
function() { return base; },
|
function() { return partial.__createBase(); },
|
||||||
traits
|
traits
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// allows overriding default behavior
|
||||||
|
__createBase: function()
|
||||||
|
{
|
||||||
|
return partial.extend( {} );
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return partial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -478,13 +488,19 @@ function createUse( basef, traits, nonbase )
|
||||||
return createUse(
|
return createUse(
|
||||||
function()
|
function()
|
||||||
{
|
{
|
||||||
return partial.extend( {} )
|
return partial.__createBase();
|
||||||
},
|
},
|
||||||
Array.prototype.slice.call( arguments ),
|
Array.prototype.slice.call( arguments ),
|
||||||
nonbase
|
nonbase
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// allows overriding default behavior
|
||||||
|
partial.__createBase = function()
|
||||||
|
{
|
||||||
|
return partial.extend( {} );
|
||||||
|
};
|
||||||
|
|
||||||
return partial;
|
return partial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,7 +122,8 @@ function markAbstract( args )
|
||||||
function abstractOverride( obj )
|
function abstractOverride( obj )
|
||||||
{
|
{
|
||||||
var extend = obj.extend,
|
var extend = obj.extend,
|
||||||
impl = obj.implement;
|
impl = obj.implement,
|
||||||
|
use = obj.use;
|
||||||
|
|
||||||
// wrap and apply the abstract flag, only if the method is defined (it
|
// wrap and apply the abstract flag, only if the method is defined (it
|
||||||
// may not be under all circumstances, e.g. after an implement())
|
// may not be under all circumstances, e.g. after an implement())
|
||||||
|
@ -131,6 +132,12 @@ function abstractOverride( obj )
|
||||||
return abstractOverride( impl.apply( this, arguments ) );
|
return abstractOverride( impl.apply( this, arguments ) );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
var mixin = false;
|
||||||
|
use && ( obj.use = function()
|
||||||
|
{
|
||||||
|
return abstractOverride( use.apply( this, arguments ) );
|
||||||
|
} );
|
||||||
|
|
||||||
// wrap extend, applying the abstract flag
|
// wrap extend, applying the abstract flag
|
||||||
obj.extend = function()
|
obj.extend = function()
|
||||||
{
|
{
|
||||||
|
@ -138,6 +145,14 @@ function abstractOverride( obj )
|
||||||
return extend.apply( this, arguments );
|
return extend.apply( this, arguments );
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// used by mixins; we need to mark the intermediate subtype as abstract,
|
||||||
|
// but ensure we don't throw any errors if no abstract members are mixed
|
||||||
|
// in (since thay may be mixed in later on)
|
||||||
|
obj.__createBase = function()
|
||||||
|
{
|
||||||
|
return extend( { ___$$auto$abstract$$: true } );
|
||||||
|
};
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -224,4 +224,140 @@ require( 'common' ).testCase(
|
||||||
C().doFoo();
|
C().doFoo();
|
||||||
this.assertOk( called );
|
this.assertOk( called );
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that chained mixins (that is, calling `use' multiple times
|
||||||
|
* independently) maintains the use of AbstractClass, and properly
|
||||||
|
* performs the abstract check at the final `extend' call.
|
||||||
|
*/
|
||||||
|
'Chained mixins properly carry abstract flag': function()
|
||||||
|
{
|
||||||
|
var _self = this,
|
||||||
|
Ta = this.Sut( { foo: function() {} } ),
|
||||||
|
Tc = this.Sut( { baz: function() {} } ),
|
||||||
|
Tab = this.Sut( { 'abstract baz': [] } );
|
||||||
|
|
||||||
|
// ensure that abstract definitions are carried through properly
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
// single, abstract
|
||||||
|
_self.assertOk(
|
||||||
|
_self.AbstractClass
|
||||||
|
.use( Tab )
|
||||||
|
.extend( {} )
|
||||||
|
.isAbstract()
|
||||||
|
);
|
||||||
|
|
||||||
|
// single, concrete
|
||||||
|
_self.assertOk(
|
||||||
|
_self.AbstractClass
|
||||||
|
.use( Ta )
|
||||||
|
.extend( { 'abstract baz': [] } )
|
||||||
|
.isAbstract()
|
||||||
|
);
|
||||||
|
|
||||||
|
// chained, both
|
||||||
|
_self.assertOk(
|
||||||
|
_self.AbstractClass
|
||||||
|
.use( Ta )
|
||||||
|
.use( Tab )
|
||||||
|
.extend( {} )
|
||||||
|
.isAbstract()
|
||||||
|
|
||||||
|
);
|
||||||
|
_self.assertOk(
|
||||||
|
_self.AbstractClass
|
||||||
|
.use( Tab )
|
||||||
|
.use( Ta )
|
||||||
|
.extend( {} )
|
||||||
|
.isAbstract()
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
// and then ensure that we will properly throw an exception if not
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// not abstract
|
||||||
|
_self.AbstractClass.use( Tc ).extend( {} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// initially abstract, but then not (by extend)
|
||||||
|
_self.AbstractClass.use( Tab ).extend(
|
||||||
|
{
|
||||||
|
// concrete definition; no longer abstract
|
||||||
|
baz: function() {},
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// initially abstract, but then second mix provides a concrete
|
||||||
|
// definition
|
||||||
|
_self.AbstractClass.use( Tab ).use( Tc ).extend( {} );
|
||||||
|
} );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mixins can make a class auto-abstract (that is, not require the use
|
||||||
|
* of AbstractClass for the mixin) in order to permit the use of
|
||||||
|
* Type.use when the intent is not to subclass, but to decorate (yes,
|
||||||
|
* the result is still a subtype). Let's make sure that we're not
|
||||||
|
* breaking the AbstractClass requirement, whose sole purpose is to aid
|
||||||
|
* in documentation by creating self-documenting code.
|
||||||
|
*/
|
||||||
|
'Explicitly-declared class will not be automatically abstract':
|
||||||
|
function()
|
||||||
|
{
|
||||||
|
var _self = this,
|
||||||
|
Tc = this.Sut( { foo: function() {} } ),
|
||||||
|
Ta = this.Sut( { 'abstract foo': [], } );
|
||||||
|
|
||||||
|
// if we provide no abstract methods, then declaring the class as
|
||||||
|
// abstract should result in an error
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
// no abstract methods
|
||||||
|
_self.assertOk( !(
|
||||||
|
_self.AbstractClass.use( Tc ).extend( {} ).isAbstract()
|
||||||
|
) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// similarily, if we provide abstract methods, then there should be
|
||||||
|
// no error
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
// abstract methods via extend
|
||||||
|
_self.assertOk(
|
||||||
|
_self.AbstractClass.use( Tc ).extend(
|
||||||
|
{
|
||||||
|
'abstract bar': [],
|
||||||
|
} ).isAbstract()
|
||||||
|
);
|
||||||
|
|
||||||
|
// abstract via trait
|
||||||
|
_self.assertOk(
|
||||||
|
_self.AbstractClass.use( Ta ).extend( {} ).isAbstract()
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
|
||||||
|
// if we provide abstract methods, then we should not be able to
|
||||||
|
// declare a class as concrete
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
_self.Class.use( Tc ).extend(
|
||||||
|
{
|
||||||
|
'abstract bar': [],
|
||||||
|
} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// similar to above, but via trait
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
_self.Class.use( Ta ).extend();
|
||||||
|
} );
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -59,6 +59,8 @@ require( 'common' ).testCase(
|
||||||
{ 'protected foo': function() {} },
|
{ 'protected foo': function() {} },
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
this.base = [ this.Class ];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -329,6 +331,43 @@ require( 'common' ).testCase(
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that the staging object created by the `implement' call
|
||||||
|
* exposes a `use' method (and properly applies it).
|
||||||
|
*/
|
||||||
|
'Can mix traits into class after implementing interface': function()
|
||||||
|
{
|
||||||
|
var _self = this,
|
||||||
|
called = false,
|
||||||
|
|
||||||
|
T = this.Sut( { foo: function() { called = true; } } ),
|
||||||
|
I = this.Interface( { bar: [] } ),
|
||||||
|
A = null;
|
||||||
|
|
||||||
|
// by declaring this abstract, we ensure that the interface was
|
||||||
|
// actually implemented (otherwise, all methods would be concrete,
|
||||||
|
// resulting in an error)
|
||||||
|
this.assertDoesNotThrow( function()
|
||||||
|
{
|
||||||
|
A = _self.AbstractClass.implement( I ).use( T ).extend( {} );
|
||||||
|
_self.assertOk( A.isAbstract() );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// ensure that we actually fail if there's no interface implemented
|
||||||
|
// (and thus no abstract members); if we fail and the previous test
|
||||||
|
// succeeds, that implies that somehow the mixin is causing the
|
||||||
|
// class to become abstract, and that is an issue (and the reason
|
||||||
|
// for this seemingly redundant test)
|
||||||
|
this.assertThrows( function()
|
||||||
|
{
|
||||||
|
_self.Class.implement( I ).use( T ).extend( {} );
|
||||||
|
} );
|
||||||
|
|
||||||
|
A.extend( { bar: function() {} } )().foo();
|
||||||
|
this.assertOk( called );
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a trait is mixed into a class, it acts as though it is part of
|
* 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
|
* that class. Therefore, it should stand to reason that, when a mixed
|
||||||
|
|
Loading…
Reference in New Issue