Protected Protocol

David Bruant bruant.d at gmail.com
Sun Apr 1 15:28:00 PDT 2012


Hi,

A while ago, I posted a challenge on es-discuss [1]. It was a challenge
related to code reuse, modularity and composition. There has been an
interesting suggestion [2] [3] which left me a bit unsatisfied since
relying on constructor arguments while it may not be always possible to
do that.
What I describe as the "protected protocol" is my response to my own
challenge.
Before explaining it, I'd like to take a step back and discuss about the
object oriented properties of JavaScript.


Two expected properties of object-oriented languages are encapsulation
and inheritance.

Encapsulation allows to define objects which can only be interacted with
through a given an interface, thus hiding implementation details. In the
long run, encapsulation makes the maintenance of components easier since
the implementation of an object can be changed without affecting its
clients.

Encapsulation can be implemented in JavaScript. One usual way is to do
as follow:

    function Storage(){
        var array = Array.prototype.slice.call(arguments, 0);
        
        return {
            add: function(e){
                array.push(e);
            },
            read: function(i){
                return array[+i];
            }
        };
    }

Here, access to the array is restricted to the interface ("add" and
"read" methods).


Inheritance allows to better reuse code by specializing classes.
This can be implemented as well, usually with this pattern:

    function Storage(){
        this.array = Array.prototype.slice.call(arguments, 0);
    }
    
    Storage.prototype = {
        add: function(e){
            this.array.push(e);
        },
        read: function(i){
            return this.array[+i];
        }
    };


    function StorageWithDeletion(){
        Storage.call(this); // equivalent to "super";
    }
    
    StorageWithDeletion.prototype = Object.create(Storage.prototype);
    StorageWithDeletion.prototype.delete = function(i){
        delete this.array[+i];
    };
    

To define a storageWithDeletion, there is no need to redefine the add
and read method. To extend Storage, StorageWithDeletion here only needs
access to the Storage function in the global scope (or Storage module
when these will be deployed). This has the nice property that Storage
and StorageWithDeletion can be defined in different files since they
don't need to share a common function scope

I have made obvious by juxtaposing both examples that JavaScript
requires from the programmer to choose between encapsulation and
inheritance. Indeed, as demonstarted above, encapsulation requires a
shared scope.

Solutions where Storage and StorageWithDeletion would be defined with a
shared scope could be explored, but it would require to define both in
the same file, which, I don't want to be a necessity.

The trick of the protected protocol is that encapsulation does not
really require a shared scope, it's just the most natural way to do it
in JavaScript.

When a method located in the prototype chain of an object is called, its
'this' references the object from which the method has been called.
Programmers have naturally stored the state of an object as own (public)
properties of the object as it's the most natural way to bind
information to an object.
It doesn't have to be this way, though. WeakMaps offer an
straightforward API to bind information to an object and it is not
necessary to share this WeakMap with the rest of the world.


The protected protocol and an example of how to use it can be found at
[4] (which is a bit different than a version I tweeted earlier [5]).

The most important part in my opinion is that the Person.js and
ComputerSavvyPerson.js are defined in 2 different files. The only thing
the latter needs is a reference to the constructor it inherits from
(here, Person).

Things that are expected to be secret can be kept as such. First, a
Person's secret doesn't leak, second, even a ComputerSavvyPerson's
secret doesn't leak. A bit more tricky was to make that defining a new
subclass doesn't leak secrets of the base class.

In my opinion, the boilerplate code for this to work is minimal enough,
but I'm interesting on feedback on this aspect. Regardless, the code of
the constructor and of the prototype methods are clean of boilerplate
code and only require to use the "Protected(this)" convention when
intending to work with private-to-the-inheritance-tree parts of the
object-state.

The implementation of the 2 necessary functions take 60 lines which is
rather reasonable.

So all the properties I expected are here. On the negative side,
prototype object equality had to be broken for the sake of preventing
leaks of "protected" properties, so instanceof is necessarily broken. I
think it's not a big deal since I could reimplement one myself if really
necessary.

It's ready for private names and these would need to be defined inside
the "declaration code" in order for them not to leak outside.
I intuit that assuming class syntax can be embedded in a function, I
could wrap it and add support for the Protected convention if I want to.

Anecdotes:
1) In Person.js, I've been burned by non-lexical this when writing the
code that increased the age over time. We really need functions with
lexical this :-)
2) After some feedback by Brendan on Twitter, I've created a
WeakMap.prototype.getset function following a discussion we had on
es-discuss about setting a value when there is not already one.


I'm interested in any feedback, opinions or questions you'd have.

Thanks,

David

[1] https://mail.mozilla.org/pipermail/es-discuss/2012-March/021292.html
[2] https://mail.mozilla.org/pipermail/es-discuss/2012-March/021304.html
[3] https://gist.github.com/2053624
[4] https://gist.github.com/2279059
[5]
https://gist.github.com/c7d4947b41936680810d/a531bd4412b67dc0fed180d1d776314bf8c4747b


More information about the es-discuss mailing list