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
|
@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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue