Implemented AbstractClass
- Some of this functionality requires further refactoringclosure/master
parent
623c3df429
commit
e0de030cee
16
lib/class.js
16
lib/class.js
|
@ -329,10 +329,10 @@ function createImplement( base, ifaces, cname )
|
||||||
// If neither of those are available, extend from an empty class.
|
// If neither of those are available, extend from an empty class.
|
||||||
ifaces.push( base || ext_base || extend( {} ) );
|
ifaces.push( base || ext_base || extend( {} ) );
|
||||||
|
|
||||||
return extend.apply( null, [
|
return extend.call( null,
|
||||||
implement.apply( this, ifaces ),
|
implement.apply( this, ifaces ),
|
||||||
def
|
def
|
||||||
] );
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -385,8 +385,9 @@ var implement = function()
|
||||||
len = args.length,
|
len = args.length,
|
||||||
arg = null,
|
arg = null,
|
||||||
|
|
||||||
abstract_list = [],
|
implemented = [],
|
||||||
implemented = [];
|
make_abstract = false
|
||||||
|
;
|
||||||
|
|
||||||
// add each of the interfaces
|
// add each of the interfaces
|
||||||
for ( var i = 0; i < len; i++ )
|
for ( var i = 0; i < len; i++ )
|
||||||
|
@ -398,11 +399,18 @@ var implement = function()
|
||||||
method: function( name, func, is_abstract, keywords )
|
method: function( name, func, is_abstract, keywords )
|
||||||
{
|
{
|
||||||
dest[ name ] = func;
|
dest[ name ] = func;
|
||||||
|
make_abstract = true;
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
implemented.push( arg );
|
implemented.push( arg );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// xxx: temporary
|
||||||
|
if ( make_abstract )
|
||||||
|
{
|
||||||
|
dest.___$$abstract$$ = true;
|
||||||
|
}
|
||||||
|
|
||||||
// create a new class with the implemented abstract methods
|
// create a new class with the implemented abstract methods
|
||||||
var class_new = module.exports.extend( base, dest );
|
var class_new = module.exports.extend( base, dest );
|
||||||
class_builder.getMeta( class_new ).implemented = implemented;
|
class_builder.getMeta( class_new ).implemented = implemented;
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/**
|
||||||
|
* Wrapper permitting the definition of abstract classes
|
||||||
|
*
|
||||||
|
* This doesn't actually introduce any new functionality. Rather, it sets a flag
|
||||||
|
* to allow abstract methods within a class, forcing users to clearly state
|
||||||
|
* that a class is abstract.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2010 Mike Gerwitz
|
||||||
|
*
|
||||||
|
* This file is part of ease.js.
|
||||||
|
*
|
||||||
|
* ease.js is free software: you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Lesser General Public License as published by the Free
|
||||||
|
* Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
||||||
|
* for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* @author Mike Gerwitz
|
||||||
|
* @package core
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Class = require( __dirname + '/class' );
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = exports = function()
|
||||||
|
{
|
||||||
|
markAbstract( arguments );
|
||||||
|
|
||||||
|
// forward everything to Class
|
||||||
|
return Class.apply( this, arguments );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.extend = function()
|
||||||
|
{
|
||||||
|
markAbstract( arguments );
|
||||||
|
|
||||||
|
return Class.extend.apply( this, arguments );
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
exports.implement = function()
|
||||||
|
{
|
||||||
|
var impl = Class.implement.apply( this, arguments ),
|
||||||
|
extend = impl.extend;
|
||||||
|
|
||||||
|
impl.extend = function()
|
||||||
|
{
|
||||||
|
markAbstract( arguments );
|
||||||
|
return extend.apply( this, arguments );
|
||||||
|
};
|
||||||
|
|
||||||
|
return impl;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function markAbstract( args )
|
||||||
|
{
|
||||||
|
// the last argument _should_ be the definition
|
||||||
|
var dfn = args[ args.length - 1 ];
|
||||||
|
|
||||||
|
if ( typeof dfn === 'object' )
|
||||||
|
{
|
||||||
|
// mark as abstract
|
||||||
|
dfn.___$$abstract$$ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -263,6 +263,8 @@ exports.build = function extend()
|
||||||
|
|
||||||
attachFlags( new_class, props );
|
attachFlags( new_class, props );
|
||||||
|
|
||||||
|
validateAbstract( new_class, cname, abstract_methods );
|
||||||
|
|
||||||
// We reduce the overall cost of this definition by defining it on the
|
// We reduce the overall cost of this definition by defining it on the
|
||||||
// prototype rather than during instantiation. While this does increase the
|
// prototype rather than during instantiation. While this does increase the
|
||||||
// amount of time it takes to access the property through the prototype
|
// amount of time it takes to access the property through the prototype
|
||||||
|
@ -286,6 +288,40 @@ exports.build = function extend()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates abstract class requirements
|
||||||
|
*
|
||||||
|
* @param {function()} ctor class
|
||||||
|
* @param {string} cname class name
|
||||||
|
* @param {Object} abstract_methods
|
||||||
|
*
|
||||||
|
* @return {undefined}
|
||||||
|
*/
|
||||||
|
function validateAbstract( ctor, cname, abstract_methods )
|
||||||
|
{
|
||||||
|
if ( ctor.___$$abstract$$ )
|
||||||
|
{
|
||||||
|
if ( abstract_methods.__length === 0 )
|
||||||
|
{
|
||||||
|
throw TypeError(
|
||||||
|
"Class " + ( cname || "(anonymous)" ) + " was declared as " +
|
||||||
|
"abstract, but contains no abstract members"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ( abstract_methods.__length > 0 )
|
||||||
|
{
|
||||||
|
throw TypeError(
|
||||||
|
"Class " + ( cname || "(anonymous)" ) + " contains abstract " +
|
||||||
|
"members and must therefore be declared abstract"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the constructor for a new class
|
* Creates the constructor for a new class
|
||||||
*
|
*
|
||||||
|
@ -1055,9 +1091,10 @@ function attachId( ctor, id )
|
||||||
function attachFlags( ctor, props )
|
function attachFlags( ctor, props )
|
||||||
{
|
{
|
||||||
ctor.___$$final$$ = !!( props.___$$final$$ );
|
ctor.___$$final$$ = !!( props.___$$final$$ );
|
||||||
|
ctor.___$$abstract$$ = !!( props.___$$abstract$$ );
|
||||||
|
|
||||||
// The properties are no longer needed. Set to undefined rather than delete
|
// The properties are no longer needed. Set to undefined rather than delete
|
||||||
// (v8 performance)
|
// (v8 performance)
|
||||||
props.___$$final$$ = undefined;
|
props.___$$final$$ = props.___$$abstract$$ = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,103 @@
|
||||||
|
|
||||||
var common = require( './common' ),
|
var common = require( './common' ),
|
||||||
assert = require( 'assert' ),
|
assert = require( 'assert' ),
|
||||||
|
util = common.require( 'util' ),
|
||||||
|
|
||||||
Class = common.require( 'class' ),
|
Class = common.require( 'class' ),
|
||||||
util = common.require( 'util' );
|
AbstractClass = common.require( 'class_abstract' )
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In order to ensure the code documents itself, we should require that all
|
||||||
|
* classes containing abstract members must themselves be declared as abstract.
|
||||||
|
* Otherwise, you are at the mercy of the developer's documentation/comments to
|
||||||
|
* know whether or not the class is indeed abstract without looking through its
|
||||||
|
* definition.
|
||||||
|
*/
|
||||||
|
( function testMustDeclareClassesWithAbstractMembersAsAbstract()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// should fail; class not declared as abstract
|
||||||
|
Class( 'Foo',
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
assert.ok(
|
||||||
|
e.message.search( 'Foo' ) !== -1,
|
||||||
|
"Abstract class declaration error should contain class name"
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.fail(
|
||||||
|
"Should not be able to declare abstract members unless class is also " +
|
||||||
|
"declared as abstract"
|
||||||
|
);
|
||||||
|
} )();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract members should be permitted if the class itself is declared as
|
||||||
|
* abstract
|
||||||
|
*/
|
||||||
|
( function testCanDeclareClassAsAbstract()
|
||||||
|
{
|
||||||
|
AbstractClass(
|
||||||
|
{
|
||||||
|
'abstract foo': [],
|
||||||
|
} );
|
||||||
|
} )();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a class is declared as abstract, it should contain at least one abstract
|
||||||
|
* method. Otherwise, the abstract definition is pointless and unnecessarily
|
||||||
|
* confusing. The whole point of the declaration is self-documenting code.
|
||||||
|
*/
|
||||||
|
( function testAbstractClassesMustContainAbstractMethods()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// should fail; class not declared as abstract
|
||||||
|
AbstractClass( 'Foo', {} );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
assert.ok(
|
||||||
|
e.message.search( 'Foo' ) !== -1,
|
||||||
|
"Abstract class declaration error should contain class name"
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.fail(
|
||||||
|
"Abstract classes should contain at least one abstract method"
|
||||||
|
);
|
||||||
|
} )();
|
||||||
|
|
||||||
|
|
||||||
|
( function testAbstractClassContainsExtendMethod()
|
||||||
|
{
|
||||||
|
assert.ok( typeof AbstractClass.extend === 'function',
|
||||||
|
"AbstractClass contains extend method"
|
||||||
|
);
|
||||||
|
} )();
|
||||||
|
|
||||||
|
|
||||||
|
( function testAbstractClassContainsImplementMethod()
|
||||||
|
{
|
||||||
|
assert.ok( typeof AbstractClass.implement === 'function',
|
||||||
|
"AbstractClass contains implement method"
|
||||||
|
);
|
||||||
|
} )();
|
||||||
|
|
||||||
|
|
||||||
// not abstract
|
// not abstract
|
||||||
var Foo = Class.extend( {} );
|
var Foo = Class.extend( {} );
|
||||||
|
@ -33,7 +128,7 @@ var Foo = Class.extend( {} );
|
||||||
// abstract (ctor_called is not a class member to ensure that visibility bugs do
|
// abstract (ctor_called is not a class member to ensure that visibility bugs do
|
||||||
// not impact our test)
|
// not impact our test)
|
||||||
var ctor_called = false,
|
var ctor_called = false,
|
||||||
AbstractFoo = Class.extend(
|
AbstractFoo = AbstractClass.extend(
|
||||||
{
|
{
|
||||||
__construct: function()
|
__construct: function()
|
||||||
{
|
{
|
||||||
|
@ -48,7 +143,7 @@ var ctor_called = false,
|
||||||
|
|
||||||
// still abstract (didn't provide a concrete implementation of both abstract
|
// still abstract (didn't provide a concrete implementation of both abstract
|
||||||
// methods)
|
// methods)
|
||||||
var SubAbstractFoo = AbstractFoo.extend(
|
var SubAbstractFoo = AbstractClass.extend( AbstractFoo,
|
||||||
{
|
{
|
||||||
second: function()
|
second: function()
|
||||||
{
|
{
|
||||||
|
@ -56,7 +151,7 @@ var SubAbstractFoo = AbstractFoo.extend(
|
||||||
});
|
});
|
||||||
|
|
||||||
// concrete
|
// concrete
|
||||||
var ConcreteFoo = AbstractFoo.extend(
|
var ConcreteFoo = Class.extend( AbstractFoo,
|
||||||
{
|
{
|
||||||
method: function( one, two, three )
|
method: function( one, two, three )
|
||||||
{
|
{
|
||||||
|
@ -193,7 +288,7 @@ var ConcreteFoo = AbstractFoo.extend(
|
||||||
assert.doesNotThrow(
|
assert.doesNotThrow(
|
||||||
function()
|
function()
|
||||||
{
|
{
|
||||||
AbstractFoo.extend(
|
AbstractClass.extend( AbstractFoo,
|
||||||
{
|
{
|
||||||
// incorrect number of arguments
|
// incorrect number of arguments
|
||||||
'abstract method': [ 'one', 'two', 'three', 'four' ],
|
'abstract method': [ 'one', 'two', 'three', 'four' ],
|
||||||
|
@ -212,7 +307,7 @@ var ConcreteFoo = AbstractFoo.extend(
|
||||||
assert.doesNotThrow(
|
assert.doesNotThrow(
|
||||||
function()
|
function()
|
||||||
{
|
{
|
||||||
AbstractFoo.extend(
|
AbstractClass.extend( AbstractFoo,
|
||||||
{
|
{
|
||||||
second: function( foo )
|
second: function( foo )
|
||||||
{
|
{
|
||||||
|
@ -257,7 +352,8 @@ var ConcreteFoo = AbstractFoo.extend(
|
||||||
{
|
{
|
||||||
assert.doesNotThrow( function()
|
assert.doesNotThrow( function()
|
||||||
{
|
{
|
||||||
SubAbstractFoo.extend( {
|
Class.extend( SubAbstractFoo,
|
||||||
|
{
|
||||||
// concrete, so the result would otherwise not be abstract
|
// concrete, so the result would otherwise not be abstract
|
||||||
'method': function( one, two, three ) {},
|
'method': function( one, two, three ) {},
|
||||||
|
|
||||||
|
|
|
@ -24,8 +24,11 @@
|
||||||
|
|
||||||
var common = require( './common' ),
|
var common = require( './common' ),
|
||||||
assert = require( 'assert' ),
|
assert = require( 'assert' ),
|
||||||
|
|
||||||
Class = common.require( 'class' ),
|
Class = common.require( 'class' ),
|
||||||
Interface = common.require( 'interface' );
|
Interface = common.require( 'interface' ),
|
||||||
|
AbstractClass = common.require( 'class_abstract' )
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
var Type = Interface.extend( {
|
var Type = Interface.extend( {
|
||||||
|
@ -97,7 +100,7 @@ var Type = Interface.extend( {
|
||||||
*/
|
*/
|
||||||
( function testAbstractMethodsCopiedIntoNewClassUsingEmptyBase()
|
( function testAbstractMethodsCopiedIntoNewClassUsingEmptyBase()
|
||||||
{
|
{
|
||||||
Foo = Class.implement( Type, Type2 ).extend( {} );
|
Foo = AbstractClass.implement( Type, Type2 ).extend( {} );
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
( ( Foo.prototype.foo instanceof Function )
|
( ( Foo.prototype.foo instanceof Function )
|
||||||
|
@ -136,7 +139,7 @@ var Type = Interface.extend( {
|
||||||
|
|
||||||
( function testAbstractMethodsCopiedIntoNewClassUsingExistingBase()
|
( function testAbstractMethodsCopiedIntoNewClassUsingExistingBase()
|
||||||
{
|
{
|
||||||
PlainFoo2 = PlainFoo.implement( Type, Type2 ).extend( {} );
|
PlainFoo2 = AbstractClass.implement( Type, Type2 ).extend( PlainFoo, {} );
|
||||||
|
|
||||||
assert.ok(
|
assert.ok(
|
||||||
( ( PlainFoo2.prototype.foo instanceof Function )
|
( ( PlainFoo2.prototype.foo instanceof Function )
|
||||||
|
@ -224,7 +227,7 @@ var Type = Interface.extend( {
|
||||||
function()
|
function()
|
||||||
{
|
{
|
||||||
// this /should/ work
|
// this /should/ work
|
||||||
Class.implement( Type ).extend( PlainFoo, {} );
|
AbstractClass.implement( Type ).extend( PlainFoo, {} );
|
||||||
},
|
},
|
||||||
Error,
|
Error,
|
||||||
"Can specify parent for exetnd() when implementing atop an " +
|
"Can specify parent for exetnd() when implementing atop an " +
|
||||||
|
|
|
@ -26,6 +26,7 @@ var common = require( './common' ),
|
||||||
assert = require( 'assert' ),
|
assert = require( 'assert' ),
|
||||||
|
|
||||||
Class = common.require( 'class' ),
|
Class = common.require( 'class' ),
|
||||||
|
AbstractClass = common.require( 'class_abstract' ),
|
||||||
Interface = common.require( 'interface' )
|
Interface = common.require( 'interface' )
|
||||||
;
|
;
|
||||||
|
|
||||||
|
@ -128,7 +129,7 @@ var common = require( './common' ),
|
||||||
|
|
||||||
// abstract
|
// abstract
|
||||||
assert.equal(
|
assert.equal(
|
||||||
Class( { 'abstract foo': [] } ).toString(),
|
AbstractClass( { 'abstract foo': [] } ).toString(),
|
||||||
'(AbstractClass)',
|
'(AbstractClass)',
|
||||||
"Converting abstract anonymous class to string yields class string"
|
"Converting abstract anonymous class to string yields class string"
|
||||||
);
|
);
|
||||||
|
@ -152,7 +153,7 @@ var common = require( './common' ),
|
||||||
|
|
||||||
// abstract
|
// abstract
|
||||||
assert.equal(
|
assert.equal(
|
||||||
Class( name, { 'abstract foo': [] } ).toString(),
|
AbstractClass( name, { 'abstract foo': [] } ).toString(),
|
||||||
name,
|
name,
|
||||||
"Converting abstract named class to string yields string with name " +
|
"Converting abstract named class to string yields string with name " +
|
||||||
"of class"
|
"of class"
|
||||||
|
|
|
@ -29,7 +29,7 @@ RMTRAIL="$PATH_TOOLS/rmtrail"
|
||||||
|
|
||||||
# order matters
|
# order matters
|
||||||
CAT_MODULES="prop_parser util propobj member_builder class_builder"
|
CAT_MODULES="prop_parser util propobj member_builder class_builder"
|
||||||
CAT_MODULES="$CAT_MODULES class class_final interface"
|
CAT_MODULES="$CAT_MODULES class class_final class_abstract interface"
|
||||||
|
|
||||||
##
|
##
|
||||||
# Output template header
|
# Output template header
|
||||||
|
|
Loading…
Reference in New Issue