1
0
Fork 0

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
Mike Gerwitz 2017-10-27 23:46:16 -04:00
parent a3d01a65d9
commit 6c6e41c415
Signed by: mikegerwitz
GPG Key ID: 8C917B7F5DC51BA2
3 changed files with 143 additions and 39 deletions

View File

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

View File

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

View File

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