From 14bd55236145465f65bbdf15b9682f3b0b8e7444 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 28 Feb 2014 23:55:24 -0500 Subject: [PATCH] Trait can now implement interfaces Note the incomplete test case: the next commit will introduce the ability for mixins to override methods that may have already been defined. --- lib/Trait.js | 96 ++++++++++++++++- lib/class.js | 4 +- test/Trait/ClassVirtualTest.js | 184 +++++++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+), 5 deletions(-) create mode 100644 test/Trait/ClassVirtualTest.js diff --git a/lib/Trait.js b/lib/Trait.js index 60ff786..e7544de 100644 --- a/lib/Trait.js +++ b/lib/Trait.js @@ -74,6 +74,9 @@ function createNamedTrait( name, dfn ) Trait.extend = function( dfn ) { + // we may have been passed some additional metadata + var meta = this.__$$meta || {}; + // store any provided name, since we'll be clobbering it (the definition // object will be used to define the hidden abstract class) var name = dfn.__name || '(Trait)'; @@ -91,8 +94,15 @@ Trait.extend = function( dfn ) throw Error( "Cannot instantiate trait" ); }; + // implement interfaces if indicated + var base = AbstractClass; + if ( meta.ifaces ) + { + base = base.implement.apply( null, meta.ifaces ); + } + // and here we can see that traits are quite literally abstract classes - var tclass = AbstractClass( dfn ); + var tclass = base.extend( dfn ); TraitType.__trait = true; TraitType.__acls = tclass; @@ -114,10 +124,33 @@ Trait.extend = function( dfn ) mixin( TraitType, dfn, tc ); }; + // mixes in implemented types + TraitType.__mixinImpl = function( dest_meta ) + { + mixinImpl( tclass, dest_meta ); + }; + return TraitType; }; +Trait.implement = function() +{ + var ifaces = arguments; + + return { + extend: function() + { + // pass our interface metadata as the invocation context + return Trait.extend.apply( + { __$$meta: { ifaces: ifaces } }, + arguments + ); + }, + }; +}; + + Trait.isTrait = function( trait ) { return !!( trait || {} ).__trait; @@ -260,16 +293,71 @@ function createVirtProxy( acls, dfn ) function mixin( trait, dfn, tc ) { // the abstract class hidden within the trait - var acls = trait.__acls, - methods = acls.___$$methods$$; + var acls = trait.__acls; // retrieve the private member name that will contain this trait object var iname = addTraitInst( trait, dfn, tc ); + // recursively mix in trait's underlying abstract class (ensuring that + // anything that the trait inherits from is also properly mixed in) + mixinCls( acls, dfn, iname ); + return dfn; +} + + +/** + * Recursively mix in class methods + * + * If CLS extends another class, its methods will be recursively processed + * to ensure that the entire prototype chain is properly proxied. + * + * For an explanation of the iname parameter, see the mixin function. + * + * @param {Class} cls class to mix in + * @param {Object} dfn definition object to merge into + * @param {string} iname trait object private member instance name + * + * @return {undefined} + */ +function mixinCls( cls, dfn, iname ) +{ + var methods = cls.___$$methods$$; + mixMethods( methods['public'], dfn, 'public', iname ); mixMethods( methods['protected'], dfn, 'protected', iname ); - return dfn; + // if this class inherits from another class that is *not* the base + // class, recursively process its methods; otherwise, we will have + // incompletely proxied the prototype chain + var parent = methods['public'].___$$parent$$; + if ( parent && ( parent.constructor !== ClassBuilder.ClassBase ) ) + { + mixinCls( parent.constructor, dfn, iname ); + } +} + + +/** + * Mix implemented types into destination object + * + * The provided destination object will ideally be the `implemented' array + * of the destination class's meta object. + * + * @param {Class} cls source class + * @param {Object} dest_meta destination object to copy into + * + * @return {undefined} + */ +function mixinImpl( cls, dest_meta ) +{ + var impl = ClassBuilder.getMeta( cls ).implemented || [], + i = impl.length; + + while ( i-- ) + { + // TODO: this could potentially result in duplicates + dest_meta.push( impl[ i ] ); + } } diff --git a/lib/class.js b/lib/class.js index 81f56e1..2d4b93d 100644 --- a/lib/class.js +++ b/lib/class.js @@ -431,9 +431,11 @@ function createMixedClass( base, traits ) // add each trait to the list of implemented types so that the // class is considered to be of type T in traits + var impl = meta.implemented; for ( var i = 0, n = traits.length; i < n; i++ ) { - meta.implemented.push( traits[ i ] ); + impl.push( traits[ i ] ); + traits[ i ].__mixinImpl( impl ); } return C; diff --git a/test/Trait/ClassVirtualTest.js b/test/Trait/ClassVirtualTest.js new file mode 100644 index 0000000..7cdcb5b --- /dev/null +++ b/test/Trait/ClassVirtualTest.js @@ -0,0 +1,184 @@ +/** + * Tests overriding virtual class methods using mixins + * + * Copyright (C) 2014 Mike Gerwitz + * + * This file is part of GNU ease.js. + * + * ease.js is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * These tests vary from those in VirtualTest in that, rather than a class + * overriding a virtual method defined within a trait, a trait is overriding + * a method in the class that it is mixed into. In particular, since + * overrides require that the super method actually exist, this means that a + * trait must implement or extend a common interface. + * + * It is this very important (and powerful) system that allows traits to be + * used as stackable modifications, similar to how one would use the + * decorator pattern (but more tightly coupled). + */ + +require( 'common' ).testCase( +{ + caseSetUp: function() + { + this.Sut = this.require( 'Trait' ); + this.Class = this.require( 'class' ); + this.AbstractClass = this.require( 'class_abstract' ); + this.Interface = this.require( 'interface' ); + }, + + + /** + * A trait may implement an interface I for a couple of reasons: to have + * the class mixed into be considered to of type I and to override + * methods. But, regardless of the reason, let's start with the + * fundamentals. + */ + 'Traits may implement an interface': function() + { + var _self = this; + + // simply make sure that the API is supported; nothing more. + this.assertDoesNotThrow( function() + { + _self.Sut.implement( _self.Interface( {} ) ).extend( {} ); + } ); + }, + + + /** + * We would expect that the default behavior of implementing an + * interface I into a trait would create a trait with all abstract + * methods defined by I. + */ + 'Traits implementing interfaces define abstract methods': function() + { + var I = this.Interface( { foo: [], bar: [] } ), + T = this.Sut.implement( I ).extend( {} ); + + var Class = this.Class, + AbstractClass = this.AbstractClass; + + // T should contain both foo and bar as abstract methods, which we + // will test indirectly in the assertions below + + // should fail because of abstract foo and bar + this.assertThrows( function() + { + Class.use( T ).extend( {} ); + } ); + + // should succeed, since we can have abstract methods within an + // abstract class + this.assertDoesNotThrow( function() + { + AbstractClass.use( T ).extend( {} ); + } ); + + // one remaining abstract method + this.assertDoesNotThrow( function() + { + AbstractClass.use( T ).extend( { foo: function() {} } ); + } ); + + // both concrete + this.assertDoesNotThrow( function() + { + Class.use( T ).extend( + { + foo: function() {}, + bar: function() {}, + } ); + } ); + }, + + + /** + * Just as classes implementing interfaces may choose to immediately + * provide concrete definitions for the methods declared in the + * interface (instead of becoming an abstract class), so too may traits. + */ + 'Traits may provide concrete methods for interfaces': function() + { + var called = false; + + var I = this.Interface( { foo: [] } ), + T = this.Sut.implement( I ).extend( + { + foo: function() + { + called = true; + }, + } ); + + var Class = this.Class; + this.assertDoesNotThrow( function() + { + // should invoke concrete foo; class definition should not fail, + // because foo is no longer abstract + Class.use( T )().foo(); + } ); + + this.assertOk( called ); + }, + + + /** + * Instances of class C mixing in some trait T implementing I will be + * considered to be of type I, since any method of I would either be + * defined within T, or would be implicitly abstract in T, requiring its + * definition within C; otherwise, C would have to be declared astract. + */ + 'Instance of class mixing in trait implementing I is of type I': + function() + { + var I = this.Interface( {} ), + T = this.Sut.implement( I ).extend( {} ); + + this.assertOk( + this.Class.isA( I, this.Class.use( T )() ) + ); + }, + + + /** + * The API for multiple interfaces should be the same for traits as it + * is for classes. + */ + 'Trait can implement multiple interfaces': function() + { + var Ia = this.Interface( {} ), + Ib = this.Interface( {} ), + T = this.Sut.implement( Ia, Ib ).extend( {} ), + o = this.Class.use( T ).extend( {} )(); + + this.assertOk( this.Class.isA( Ia, o ) ); + this.assertOk( this.Class.isA( Ib, o ) ); + }, + + + /** + * This is a concept borrowed from Scala: consider class C and trait T, + * both implementing interface I which declares method M. T should be + * able to override C.M so long as it is concrete, but to do so, we need + * some way of telling ease.js that we are overriding at time of mixin; + * otherwise, override does not make sense, because I.M is clearly + * abstract and there is nothing to override. + */ + 'Trait can override virtual concrete interface methods at mixin': + function() + { + }, +} );