1
0
Fork 0

Began adding section on visibility

closure/master
Mike Gerwitz 2011-03-16 00:27:09 -04:00
parent 4cadbcb901
commit 1c5fefa58d
1 changed files with 249 additions and 0 deletions

View File

@ -70,6 +70,8 @@ ease.js, until such a point where prototypes are no longer adequate.
@menu @menu
* Declaring Classes:: Learn how to declare a class with ease.js * Declaring Classes:: Learn how to declare a class with ease.js
* Member Visibility:: Encapsulation is a core concept of Object-Oriented
programming
@end menu @end menu
@ -288,3 +290,250 @@ 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 declare static methods so that they may be accessed without the overhead of
creating a new class instance. 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 the member. Public methods expose an API by which
users may use your class. Public properties, however, should be less common, for
a very important reason.
This introduces the concept of @dfn{encapsulation}, which 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() )
{
// ...
}
},
'protected 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::
@end menu
@node Private Members
@subsection Private Members
Let's first explore private members. The majority of the members in the
@var{Dog} class 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. This means that we cannot call a private member from outside the class
define in @ref{f:encapsulation,}:
@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. 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 talk 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 the dog after attempting the above change, 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 @code{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
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 consider 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 everything 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.
@ref{Private Members,Private} members 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.
@ref{f:encapsulation,} defined a single method as protected - @code{stand()}.
Because it is protected, it is able to 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( 'TwoLeggedDog' ).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.