1
0
Fork 0
easejs/doc/classes.texi

1421 lines
52 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
* Member Visibility:: Encapsulation is a core concept of Object-Oriented
programming
* Static Members:: Members whose use do not require instantiation
@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.
@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 Keywords
@float Table, t:keywords
@multitable @columnfractions .10 .9
@headitem Keyword @tab Description
@item @code{public}
@tab Access modifier. Places member @var{name} in public interface (accessible
outside of @var{C} or instance of @var{C}; accessible by subtypes). Implied if
no other access modifier is provided. May not be used in conjunction with other
access modifiers. @xref{Member Visibility}.
@item @code{protected}
@tab Access modifier. Places member @var{name} in protected interface
(accessible only within @var{C} or instance of @var{C}; accessible by subtypes).
May not be used in conjunction with other access modifiers. @xref{Member
Visibility}.
@item @code{private}
@tab Access modifier. Places member @var{name} in private interface (accessible
only within @var{C} or instance of @var{C}; not accessible by subtypes). May not
be used in conjunction with other access modifiers. @xref{Member Visibility}.
@item @code{static}
@tab Binds member @var{name} to class @var{C} rather than instance of @var{C}.
Member data shared with each instance of type @var{C}. @xref{Static Members}.
@item @code{abstract}
@tab Declares member @var{name} and defers definition to subtype. @var{value}
is interpreted as an argument list and must be of type @code{array}. May only be
used with methods.
@item @code{const}
@tab Defines an immutable property @var{name}. May not be used with methods or
getters/setters. @xref{Constants}.
@item @code{virtual}
@tab Declares that method @var{name} may be overridden by subtypes. May only be
used with methods. Methods without this keyword may not be overridden. May only
be used with methods. @xref{Inheritance}.
@item override
@tab Overrides method @var{name} of supertype of @var{C} with @var{value}. May
only override virtual methods. May only be used with methods.
@xref{Inheritance}.
@end multitable
@caption{Supported keywords}
@end float
Only the keywords in @ref{t:keywords} are supported as valid tokens within
@var{keywords}.
@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
Inheritance can be a touchy subject among Object-Oriented developers. It is a
powerful feature that is easily abused. @dfn{Inheritance} is the term used to
describe the process of creating a @dfn{child} class that @dfn{extends}
(inherits members from) another @dfn{parent} class. The parent class is also
referred to as the @dfn{supertype} and the child is called the @dfn{subtype}.
Let's consider the following example, where we have a dog, a lazy dog, and a dog
that walks on two legs:
@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{Member Visibility}). 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
@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
Member Visibility section (@pxref{Member Visibility}), 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 Member Visibility section (@pxref{Member
Visibility}).}. 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 Member Visibility
@section Member Visibility
One of the major hurdles ease.js aimed to address (indeed, one of the core
reasons for its creation) was that of encapsulation. JavaScript's prototype
model provides limited means of encapsulating data. Since functions limit scope,
they may be used to mimic private members. These are often referred to as
@dfn{privileged members}. However, declaring classes in this manner can be
messy. ease.js aims to provide an elegant implementation that is both a pleasure
to work with and able to support protected members.
The term @dfn{visibility} refers to how a class member may be accessed. There
are three levels of visibility implemented by ease.js, which are listed here
from most visible to least:
@table @dfn
@item public
Accessible outside of the instance (e.g. @samp{foo.publicProp}). Inherited by
subtypes.
@item protected
Not accessible outside of the instance (only accessible by
@samp{this.protectedProp}). Inherited by subtypes.
@item private
Not accessible outside of the instance. Not inherited by subtypes.
@end table
By default, all members are public. This means that the members can be accessed
and modified from within an instance and from outside. Subtypes (classes that
inherit from it) will inherit public members. Public methods expose an API by
which users may use your class. Public properties, however, should be less
common in practice for a very important reason, which is explored throughout the
rest of this section.
@dfn{Encapsulation} is the act of hiding information within a class or instance.
Classes should be thought of black boxes. We want them to do their job, but we
do not care @emph{how} they do their job. Encapsulation takes the complexity out
of a situation and allows the developer to focus on accomplishing the task using
familiar concepts. For example, consider a class named @var{Dog} which has a
method @code{walk()}. To walk a dog, we simply call @code{Dog().walk()}. The
@code{walk()} method could be doing anything. By preventing the details of the
method from being exposed, we present the developer with a very simple
interface. Rather than the developer having to be concerned with moving each of
the dog's legs, all they have to do is understand that the dog is being walked.
Let's consider our @var{Dog} class in more detail:
@float Figure, f:encapsulation
@verbatim
Class( 'Dog',
{
'private _legs': {},
'private _body': {},
// ...
'public walk': function()
{
this.stand();
this._moveFrontLeg( 0 );
this._moveBackLeg( 1 );
this._moveFrontLeg( 1 );
this._moveBackLeg( 0 );
},
'protected stand': function()
{
if ( this.isSitting() )
{
// ...
}
},
'public rollOver': function()
{
this._body.roll();
},
'private _moveFrontLeg': function( leg )
{
this._legs.front[ leg ].move();
},
'private _moveBackLeg': function( leg )
{
this._legs.back[ leg ].move();
},
// ...
} );
@end verbatim
@caption{Encapsulating behavior of a class}
@end float
As you can see above, the act of making the dog move forward is a bit more
complicated than the developer may have originally expected. The dog has four
separate legs that need to be moved individually. The dog must also first stand
before it can be walked, but it can only stand if it's sitting. Detailed tasks
such as these occur all the time in classes, but they are hidden from the
developer using the public API. Why should the developer be concerned with all
of the legs?
As a general rule of thumb, you should use the @emph{lowest} level of visibility
possible unless you have a strong reason for increasing it. Start by declaring
everything as private and work from there.
@menu
* Private Members::
* Protected Members::
* Visibility Escalation:: Increasing visibility of inherited members
@end menu
@node Private Members
@subsection Private Members
Let's first explore private members. The majority of the members in the
@var{Dog} class (@pxref{f:encapsulation,}) are private. This is the lowest level
of visibility (and consequently the @emph{highest} level of encapsulation). By
convention, we prefix private members with an underscore. Private members are
available @emph{only to the class that defined it} and are not available outside
the class.
@float Figure, f:encapsulation-call-priv
@verbatim
var dog = Dog();
dog._moveFrontLeg( 1 );
// TypeError: Object #<Dog> has no method '_moveFrontLeg'
@end verbatim
@caption{Cannot access private members outside the class}
@end float
You will notice that the dog's legs are declared private as well
(@pxref{f:encapsulation,}). This is to ensure we look at the dog as a whole; we
don't care about what the dog is made up of. Legs, fur, tail, teeth, tongue, etc
- they are all irrelevant to our purpose. We just want to walk the dog.
Encapsulating those details also ensures that they will not be tampered with,
which will keep the dog in a consistent, predictable state.
Private members cannot be inherited. Let's say we want to make a class called
@var{TwoLeggedDog} to represent a dog that was trained to walk only on two feet.
We could approach this in a couple different ways. The first way would be to
prevent the front legs from moving. What happens when we explore that approach:
@float Figure, f:encapsulation-inherit-priv
@verbatim
var two_legged_dog = Class( 'TwoLeggedDog' ).extend( Dog,
{
/**
* This won't override the parent method.
*/
'private _moveFrontLeg': function( leg )
{
// don't do anything
return;
},
} )();
two_legged_dog.walk();
@end verbatim
@caption{Cannot override private members of supertype}
@end float
If you were to attempt to walk a @var{TwoLeggedDog}, you would find that
@emph{the dog's front legs still move}! This is because, as mentioned before,
private methods are not inherited. Rather than overriding the parent's
@var{_moveFrontLeg} method, you are instead @emph{defining a new method}, with
the name @var{_moveFrontLeg}. The old method will still be called. Instead, we
would have to override the public @var{walk} method to prevent our dog from
moving his front feet.
@node Protected Members
@subsection Protected Members
Protected members are often misunderstood. Many developers will declare all
of their members as either public or protected under the misconception that they
may as well allow subclasses to override whatever functionality they want. This
makes the class more flexible.
While it is true that the class becomes more flexible to work with for subtypes,
this is a dangerous practice. In fact, doing so @emph{violates encapsulation}.
Let's reconsider the levels of visibility in this manner:
@table @strong
@item public
Provides an API for @emph{users of the class}.
@item protected
Provides an API for @emph{subclasses}.
@item private
Provides an API for @emph{the class itself}.
@end table
Just as we want to hide data from the public API, we want to do the same for
subtypes. If we simply expose all members to any subclass that comes by, that
acts as a peephole in our black box. We don't want people spying into our
internals. Subtypes shouldn't care about the dog's implementation either.
Private members (@pxref{Private Members,Private}) should be used whenever
possible, unless you are looking to provide subtypes with the ability to access
or override methods. In that case, we can move up to try protected members.
Remember not to make a member public unless you wish it to be accessible to the
entire world.
@var{Dog} (@pxref{f:encapsulation,}) defined a single method as protected -
@code{stand()}. Because the method is protected, it can be inherited by
subtypes. Since it is inherited, it may also be overridden. Let's define another
subtype, @var{LazyDog}, which refuses to stand.
@float Figure, f:encapsulation-inherit-prot
@verbatim
var lazy_dog = Class( 'LazyDog' ).extend( Dog,
{
/**
* Overrides parent method
*/
'protected stand': function()
{
// nope!
this.rollOver();
return false;
},
} )();
lazy_dog.walk();
@end verbatim
@caption{Protected members are inherited by subtypes}
@end float
There are a couple important things to be noted from the above example. Firstly,
we are able to override the @code{walk()} method, because it was inherited.
Secondly, since @code{rollOver()} was also inherited from the parent, we are
able to call that method, resulting in an upside-down dog that refuses to stand
up, just moving his feet.
Another important detail to notice is that @code{Dog.rollOver()} accesses a
private property of @var{Dog} -- @var{_body}. Our subclass does not have access
to that variable. Since it is private, it was not inherited. However, since the
@code{rollOver()} method is called within the context of the @var{Dog} class,
the @emph{method} has access to the private member, allowing our dog to
successfully roll over. If, on the other hand, we were to override
@code{rollOver()}, our code would @emph{not} have access to that private object.
Calling @samp{this.__super()} from within the overridden method would, however,
call the parent method, which would again have access to its parent's private
members.
@node Visibility Escalation
@subsection Visibility Escalation
@dfn{Visibility escalation} is the act of increasing the visibility of a member.
Since private members cannot be inherited, this essentially means making a
protected member public.
If you override a protected method, you may increase its visibility to public
without any problems. If you follow the convention of prefixing private members
with an underscore, you may find that it's not recommended doing so for
protected members. This is because subtypes may decide to make the member
public.
In order to increase the visibility, you do have to override the member. For
properties, this has no discernible effect; you're just redefining it. For
methods, this means that you are overriding the entire body. 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 go from public to protected}. This will throw an
error. You can only increase the level of visibility. This ensures that once a
class defines an API, subclasses cannot alter it. That API is forever for all
subtypes. This means that, if you are expecting a certain type, you can rest
assured that whatever you are given, even if it is a subtype, has the API you
are expecting.
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 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{Member Visibility, 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