1
0
Fork 0

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
Mike Gerwitz 2014-03-09 21:13:11 -04:00
parent 696b8d05a6
commit 255a60e425
4 changed files with 210 additions and 4 deletions

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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();
} );
},
} ); } );

View File

@ -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