From 6c6e41c415c94cae32f2211ea9511b5c12707e58 Mon Sep 17 00:00:00 2001 From: Mike Gerwitz Date: Fri, 27 Oct 2017 23:46:16 -0400 Subject: [PATCH] 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". --- doc/classes.texi | 105 ++++++++++++++++++++++++-------------- lib/class.js | 35 ++++++++++++- test/Class/GeneralTest.js | 42 +++++++++++++++ 3 files changed, 143 insertions(+), 39 deletions(-) diff --git a/doc/classes.texi b/doc/classes.texi index 25fb45c..0b7f9fa 100644 --- a/doc/classes.texi +++ b/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 diff --git a/lib/class.js b/lib/class.js index e67d5db..2f40caa 100644 --- a/lib/class.js +++ b/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 * diff --git a/test/Class/GeneralTest.js b/test/Class/GeneralTest.js index 76fddff..fe305fe 100644 --- a/test/Class/GeneralTest.js +++ b/test/Class/GeneralTest.js @@ -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 ); + } + }, } );