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

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

View File

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