1
0
Fork 0

Implemented AbstractClass

- Some of this functionality requires further refactoring
closure/master
Mike Gerwitz 2011-05-22 13:57:56 -04:00
parent 623c3df429
commit e0de030cee
7 changed files with 244 additions and 24 deletions

View File

@ -329,10 +329,10 @@ function createImplement( base, ifaces, cname )
// If neither of those are available, extend from an empty class.
ifaces.push( base || ext_base || extend( {} ) );
return extend.apply( null, [
return extend.call( null,
implement.apply( this, ifaces ),
def
] );
);
},
};
}
@ -385,8 +385,9 @@ var implement = function()
len = args.length,
arg = null,
abstract_list = [],
implemented = [];
implemented = [],
make_abstract = false
;
// add each of the interfaces
for ( var i = 0; i < len; i++ )
@ -398,11 +399,18 @@ var implement = function()
method: function( name, func, is_abstract, keywords )
{
dest[ name ] = func;
make_abstract = true;
},
} );
implemented.push( arg );
}
// xxx: temporary
if ( make_abstract )
{
dest.___$$abstract$$ = true;
}
// create a new class with the implemented abstract methods
var class_new = module.exports.extend( base, dest );
class_builder.getMeta( class_new ).implemented = implemented;

View File

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

View File

@ -263,6 +263,8 @@ exports.build = function extend()
attachFlags( new_class, props );
validateAbstract( new_class, cname, abstract_methods );
// We reduce the overall cost of this definition by defining it on the
// prototype rather than during instantiation. While this does increase the
// 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
*
@ -1055,9 +1091,10 @@ function attachId( ctor, id )
function attachFlags( ctor, props )
{
ctor.___$$final$$ = !!( props.___$$final$$ );
ctor.___$$abstract$$ = !!( props.___$$abstract$$ );
// The properties are no longer needed. Set to undefined rather than delete
// (v8 performance)
props.___$$final$$ = undefined;
props.___$$final$$ = props.___$$abstract$$ = undefined;
}

View File

@ -24,8 +24,103 @@
var common = require( './common' ),
assert = require( 'assert' ),
util = common.require( 'util' ),
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
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
// not impact our test)
var ctor_called = false,
AbstractFoo = Class.extend(
AbstractFoo = AbstractClass.extend(
{
__construct: function()
{
@ -48,7 +143,7 @@ var ctor_called = false,
// still abstract (didn't provide a concrete implementation of both abstract
// methods)
var SubAbstractFoo = AbstractFoo.extend(
var SubAbstractFoo = AbstractClass.extend( AbstractFoo,
{
second: function()
{
@ -56,7 +151,7 @@ var SubAbstractFoo = AbstractFoo.extend(
});
// concrete
var ConcreteFoo = AbstractFoo.extend(
var ConcreteFoo = Class.extend( AbstractFoo,
{
method: function( one, two, three )
{
@ -193,7 +288,7 @@ var ConcreteFoo = AbstractFoo.extend(
assert.doesNotThrow(
function()
{
AbstractFoo.extend(
AbstractClass.extend( AbstractFoo,
{
// incorrect number of arguments
'abstract method': [ 'one', 'two', 'three', 'four' ],
@ -212,7 +307,7 @@ var ConcreteFoo = AbstractFoo.extend(
assert.doesNotThrow(
function()
{
AbstractFoo.extend(
AbstractClass.extend( AbstractFoo,
{
second: function( foo )
{
@ -257,7 +352,8 @@ var ConcreteFoo = AbstractFoo.extend(
{
assert.doesNotThrow( function()
{
SubAbstractFoo.extend( {
Class.extend( SubAbstractFoo,
{
// concrete, so the result would otherwise not be abstract
'method': function( one, two, three ) {},

View File

@ -24,8 +24,11 @@
var common = require( './common' ),
assert = require( 'assert' ),
Class = common.require( 'class' ),
Interface = common.require( 'interface' );
Interface = common.require( 'interface' ),
AbstractClass = common.require( 'class_abstract' )
;
var Type = Interface.extend( {
@ -97,7 +100,7 @@ var Type = Interface.extend( {
*/
( function testAbstractMethodsCopiedIntoNewClassUsingEmptyBase()
{
Foo = Class.implement( Type, Type2 ).extend( {} );
Foo = AbstractClass.implement( Type, Type2 ).extend( {} );
assert.ok(
( ( Foo.prototype.foo instanceof Function )
@ -136,7 +139,7 @@ var Type = Interface.extend( {
( function testAbstractMethodsCopiedIntoNewClassUsingExistingBase()
{
PlainFoo2 = PlainFoo.implement( Type, Type2 ).extend( {} );
PlainFoo2 = AbstractClass.implement( Type, Type2 ).extend( PlainFoo, {} );
assert.ok(
( ( PlainFoo2.prototype.foo instanceof Function )
@ -224,7 +227,7 @@ var Type = Interface.extend( {
function()
{
// this /should/ work
Class.implement( Type ).extend( PlainFoo, {} );
AbstractClass.implement( Type ).extend( PlainFoo, {} );
},
Error,
"Can specify parent for exetnd() when implementing atop an " +

View File

@ -26,6 +26,7 @@ var common = require( './common' ),
assert = require( 'assert' ),
Class = common.require( 'class' ),
AbstractClass = common.require( 'class_abstract' ),
Interface = common.require( 'interface' )
;
@ -128,7 +129,7 @@ var common = require( './common' ),
// abstract
assert.equal(
Class( { 'abstract foo': [] } ).toString(),
AbstractClass( { 'abstract foo': [] } ).toString(),
'(AbstractClass)',
"Converting abstract anonymous class to string yields class string"
);
@ -152,7 +153,7 @@ var common = require( './common' ),
// abstract
assert.equal(
Class( name, { 'abstract foo': [] } ).toString(),
AbstractClass( name, { 'abstract foo': [] } ).toString(),
name,
"Converting abstract named class to string yields string with name " +
"of class"

View File

@ -29,7 +29,7 @@ RMTRAIL="$PATH_TOOLS/rmtrail"
# order matters
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