1
0
Fork 0
easejs/doc/interop.texi

365 lines
13 KiB
Plaintext

@c This document is part of the GNU ease.js manual.
@c Copyright (C) 2014 Free Software Foundation, Inc.
@c Permission is granted to copy, distribute and/or modify this document
@c under the terms of the GNU Free Documentation License, Version 1.3 or
@c any later version published by the Free Software Foundation; with no
@c Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts.
@c A copy of the license is included in the section entitled ``GNU Free
@c Documentation License''.
@node Interoperability
@chapter Interoperability
GNU ease.js is not for everyone, so it is important to play nicely with
vanilla ECMAScript so that prototypes and objects can be integrated with
the strict restrictions of ease.js (imposed by classical OOP). In general,
you should not have to worry about this: everything is designed to work
fairly transparently. This chapter will go over what ease.js intentionally
supports and some interesting concepts that may even be useful even if you
have adopted ease.js for your own projects.
@menu
* Using GNU ease.js Classes Outside of ease.js::
* Prototypally Extending Classes::
* Interoperable Polymorphism::
@end menu
@node Using GNU ease.js Classes Outside of ease.js
@section Using GNU ease.js Classes Outside of ease.js
GNU ease.js is a prototype generator---it takes the class definition,
applies its validations and conveniences, and generates a prototype and
constructor that can be instantiated and used just as any other ECMAScript
constructor/prototype. One thing to note immediately, as mentioned in
the section @ref{Defining Classes,,Defining Classes}, is that constructors
generated by ease.js may be instantiated either with or without the
@code{new} keyword:
@float Figure, f:interop-new
@verbatim
const Foo = Class( { /*...*/ } );
// both of these are equivalent
Foo();
new Foo();
@end verbatim
@caption{Constructors generated by ease.js may omit the @code{new} keyword}
@end float
ease.js convention is to omit the keyword for more concise code that is more
easily chained, but you should follow the coding conventions of the project
that you are working on.
@node Prototypally Extending Classes
@section Prototypally Extending Classes
Since @ref{Classes,,classes} are also constructors with prototypes, they may
be used as part of a prototype chain. There are, however, some important
considerations when using any sort of constructor as part of a prototype
chain.
Conventionally, prototypes are subtyped by using a new instance as the
prototype of the subtype's constructor, as so:
@float Figure, f:interop-protochain-incorrect
@verbatim
const Foo = Class( { /*...*/ } );
// extending class as a prototype
function SubFoo() {};
SubFoo.prototype = Foo(); // INCORRECT
SubFoo.prototype.constructor = SubFoo;
@end verbatim
@caption{Incorrectly prototypally extending GNU ease.js classes}
@end float
The problem with this approach is that constructors may perform validations
on their arguments to ensure that the instance is in a consistent state. GNU
ease.js solves this problem by introducing an @code{asPrototype} method on
all classes:
@float Figure, f:interop-protochain
@verbatim
const Foo = Class( { /*...*/ } );
// extending class as a prototype
function SubFoo()
{
// it is important to call the constructor ourselves; this is a
// generic method that should work for all subtypes, even if SubFoo
// implements its own __construct method
this.constructor.prototype.__construct.apply( this, arguments );
// OR, if SubFoo does not define its own __construct method, you can
// alternatively do this:
this.__construct();
};
SubFoo.prototype = Foo.asPrototype(); // Correct
SubFoo.prototype.constructor = SubFoo;
@end verbatim
@caption{Correctly prototypally extending GNU ease.js classes}
@end float
The @code{asPrototype} method instantiates the class, but does not execute
the constructor. This allows it to be used as the prototype without any
issues, but it is important that the constructor of the subtype invokes the
constructor of the class, as in @ref{f:interop-protochain}. Otherwise, the
state of the subtype is undefined.
Keep in mind the following when using classes as part of the prototype
chain:
@itemize
@item
GNU ease.js member validations are not enforced; you will not be warned if
an abstract method remains unimplemented or if you override a non-virtual
method, for example. Please exercise diligence.
@item
It is not wise to override non-@ref{Inheritance,,virtual} methods, because
the class designer may not have exposed a proper API for accessing and
manipulating internal state, and may not provide proper protections to
ensure consistent state after the method call.
@item
Note the @ref{Private Member Dilemma} to ensure that your prototype works
properly in pre-ES5 environments and with potential future ease.js
optimizations for production environments: you should not define or
manipulate properties on the prototype that would conflict with private
members of the subtype. This is an awkward situation, since private
members are unlikely to be included in API documentation for a class;
ease.js normally prevents this from happening automatically.
@end itemize
@node Interoperable Polymorphism
@section Interoperable Polymorphism
GNU ease.js encourages polymorphism through type checking. In the case of
@ref{Prototypally Extending Classes,,prototypal subtyping}, type checks will
work as expected:
@float Figure, f:typecheck-protosub
@verbatim
const Foo = Class( {} );
function SubFoo() {};
SubFoo.prototype = Foo.asPrototype();
SubFoo.constructor = Foo;
const SubSubFoo = Class.extend( SubFoo, {} );
// vanilla ECMAScript
( new Foo() ) instanceof Foo; // true
( new Subfoo() ) instanceof Foo; // true
( new SubSubFoo() ) instanceof Foo; // true
( new SubSubFoo() ) instanceof SubFoo; // true
// GNU ease.js
Class.isA( Foo, ( new Foo() ) ); // true
Class.isA( Foo, ( new SubFoo() ) ); // true
Class.isA( Foo, ( new SubSubFoo() ) ); // true
Class.isA( SubFoo, ( new SubSubFoo() ) ); // true
@end verbatim
@caption{Type checking with prototypal subtypes of GNU ease.js classes}
@end float
Plainly---this means that prototypes that perform type checking for
polymorphism will accept GNU ease.js classes and vice versa. But this is not
the only form of type checking that ease.js supports.
This is the simplest type of polymorphism and is directly compatible with
ECMAScript's prototypal mode. However, GNU ease.js offers other features
that are alien to ECMAScript on its own.
@menu
* Interface Interop:: Using GNU ease.js interfaces in conjunction with
vanilla ECMAScript
@end menu
@node Interface Interop
@subsection Interface Interop
@ref{Interfaces}, when used within the bounds of GNU ease.js, allow for
strong typing of objects. Further, two interfaces that share the same API
are not equivalent; this permits conveying intent: Consider two interfaces
@code{Enemy} and @code{Toad}, each defining a method @code{croak}. The
method for @code{Enemy} results in its death, whereas the method for
@code{Toad} produces a bellowing call. Clearly classes implementing these
interfaces will have different actions associated with them; we would
probably not want an invincible enemy that croaks like a toad any time you
try to kill it (although that'd make for amusing gameplay).
@float figure, f:interface-croak
@verbatim
const Enemy = Interface( { croak: [] } );
const Toad = Interface( { croak: [] } );
const AnEnemy = Class.implement( Enemy ).extend( /*...*/ );
const AToad = Class.implement( Toad ).extend( /*...*/ );
// GNU ease.js does not consider these interfaces to be equivalent
Class.isA( Enemy, AnEnemy() ); // true
Class.isA( Toad, AnEnemy() ); // false
Class.isA( Enemy, AToad() ); // false
Class.isA( Toad, AToad() ); // true
defeatEnemy( AnEnemy() ); // okay; is an enemy
defeatEnemy( AToad() ); // error; is a toad
function defeatEnemy( enemy )
{
if ( !( Class.isA( Enemy, enemy ) ) ) {
throw TypeError( "Expecting enemy" );
}
enemy.croak();
}
@end verbatim
@caption{Croak like an enemy or a toad?}
@end float
In JavaScript, it is common convention to instead use @emph{duck typing},
which does not care what the intent of the interface is---it merely cares
whether the method being invoked actually exists.@footnote{``When I see a
bird that walks like a duck and swims like a duck and quacks like a duck, I
call that bird a duck.'' (James Whitcomb Riley).} So, in the case of the
above example, it is not a problem that an toad may be used in place of an
enemy---they both implement @code{croak} and so @emph{something} will
happen. This is most often exemplified by the use of object literals to
create ad-hoc instances of sorts:
@float figure, f:interface-objlit
@verbatim
const enemy = { croak() { /* ... */ ) };
const toad = { croak() { /* ... */ ) };
defeatEnemy( enemy ); // okay; duck typing
defeatEnemy( toad ); // okay; duck typing
// TypeError: object has no method 'croak'
defeatEnemy( { moo() { /*...*/ } } );
function defeatEnemy( enemy )
{
enemy.croak();
}
@end verbatim
@caption{Duck typing with object literals}
@end float
Duck typing has the benefit of being ad-hoc and concise, but places the onus
on the developer to realize the interface and ensure that it is properly
implemented. Therefore, there are two situations to address for GNU ease.js
users that prefer strongly typed interfaces:
@enumerate
@item
Ensure that non-ease.js users can create objects acceptable to the
strongly-typed API; and
@item
Allow ease.js classes to require a strong API for existing objects.
@end enumerate
These two are closely related and rely on the same underlying concepts.
@menu
* Object Interface Compatibility:: Using vanilla ECMAScript objects where
type checking is performed on GNU ease.js
interfaces
* Building Interfaces Around Objects:: Using interfaces to validate APIs of
ECMAScript objects
@end menu
@node Object Interface Compatibility
@subsubsection Object Interface Compatibility
It is clear that GNU ease.js' distinction between two separate interfaces
that share the same API is not useful for vanilla ECMAScript objects,
because those objects do not have an API for implementing interfaces (and if
they did, they wouldn't be ease.js' interfaces). Therefore, in order to
design a transparently interoperable system, this distinction must be
removed (but will be @emph{retained} within ease.js' system).
The core purpose of an interface is to declare an expected API, providing
preemptive warnings and reducing the risk of runtime error. This is in
contrast with duck typing, which favors recovering from errors when (and if)
they occur. Since an ECMAScript object cannot implement an ease.js interface
(if it did, it'd be using ease.js), the conclusion is that ease.js should
fall back to scanning the object to ensure that it is compatible with a
given interface.
A vanilla ECMAScript object is compatible with an ease.js interface if it
defines all interface members and meets the parameter count requirements of
those members.
@float Figure, f:interface-compat
@verbatim
const Duck = Interface( {
quack: [ 'str' ],
waddle: [],
} );
// false; no quack
Class.isA( Duck, { waddle() {} } );
// false; quack requires one parameter
Class.isA( Duck, {
quack() {},
waddle() {},
} );
// true
Class.isA( Duck, {
quack( str ) {},
waddle() {},
} );
// true
function ADuck() {};
ADuck.prototype = {
quack( str ) {},
waddle() {},
};
Class.isA( Duck, ( new ADuck() ) );
@end verbatim
@caption{Vanilla ECMAScript object interface compatibility}
@end float
@node Building Interfaces Around Objects
@subsubsection Building Interfaces Around Objects
A consequence of @ref{Object Interface Compatibility,,the previous section}
is that users of GNU ease.js can continue to use strongly typed interfaces
even if the objects they are interfacing with do not support ease.js'
interfaces. Consider, for example, a system that uses @code{XMLHttpRequest}:
@float Figure, f:interface-xmlhttp
@verbatim
// modeled around XMLHttpRequest
const HttpRequest = Interface(
{
abort: [],
open: [ 'method', 'url', 'async', 'user', 'password' ],
send: [],
} );
const FooApi = Class(
{
constructor( httpreq )
{
this.assertIsA( HttpRequest, httpreq );
// ...
}
} );
FooApi( new XMLHttpRequest() ); // okay
@end verbatim
@caption{Building an interface around needed functionality of
XMLHttpRequest}
@end float
This feature permits runtime polymorphism with preemptive failure instead of
inconsistently requiring duck typing for external objects, but interfaces for
objects handled through ease.js.