Add Class.assert{InstanceOf,isA}
* lib/class.js (assertInstanceOf, assertIsA): New methods. * test/Class/GeneralTest.js: Add respective tests. * doc/classes.texi (Type Checks and Polymorphism): Add reference for methods. Update and format text. Add indexes for "polymorphism", "type checking", and "duck typing".master
parent
a3d01a65d9
commit
6c6e41c415
105
doc/classes.texi
105
doc/classes.texi
|
@ -1086,6 +1086,8 @@ the @emph{supertype} upon invocation.}
|
||||||
|
|
||||||
@node Type Checks and Polymorphism
|
@node Type Checks and Polymorphism
|
||||||
@subsection Type Checks and Polymorphism
|
@subsection Type Checks and Polymorphism
|
||||||
|
@cindex polymorphism
|
||||||
|
@cindex type checking
|
||||||
The fact that the API of the parent is inherited is a very important detail.
|
The fact that the API of the parent is inherited is a very important detail.
|
||||||
If the API of subtypes is guaranteed to be @emph{at least} that of the
|
If the API of subtypes is guaranteed to be @emph{at least} that of the
|
||||||
parent, then this means that a function expecting a certain type can also
|
parent, then this means that a function expecting a certain type can also
|
||||||
|
@ -1104,33 +1106,46 @@ him/her.
|
||||||
@end float
|
@end float
|
||||||
|
|
||||||
Type checks are traditionally performed in JavaScript using the
|
Type checks are traditionally performed in JavaScript using the
|
||||||
@code{instanceOf} operator. While this can be used in most inheritance cases
|
@code{instanceOf} operator.
|
||||||
with ease.js, it is not recommended. Rather, you are encouraged to use
|
You are encouraged to use ease.js' own methods for determining
|
||||||
ease.js's own methods for determining instance type@footnote{The reason for
|
instance type@footnote{
|
||||||
this will become clear in future chapters. ease.js's own methods permit
|
The reason for this will become clear in future chapters.
|
||||||
checking for additional types, such as Interfaces.}. Support for the
|
ease.js's own methods permit checking for additional types,
|
||||||
@code{instanceOf} operator is not guaranteed.
|
such as Interfaces.};
|
||||||
|
support for the @code{instanceOf} operator,
|
||||||
Instead, you have two choices with ease.js:
|
while it may often work as expected,
|
||||||
|
is not guaranteed and will not work in certain scenarios.
|
||||||
|
|
||||||
@table @code
|
@table @code
|
||||||
@item Class.isInstanceOf( type, instance );
|
@item Class.isInstanceOf( type, instance );
|
||||||
Returns @code{true} if @var{instance} is of type @var{type}. Otherwise,
|
Returns @code{true} if @var{instance} is of type @var{type};
|
||||||
returns @code{false}.
|
otherwise,
|
||||||
|
returns @code{false}.
|
||||||
|
|
||||||
@item Class.isA( type, instance );
|
@item Class.isA( type, instance );
|
||||||
Alias for @code{Class.isInstanceOf()}. Permits code that may read better
|
Alias for @code{Class.isInstanceOf}.
|
||||||
depending on circumstance and helps to convey the ``is a'' relationship that
|
Permits code that may read better depending on circumstance and helps to
|
||||||
inheritance creates.
|
convey the ``is a'' relationship that inheritance creates.
|
||||||
|
|
||||||
|
@item Class.assertInstanceOf( type, instance[, message] );
|
||||||
|
Perform the same check as the above two methods,
|
||||||
|
but if the check fails,
|
||||||
|
throw a@tie{}@code{TypeError}.
|
||||||
|
The error message will be that of @var{message} if provided,
|
||||||
|
otherwise will be generated in the format @samp{Expected instance of `%s'},
|
||||||
|
where @samp{%s} is replaced by @samp{type.toString()}.
|
||||||
|
|
||||||
|
@item Class.assertIsA( type, instance[, message] );
|
||||||
|
Alias for @code{Class.assertInstanceOf}.
|
||||||
@end table
|
@end table
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
@float Figure, f:instanceof-ex
|
@float Figure, f:instanceof-ex
|
||||||
@verbatim
|
@verbatim
|
||||||
var dog = Dog()
|
const dog = Dog();
|
||||||
lazy = LazyDog(),
|
const lazy = LazyDog();
|
||||||
angry = AngryDog();
|
const angry = AngryDog();
|
||||||
|
|
||||||
Class.isInstanceOf( Dog, dog ); // true
|
Class.isInstanceOf( Dog, dog ); // true
|
||||||
Class.isA( Dog, dog ); // true
|
Class.isA( Dog, dog ); // true
|
||||||
|
@ -1140,17 +1155,27 @@ For example:
|
||||||
|
|
||||||
// we must check an instance
|
// we must check an instance
|
||||||
Class.isA( Dog, LazyDog ); // false; instance expected, class given
|
Class.isA( Dog, LazyDog ); // false; instance expected, class given
|
||||||
|
|
||||||
|
// TypeError: Expected instance of `Dog'
|
||||||
|
Class.assertIsA( Dog, {} );
|
||||||
|
|
||||||
|
// TypeError: Not a Dog!
|
||||||
|
Class.assertIsA( Dog, {}, "Not a Dog!" );
|
||||||
@end verbatim
|
@end verbatim
|
||||||
@caption{Using ease.js to determine instance type}
|
@caption{Using ease.js to determine instance type}
|
||||||
@end float
|
@end float
|
||||||
|
|
||||||
It is important to note that, as demonstrated in @ref{f:instanceof-ex}
|
It is important to note that,
|
||||||
above, an @emph{instance} must be passed as a second argument, not a class.
|
as demonstrated in @ref{f:instanceof-ex} above,
|
||||||
|
an @emph{instance} must be passed as a second argument,
|
||||||
|
not a class.
|
||||||
|
|
||||||
Using this method, we can ensure that the @var{DogTrainer} may only be used
|
Using this method,
|
||||||
with an instance of @var{Dog}. It doesn't matter what instance of @var{Dog}
|
we can ensure that the @var{DogTrainer} may only be used with an
|
||||||
- be it a @var{LazyDog} or otherwise. All that matters is that we are given
|
instance of @var{Dog}.
|
||||||
a @var{Dog}.
|
It doesn't matter what instance of @var{Dog}---be it a @var{LazyDog} or
|
||||||
|
otherwise;
|
||||||
|
all that matters is that we are given a@tie{}@var{Dog}.
|
||||||
|
|
||||||
@float Figure, f:polymorphism-easejs
|
@float Figure, f:polymorphism-easejs
|
||||||
@verbatim
|
@verbatim
|
||||||
|
@ -1158,11 +1183,7 @@ a @var{Dog}.
|
||||||
{
|
{
|
||||||
'public __construct': function( dog )
|
'public __construct': function( dog )
|
||||||
{
|
{
|
||||||
// ensure that we are given an instance of Dog
|
this.assertIsA( Dog, dog );
|
||||||
if ( Class.isA( Dog, dog ) === false )
|
|
||||||
{
|
|
||||||
throw TypeError( "Expected instance of Dog" );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -1181,17 +1202,27 @@ a @var{Dog}.
|
||||||
@caption{Polymorphism in ease.js}
|
@caption{Polymorphism in ease.js}
|
||||||
@end float
|
@end float
|
||||||
|
|
||||||
It is very important that you use @emph{only} the API of the type that you
|
For polymorphism to be effective,
|
||||||
are expecting. For example, only @var{LazyDog} and @var{AngryDog} implement
|
it is important that you use only the API of the type that you
|
||||||
a @code{poke()} method. It is @emph{not} a part of @var{Dog}'s API.
|
are expecting.
|
||||||
Therefore, it should not be used in the @var{DogTrainer} class. Instead, if
|
For example,
|
||||||
you wished to use the @code{poke()} method, you should require that an
|
only @var{LazyDog} and @var{AngryDog} implement a @code{poke()} method;
|
||||||
instance of @var{LazyDog} be passed in, which would also permit
|
it is not a part of @var{Dog}'s API,
|
||||||
@var{AngryDog} (since it is a subtype of @var{LazyDog}).
|
and therefore should not be used in the @var{DogTrainer} class.
|
||||||
|
@cindex duck typing
|
||||||
|
If you want to use the @code{poke()} method,
|
||||||
|
you should instead require that an instance of @var{LazyDog} be provided
|
||||||
|
(which would also permit @var{AngryDog},
|
||||||
|
since it is a subtype of @var{LazyDog}).@footnote{
|
||||||
|
An alternative practice to strict polymorphism is @dfn{duck typing},
|
||||||
|
where an implementation attempts to indiscriminately invoke a
|
||||||
|
method on any object it is given,
|
||||||
|
catching exceptions in case the method does not exist.
|
||||||
|
This method is less formal and defers type checks until the last
|
||||||
|
possible moment,
|
||||||
|
which means that logic errors aren't caught during
|
||||||
|
initialization.}
|
||||||
|
|
||||||
Currently, it is necessary to perform this type check yourself. In future
|
|
||||||
versions, ease.js will allow for argument type hinting/strict typing, which
|
|
||||||
will automate this check for you.
|
|
||||||
|
|
||||||
@node Visibility Escalation
|
@node Visibility Escalation
|
||||||
@subsection Visibility Escalation
|
@subsection Visibility Escalation
|
||||||
|
|
35
lib/class.js
35
lib/class.js
|
@ -214,7 +214,7 @@ module.exports.isClassInstance = function( obj )
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if the class is an instance of the given type
|
* Determines if INST is an instance of the given type TYPE
|
||||||
*
|
*
|
||||||
* The given type can be a class, interface, trait or any other type of object.
|
* 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
|
* It may be used in place of the 'instanceof' operator and contains additional
|
||||||
|
@ -230,7 +230,7 @@ module.exports.isInstanceOf = ClassBuilder.isInstanceOf;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Alias for isInstanceOf()
|
* Alias for `#isInstanceOf'
|
||||||
*
|
*
|
||||||
* May read better in certain situations (e.g. Cat.isA( Mammal )) and more
|
* May read better in certain situations (e.g. Cat.isA( Mammal )) and more
|
||||||
* accurately conveys the act of inheritance, implementing interfaces and
|
* accurately conveys the act of inheritance, implementing interfaces and
|
||||||
|
@ -239,6 +239,37 @@ module.exports.isInstanceOf = ClassBuilder.isInstanceOf;
|
||||||
module.exports.isA = module.exports.isInstanceOf;
|
module.exports.isA = module.exports.isInstanceOf;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws a TypeError if INST is not an instance of the given type TYPE
|
||||||
|
*
|
||||||
|
* If a message MESSAGE is not provided, one will be generated in the format:
|
||||||
|
* "Expected instance of `%s'".
|
||||||
|
*
|
||||||
|
* See `#isInstanceOf'.
|
||||||
|
*
|
||||||
|
* @param {Object} type expected type
|
||||||
|
* @param {Object} instance instance to check
|
||||||
|
* @param {string=} message optional message
|
||||||
|
*/
|
||||||
|
module.exports.assertInstanceOf = function( type, instance, message )
|
||||||
|
{
|
||||||
|
if ( ClassBuilder.isInstanceOf( type, instance ) )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw TypeError(
|
||||||
|
message || ( "Expected instance of `" + type.toString() + "'" )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias for `#assertInstanceOf'
|
||||||
|
*/
|
||||||
|
module.exports.assertIsA = module.exports.assertInstanceOf;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new anonymous Class from the given class definition
|
* Creates a new anonymous Class from the given class definition
|
||||||
*
|
*
|
||||||
|
|
|
@ -31,6 +31,8 @@ require( 'common' ).testCase(
|
||||||
{
|
{
|
||||||
value: 'foo',
|
value: 'foo',
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
this.asserts = [ 'assertInstanceOf', 'assertIsA' ];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,4 +286,44 @@ require( 'common' ).testCase(
|
||||||
( this.Foo.prototype.__cid !== undefined )
|
( this.Foo.prototype.__cid !== undefined )
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When enforcing polymorphism (as opposed to duck typing), assertions
|
||||||
|
* are common; it's a lot of boilerplate.
|
||||||
|
*/
|
||||||
|
'@each(asserts) assertIsA throws TypeError if not instance of class':
|
||||||
|
function( assertm )
|
||||||
|
{
|
||||||
|
var FooType = this.Sut( 'FooType' ).extend( {} );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.Sut[ assertm ]( FooType, {} );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
this.assertOk( e instanceof TypeError );
|
||||||
|
this.assertOk( /instance of `FooType'/.test( e.message ) );
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as above, but with the ability to add a custom error message.
|
||||||
|
*/
|
||||||
|
'@each(asserts) assertIsA throws TypeError with custom message':
|
||||||
|
function( assertm )
|
||||||
|
{
|
||||||
|
var expected = "Test assertIsA message";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.Sut[ assertm ]( this.Foo, {}, expected );
|
||||||
|
}
|
||||||
|
catch ( e )
|
||||||
|
{
|
||||||
|
this.assertEqual( e.message, expected );
|
||||||
|
}
|
||||||
|
},
|
||||||
} );
|
} );
|
||||||
|
|
Loading…
Reference in New Issue