From 7a579ab2aa56026cd0db0aa80096ee117fca4e23 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Tue, 9 Aug 2011 17:27:26 -0400 Subject: [PATCH] Initial refactoring of class_builder module into ClassBuilder ctor (#25) --- lib/{class_builder.js => ClassBuilder.js} | 530 +++++++++--------- lib/class.js | 18 +- test/test-class_builder-const.js | 4 +- test/test-class_builder-final.js | 4 +- .../test-class_builder-member-restrictions.js | 22 +- test/test-class_builder-static.js | 10 +- test/test-class_builder-visibility.js | 4 +- test/test-member_builder-method-hiding.js | 4 +- tools/combine | 2 +- 9 files changed, 323 insertions(+), 275 deletions(-) rename lib/{class_builder.js => ClassBuilder.js} (92%) diff --git a/lib/class_builder.js b/lib/ClassBuilder.js similarity index 92% rename from lib/class_builder.js rename to lib/ClassBuilder.js index 73ab67b..b4e9ab8 100644 --- a/lib/class_builder.js +++ b/lib/ClassBuilder.js @@ -27,42 +27,26 @@ * class tests in tact for a higher-level test. */ -var util = require( __dirname + '/util' ), - warn = require( __dirname + '/warn' ), - member_builder = require( __dirname + '/member_builder' ), - propobj = require( __dirname + '/propobj' ), - +var util = require( __dirname + '/util' ), + warn = require( __dirname + '/warn' ), + propobj = require( __dirname + '/propobj' ), Warning = warn.Warning, /** - * Class id counter, to be increment on each new definition - * @type {number} - */ - class_id = 0, - - /** - * Instance id counter, to be incremented on each new instance - * @type {number} - */ - instance_id = 0, - - /** - * Set to TRUE when class is in the process of being extended to ensure that - * a constructor can be instantiated (to use as the prototype) without - * invoking the class construction logic + * IE contains a nasty enumeration "bug" (poor implementation) that makes + * toString unenumerable. This means that, if you do obj.toString = foo, + * toString will NOT show up in `for` or hasOwnProperty(). This is a problem. * - * @type {boolean} + * This test will determine if this poor implementation exists. */ - extending = false, - - /** - * A flag to let the system know that we are currently attempting to access - * a static property from within a method. This means that the caller should - * be given access to additional levels of visibility. - * - * @type {boolean} - */ - sprop_internal = false, + enum_bug = ( + Object.prototype.propertyIsEnumerable.call( + { toString: function() {} }, + 'toString' + ) === false + ) + ? true + : false, /** * Hash of reserved members @@ -88,26 +72,62 @@ var util = require( __dirname + '/util' ), '__construct': true, 'toString': true, '__toString': true, - } -; + }; /** - * IE contains a nasty enumeration "bug" (poor implementation) that makes - * toString unenumerable. This means that, if you do obj.toString = foo, - * toString will NOT show up in `for` or hasOwnProperty(). This is a problem. + * Initializes class builder with given member builder * - * This test will determine if this poor implementation exists. + * The 'new' keyword is not required when instantiating this constructor. + * + * @param {Object} member_builder member builder */ -var enum_bug = ( - Object.prototype.propertyIsEnumerable.call( - { toString: function() {} }, - 'toString' - ) === false - ) - ? true - : false -; +module.exports = exports = +function ClassBuilder( member_builder ) +{ + // allow ommitting the 'new' keyword + if ( !( this instanceof exports ) ) + { + return new exports( member_builder ); + } + + /** + * Used for building class members + * @type {Object} + */ + this._memberBuilder = member_builder; + + + /** + * Class id counter, to be increment on each new definition + * @type {number} + */ + this._classId = 0; + + /** + * Instance id counter, to be incremented on each new instance + * @type {number} + */ + this._instanceId = 0; + + /** + * Set to TRUE when class is in the process of being extended to ensure that + * a constructor can be instantiated (to use as the prototype) without + * invoking the class construction logic + * + * @type {boolean} + */ + this._extending = false; + + /** + * A flag to let the system know that we are currently attempting to access + * a static property from within a method. This means that the caller should + * be given access to additional levels of visibility. + * + * @type {boolean} + */ + this._spropInternal = false; +}; /** @@ -117,6 +137,7 @@ var enum_bug = ( */ exports.ClassBase = function Class() {}; + /** * Default static property method * @@ -168,6 +189,73 @@ exports.getForcedPublicMethods = function() }; +/** + * Returns reference to metadata for the requested class + * + * Since a reference is returned (rather than a copy), the returned object can + * be modified to alter the metadata. + * + * @param {Class} cls class from which to retrieve metadata + * + * @return {Object} + */ +exports.getMeta = function( cls ) +{ + return cls.___$$meta$$ || {}; +} + + +/** + * Determines if the class is an instance of the given type + * + * The given type can be a class, interface, trait or any other type of object. + * It may be used in place of the 'instanceof' operator and contains additional + * enhancements that the operator is unable to provide due to prototypal + * restrictions. + * + * @param {Object} type expected type + * @param {Object} instance instance to check + * + * @return {boolean} true if instance is an instance of type, otherwise false + */ +exports.isInstanceOf = function( type, instance ) +{ + var meta, implemented, i; + + try + { + // check prototype chain (will throw an error if type is not a + // constructor (function) + if ( instance instanceof type ) + { + return true; + } + } + catch ( e ) {} + + // if no metadata is available, then our remaining checks cannot be + // performed + if ( !instance.__cid || !( meta = exports.getMeta( instance ) ) ) + { + return false; + } + + implemented = meta.implemented; + i = implemented.length; + + // check implemented interfaces + while ( i-- ) + { + if ( implemented[ i ] === type ) + { + return true; + } + } + + return false; +}; + + /** * Mimics class inheritance * @@ -180,10 +268,10 @@ exports.getForcedPublicMethods = function() * * @return {Object} extended class */ -exports.build = function extend() +exports.prototype.build = function extend() { // ensure we'll be permitted to instantiate abstract classes for the base - extending = true; + this._extending = true; var args = Array.prototype.slice.call( arguments ), props = args.pop() || {}, @@ -191,11 +279,11 @@ exports.build = function extend() prototype = new base(), cname = '', - prop_init = member_builder.initMembers(), - members = member_builder.initMembers( prototype ), + prop_init = this._memberBuilder.initMembers(), + members = this._memberBuilder.initMembers( prototype ), static_members = { - methods: member_builder.initMembers(), - props: member_builder.initMembers(), + methods: this._memberBuilder.initMembers(), + props: this._memberBuilder.initMembers(), } abstract_methods = @@ -235,14 +323,14 @@ exports.build = function extend() } // increment class identifier - class_id++; + this._classId++; // build the various class components (xxx: this is temporary; needs // refactoring) try { - buildMembers( props, - class_id, + this.buildMembers( props, + this._classId, base, prop_init, abstract_methods, @@ -271,7 +359,7 @@ exports.build = function extend() prototype.___$$parent$$ = base.prototype; // set up the new class - var new_class = createCtor( cname, abstract_methods, members ); + var new_class = this.createCtor( cname, abstract_methods, members ); // closure to hold static initialization to be used later by subtypes initStaticVisibilityObj( new_class, static_members ); @@ -281,7 +369,7 @@ exports.build = function extend() } staticInit( new_class, false ); - attachPropInit( prototype, prop_init, members, new_class, class_id ); + attachPropInit( prototype, prop_init, members, new_class, this._classId ); new_class.prototype = prototype; new_class.constructor = new_class; @@ -307,15 +395,128 @@ exports.build = function extend() meta.name = cname; attachAbstract( new_class, abstract_methods ); - attachId( new_class, class_id ); + attachId( new_class, this._classId ); // we're done with the extension process - extending = false; + this._extending = false; return new_class; }; +exports.prototype.buildMembers = function buildMembers( + props, class_id, base, prop_init, abstract_methods, members, + static_members, staticInstLookup +) +{ + var hasOwn = Array.prototype.hasOwnProperty, + defs = {}, + + smethods = static_members.methods, + sprops = static_members.props, + + _self = this + ; + + util.propParse( props, { + each: function( name, value, keywords ) + { + // disallow use of our internal __initProps() method + if ( reserved_members[ name ] === true ) + { + throw Error( + ( ( cname ) ? cname + '::' : '' ) + + ( name + " is reserved" ) + ); + } + + // if a member was defined multiple times in the same class + // declaration, throw an error + if ( hasOwn.call( defs, name ) ) + { + throw Error( + "Cannot redefine method '" + name + "' in same declaration" + ); + } + + // keep track of the definitions (only during class declaration) + // to catch duplicates + defs[ name ] = 1; + }, + + property: function( name, value, keywords ) + { + var dest = ( keywordStatic( keywords ) ) ? sprops : prop_init; + + // build a new property, passing in the other members to compare + // against for preventing nonsensical overrides + _self._memberBuilder.buildProp( + dest, null, name, value, keywords, base + ); + }, + + getter: function( name, value, keywords ) + { + var dest = ( keywordStatic( keywords ) ) ? smethods : members; + + _self._memberBuilder.buildGetter( + dest, null, name, value, keywords, base + ); + }, + + setter: function( name, value, keywords ) + { + var dest = ( keywordStatic( keywords ) ) ? smethods : members; + + _self._memberBuilder.buildSetter( + dest, null, name, value, keywords, base + ); + }, + + method: function( name, func, is_abstract, keywords ) + { + var is_static = keywordStatic( keywords ), + dest = ( is_static ) ? smethods : members, + instLookup = ( is_static ) + ? staticInstLookup + : getMethodInstance + ; + + // constructor check + if ( public_methods[ name ] === true ) + { + if ( keywords[ 'protected' ] || keywords[ 'private' ] ) + { + throw TypeError( + name + " must be public" + ); + } + } + + _self._memberBuilder.buildMethod( + dest, null, name, func, keywords, instLookup, + class_id, base + ); + + if ( is_abstract ) + { + abstract_methods[ name ] = true; + abstract_methods.__length++; + } + else if ( ( hasOwn.call( abstract_methods, name ) ) + && ( is_abstract === false ) + ) + { + // if this was a concrete method, then it should no longer + // be marked as abstract + delete abstract_methods[ name ]; + abstract_methods.__length--; + } + }, + } ); +} + + /** * Validates abstract class requirements * @@ -362,17 +563,17 @@ function validateAbstract( ctor, cname, abstract_methods ) * * @return {Function} constructor */ -function createCtor( cname, abstract_methods, members ) +exports.prototype.createCtor = function( cname, abstract_methods, members ) { // concrete class if ( abstract_methods.__length === 0 ) { - return createConcreteCtor( cname, members ); + return this.createConcreteCtor( cname, members ); } // abstract class else { - return createAbstractCtor( cname ); + return this.createAbstractCtor( cname ); } } @@ -388,9 +589,10 @@ function createCtor( cname, abstract_methods, members ) * * @return {function()} constructor */ -function createConcreteCtor( cname, members ) +exports.prototype.createConcreteCtor = function( cname, members ) { - var args = null; + var args = null, + _self = this; // constructor function to be returned (the name is set to ClassInstance // because some debuggers (e.g. v8) will show the name of this function for @@ -411,13 +613,13 @@ function createConcreteCtor( cname, members ) // If we're extending, we don't actually want to invoke any class // construction logic. The above is sufficient to use this class in a // prototype, so stop here. - if ( extending ) + if ( _self._extending ) { return; } // generate and store unique instance id - attachInstanceId( this, ++instance_id ); + attachInstanceId( this, ++_self._instanceId ); // call the constructor, if one was provided if ( this.__construct instanceof Function ) @@ -477,11 +679,13 @@ function createConcreteCtor( cname, members ) * * @return {function()} constructor */ -function createAbstractCtor( cname ) +exports.prototype.createAbstractCtor = function( cname ) { + var _self = this; + var __abstract_self = function() { - if ( !extending ) + if ( !_self._extending ) { throw Error( "Abstract class " + ( cname || '(anonymous)' ) + @@ -505,117 +709,6 @@ function createAbstractCtor( cname ) } -function buildMembers( - props, class_id, base, prop_init, abstract_methods, members, - static_members, staticInstLookup -) -{ - var hasOwn = Array.prototype.hasOwnProperty, - defs = {}, - - smethods = static_members.methods, - sprops = static_members.props - ; - - util.propParse( props, { - each: function( name, value, keywords ) - { - // disallow use of our internal __initProps() method - if ( reserved_members[ name ] === true ) - { - throw Error( - ( ( cname ) ? cname + '::' : '' ) + - ( name + " is reserved" ) - ); - } - - // if a member was defined multiple times in the same class - // declaration, throw an error - if ( hasOwn.call( defs, name ) ) - { - throw Error( - "Cannot redefine method '" + name + "' in same declaration" - ); - } - - // keep track of the definitions (only during class declaration) - // to catch duplicates - defs[ name ] = 1; - }, - - property: function( name, value, keywords ) - { - var dest = ( keywordStatic( keywords ) ) ? sprops : prop_init; - - // build a new property, passing in the other members to compare - // against for preventing nonsensical overrides - member_builder.buildProp( - dest, null, name, value, keywords, base - ); - }, - - getter: function( name, value, keywords ) - { - var dest = ( keywordStatic( keywords ) ) ? smethods : members; - - member_builder.buildGetter( - dest, null, name, value, keywords, base - ); - }, - - setter: function( name, value, keywords ) - { - var dest = ( keywordStatic( keywords ) ) ? smethods : members; - - member_builder.buildSetter( - dest, null, name, value, keywords, base - ); - }, - - method: function( name, func, is_abstract, keywords ) - { - var is_static = keywordStatic( keywords ), - dest = ( is_static ) ? smethods : members, - instLookup = ( is_static ) - ? staticInstLookup - : getMethodInstance - ; - - // constructor check - if ( public_methods[ name ] === true ) - { - if ( keywords[ 'protected' ] || keywords[ 'private' ] ) - { - throw TypeError( - name + " must be public" - ); - } - } - - member_builder.buildMethod( - dest, null, name, func, keywords, instLookup, - class_id, base - ); - - if ( is_abstract ) - { - abstract_methods[ name ] = true; - abstract_methods.__length++; - } - else if ( ( hasOwn.call( abstract_methods, name ) ) - && ( is_abstract === false ) - ) - { - // if this was a concrete method, then it should no longer - // be marked as abstract - delete abstract_methods[ name ]; - abstract_methods.__length--; - } - }, - } ); -} - - /** * Determines if the given keywords should result in a static member * @@ -714,6 +807,8 @@ function attachPropInit( prototype, properties, members, ctor, cid ) */ function initStaticVisibilityObj( ctor ) { + var _self = this; + // the object will simply be another layer in the prototype chain to // prevent protected/private members from being mixed in with the public var sobj = function() {}; @@ -731,9 +826,9 @@ function initStaticVisibilityObj( ctor ) // internal flag cannot be modified by conventional means. sobji.$ = function() { - sprop_internal = true; + _self._spropInternal = true; var val = ctor.$.apply( ctor, arguments ); - sprop_internal = false; + _self._spropInternal = false; return val; }; @@ -761,7 +856,9 @@ function initStaticVisibilityObj( ctor ) function attachStatic( ctor, members, base, inheriting ) { var methods = members.methods, - props = members.props; + props = members.props, + _self = this + ; // "Inherit" the parent's static methods by running the parent's static // initialization method. It is important that we do this before anything, @@ -805,7 +902,7 @@ function attachStatic( ctor, members, base, inheriting ) // to check other levels of visibility. `found` will contain the // visibility level the property was found in, or false. found = has.call( props[ 'public' ], prop ) && 'public'; - if ( !found && sprop_internal ) + if ( !found && _self._spropInternal ) { // Check for protected/private. We only check for private // properties if we are not currently checking the properties of @@ -898,22 +995,6 @@ function createMeta( func, cparent ) } -/** - * Returns reference to metadata for the requested class - * - * Since a reference is returned (rather than a copy), the returned object can - * be modified to alter the metadata. - * - * @param {Class} cls class from which to retrieve metadata - * - * @return {Object} - */ -exports.getMeta = function( cls ) -{ - return cls.___$$meta$$ || {}; -} - - /** * Attaches an instance identifier to a class instance * @@ -1014,57 +1095,6 @@ function getMethodInstance( inst, cid ) } -/** - * Determines if the class is an instance of the given type - * - * The given type can be a class, interface, trait or any other type of object. - * It may be used in place of the 'instanceof' operator and contains additional - * enhancements that the operator is unable to provide due to prototypal - * restrictions. - * - * @param {Object} type expected type - * @param {Object} instance instance to check - * - * @return {boolean} true if instance is an instance of type, otherwise false - */ -exports.isInstanceOf = function( type, instance ) -{ - var meta, implemented, i; - - try - { - // check prototype chain (will throw an error if type is not a - // constructor (function) - if ( instance instanceof type ) - { - return true; - } - } - catch ( e ) {} - - // if no metadata is available, then our remaining checks cannot be - // performed - if ( !instance.__cid || !( meta = exports.getMeta( instance ) ) ) - { - return false; - } - - implemented = meta.implemented; - i = implemented.length; - - // check implemented interfaces - while ( i-- ) - { - if ( implemented[ i ] === type ) - { - return true; - } - } - - return false; -}; - - /** * Attaches isAbstract() method to the class * diff --git a/lib/class.js b/lib/class.js index b4dc493..88391f8 100644 --- a/lib/class.js +++ b/lib/class.js @@ -22,8 +22,12 @@ * @package core */ -var util = require( __dirname + '/util' ), - class_builder = require( __dirname + '/class_builder' ) +var util = require( __dirname + '/util' ), + ClassBuilder = require( __dirname + '/ClassBuilder' ), + + class_builder = ClassBuilder( + require( __dirname + '/member_builder' ) + ) ; @@ -111,7 +115,7 @@ module.exports.isClass = function( obj ) { obj = obj || {}; - return ( obj.prototype instanceof class_builder.ClassBase ) + return ( obj.prototype instanceof ClassBuilder.ClassBase ) ? true : false ; @@ -131,7 +135,7 @@ module.exports.isClassInstance = function( obj ) { obj = obj || {}; - return ( obj instanceof class_builder.ClassBase ) + return ( obj instanceof ClassBuilder.ClassBase ) ? true : false; }; @@ -150,7 +154,7 @@ module.exports.isClassInstance = function( obj ) * * @return {boolean} true if instance is an instance of type, otherwise false */ -module.exports.isInstanceOf = class_builder.isInstanceOf; +module.exports.isInstanceOf = ClassBuilder.isInstanceOf; /** @@ -353,7 +357,7 @@ function createImplement( base, ifaces, cname ) function extend() { // set up the new class - var new_class = class_builder.build.apply( null, arguments ); + var new_class = class_builder.build.apply( class_builder, arguments ); // set up some additional convenience props setupProps( new_class ); @@ -413,7 +417,7 @@ var implement = function() // create a new class with the implemented abstract methods var class_new = module.exports.extend( base, dest ); - class_builder.getMeta( class_new ).implemented = implemented; + ClassBuilder.getMeta( class_new ).implemented = implemented; return class_new; } diff --git a/test/test-class_builder-const.js b/test/test-class_builder-const.js index c6f7dcd..57565d1 100644 --- a/test/test-class_builder-const.js +++ b/test/test-class_builder-const.js @@ -24,7 +24,9 @@ var common = require( './common' ), assert = require( 'assert' ), - builder = common.require( 'class_builder' ) + builder = common.require( 'ClassBuilder' )( + common.require( 'member_builder' ) + ) ; diff --git a/test/test-class_builder-final.js b/test/test-class_builder-final.js index 30ef6ca..a7d1dfb 100644 --- a/test/test-class_builder-final.js +++ b/test/test-class_builder-final.js @@ -24,7 +24,9 @@ var common = require( './common' ), assert = require( 'assert' ), - builder = common.require( 'class_builder' ), + builder = common.require( 'ClassBuilder' )( + common.require( 'member_builder' ) + ), Class = common.require( 'class' ) FinalClass = common.require( 'class_final' ) diff --git a/test/test-class_builder-member-restrictions.js b/test/test-class_builder-member-restrictions.js index 8004d06..96baca9 100644 --- a/test/test-class_builder-member-restrictions.js +++ b/test/test-class_builder-member-restrictions.js @@ -24,7 +24,11 @@ var common = require( './common' ), assert = require( 'assert' ), - builder = common.require( 'class_builder' ) + + ClassBuilder = common.require( 'ClassBuilder' ), + builder = ClassBuilder( + common.require( 'member_builder' ) + ) ; @@ -35,7 +39,7 @@ var common = require( './common' ), */ ( function testCanRetrieveListOfReservedMembers() { - var reserved = builder.getReservedMembers(); + var reserved = ClassBuilder.getReservedMembers(); assert.ok( reserved instanceof Object, "Can retrieve hash of reserved members" @@ -60,10 +64,10 @@ var common = require( './common' ), var val = 'foo'; // attempt to add to list - builder.getReservedMembers().foo = val; + ClassBuilder.getReservedMembers().foo = val; assert.notEqual( - builder.getReservedMembers().foo, + ClassBuilder.getReservedMembers().foo, val, "Cannot alter internal list of reserved members" ); @@ -76,7 +80,7 @@ var common = require( './common' ), */ ( function testAllReservedMembersAreActuallyReserved() { - var reserved = builder.getReservedMembers(), + var reserved = ClassBuilder.getReservedMembers(), count = 0; // test each of the reserved members @@ -126,7 +130,7 @@ var common = require( './common' ), */ ( function testCanRetrieveListOfForcedPublicMethods() { - var pub = builder.getForcedPublicMethods(), + var pub = ClassBuilder.getForcedPublicMethods(), count = 0; assert.ok( pub instanceof Object, @@ -153,10 +157,10 @@ var common = require( './common' ), var val = 'foo'; // attempt to add to list - builder.getForcedPublicMethods().foo = val; + ClassBuilder.getForcedPublicMethods().foo = val; assert.notEqual( - builder.getForcedPublicMethods().foo, + ClassBuilder.getForcedPublicMethods().foo, val, "Cannot alter internal list of forced-public methods" ); @@ -169,7 +173,7 @@ var common = require( './common' ), */ ( function testAllForcedPublicMethodsAreForcedToPublic() { - var pub = builder.getForcedPublicMethods(); + var pub = ClassBuilder.getForcedPublicMethods(); // test each of the reserved members for ( name in pub ) diff --git a/test/test-class_builder-static.js b/test/test-class_builder-static.js index aed851d..3c23cfe 100644 --- a/test/test-class_builder-static.js +++ b/test/test-class_builder-static.js @@ -22,10 +22,12 @@ * @package test */ -var common = require( './common' ), - assert = require( 'assert' ), - builder = common.require( 'class_builder' ), - fallback = common.require( 'util' ).definePropertyFallback() +var common = require( './common' ), + assert = require( 'assert' ), + fallback = common.require( 'util' ).definePropertyFallback() + builder = common.require( 'ClassBuilder' )( + common.require( 'member_builder' ) + ) ; diff --git a/test/test-class_builder-visibility.js b/test/test-class_builder-visibility.js index 98861c6..d7800fc 100644 --- a/test/test-class_builder-visibility.js +++ b/test/test-class_builder-visibility.js @@ -27,7 +27,9 @@ var common = require( './common' ), assert = require( 'assert' ), util = common.require( 'util' ), - builder = common.require( 'class_builder' ) + builder = common.require( 'ClassBuilder' )( + common.require( 'member_builder' ) + ) ; diff --git a/test/test-member_builder-method-hiding.js b/test/test-member_builder-method-hiding.js index ef596c0..0eca016 100644 --- a/test/test-member_builder-method-hiding.js +++ b/test/test-member_builder-method-hiding.js @@ -24,8 +24,10 @@ var common = require( './common' ), assert = require( 'assert' ), - builder = common.require( 'class_builder' ), warn = common.require( 'warn' ) + builder = common.require( 'ClassBuilder' )( + common.require( 'member_builder' ) + ) ; diff --git a/tools/combine b/tools/combine index ed2bf6f..d624bfc 100755 --- a/tools/combine +++ b/tools/combine @@ -29,7 +29,7 @@ TPL_VAR='/**{CONTENT}**/' RMTRAIL="$PATH_TOOLS/rmtrail" # order matters -CAT_MODULES="warn prop_parser util propobj member_builder class_builder" +CAT_MODULES="warn prop_parser util propobj member_builder ClassBuilder" CAT_MODULES="$CAT_MODULES class class_final class_abstract interface" ##