Previous: Access Modifiers Discussion, Up: Access Modifiers


3.1.2 Example

Let's consider our Dog class in more detail. We will not go so far as to implement an entire nervous system in our example. Instead, let's think of our Dog similar to a wind-up toy:

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();
    },

    // ...
} );

Figure 3.1: Encapsulating behavior of a class

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. The developer should not be concerned with all of the legs. Worrying about such details brings the developer outside of the problem domain and into a new problem domain - how to get the dog to walk.

3.1.3 Private Members

Let's first explore private members. The majority of the members in the Dog class (see Figure 3.1) are private. This is the lowest level of visibility (and consequently the highest level of encapsulation). By convention, we prefix private members with an underscore. Private members are available only to the class that defined it and are not available outside the class.

    var dog = Dog();
    dog._moveFrontLeg( 1 );

    // TypeError: Object #<Dog> has no method '_moveFrontLeg'

Figure 3.2: Cannot access private members outside the class

You will notice that the dog's legs are declared private as well (see Figure 3.1). 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 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:

    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();

Figure 3.3: Cannot override private members of supertype

If you were to attempt to walk a TwoLeggedDog, you would find that the dog's front legs still move! This is because, as mentioned before, private methods are not inherited. Rather than overriding the parent's _moveFrontLeg method, you are instead defining a new method, with the name _moveFrontLeg. The old method will still be called. Instead, we would have to override the public walk method to prevent our dog from moving his front feet.

Note that GNU ease.js is optimized for private member access; see Property Proxies and Method Wrapping for additional details.

3.1.4 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 violates encapsulation. Let's reconsider the levels of visibility in this manner:

public
Provides an API for users of the class.
protected
Provides an API for subclasses.
private
Provides an API for the class itself.

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 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.

Dog (see Figure 3.1) defined a single method as protected - 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, LazyDog, which refuses to stand.

    var lazy_dog = Class( 'LazyDog' ).extend( Dog,
    {
        /**
         * Overrides parent method
         */
         'protected stand': function()
         {
            // nope!
            this.rollOver();
            return false;
         },
    } )();

    lazy_dog.walk();

Figure 3.4: Protected members are inherited by subtypes

There are a couple important things to be noted from the above example. Firstly, we are able to override the walk() method, because it was inherited. Secondly, since 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 Dog.rollOver() accesses a private property of Dog_body. Our subclass does not have access to that variable. Since it is private, it was not inherited. However, since the rollOver() method is called within the context of the Dog class, the method has access to the private member, allowing our dog to successfully roll over. If, on the other hand, we were to override rollOver(), our code would not have access to that private object. Calling ‘this.__super()’ from within the overridden method would, however, call the parent method, which would again have access to its parent's private members.