1728 lines
62 KiB
Plaintext
1728 lines
62 KiB
Plaintext
@c This document is part of the ease.js manual
|
|
@c Copyright (c) 2011 Mike Gerwitz
|
|
@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
|
|
@c or any later version published by the Free Software Foundation;
|
|
@c with no Invariant Sections, no Front-Cover Texts, and no Back-Cover
|
|
@c Texts. A copy of the license is included in the section entitled ``GNU
|
|
@c Free 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"
|
|
var 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()
|
|
var 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 their definition to subtypes
|
|
@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 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
|
|
var Class = require( 'easejs' ).Class;
|
|
|
|
var MyClass = Class(
|
|
{
|
|
'public prop': 'foobar',
|
|
|
|
'public getProp': function()
|
|
{
|
|
return this.prop;
|
|
}
|
|
} );
|
|
|
|
// create a new instance of the class and execute doStuff()
|
|
var 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), but always recommended.
|
|
Future versions of ease.js may provide warnings when the visibility is omitted.
|
|
We will get more into visibility later on.
|
|
|
|
@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{var Class = easejs.Class}. 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
|
|
* 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 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
|
|
var 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
|
|
var MyFoo = Class( 'MyFoo', {} ),
|
|
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
|
|
In JavaScript, we are used to functions themselves being a constructor because
|
|
only constructors can be instantiated. With ease.js, constructors are handled in
|
|
a manner similar to most other languages, by providing a separate method. The
|
|
implementation ease.js chose is very similar to that of PHP's
|
|
(@pxref{Constructor Implementation}).
|
|
|
|
@float Figure, f:constructor
|
|
@verbatim
|
|
var Foo = Class( 'Foo',
|
|
{
|
|
'public __construct': function( 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
|
|
|
|
When the class is instantiated, the constructor is invoked, permitting you do to
|
|
any necessary initialization tasks before the class can be used. The constructor
|
|
operates exactly how you would expect a constructor to in JavaScript, with one
|
|
major difference. Returning an object in the constructor does @emph{not} return
|
|
that object instead of the new class instance, since this does not make sense in
|
|
a Class-based model.
|
|
|
|
If you wish to prevent a class from being instantiated, simply throw an
|
|
exception within the constructor. This is useful if the class is intended to
|
|
provide only static methods, or if you wish to enforce a single instance
|
|
(one means of achieving a Singleton).
|
|
|
|
@float Figure, f:constructor-prevent
|
|
@verbatim
|
|
var Foo = Class( 'Foo',
|
|
{
|
|
'public __construct': function( name )
|
|
{
|
|
throw Error( "Cannot instantiate class Foo" );
|
|
}
|
|
} );
|
|
@end verbatim
|
|
@caption{Prevent class from being instantiated}
|
|
@end float
|
|
|
|
Constructors are optional. By default, nothing is done after the class is
|
|
instantiated.
|
|
|
|
@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
|
|
var foo = Class(
|
|
{
|
|
'public bar': function()
|
|
{
|
|
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
|
|
var name = Foo().getName();
|
|
|
|
// method chaining
|
|
var car = VehicleFactory().createBody().addWheel( 4 ).addDoor( 2 ).build();
|
|
|
|
// temporary class with callback
|
|
HttpRequest( host, port ).get( path, function( 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} and is not private. 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.
|
|
|
|
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.
|
|
@itemize
|
|
@item
|
|
Note that @var{dfn\_n\^C'} will not become @ref{Member
|
|
Keywords,,@code{virtual}} by default (unlike languages such as C++); they
|
|
must be explicitly declared as such.
|
|
@end itemize
|
|
@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'}.
|
|
@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.
|
|
@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)
|
|
var Dog = Class( 'Dog',
|
|
{
|
|
'virtual public walk': function()
|
|
{
|
|
console.log( 'Walking the dog' );
|
|
},
|
|
|
|
'public bark': function()
|
|
{
|
|
console.log( 'Woof!' );
|
|
}
|
|
} );
|
|
|
|
// subclass (child), as a named class
|
|
var LazyDog = Class( 'LazyDog' ).extend( Dog,
|
|
{
|
|
'override public walk': function()
|
|
{
|
|
console.log( 'Lazy dog refuses to walk.' );
|
|
}
|
|
} );
|
|
|
|
// subclass (child), as an anonymous class
|
|
var TwoLeggedDog = Dog.extend(
|
|
{
|
|
'override public walk': function()
|
|
{
|
|
console.log( 'Walking the dog on two feet' );
|
|
}
|
|
} );
|
|
@end verbatim
|
|
@caption{Inheritance in ease.js}
|
|
@end float
|
|
|
|
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
|
|
* 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
|
|
var LazyDog = Class( 'LazyDog' ).extend( Dog,
|
|
{
|
|
/**
|
|
* Bark when we're poked
|
|
*/
|
|
'public poke': function()
|
|
{
|
|
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
|
|
var AngryDog = Class( 'AngryDog' ).extend( LazyDog,
|
|
{
|
|
'public poke': function()
|
|
{
|
|
// 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.
|
|
|
|
@node Type Checks and Polymorphism
|
|
@subsection Type Checks and Polymorphism
|
|
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. 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:
|
|
|
|
@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.
|
|
@end table
|
|
|
|
For example:
|
|
|
|
@float Figure, f:instanceof-ex
|
|
@verbatim
|
|
var dog = Dog()
|
|
lazy = LazyDog(),
|
|
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
|
|
@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 @var{Dog}.
|
|
|
|
@float Figure, f:polymorphism-easejs
|
|
@verbatim
|
|
var DogTrainer = Class( 'DogTrainer',
|
|
{
|
|
'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" );
|
|
}
|
|
}
|
|
} );
|
|
|
|
// 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
|
|
|
|
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}).
|
|
|
|
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
|
|
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
|
|
var Foo = Class(
|
|
{
|
|
'protected canEscalate': 'baz',
|
|
|
|
'protected escalateMe': function( arg )
|
|
{
|
|
console.log( 'In escalateMe' );
|
|
},
|
|
|
|
'public cannotMakeProtected': function()
|
|
{
|
|
}
|
|
} ),
|
|
|
|
SubFoo = Foo.extend(
|
|
{
|
|
/**
|
|
* Escalating a property means redefining it
|
|
*/
|
|
'public canEscalate': 'baz',
|
|
|
|
/**
|
|
* We can go protected -> public
|
|
*/
|
|
'public escalateMe': function( 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 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
|
|
var 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': function( 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': function( 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': function()
|
|
{
|
|
return this.$('_count');
|
|
}
|
|
|
|
|
|
/**
|
|
* Construct a new big bang
|
|
*
|
|
* @param {string} type big bang type
|
|
* @param {object} data initialization data
|
|
*
|
|
* @return {undefined}
|
|
*/
|
|
'public __construct': function( 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
|
|
var brane_bang = BigBang.fromBraneCollision( branes ),
|
|
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
|
|
var Foo = Class( 'Foo',
|
|
{
|
|
'public static bar': 'baz',
|
|
},
|
|
|
|
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': [],
|
|
|
|
|
|
'public __construct': function()
|
|
{
|
|
var 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{args}
|
|
Declare an abstract method @var{name} as having @var{args} arguments, having
|
|
optional additional keywords
|
|
@var{keywords}.
|
|
@end table
|
|
Abstract members permit defining an API, deferring the implementation to a
|
|
subtype. Abstract methods are declared as an array of string argument names
|
|
@var{args}.
|
|
|
|
@verbatim
|
|
// declares abstract method 'connect' expecting the two arguments, '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 arguments declared in
|
|
@var{args}, but the names needn't match.
|
|
@itemize
|
|
@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
|
|
var 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
|
|
var WebSocket = Class( 'WebSocket' ).implement( Socket ).extend(
|
|
{
|
|
'public connect': function( host, port )
|
|
{
|
|
// ...
|
|
},
|
|
|
|
// ...
|
|
} ),
|
|
|
|
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
|
|
var ChatClient = Class(
|
|
{
|
|
'private _socket': null,
|
|
|
|
__construct: function( socket )
|
|
{
|
|
// only allow sockets
|
|
if ( !( Class.isA( Socket, socket ) ) )
|
|
{
|
|
throw TypeError( 'Expected socket' );
|
|
}
|
|
|
|
this._socket = socket;
|
|
},
|
|
|
|
'public sendMessage': function( 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
|
|
var 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,
|
|
|
|
__construct: function( factory )
|
|
{
|
|
if ( !( Class.isA( WidgetFactory, factory ) ) )
|
|
{
|
|
throw TypeError( 'Expected WidgetFactory' );
|
|
}
|
|
|
|
this._factory = factory;
|
|
},
|
|
|
|
'public open': function()
|
|
{
|
|
// before we open the dialog, we need to create and add the widgets
|
|
var 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': function( id, label )
|
|
{
|
|
// note that this is a call to an abstract method; the implementation is
|
|
// not yet defined
|
|
var 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': function()
|
|
{
|
|
// ...
|
|
},
|
|
} );
|
|
|
|
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.
|
|
|