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
|
||||
@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.
|
||||
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
|
||||
|
@ -1104,33 +1106,46 @@ him/her.
|
|||
@end float
|
||||
|
||||
Type checks are traditionally performed in JavaScript using the
|
||||
@code{instanceOf} operator. While this can be used in most inheritance cases
|
||||
with ease.js, it is not recommended. Rather, you are encouraged to use
|
||||
ease.js's own methods for determining instance type@footnote{The reason for
|
||||
this will become clear in future chapters. ease.js's own methods permit
|
||||
checking for additional types, such as Interfaces.}. Support for the
|
||||
@code{instanceOf} operator is not guaranteed.
|
||||
|
||||
Instead, you have two choices with ease.js:
|
||||
@code{instanceOf} operator.
|
||||
You are encouraged to use ease.js' own methods for determining
|
||||
instance type@footnote{
|
||||
The reason for this will become clear in future chapters.
|
||||
ease.js's own methods permit checking for additional types,
|
||||
such as Interfaces.};
|
||||
support for the @code{instanceOf} operator,
|
||||
while it may often work as expected,
|
||||
is not guaranteed and will not work in certain scenarios.
|
||||
|
||||
@table @code
|
||||
@item Class.isInstanceOf( type, instance );
|
||||
Returns @code{true} if @var{instance} is of type @var{type}. Otherwise,
|
||||
returns @code{false}.
|
||||
Returns @code{true} if @var{instance} is of type @var{type};
|
||||
otherwise,
|
||||
returns @code{false}.
|
||||
|
||||
@item Class.isA( type, instance );
|
||||
Alias for @code{Class.isInstanceOf()}. Permits code that may read better
|
||||
depending on circumstance and helps to convey the ``is a'' relationship that
|
||||
inheritance creates.
|
||||
Alias for @code{Class.isInstanceOf}.
|
||||
Permits code that may read better depending on circumstance and helps to
|
||||
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
|
||||
|
||||
For example:
|
||||
|
||||
@float Figure, f:instanceof-ex
|
||||
@verbatim
|
||||
var dog = Dog()
|
||||
lazy = LazyDog(),
|
||||
angry = AngryDog();
|
||||
const dog = Dog();
|
||||
const lazy = LazyDog();
|
||||
const angry = AngryDog();
|
||||
|
||||
Class.isInstanceOf( Dog, dog ); // true
|
||||
Class.isA( Dog, dog ); // true
|
||||
|
@ -1140,17 +1155,27 @@ For example:
|
|||
|
||||
// we must check an instance
|
||||
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
|
||||
@caption{Using ease.js to determine instance type}
|
||||
@end float
|
||||
|
||||
It is important to note that, as demonstrated in @ref{f:instanceof-ex}
|
||||
above, an @emph{instance} must be passed as a second argument, not a class.
|
||||
It is important to note that,
|
||||
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
|
||||
with an instance of @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 @var{Dog}.
|
||||
Using this method,
|
||||
we can ensure that the @var{DogTrainer} may only be used with an
|
||||
instance of @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
|
||||
@verbatim
|
||||
|
@ -1158,11 +1183,7 @@ a @var{Dog}.
|
|||
{
|
||||
'public __construct': function( dog )
|
||||
{
|
||||
// ensure that we are given an instance of Dog
|
||||
if ( Class.isA( Dog, dog ) === false )
|
||||
{
|
||||
throw TypeError( "Expected instance of Dog" );
|
||||
}
|
||||
this.assertIsA( Dog, dog );
|
||||
}
|
||||
} );
|
||||
|
||||
|
@ -1181,17 +1202,27 @@ a @var{Dog}.
|
|||
@caption{Polymorphism in ease.js}
|
||||
@end float
|
||||
|
||||
It is very important that you use @emph{only} the API of the type that you
|
||||
are expecting. For example, only @var{LazyDog} and @var{AngryDog} implement
|
||||
a @code{poke()} method. It is @emph{not} a part of @var{Dog}'s API.
|
||||
Therefore, it should not be used in the @var{DogTrainer} class. Instead, if
|
||||
you wished to use the @code{poke()} method, you should require that an
|
||||
instance of @var{LazyDog} be passed in, which would also permit
|
||||
@var{AngryDog} (since it is a subtype of @var{LazyDog}).
|
||||
For polymorphism to be effective,
|
||||
it is important that you use only the API of the type that you
|
||||
are expecting.
|
||||
For example,
|
||||
only @var{LazyDog} and @var{AngryDog} implement a @code{poke()} method;
|
||||
it is not a part of @var{Dog}'s API,
|
||||
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
|
||||
@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.
|
||||
* 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
|
||||
* accurately conveys the act of inheritance, implementing interfaces and
|
||||
|
@ -239,6 +239,37 @@ module.exports.isInstanceOf = ClassBuilder.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
|
||||
*
|
||||
|
|
|
@ -31,6 +31,8 @@ require( 'common' ).testCase(
|
|||
{
|
||||
value: 'foo',
|
||||
} );
|
||||
|
||||
this.asserts = [ 'assertInstanceOf', 'assertIsA' ];
|
||||
},
|
||||
|
||||
|
||||
|
@ -284,4 +286,44 @@ require( 'common' ).testCase(
|
|||
( 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