Began adding section on visibility
parent
4cadbcb901
commit
1c5fefa58d
249
doc/classes.texi
249
doc/classes.texi
|
@ -70,6 +70,8 @@ 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
|
||||
|
||||
|
||||
|
@ -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
|
||||
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.
|
||||
|
||||
|
|
Loading…
Reference in New Issue