diff --git a/doc/classes.texi b/doc/classes.texi index fe900bb..bdae963 100644 --- a/doc/classes.texi +++ b/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 # 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. +