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. // 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;

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

View File

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

View File

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

View File

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

View File

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