608 lines
23 KiB
Plaintext
608 lines
23 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
|
|
* Declaring Classes:: Learn how to declare a class with ease.js
|
|
* Member Visibility:: Encapsulation is a core concept of Object-Oriented
|
|
programming
|
|
@end menu
|
|
|
|
|
|
@node Declaring Classes
|
|
@section Declaring Classes
|
|
We just took a look at what it's like declaring a class using prototypes
|
|
(@pxref{f:class-js,}). This method is preferred for many developers, but it is
|
|
important to recognize that there is a distinct difference between Prototypal
|
|
and Object-Oriented development models. 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
|
|
// if client-side, use: var Class = easejs.Class;
|
|
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::
|
|
* 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 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 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.
|
|
|