1
0
Fork 0
easejs/doc/classes.texi

2302 lines
80 KiB
Plaintext

@c This document is part of the GNU ease.js manual.
@c Copyright (C) 2011, 2012, 2013, 2014, 2015, 2016
@c Free Software Foundation, Inc.
@c
@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 Classes
@chapter Working With Classes
In Object-Oriented programming, the most common term you are likely to
encounter is ``Class''. A @dfn{class} is like a blueprint for creating an
@dfn{object}, which is an @dfn{instance} of that class. Classes contain
@dfn{members}, which include primarily properties and methods. A
@dfn{property} is a value, much like a variable, that a class ``owns''. A
@dfn{method}, when comparing with JavaScript, is a function that is
``owned'' by a class. As a consequence, properties and methods are not part
of the global scope.
JavaScript does not support classes in the manner traditionally understood
by Object-Oriented programmers. This is because JavaScript follows a
different model which instead uses prototypes. Using this model, JavaScript
supports basic instantiation and inheritance. Rather than instantiating
classes, JavaScript instantiates constructors, which are functions. The
following example illustrates how you would typically create a class-like
object in JavaScript:
@float Figure, f:class-js
@verbatim
/**
* Declaring "classes" WITHOUT ease.js
*/
// our "class"
const MyClass = function()
{
this.prop = 'foobar';
}
// a class method
MyClass.prototype.getProp = function()
{
return this.prop;
};
// create a new instance of the class and execute doStuff()
const foo = new MyClass();
console.log( foo.getProp() ); // outputs "foobar"
@end verbatim
@caption{Basic ``Class'' in JavaScript @emph{without} using ease.js}
@end float
This gets the job done, but the prototypal paradigm has a number of
limitations amongst its incredible flexibility. For Object-Oriented
programmers, it's both alien and inadequate. That is not to say that it is
not useful. In fact, it is so flexible that an entire Object-Oriented
framework was able to be built atop of it.
ease.js aims to address the limitations of the prototype model and provide a
familiar environment for Object-Oriented developers. Developers should not
have to worry about @emph{how} classes are implemented in JavaScript
(indeed, those details should be encapsulated). You, as a developer, should
be concerned with only how to declare and use the classes. If you do not
understand what a prototype is, that should be perfectly fine. You shouldn't
need to understand it in order to use the library (though, it's always good
to understand what a prototype is when working with JavaScript).
In this chapter and those that follow, we will see the limitations that
ease.js addresses. We will also see how to declare the classes using both
prototypes and ease.js, until such a point where prototypes are no longer
adequate.
@menu
* Defining Classes:: Learn how to define a class with ease.js
* Inheritance:: Extending classes from another
* Static Members:: Members whose use do not require instantiation
* Abstract Members:: Declare members, deferring definition to subtypes
* Method Proxies:: Methods that proxy calls to another object
@end menu
@node Defining Classes
@section Defining Classes
@table @code
@item C = Class( string @var{name}, Object @var{dfn} )
Define named class @var{C} identified by @var{name} described by @var{dfn}.
@item C = Class( string @var{name} ).extend( Object @var{dfn} )
Define named class @var{C} identified by @var{name} described by @var{dfn}.
@item C = Class( Object @var{dfn} )
Define anonymous class @var{C} as described by @var{dfn}.
@item C = Class.extend( Object @var{dfn } )
Define anonymous class @var{C} as described by @var{dfn}.
@end table
Class @var{C} can be defined in a number of manners, as listed above,
provided a @dfn{definition object} @var{dfn} containing the class members
and options. An optional string @var{name} may be provided to set an
internal identifier for @var{C}, which may be used for reflection and error
messages. If @var{name} is omitted, @var{C} will be declared anonymous.
@code{Class} must be imported (@pxref{Including}) from @code{easejs.Class};
it is not available in the global scope.
@anchor{dfnobj}
@subsection Definition Object
@table @code
@item dfn = @{ '[@var{keywords}] @var{name}': @var{value}[, ...] @}
Define definition object @var{dfn} containing a member identified by
@var{name}, described by optional @var{keywords} with the value of
@var{value}. The member type is determined by @code{typeof} @var{value}.
Multiple members may be provided in a single definition object.
@end table
The definition object @var{dfn} has the following properties:
@enumerate
@item
The keys represent the @dfn{member declaration}, which may optionally
contain one or more @var{keywords} delimited by spaces. A space must delimit
the final keyword and @var{name}.
@enumerate
@item
@var{keywords} must consist only of recognized tokens, delimited by
spaces.
@item
Each token in @var{keywords} must be unique per @var{name}.
@end enumerate
@item
The @var{value} represents the @dfn{member definition}, the type of which
determines what type of member will be declared.
@enumerate
@item
A @var{value} of type @code{function} will define a @dfn{method}, which is
an invokable member whose context is assigned to the class or class
instance depending on @var{keywords}.
@item
All other types of @var{value} will define a @dfn{property} - a mutable
value equal to @var{value}, assigned to a class or instance depending on
@var{keywords}. Properties may be made immutable using @var{keywords}.
@item
Getters/setters may be defined in an ECMAScript 5 or greater environment.
Getters/setters must share the same value for @var{keywords}.
@end enumerate
@item
@var{name} must be unique across all members of @var{dfn}.
@end enumerate
@subsection Member Validations
For any member @var{name}:
@itemize
@item
@var{keywords} of member @var{name} may contain only one access modifier
(@pxref{Access Modifiers}).
@item
See @ref{Member Keywords,,Member Keywords} for @var{keywords} restrictions.
@end itemize
For any member @var{name} declared as a @emph{method}, the following must
hold
true:
@itemize
@item
@var{keywords} of member @var{name} may not contain
@ref{Member Keywords,,@code{override}} without a super method of the same
@var{name} (@pxref{Inheritance}).
@item
@var{keywords} of member @var{name} may not contain both
@ref{Member Keywords,,@code{static}} and @ref{Member
Keywords,,@code{virtual}} keywords (@pxref{Static Members} and
@ref{Inheritance}).
@item
@var{keywords} of member @var{name} may not contain the
@ref{Member Keywords,,@code{const}} keyword.
@item
For any member @var{name} that contains the keyword
@ref{t:keywords,,@code{abstract}} in @var{keywords}, class @var{C} must
instead be declared as an @code{AbstractClass} (@pxref{Abstract Classes}).
@end itemize
@subsection Discussion
In @ref{f:class-js}, we saw how one would conventionally declare a
class-like object (a prototype) in JavaScript. This method is preferred for
many developers, but it is important to recognize that there is a distinct
difference between Prototypal and Classical Object-Oriented development
models. Prototypes lack many of the conveniences and features that are
provided by Classical languages, but they can be emulated with prototypes.
As an Object-Oriented developer, you shouldn't concern yourself with
@emph{how} a class is declared in JavaScript. In true OO fashion, that
behavior should be encapsulated. With ease.js, it is.
Let's take a look at how to declare that exact same class using ease.js:
@float Figure, f:class-easejs
@verbatim
const { Class } = require( 'easejs' );
// or: var Class = require( 'easejs' ).Class;
const MyClass = Class(
{
'public prop': 'foobar',
'public getProp'()
{
return this.prop;
}
} );
// create a new instance of the class and execute doStuff()
const foo = MyClass();
console.log( foo.getProp() ); // outputs "foobar"
@end verbatim
@caption{Basic anonymous class declaration using ease.js}
@end float
That should look much more familiar to Object-Oriented developers. There are
a couple important notes before we continue evaluating this example:
@itemize
@item
The first thing you will likely notice is our use of the @code{public}
keyword;
this is optional (the default visibility is public);
it may be omitted for a more traditional JavaScript feel.
We will get more into visibility later on (@pxref{Access Modifiers}).
@item
Unlike @ref{f:class-js,}, we do not use the @code{new} keyword in order to
instantiate our class. You are more than welcome to use the @code{new}
keyword if you wish, but it is optional when using ease.js. This is mainly
because without this feature, if the keyword is omitted, the constructor is
called as a normal function, which could have highly negative consequences.
This style of instantiation also has its benefits, which will be discussed
later on.
@item
ease.js's class module is imported using @code{require()} in the above
example. If using ease.js client-side (@pxref{Client-Side Include}), you
can instead use @samp{const @{Class@} = easejs}. From this point on,
importing the module will not be included in examples.
@end itemize
The above example declares an anonymous class, which is stored in the
variable @var{MyClass}. By convention, we use CamelCase, with the first
letter capital, for class names (and nothing else).
@menu
* Class Caveats:: Important things to note about using ease.js classes
* Anonymous vs. Named Classes::
* Constructors:: How to declare a constructor
* Temporary Classes:: Throwaway classes that only need to be used once
* Temporary Instances:: Throwaway instances that only need to be used once
@end menu
@node Class Caveats
@subsection Class Caveats
ease.js tries to make classes act as in traditional Classical@tie{}OOP
as much as possible,
but there are certain limitations,
especially when supporting ECMAScript@tie{}3.
These situations can cause some subtle bugs,
so it's important to note and understand them.
@subsubsection Returning Self
Returning @code{this} is a common practice for method
chaining.@footnote{
An interface that performs method chaining is less frequently
referred to as a ``fluent interface''.
This manual does not use that terminology.
Note also that method chaining implies that the class has state:
consider making your objects immutable instead,
which creates code that is easier to reason about.}
In the majority of cases, this works fine in ease.js
(see also @ref{Temporary Classes}):
@float Figure, f:method-chain
@verbatim
const Foo = Class( 'Foo',
{
'public beginning'()
{
return this;
},
'public middle'()
{
return this;
},
'public end'()
{
// ...
}
} );
Foo().beginning().middle().end();
@end verbatim
@caption{Using @code{this} for method chaining}
@end float
Within the context of the method, @code{this} is a reference to
the@tie{}privacy visibility object for that instance
(@pxref{The Visibility Object}).
That is---it exposes all of the object's internal state.
When it is returned from a method call, ease.js recognizes this and
replaces it with a reference to the @emph{public} visibility
object---the object that the rest of the world interacts with.
But what if you produce @code{this} in some other context?
A callback, for example:
@float Figure, f:method-this-callback
@verbatim
const Foo = Class( 'Foo',
{
'private _foo': 'good',
'public beginning'( c )
{
// XXX: `this' is the private visibility object
c( this );
},
'public end'()
{
return this._foo;
}
} );
// result: 'bad'
Foo()
.beginning( self =>
{
// has access to internal state
self._foo = 'bad';
} )
.end();
@end verbatim
@caption{Accidentally revealing internal state via callback}
@end float
In @ref{f:method-this-callback},
@code{beginning} applies the callback with a reference to what most
would believe to be the class instance
(which is a reasonable assumption,
considering that ease.js usually maintains that facade).
Since @code{this} is a reference to the private visibility object,
the callback has access to all its internal state,
and therefore the ability to set @code{_foo}.
To solve this problem,
use @code{this.__inst},
which is a reference to the @emph{public} visibility object
(the same one that ease.js would normally translate to on your
behalf):
@float Figure, f:method-callback-inst
@verbatim
const Foo = Class( 'Foo',
{
'private _foo': 'good',
'public beginning'( c )
{
// OK
c( this.__inst );
},
'public end'()
{
return this._foo;
}
} );
// result: 'good'
Foo()
.beginning( self =>
{
// sets public property `_foo', since `self' is now the public
// visibility object
self._foo = 'bad';
} )
.end();
@end verbatim
@caption{Providing public visibility object using @code{this.__inst}}
@end float
@node Anonymous vs. Named Classes
@subsection Anonymous vs. Named Classes
We state that @ref{f:class-easejs,} declared an @dfn{anyonmous class}
because the class was not given a name. Rather, it was simply assigned to a
variable, which itself has a name. To help keep this idea straight, consider
the common act of creating anonymous functions in JavaScript:
@float Figure, f:anon-func
@verbatim
// anonymous
const myFunc = function() {};
// named
function myNamedFunc() {};
@end verbatim
@caption{Anonymous functions in JavaScript}
@end float
If the function itself is not given a name, it is considered to be
anonymous, even though it is stored within a variable. Just as the engine
has no idea what that function is named, ease.js has no idea what the class
is named because it does not have access to the name of the variable to
which it was assigned.
Names are not required for classes, but they are recommended. For example,
consider what may happen when your class is output in an error message.
@float Figure, f:anon-err
@verbatim
// call non-existent method
foo.baz();
// TypeError: Object #<anonymous> has no method 'baz'
@end verbatim
@caption{Anonymous classes do not make for useful error messages}
@end float
If you have more than a couple classes in your software, that error message
is not too much help. You are left relying on the stack trace to track down
the error. This same output applies to converting a class to a string or
viewing it in a debugger. It is simply not helpful. If anything, it is
confusing. If you've debugged large JS applications that make liberal use of
anonymous functions, you might be able to understand that frustration.
Fortunately, ease.js permits you to declare a named class. A @dfn{named
class} is simply a class that is assigned a string for its name, so that
error messages, debuggers, etc provide more useful information. @emph{There
is functionally no difference between named and anonymous classes.}
@float Figure, f:class-named
@verbatim
const MyFoo = Class( 'MyFoo', {} );
const foo = MyFoo();
// call non-existent method
foo.baz();
// TypeError: Object #<MyFoo> has no method 'baz'
@end verbatim
@caption{Declaring an empty @emph{named} class}
@end float
Much better! We now have a useful error message and immediately know which
class is causing the issue.
@node Constructors
@subsection Constructors
ease.js defines object constructors in a manner consistent with many
other classical object-oriented languages:
a specially named method on the@tie{}class.
Traditionally, a ``constructor'' in JavaScript is a function intended to
initialize a new object when invoked with the@tie{}@code{new}
keyword.
That function also contains a @code{prototype} property that defines the
object's prototype.
ECMAScript@tie{}6,
with the introduction of the@tie{}@code{class} keyword,
provided a standard @code{constructor} method that generates
constructor for the prototype.
ease.js was written long before ES6 was ratified. The implementation
it chose is very similar to that of PHP's (@pxref{Constructor
Implementation}), using a @code{__construct}@tie{}method:
@float Figure, f:constructor
@verbatim
const Foo = Class( 'Foo',
{
// may also use `constructor`; see below
__construct( name )
{
console.log( 'Hello, ' + name + '!' );
}
} );
// instantiate the class, invoking the constructor
Foo( 'World' );
// Output:
// Hello, World!
@end verbatim
@caption{Declaring constructors using ease.js}
@end float
ease.js introduced the @code{constructor} method in version@tie{}0.2.7
to match the ES6 ``class'' implementation;
it is an alias for @code{__construct},
and they may be used interchangeably.@footnote{
With one caveat:
when referencing the constructor method from another method
(dare I ask why you would do such a thing?),
you must use e.g.@tie{}@samp{this.__construct};
using @samp{this.constructor} would not be very useful,
as this is always set (in JavaScript) to the function
that instantiated the object.}
This method name may also be used prior to ES6.
@float Figure, f:constructor-es6
@verbatim
// ECMAScript 6 syntax
const Foo = Class( 'Foo',
{
// you may still use __construct if you'd prefer, as shown above
constructor( name )
{
console.log( 'Hello, ' + name + '!' );
}
} );
// instantiate the class, invoking the constructor
Foo( 'World' );
// Output:
// Hello, World!
@end verbatim
@caption{Declaring constructors in an ECMAScript 6 style}
@end float
The constructor is invoked just after the class is instantiated,
allowing for necessary initialization tasks before the class can be
used.
It is good practice to use the constructor @emph{only} for
initialization that is critical to the object's integrity (such as
initializing internal state).
It is considered poor design for the constructor to have side-effects;
that is,
the act of instantiating an object should not manipulate global
variables or the environment.
The constructor operates exactly how you would expect a constructor to
in JavaScript,
with one important difference:
returning an object in the constructor does @emph{not} return that
object in place of the new class instance.
If you wish to prevent a class from being instantiated,
simply throw an exception within the constructor.
@float Figure, f:constructor-prevent
@verbatim
const Foo = Class( 'Foo',
{
constructor( name )
{
throw Error( "Cannot instantiate class Foo" );
}
} );
@end verbatim
@caption{Prevent class from being instantiated}
@end float
Constructors are always public;
there is no harm in explicitly specifying the keyword,
but it's usually omitted for brevity.
It is not permitted to make a constructor protected or private
(@pxref{Access Modifiers}).
Unlike all other methods,
constructors are @ref{Member Keywords,,@code{virtual}} by default.
Many other languages (C++, Java, C#, and others) do not inherit
class constructors from their supertypes.
ease.js classes are prototypes,
and uninstantiated prototypes are functions (constructors),
so classes are effectively first-class objects in JavaScript.
Consequently,
they can be passed around and invoked like any other function,
which can be a convenient alternative to factories
(and a transparent alternative to functions that create objects).
It is therefore useful to have the constructor as part of the class's
public API.
However,
it is also important that subtypes always be able to override the
constructor (@pxref{Overriding Methods});
otherwise subtypes may not be able to initialize properly,
making for a very clumsy implementation.
Constructors are optional,
and no constructor is defined by default.@footnote{
That is, no user-facing constructor is defined by default;
ease.js handles plenty of its own internal tasks during
construction,
which cannot be overridden or inhibited.}
@node Temporary Classes
@subsection Temporary Classes
In @ref{f:class-easejs,}, we saw that the @code{new} keyword was unnecessary
when instantiating classes. This permits a form of shorthand that is very
useful for creating @dfn{temporary classes}, or ``throwaway`` classes which
are used only once.
Consider the following example:
@float Figure, f:class-tmp
@verbatim
// new instance of anonymous class
const foo = Class(
{
'public bar'()
{
return 'baz';
}
} )();
foo.bar(); // returns 'baz'
@end verbatim
@caption{Declaring a temporary (throwaway) class}
@end float
In @ref{f:class-tmp,} above, rather than declaring a class, storing that in
a variable, then instantiating it separately, we are doing it in a single
command. Notice the parenthesis at the end of the statement. This invokes
the constructor. Since the @code{new} keyword is unnecessary, a new instance
of the class is stored in the variable @var{foo}.
We call this a temporary class because it is used only to create a single
instance. The class is then never referenced again. Therefore, we needn't
even store it - it's throwaway.
The downside of this feature is that it is difficult to notice unless the
reader is paying very close attention. There is no keyword to tip them off.
Therefore, it is very important to clearly document that you are storing an
instance in the variable rather than an actual class definition. If you
follow the CamelCase convention for class names, then simply do not
capitalize the first letter of the destination variable for the instance.
@node Temporary Instances
@subsection Temporary Instances
Similar to @ref{Temporary Classes,}, you may wish to use an @emph{instance}
temporarily to invoke a method or chain of methods. @dfn{Temporary
instances} are instances that are instantiated in order to invoke a method
or chain of methods, then are immediately discarded.
@float Figure, f:inst-tmp
@verbatim
// retrieve the name from an instance of Foo
const name = Foo().getName();
// method chaining
const car = VehicleFactory()
.createBody()
.addWheel( 4 )
.addDoor( 2 )
.build();
// temporary class with callback
HttpRequest( host, port )
.get( path, data => console.log( data ) );
// Conventionally (without ease.js), you'd accomplish the above using
// the 'new' keyword. You may still do this with ease.js, though it is
// less clean looking.
( new Foo() ).someMethod();
@end verbatim
@caption{Declaring a temporary (throwaway) class}
@end float
Rather than storing the class instance, we are using it simply to invoke
methods. The results of those methods are stored in the variable rather than
the class instance. The instance is immediately discarded, since it is no
longer able to be referenced, and is as such a temporary instance.
In order for method chaining to work, each method must return itself.
This pattern is useful for when a class requires instantiation in order to
invoke a method. Classes that intend to be frequently used in this manner
should declare static methods so that they may be accessed without the
overhead of creating a new class instance.
@node Inheritance
@section Inheritance
@table @code
@item C' = Class( string @var{name} ).extend( Object @var{base}, Object
@var{dfn} ) Define named class @var{C'} identified by @var{name} as a
subtype of @var{base}, described by @var{dfn}. @var{base} may be of type
@code{Class} or may be any enumerable object.
@item C' = C.extend( Object @var{dfn} )
Define anonymous class @var{C'} as a subtype of class @var{C}, described by
@var{dfn}.
@item C' = Class.extend( Object @var{base}, Object @var{dfn} )
Define anonymous class @var{C'} as a subtype of @var{base}, described by
@var{dfn}. @var{base} may be of type @code{Class} or may be any enumerable
object.
@end table
@var{C} is a class as defined in @ref{Defining Classes}. @var{base} may be
any class or object containing enumerable members. @var{dfn} is to be a
definition object as defined in @ref{dfnobj,,Definition Object}.
Provided non-final @var{C} or @var{base} to satisfy requirements of @var{C},
class @var{C'} will be defined as a @dfn{subtype} (child) of @dfn{supertype}
(parent) class @var{C}. Provided @var{base} that does @emph{not} satisfy
requirements of @var{C}, @var{C'} will be functionally equivalent to a
subtype of anonymous class @var{B} as defined by @var{B} =
Class( @var{base} ).
@subsection Member Inheritance
Let @var{dfn\_n\^c} denote a member of @var{dfn} in regards to class @var{c}
that matches (case-sensitive) name @var{n}. Let @var{o\_n} denote an
override, represented as boolean value that is true under the condition that
both @var{dfn\_n\^C'} and @var{dfn\_n\^C} are defined values.
@var{C'} will @dfn{inherit} all public and protected members of supertype
@var{C} such that @var{dfn\_n\^C'} = @var{dfn\_n\^C} for each @var{dfn\^C}.
For any positive condition @var{o\_n}, member @var{dfn\_n\^C'} will be said
to @dfn{override} member @var{dfn\_n\^C}, provided that overriding member
@var{n} passes all validation rules associated with the operation. A
@code{protected} member may be @dfn{escalated} to @code{public}, but the
reverse is untrue. @code{private} members are invisible to
subtypes.@footnote{This is true conceptually, but untrue in pre-ES5
environments where ease.js is forced to fall back (@pxref{Private Member
Dilemma}). As such, one should always develop in an ES5 or later environment
to ensure visibility restrictions are properly enforced.}
For any positive condition @var{o\_n} where member @var{n} is defined as a
@emph{method}:
@itemize
@item
One of the following conditions must always be true:
@itemize
@item
@var{dfn\_n\^C} is declared with the @ref{Member Keywords,,@code{virtual}}
keyword and @var{dfn\_n\^C'} is declared with the
@ref{Member Keywords,,@code{override}} keyword.
@item
@var{dfn\_n\^C} is declared with the @ref{Member Keywords,,@code{override}}
keyword and @var{dfn\_n\^C'} is also declared with the
@ref{Member Keywords,,@code{override}} keyword.
@item
@var{dfn\_n\^C} is declared with the @ref{Member Keywords,,@code{abstract}}
keyword and @var{dfn\_n\^C'} omits the @ref{Member Keywords,,@code{override}}
keywords.
@end itemize
@item
The argument count of method @var{dfn\_n\^C'} must be >= the argument count
of method @var{dfn\_n\^C} to permit polymorphism.
@item
A reference to super method @var{dfn\_n\^C} will be preserved and assigned
to @samp{this.__super} within context of method
@var{dfn\_n\^C'}.@footnote{Due to an @ref{Method
Wrapping,,implementation detail}, @samp{this.__super} may remain in
scope after invoking a private method; this behavior is undefined and
should not be relied on.}
@item
A method is said to be @dfn{concrete} when it provides a definition and
@dfn{abstract} when it provides only a declaration
(@pxref{dfnobj,,Definition Object}).
@itemize
@item
Any method @var{n} such that @var{dfn\_n\^C} is declared @code{abstract}
may be overridden by a concrete or abstract method @var{dfn\_n\^C'}.
@item
A method @var{n} may @emph{not} be declared
@ref{Member Keywords,,@code{abstract}} if @var{dfn\_n\^C} is concrete.
@end itemize
@item
Member @var{dfn\_n\^C'} must be a method.
@item
Member @var{dfn\_n\^C} must not have been declared @ref{Member
Keywords,,@code{private}} (@pxref{Private Member Dilemma}).
@end itemize
Members that have been declared @code{static} cannot be overridden
(@pxref{Static Members}).
@subsection Discussion
Inheritance can be a touchy subject among many Object-Oriented developers
due to encapsulation concerns and design considerations over method
overrides. The decision of whether or not inheritance is an appropriate
choice over composition is left to the developer; ease.js provides the
facilities for achieving classical inheritance where it is desired.
@float Figure, f:inheritance-ex
@image{img/inheritance-ex}
@caption{Basic inheritance example}
@end float
In the above example, we would say that @var{LazyDog} and @var{TwoLeggedDog}
are @emph{subtypes} of @var{Dog}, and that @var{Dog} is the @emph{supertype}
of the two. We describe inheritance as an ``is a'' relationship. That is:
@itemize
@item
@var{LazyDog} is a @var{Dog}.
@item
@var{TwoLeggedDog} is also a @var{Dog}.
@item
@var{Dog} is @emph{not} a @var{LazyDog} or a @var{TwoLeggedDog}.
@end itemize
Subtypes @dfn{inherit} all public and protected members of their supertypes
(@pxref{Access Modifiers}). This means that, in the case of our above
example, the @code{walk()} and @code{bark()} methods would be available to
our subtypes. If the subtype also defines a method of the same name, as was
done above, it will @dfn{override} the parent functionality. For now, we
will limit our discussion to public members. How would we represent these
classes using ease.js?
@float Figure, f:inheritance
@verbatim
// our parent class (supertype)
const Dog = Class( 'Dog',
{
'virtual public walk'()
{
console.log( 'Walking the dog' );
},
'public bark'()
{
console.log( 'Woof!' );
}
} );
// subclass (child), as a named class
const LazyDog = Class( 'LazyDog' ).extend( Dog,
{
'override public walk'()
{
console.log( 'Lazy dog refuses to walk.' );
}
} );
// subclass (child), as an anonymous class
const TwoLeggedDog = Dog.extend(
{
'override public walk'()
{
console.log( 'Walking the dog on two feet' );
}
} );
// supertype override is implicitly virtual
const ReallyLazyDog = LazyDog.extend(
{
'override public walk'()
{
// ...
}
} );
@end verbatim
@caption{Inheritance in ease.js}
@end float
(The above inheritance tree is a bad idea and is for illustration
purposes only:
if you want to layer attributes,
use Traits or a composition-based pattern like Decorators.)
You should already understand how to define a class (@pxref{Defining
Classes}). The above example introduced two means of @dfn{extending}
classes -- defining a new class that inherits from a parent:
@table @strong
@item Named Subclasses
@var{LazyDog} is defined as a @emph{named} subclass (@pxref{Anonymous vs.
Named Classes}). This syntax requires the use of @samp{Class( 'Name' )}. The
@code{extend()} method then allows you to extend from an existing class by
passing the class reference in as the first argument.
@item Anonymous Subclasses
@var{TwoLeggedDog} was declared as an @emph{anonymous} subclass. The syntax
for this declaration is a bit more concise, but you forfeit the benefits of
named classes (@pxref{Anonymous vs. Named Classes}). In this case, you can
simply call the supertype's @code{extend()} method. Alternatively, you can
use the @samp{Class.extend( Base, @{@} )} syntax, as was used with the named
subclass @var{LazyDog}.
@end table
You are @emph{always} recommended to use the named syntax when declaring
classes in order to provide more useful error messages. If you are willing
to deal with the less helpful error messages, feel free to use anonymous
classes for their conciseness.
@menu
* Understanding Member Inheritance:: How to work with inherited members
* Overriding Methods:: Overriding inherited methods
* Type Checks and Polymorphism:: Substituting similar classes for
one-another
* Visibility Escalation:: Increasing visibility of inherited
members
* Error Subtypes:: Transparent Error subtyping
* Final Classes:: Classes that cannot be inherited from
@end menu
@node Understanding Member Inheritance
@subsection Understanding Member Inheritance
In @ref{f:inheritance}, we took a look at how to inherit from a parent
class. What does it mean when we ``inherit'' from a parent? What are we
inheriting? The answer is: the API.
There are two types of APIs that subtypes can inherit from their parents:
@table @emph
@item Public API
This is the API that is accessible to everyone using your class. It contains
all public members. We will be focusing on public members in this chapter.
@item Protected API
Protected members make up a protected API, which is an API available to
subclasses but @emph{not} the outside world. This is discussed more in the
Access Modifiers section (@pxref{Access Modifiers}), so we're going to leave
this untouched for now.
@end table
When a subtype inherits a member from its parent, it acts almost as if that
member was defined in the class itself@footnote{This statement is not to
imply that inheritance is a case of copy-and-paste. There are slight
variations, which are discussed in more detail in the Access Modifiers
section (@pxref{Access Modifiers}).}. This means that the subtype can use
the inherited members as if they were its own (keep in mind that members
also include properties). This means that we @emph{do not} have to redefine
the members in order to use them ourselves.
@var{LazyDog} and @var{TwoLeggedDog} both inherit the @code{walk()} and
@code{bark()} methods from the @var{Dog} supertype. Using @var{LazyDog} as
an example, let's see what happens when we attempt to use the @code{bark()}
method inherited from the parent.
@float Figure, f:using-inherited-members
@verbatim
const LazyDog = Class( 'LazyDog' ).extend( Dog,
{
/**
* Bark when we're poked
*/
'virtual public poke'()
{
this.bark();
}
} );
// poke() a new instance of LazyDog
LazyDog().poke();
// Output:
// Woof!
@end verbatim
@caption{Using inherited members}
@end float
In @ref{f:using-inherited-members} above, we added a @code{poke()} method to
our @var{LazyDog} class. This method will call the @code{bark()} method that
was inherited from @var{Dog}. If we actually run the example, you will
notice that the dog does indeed bark, showing that we are able to call our
parent's method even though we did not define it ourselves.
@node Overriding Methods
@subsection Overriding Methods
When a method is inherited, you have the option of either keeping the
parent's implementation or overriding it to provide your own. When you
override a method, you replace whatever functionality was defined by the
parent. This concept was used to make our @var{LazyDog} lazy and our
@var{TwoLeggedDog} walk on two legs in @ref{f:inheritance}.
After overriding a method, you may still want to invoke the parent's method.
This allows you to @emph{augment} the functionality rather than replacing it
entirely. ease.js provides a magic @code{__super()} method to do this. This
method is defined only for the overriding methods and calls the parent
method that was overridden.
In order to demonstrate this, let's add an additional subtype to our
hierarchy. @var{AngryDog} will be a subtype of @var{LazyDog}. Not only is
this dog lazy, but he's rather moody.
@float Figure, f:super-method
@verbatim
const AngryDog = Class( 'AngryDog' ).extend( LazyDog,
{
'public poke'()
{
// augment the parent method
console.log( 'Grrrrrr...' );
// call the overridden method
this.__super();
}
} );
// poke a new AngryDog instance
AngryDog().poke();
// Output:
// Grrrrrr...
// Woof!
@end verbatim
@caption{Using @code{__super()} method}
@end float
If you remember from @ref{f:using-inherited-members}, we added a
@code{poke()} method to @var{LazyDog}. In @ref{f:super-method} above, we are
overriding this method so that @var{AngryDog} growls when you poke him.
However, we still want to invoke @var{LazyDog}'s default behavior when he's
poked, so we also call the @code{__super()} method. This will also make
@var{AngryDog} bark like @var{LazyDog}.
It is important to note that @code{__super()} must be invoked like any other
method. That is, if the overridden method requires arguments, you must pass
them to @code{__super()}. This allows you to modify the argument list before
it is sent to the overridden method.
@subsubsection Arbitrary Supertype Method Invocation
The aforementioned @code{__super} method satisfies invoking an overridden
method within the context of the method that is overriding it, but falls
short when needing to invoke an overridden method outside of that context.
As an example, consider that @code{AngryDog} also implemented a
@code{pokeWithDeliciousBone} method, in which case we want to bypass the
dog's angry tendencies and fall back to behaving like a @code{LazyDog} (the
supertype). This poses a problem, as we have overridden @code{LazyDog#poke},
so calling @code{this.poke} would not yield the correct result (the dog
would still respond angerly). @code{__super} cannot be used, because that
would attempt to invoke a supermethod named
@code{pokeWithDeliciousBone}; no such method even exists, so in this case,
@code{__super} wouldn't even be defined.
We can remedy this using @code{this.poke.super}, which is a strict reference
to the overridden @code{poke} method (in this case, @code{LazyDog.poke}):
@float Figure, f:arbitrary-super-method
@verbatim
const AngryDog = Class( 'AngryDog' ).extend( LazyDog,
{
'public poke'()
{
// ...
},
'public pokeWithDeliciousBone'()
{
// invoke LazyDog.poke
this.poke.super.call( this );
}
} );
// poke a new AngryDog instance with a delicious bone
AngryDog().pokeWithDeliciousBone();
// Output:
// Woof!
@end verbatim
@caption{Using the method-supecific @code{super} reference}
@end float
It is important to note that, in its current implementation, since
@code{super} is a reference to a function, its context must be provided
using the ECMAScript-native @code{apply} or @code{call} (the first argument
being the context); using @code{this} as the context (as shown above) will
invoke the method within the context of the calling
instance.@footnote{Specifically, it will invoke the method within the
context of the calling instance's private visibility object (@pxref{The
Visibility Object}). While this may seem like a bad idea---since it appears
to give the supermethod access to our private state---note that the method
wrapper for the overridden method will properly restore the private state of
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
work with any subtypes. This concept is referred to as @dfn{polymorphism},
and is a very powerful aspect of Object-Oriented programming.
Let's consider a dog trainer. A dog trainer can generally train any type of
dog (technicalities aside), so it would stand to reason that we would want
our dog trainer to be able to train @var{LazyDog}, @var{AngryDog},
@var{TwoLeggedDog}, or any other type of @var{Dog} that we may throw at
him/her.
@float Figure, f:polymorphism-uml
@image{img/composition-uml}
@caption{Class structure to demonstrate polymorphism}
@end float
Type checks are traditionally performed in JavaScript using the
@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}.
@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.
@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
const dog = Dog();
const lazy = LazyDog();
const angry = AngryDog();
Class.isInstanceOf( Dog, dog ); // true
Class.isA( Dog, dog ); // true
Class.isA( LazyDog, dog ); // false
Class.isA( Dog, lazy ); // true
Class.isA( Dog, angry ); // true
// 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.
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
const DogTrainer = Class( 'DogTrainer',
{
constructor( dog )
{
this.assertIsA( Dog, dog );
}
} );
// these are all fine
DogTrainer( Dog() );
DogTrainer( LazyDog() );
DogTrainer( AngryDog() );
DogTrainer( TwoLeggedDog() );
// this is not fine; we're passing the class itself
DogTrainer( LazyDog );
// nor is this fine, as it is not a dog
DogTrainer( {} );
@end verbatim
@caption{Polymorphism in ease.js}
@end float
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.}
@node Visibility Escalation
@subsection Visibility Escalation
Let @var{a\_n} denote a numeric level of visibility for @var{dfn\_n\^C} such
that the access modifiers (@pxref{Access Modifiers}) @code{private},
@code{protected} and @code{public} are associated with the values @code{1},
@code{2} and @code{3} respectively. Let @var{a'} represent @var{a} in
regards to @var{C'} (@pxref{Inheritance}).
For any member @var{n} of @var{dfn}, the following must be true:
@itemize
@item
@var{a'\_n} >= @var{a\_n}.
@item
@var{dfn\_n\^C'} cannot be redeclared without providing a new definition
(@var{value}).
@end itemize
@subsubsection Discussion
@dfn{Visibility escalation} is the act of increasing the visibility of a
member. Since private members cannot be inherited, this would then imply
that the only act to be considered "escallation" would be increasing the
level of visibility from @code{protected} to @code{private}.
Many follow the convention of prefixing private members with an underscore
but leaving omitting such a prefix from protected members. This is to permit
visibility escalation without renaming the member. Alternatively, a new
member can be defined without the prefix that will simply call the
overridden member (although this would then not be considered an escalation,
since the member name varies).
In order to increase the visibility, you must override the member; you
cannot simply redeclare it, leaving the parent definition in tact. For
properties, this has no discernible effect unless the @var{value} changes,
as you are simply redefining it. For methods, this means that you are
overriding the entire @var{value}. Therefore, you will either have to
provide an alternate implementation or call @samp{this.__super()} to invoke
the original method.
Note that @emph{you cannot de-escalate from public to protected}; this will
result in an error. This ensures that once a class defines an API,
subclasses cannot alter it. That API must be forever for all subtypes to
ensure that it remains polymorphic.
Let's take a look at an example.
@float Figure, f:vis-esc
@verbatim
const Foo = Class(
{
'virtual protected canEscalate': 'baz',
'virtual protected escalateMe'( arg )
{
console.log( 'In escalateMe' );
},
'virtual public cannotMakeProtected'()
{
}
} );
const SubFoo = Foo.extend(
{
/**
* Escalating a property means redefining it
*/
'public canEscalate': 'baz',
/**
* We can go protected -> public
*/
'public escalateMe'( arg )
{
// simply call the parent method
this.__super( arg );
}
} );
@end verbatim
@caption{Visibility can be escalated}
@end float
Note that, in the above example, making the public @var{cannotMakeProtected}
method protected would throw an error.
@node Error Subtypes
@subsection Error Subtypes
Extending ECMAScript's built-in @var{Error} type is a bit cumbersome (to
say the least)---it involves not only the traditional prototype chain,
but also setting specific properties within the constructor. Further,
different environments support different features (e.g. stack traces and
column numbers), and values are relative to the stack frame of the
@var{Error} subtype constructor itself.
With GNU ease.js, error subtyping is transparent:
@float Figure, f:error-extend
@verbatim
const MyError = Class( 'MyError' )
.extend( Error, {} );
const e = MyError( 'Foo' );
e.message; // Foo
e.name; // MyError
// -- if supported by environment --
e.stack; // stack beginning at caller
e.fileName; // caller filename
e.lineNumber; // caller line number
e.columnNumber; // caller column number
// general case
throw MyError( 'Foo' );
@end verbatim
@caption{Transparent @var{Error} extending in ease.js}
@end float
If ease.js detects that you are extending an @var{Error} object or any
of its subtypes, it will handle a number of things for you, depending on
environment:
@enumerate
@item Produce a default constructor method (@pxref{Constructors}) that
assigns the error message to the string passed as the first argument;
@item Sets the error name to the class name;
@item Provides a stack trace via @var{stack}, if supported by the
environment, stripping itself from the head of the stack; and
@item Sets any of @var{fileName}, @var{lineNumber}, and/or
@var{columnNumber} when supported by the environment.
@end enumerate
If a constructor method is provided in the class definition
(@pxref{Constructors}), then it will be invoked immediately after the
error object is initialized by the aforementioned default
constructor.@footnote{The reason that ease.js
does not permit overriding the generated constructor is an
implementation detail: the generated constructor is not on the
supertype, so there is not anything to actually override. Further, the
generated constructor provides a sane default behavior that should be
implicit in error classes anyway; that behavior can be overridden simply
be re-assigning the values that are assigned for you (e.g. name or line
number).} @var{this.__super} in that context refers to the constructor
of the supertype (as would be expected), @emph{not} the default error
constructor.
ease.js will automatically detect what features are supported by the
current environment, and will @emph{only} set respective values if the
environment itself would normally set them. For example, if ease.js can
determine a column number from the stack trace, but the environment does
not normally set @var{columnNumber} on @var{Error} objects, then neither
will ease.js; this leads to predictable and consistent behavior.
ease.js makes its best attempt to strip itself from the head of the
stack trace. To see why this is important, consider the generally
recommended way of creating an @var{Error} subtype in ECMAScript:
@float Figure, f:ecma-error-extend
@verbatim
function ErrorSubtype( message )
{
const err = new Error();
this.name = 'ErrorSubtype';
this.message = message || 'Error';
this.stack = err.stack;
this.lineNumber = err.lineNumber;
this.columnNumber = err.columnNumber;
this.fileName = err.fileName;
}
ErrorSubtype.prototype = new Error();
ErrorSubtype.prototype.constructor = ErrorSubtype;
@end verbatim
@caption{@var{Error} subtyping in plain ECMAScript 3}
@end float
Not only is @ref{f:ecma-error-extend} all boilerplate and messy, but
it's not entirely truthful: To get a stack trace, @var{Error} is
instantiated within the constructor @var{ErrorSubtype}; this ensures
that the stack trace will actually include the caller. Unfortunately,
it also includes the @emph{current frame}; the topmost frame in the
stack trace will be @var{ErrorSubtype} itself. To make matters worse,
all of @var{lineNumber}, @var{columNumber}, and @var{fileName} (if
defined) will be set to the stack frame of our constructor, @emph{not}
the caller.
ease.js will set each of those values to represent the caller. To do
so, it parses common stack trace formats. Should it fail, it simply
falls back to the default behavior of including itself in the stack
frame.
The end result of all of this is---hopefully---concise @var{Error}
subtypes that actually function as you would expect of an @var{Error},
without any boilerplate at all. The @var{Error} subtypes created with
ease.js can be extended like the built-ins, and may extend any of the
built-in error types (e.g. @var{TypeError} and @var{SyntaxError}).
@node Final Classes
@subsection Final Classes
@table @code
@item F = FinalClass( string @var{name}, Object @var{dfn} )
Define final named class @var{C} identified by @var{name} described by
@var{dfn}.
@item F = FinalClass( string @var{name} ).extend( Object @var{dfn} )
Define final named class @var{C} identified by @var{name} described by
@var{dfn}.
@item F = FinalClass( Object @var{dfn} )
Define anonymous final class @var{C} as described by @var{dfn}.
@item F = FinalClass.extend( Object @var{dfn } )
Define anonymous final class @var{C} as described by @var{dfn}.
@end table
Final classes operate exactly as ``normal'' classes do (@pxref{Defining
Classes}), with the exception that they cannot be inherited from.
@node Static Members
@section Static Members
@dfn{Static members} do not require instantiation of the containing class in
order to be used, but may also be called by instances. They are attached to
the class itself rather than an instance. Static members provide convenience
under certain circumstances where class instantiation is unnecessary and
permit sharing data between instances of a class. However, static members,
when used improperly, can produce poorly designed classes and tightly
coupled code that is also difficult to test. Static properties also
introduce problems very similar to global variables.
Let us consider an implementation of the factory pattern. Class
@var{BigBang} will declare two static methods in order to satisfy different
means of instantiation: @code{fromBraneCollision()} and
@code{fromBigCrunch()} (for the sake of the example, we're not going to
address every theory). Let us also consider that we want to keep track of
the number of big bangs in our universe (perhaps to study whether or not a
"Big Crunch" could have potentially happened in the past) by incrementing a
counter each time a new big bang occurs. Because we are using a static
method, we cannot use a property of an instance in order to store this data.
Therefore, we will use a static property of class @var{BigBang}.
@float Figure, f:static-ex
@verbatim
const BigBang = Class( 'BigBang',
{
/**
* Number of big bangs that has occurred
* @type {number}
*/
'private static _count': 0,
/**
* String representing the type of big bang
* @type {string}
*/
'private _type': '',
/**
* Create a new big bang from the collision of two membranes
*
* @return {BraneSet} the set of branes that collided
*
* @return {BigBang} new big bang
*/
'public static fromBraneCollision'( brane_set )
{
// do initialization tasks...
return BigBang( 'brane', brane_set.getData() );
},
/**
* Create a new big bang following a "Big Crunch"
*
* @param {BigCrunch} prior crunch
*
* @return {BigBang} new big bang
*/
'public static fromBigCrunch'( crunch )
{
// do initialization tasks...
return BigBang( 'crunch', crunch.getData() );
},
/**
* Returns the total number of big bangs that have occurred
*
* @return {number} total number of big bangs
*/
'public static getTotalCount'()
{
return this.$('_count');
}
/**
* Construct a new big bang
*
* @param {string} type big bang type
* @param {object} data initialization data
*
* @return {undefined}
*/
constructor( type, data )
{
this._type = type;
// do complicated stuff with data
// increment big bang count
this.__self.$( '_count',
this.__self.$('count') + 1
);
},
} );
// create one of each
const brane_bang = BigBang.fromBraneCollision( branes );
const crunch_bang = BigBang.fromBigCrunch( crunch_incident );
console.log( "Total number of big bangs: %d", BigBang.getTotalCount() );
// Total number of big bangs: 2
@end verbatim
@caption{Static member example using the factory pattern}
@end float
Due to limitations of pre-ECMAScript 5 implementations, ease.js's static
implementation must be broken into two separate parts: properties and
methods.
@menu
* Static Methods::
* Static Properties::
* Constants:: Immutable static properties
@end menu
@node Static Methods
@subsection Static Methods
In @ref{f:static-ex}, we implemented three static methods: two factory
methods, @code{fromBraneCollision()} and @code{FromBigCrunch()}, and one
getter method to retrieve the total number of big bangs,
@code{getTotalCount()}. These methods are very similar to instance methods
we are already used to, with a few important differences:
@enumerate
@item
Static methods are declared with the @code{static} keyword.
@item
In the body, @code{this} is bound to the class itself, rather than the
instance.
@item
Static methods cannot call any non-static methods of the same class without
first instantiating it.
@end enumerate
The final rule above is not true when the situation is reversed. Non-static
methods @emph{can} call static methods through use of the @var{__self}
object, which is a reference to the class itself. That is, @var{this} in a
static method is the same object as @var{this.__self} in a non-static
method. This is demonstrated by @code{getTotalCount}
@verbatim
this.$('_count')
@end verbatim
and @code{#construct}.
@verbatim
this.__self.$('_count')
@end verbatim
To help remember @var{__self}, consider what the name states. A class is a
definition used to create an object. The body of a method is a definition,
which is defined on the class. Therefore, even though the body of a method
may be called in the context of an instance, it is still part of the class.
As such, @var{__self} refers to the class.
@node Static Properties
@subsection Static Properties
You have likely noticed by now that static properties are handled a bit
differently than both static methods and non-static properties. This
difference is due to pre-ECMAScript 5 limitations and is discussed at length
in the @ref{Static Implementation} section.
Static properties are read from and written to using the @dfn{static
accessor method} @code{$()}. This method name was chosen because the
@code{$} prefix is common in scripting languages such as BASH, Perl (for
scalars) and PHP. The accessor method accepts two arguments, the second
being optional. If only the first argument is provided, the accessor method
acts as a getter, as in @ref{f:static-ex}'s @code{getTotalCount()}:
@verbatim
return this.$('_count');
@end verbatim
If the second argument is provided, it acts as a setter, as in
@code{#construct}:
@verbatim
this.__self.$( '_count',
this.__self.$('count') + 1
);
@end verbatim
Setting @code{undefined} values is supported. The @code{delete} operator is
not supported, as its use is both restricted by the language itself and
doesn't make sense to use in this context. As hinted by the example above,
the increment and decrement operators (@code{++} and @code{--}) are not
supported because JavaScript does not permit returning values by reference.
It is important to understand that, currently, the accessor method cannot be
omitted. Consider the following example:
@float Figure, f:static-accessor
@verbatim
const Foo = Class( 'Foo',
{
'public static bar': 'baz',
} );
const SubFoo = Class( 'SubFoo' ).extend( Foo, {} );
// correct
Foo.$( 'bar, 'baz2' );
Foo.$('bar'); // baz2
SubFoo.$('bar'); // baz2
SubFoo.$( 'bar', 'baz3' );
Foo.$('bar'); // baz3
// INCORRECT
Foo.bar = 'baz2';
Foo.bar; // baz2
SubFoo.bar; // undefined
@end verbatim
@caption{Static accessor method cannot be omitted}
@end float
@node Constants
@subsection Constants
@dfn{Constants}, in terms of classes, are immutable static properties. This
means that, once defined, a constant cannot be modified. Since the value is
immutable, it does not make sense to create instances of the property. As
such, constant values are implicitly static. This ensures that each
instance, as well as any static access, references the exact same value.
This is especially important for objects and arrays.
One important difference between other languages, such as PHP, is that
ease.js supports the @ref{Access Modifiers, visibility modifiers} in
conjunction with the @code{const} keyword. That is, you can have public,
protected and private constants. Constants are public by default, like every
other type of member. This feature permits encapsulating constant values,
which is important if you want an immutable value that shouldn't be exposed
to the rest of the world (e.g. a service URL, file path, etc). Consider the
following example in which we have a class responsible for reading mount
mounts from @file{/etc/fstab}:
@float Figure, f:const-ex
@verbatim
Class( 'MountPointIterator',
{
'private const _PATH': '/etc/fstab',
'private _mountPoints': [],
constructor()
{
const data = fs.readFileSync( this.$('_PATH') );
this._parseMountPoints( data );
},
// ...
} );
@end verbatim
@caption{Using the @code{const} keyword}
@end float
In the above example, attempting to access the @var{_PATH} constant from
outside the class would return @code{undefined}. Had the constant been
declared as public, or had the visibility modifier omitted, it could have
been accessed just like any other static property:
@verbatim
// if PATH were a public constant value
MountPointIterator.$('PATH');
@end verbatim
Any attempts to modify the value of a constant will result in an exception.
This will also work in pre-ES5 engines due to use of the @ref{Static
Properties, static accessor method} (@code{$()}).
It is important to note that constants prevent the @emph{value of the
property} from being reassigned. It @emph{does not} prevent modification of
the value that is @emph{referenced} by the property. For example, if we had
a constant @var{foo}, which references an object, such that
@verbatim
'const foo': { a: 'b' }
@end verbatim
it is perfectly legal to alter the object:
@verbatim
MyClass.$('foo').a = 'c';
@end verbatim
@node Abstract Members
@section Abstract Members
@table @code
@item 'abstract [@var{keywords}] @var{name}': @var{params}
Declare an abstract method @var{name} as having @var{params} parameters,
having optional additional keywords
@var{keywords}.
@end table
Abstract members permit declaring an API, deferring the implementation to a
subtype. Abstract methods are declared as an array of string parameter names
@var{params}.
@verbatim
// declares abstract method 'connect' expecting the two parameters,
// 'host' and 'path'
{ 'abstract connect': [ 'host', 'path' ] }
@end verbatim
@itemize
@item
Abstract members are defined using the @ref{t:keywords,,@code{abstract}}
keyword.
@itemize
@item
Except in interfaces (@pxref{Interfaces}), where the
@ref{t:keywords,,@code{abstract}} keyword is implicit.
@end itemize
@item
Currently, only methods may be declared abstract.
@item
The subtype must implement at least the number of parameters declared in
@var{params}, but the names needn't match.
@itemize
@item
Each name in @var{params} must be a valid variable name, as satisfied by
the regular expression @code{/^[a-z_][a-z0-9_]*$/i}.
@item
The names are use purely for documentation and are not semantic.
@end itemize
@end itemize
Abstract members may only be a part of one of the following:
@menu
* Interfaces::
* Abstract Classes::
@end menu
@node Interfaces
@subsection Interfaces
@table @code
@item I = Interface( string @var{name}, Object @var{dfn} )
Define named interface @var{I} identified by @var{name} described by
@var{dfn}.
@item I = Interface( string @var{name} ).extend( Object @var{dfn} )
Define named interface @var{I} identified by @var{name} described by
@var{dfn}.
@item I = Interface( Object @var{dfn} )
Define anonymous interface @var{I} as described by @var{dfn}.
@item I = Interface.extend( Object @var{dfn } )
Define anonymous interface @var{I} as described by @var{dfn}.
@end table
Interfaces are defined with a syntax much like classes (@pxref{Defining
Classes}) with the following properties:
@itemize
@item
Interface @var{I} cannot be instantiated.
@item
Every member of @var{dfn} of @var{I} is implicitly
@ref{t:keywords,,@code{abstract}}.
@itemize
@item
Consequently, @var{dfn} of @var{I} may contain only abstract methods.
@end itemize
@item
Interfaces may only extend other interfaces (@pxref{Inheritance}).
@end itemize
@code{Interface} must be imported (@pxref{Including}) from
@code{easejs.Interface}; it is not available in the global scope.
@subsubsection Implementing Interfaces
@table @code
@item C = Class( @var{name} ).implement( @var{I\_0}[, ...@var{I\_n}]
).extend( @var{dfn} ) Define named class @var{C} identified by @var{name}
implementing all interfaces @var{I}, described by @var{dfn}.
@item C = Class.implement( @var{I\_0}[, ...@var{I\_n} ).extend( @var{dfn} )
Define anonymous class @var{C} implementing all interfaces @var{I},
described by @var{dfn}.
@end table
Any class @var{C} may implement any interface @var{I}, inheriting its API.
Unlike class inheritance, any class @var{C} may implement one or more
interfaces.
@itemize
@item
Class @var{C} implementing interfaces @var{I} will be considered a subtype
of every @var{I}.
@item
Class @var{C} must either:
@itemize
@item Provide a concrete definition for every member of @var{dfn} of
@var{I},
@item or be declared as an @code{AbstractClass} (@pxref{Abstract Classes})
@itemize
@item
@var{C} may be declared as an @code{AbstractClass} while still providing
a concrete definition for some of @var{dfn} of @var{I}.
@end itemize
@end itemize
@end itemize
@subsubsection Discussion
Consider a library that provides a websocket abstraction. Not all
environments support web sockets, so an implementation may need to fall back
on long polling via AJAX, Flash sockets, etc. If websocket support @emph{is}
available, one would want to use that. Furthermore, an environment may
provide its own type of socket that our library does not include support
for. Therefore, we would want to provide developers for that environment the
ability to define their own type of socket implementation to be used in our
library.
This type of abstraction can be solved simply by providing a generic API
that any operation on websockets may use. For example, this API may provide
@code{connect()}, @code{onReceive()} and @code{send()} operations, among
others. We could define this API in a @code{Socket} interface:
@float Figure, f:interface-def
@verbatim
const Socket = Interface( 'Socket',
{
'public connect': [ 'host', 'port' ],
'public send': [ 'data' ],
'public onReceive': [ 'callback' ],
'public close': [],
} );
@end verbatim
@caption{Defining an interface}
@end float
We can then provide any number of @code{Socket} implementations:
@float Figure f:interface-impl
@verbatim
const WebSocket = Class( 'WebSocket' ).implement( Socket ).extend(
{
'public connect'( host, port )
{
// ...
},
// ...
} );
const SomeCustomSocket = Class.implement( Socket ).extend(
{
// ...
} );
@end verbatim
@caption{Implementing an interface}
@end float
Anything wishing to use sockets can work with this interface
polymorphically:
@float Figure, f:interface-poly
@verbatim
const ChatClient = Class(
{
'private _socket': null,
constructor( socket )
{
this.assertIsA( Socket, socket );
this._socket = socket;
},
'public sendMessage'( channel, message )
{
this._socket.send( {
channel: channel,
message: message,
} );
},
} );
@end verbatim
@caption{Polymorphism with interfaces}
@end float
We could now use @code{ChatClient} with any of our @code{Socket}
implementations:
@float Figure, f:interface-poly-use
@verbatim
ChatClient( WebSocket() ).sendMessage( '#lobby', "Sweet! WebSockets!" );
ChatClient( SomeCustomSocket() )
.sendMessage( '#lobby', "I can chat too!" );
@end verbatim
@caption{Obtaining flexibility via dependency injection}
@end float
The use of the @code{Socket} interface allowed us to create a powerful
abstraction that will allow our library to work across any range of systems.
The use of an interface allows us to define a common API through which all
of our various components may interact without having to worry about the
implementation details - something we couldn't worry about even if we tried,
due to the fact that we want developers to support whatever environment they
are developing for.
Let's make a further consideration. Above, we defined a @code{onReceive()}
method which accepts a callback to be called when data is received. What if
our library wished to use an @code{Event} interface as well, which would
allow us to do something like @samp{some_socket.on( 'receive', function()
@{@} )}?
@float Figure, f:interface-impl-multi
@verbatim
const AnotherSocket = Class.implement( Socket, Event ).extend(
{
'public connect': // ...
'public on': // ... part of Event
} );
@end verbatim
@caption{Implementing multiple interfaces}
@end float
Any class may implement any number of interfaces. In the above example,
@code{AnotherSocket} implemented both @code{Socket} and @code{Event},
allowing it to be used wherever either type is expected. Let's take a look:
@float Figure, f:interface-multi-isa
@verbatim
Class.isA( Socket, AnotherSocket() ); // true
Class.isA( Event, AnotherSocket() ); // true
@end verbatim
@caption{Implementors of interfaces are considered subtypes of each
implemented interface}
@end float
Interfaces do not suffer from the same problems as multiple inheritance,
because we are not providing any sort of implementation that may cause
conflicts.
One might then ask - why interfaces instead of abstract classes
(@pxref{Abstract Classes})? Abstract classes require subclassing, which
tightly couples the subtype with its parent. One may also only inherit from
a single supertype (@pxref{Inheritance}), which may cause a problem in our
library if we used an abstract class for @code{Socket}, but a developer had
to inherit from another class and still have that subtype act as a
@code{Socket}.
Interfaces have no such problem. Implementors are free to use interfaces
wherever they wish and use as many as they wish; they needn't worry that
they may be unable to use the interface due to inheritance or coupling
issues. However, although interfaces facilitate API reuse, they do not aid
in code reuse as abstract classes do@footnote{This is a problem that will
eventually be solved by the introduction of traits/mixins.}.
@node Abstract Classes
@subsection Abstract Classes
@table @code
@item A = AbstractClass( string @var{name}, Object @var{dfn} )
Define named abstract class @var{A} identified by @var{name} described by
@var{dfn}.
@item A = AbstractClass( string @var{name} ).extend( Object @var{dfn} )
Define named abstract class @var{A} identified by @var{name} described by
@var{dfn}.
@item A = AbstractClass( Object @var{dfn} )
Define anonymous abstract class @var{A} as described by @var{dfn}.
@item A = AbstractClass.extend( Object @var{dfn } )
Define anonymous abstract class @var{A} as described by @var{dfn}.
@end table
Abstract classes are defined with a syntax much like classes
(@pxref{Defining Classes}). They act just as classes do, except with the
following additional properties:
@itemize
@item
Abstract class @var{A} cannot be instantiated.
@item
Abstract class @var{A} must contain at least one member of @var{dfn} that is
explicitly declared as @ref{t:keywords,,@code{abstract}}.
@item
Abstract classes may extend both concrete and abstract classes
@end itemize
An abstract class @emph{must} be used if any member of @var{dfn} is declared
as abstract. This serves as a form of self-documenting code, as it would
otherwise not be immediately clear whether or not a class was abstract (one
would have to look through every member of @var{dfn} to make that
determination).
@code{AbstractClass} must be imported (@pxref{Including}) from
@code{easejs.AbstractClass}; it is not available in the global scope.
@subsubsection Discussion
Abstract classes allow the partial implementation of an API, deferring
portions of the implementation to subtypes (@pxref{Inheritance}). As an
example, let's consider an implementation of the @dfn{Abstract Factory}
pattern@footnote{See Abstract Factory, GoF}) which is responsible for the
instantiation and initialization of an object without knowing its concrete
type.
Our hypothetical library will be a widget abstraction. For this example, let
us consider that we need a system that will work with any number of
frameworks, including jQuery UI, Dojo, YUI and others. A particular dialog
needs to render a simple @code{Button} widget so that the user may click
"OK" when they have finished reading. We cannot instantiate the widget from
within the dialog itself, as that would tightly couple the chosen widget
subsystem (jQuery UI, etc) to the dialog, preventing us from changing it in
the future. Alternatively, we could have something akin to a switch
statement in order to choose which type of widget to instantiate, but that
would drastically inflate maintenance costs should we ever need to add or
remove support for other widget system in the future.
We can solve this problem by allowing another object, a
@code{WidgetFactory}, to perform that instantiation for us. The dialog could
accept the factory in its constructor, like so:
@float Figure, f:abstract-factory-use
@verbatim
Class( 'Dialog',
{
'private _factory': null,
constructor( factory )
{
if ( !( Class.isA( WidgetFactory, factory ) ) )
{
throw TypeError( 'Expected WidgetFactory' );
}
this._factory = factory;
},
'public open'()
{
// before we open the dialog, we need to create and add the widgets
const btn = this._factory.createButtonWidget( 'btn_ok', "OK" );
// ...
},
} );
@end verbatim
@caption{Hypothetical use case for our Abstract Factory}
@end float
We now have some other important considerations. As was previously
mentioned, @code{Dialog} itself could have determined which widget to
instantiate. By using a factory instead, we are moving that logic to the
factory, but we are now presented with a similar issue. If we use something
like a switch statement to decide what class should be instantiated, we are
stuck with modifying the factory each and every time we add or remove
support for another widget library.
This is where an abstract class could be of some benefit. Let's consider the
above call to @code{createButtonWidget()}, which accepted two arguments: an
id for the generated DOM element and a label for the button. Clearly, there
is some common initialization logic that can occur between each of the
widgets. However, we do not want to muddy the factory up with log to
determine what widget can be instantiated. The solution is to define the
common logic, but defer the actual instantiation of the @code{Widget} to
subtypes:
@float Figure, f:abstract-factory-define
@verbatim
AbstractClass( 'WidgetFactory',
{
'public createButtonWidget'( id, label )
{
// note that this is a call to an abstract method; the
// implementation is not yet defined
const widget = this.getNewButtonWidget();
// perform common initialization tasks
widget.setId( id );
widget.setLabel( label );
// return the completed widget
return widget;
},
// declared with an empty array because it has no parameters
'abstract protected getNewButtonWidget': [],
} );
@end verbatim
@caption{Defining our Abstract Factory}
@end float
As demonstrated in @ref{f:abstract-factory-define} above, we can see a very
interesting aspect of abstract classes: we are making a call to a method
that is not yet defined (@code{getNewButtonWidget()}@footnote{Note that we
declared this method as @ref{t:keywords,,@code{protected}} in order to
encapsulate which the widget creation logic (@pxref{Access Modifiers
Discussion}). Users of the class should not be concerned with how we
accomplish our job. Indeed, they should be concerned only with the fact that
we save them the trouble of determining which classes need to be
instantiated by providing them with a convenient API.}). Instead, by
declaring it @ref{t:keywords,,@code{abstract}}, we are stating that we want
to call this method, but it is up to a subtype to actually define it. It is
for this reason that abstract classes cannot be instantiated - they cannot
be used until each of the abstract methods have a defined implementation.
We can now define a concrete widget factory (@pxref{Inheritance}) for each
of the available widget libraries@footnote{Of course, the @code{Widget}
itself would be its own abstraction, which may be best accomplished by the
Adapter pattern.}:
@float Figure, f:concrete-abstract-factory
@verbatim
Class( 'JqueryUiWidgetFactory' )
.extend( WidgetFactory,
{
// concrete method
'protected getNewButtonWidget'()
{
// ...
},
} );
Class( 'DojoWidgetFactory' )
.extend( WidgetFactory,
{
// ...
} );
// ...
@end verbatim
@caption{Defining our concrete factories}
@end float
With that, we have solved our problem. Rather than using a simple switch
statement, we opted for a polymorphic solution:
@float Figure, f:abstract-factory-inject
@verbatim
// we can use whatever widget library we wish by injecting it into
// Dialog
Dialog( JqueryUiWidgetFactory() ).show();
Dialog( DojoWidgetFactory() ).show();
Dialog( YuiWidgetFactory() ).show();
@end verbatim
@caption{Using our abstract factory @code{WidgetFactory} via dependency
injection}
@end float
Now, adding or removing libraries is as simple as defining or removing a
@code{WidgetFactory} class.
Another noteworthy mention is that this solution could have just as easily
used an interface instead of an abstract class (@pxref{Interfaces}). The
reason we opted for an abstract class in this scenario is due to code reuse
(the common initialization code), but in doing so, we have tightly coupled
each subtype with the supertype @code{WidgetFactory}. There are a number of
trade-offs with each implementation; choose the one that best fits your
particular problem.
@node Method Proxies
@section Method Proxies
@table @code
@item 'proxy [@var{keywords}] @var{name}': @var{destmember}
Declare a proxy method @var{name}, having optional additional keywords
@var{keywords}, that invokes a method of the same name on object
@var{destmember} and returns its result.
@end table
Method proxies help to eliminate boilerplate code for calling methods on an
encapsulated object---a task that is very common with proxy and decorator
design patterns.
@float Figure, f:method-proxy-use
@verbatim
const Pigeon = Class( 'Pigeon',
{
'private _name': 'Flutter',
'public cheep'( chirp )
{
return this._name + ": cheep " + chirp;
}
'public rename'( name )
{
this._name = ''+name;
return this;
}
} );
const IratePigeonCheep = Class( 'IratePigeonCheep',
{
'private _pigeon': null,
constructor( pigeon )
{
this._pigeon = pigeon;
}
// add our own method
'public irateCheep'( chirp )
{
return this._pigeon.cheep( chirp ).toUpperCase();
},
// retain original methods
'proxy cheep': '_pigeon',
'proxy rename': '_pigeon',
} );
const irate = IratePigeonCheep( Pigeon() );
irate.cheep( 'chirp' );
// "Flutter: cheep chirp"
irate.setName( 'Butter' ).cheep( 'chirp' );
// "Butter: cheep chirp"
irate.irateCheep( 'chop' );
// "BUTTER: CHEEP CHOP"
@end verbatim
@caption{Using the @code{proxy} keyword to proxy @code{cheep} and
@code{rename} method calls to the object stored in property @code{_pigeon}.}
@end float
Consider some object @code{O} whoose class uses method proxies.
@itemize
@item All arguments of proxy method @code{O.name} are forwarded to
@code{destmember.name} untouched.
@item The return value provided by @code{destmember.name} is returned to
the caller of @code{O.name} untouched, except that
@itemize
@item If @code{destmember.name} returns @code{destmember} (that is,
returns @code{this}), it will be replaced with @code{O}; this ensures
that @code{destmember} remains encapsulated and preserves method
chaining.
@end itemize
@item If @code{destmember} is not an object, calls to @code{O.name} will
immediately fail in error.
@item If @code{destmember.name} is not a function, calls to @code{O.name}
will immediately fail in error.
@item @emph{N.B.: Static method proxies are not yet supported.}
@end itemize