Public, Protected, and Private Names in Backdraft Classes

There are lots of ways to handle the problem of defining public, protected, and private names in JavaScript classes. Axel Rauschmayer has written about this subject lucidly in a couple of his books here and here. The great thing about Axel's writing is that he presents all of the techniques in one place, complete with an analysis of the pros and cons of each. If this subject is new to you, I strongly recommend reading these sections (and purchasing the books to keep more great content from Axel coming).

The core of the problem is to define data and/or names that are somehow hidden from somebody or something. In order to decide which technique is best, you need to understand what you are trying to hide (a name or data) who you are hiding from (a person or other program code), and why you are trying to hide it in the first place.

Let's start with the obvious, that, unfortunately, is not so obvious to many: it is impossible to hide anything that exists in the JavaScript being executed in the browser from the user sitting at the browser. Why? Because the user can open up the debugger, set a breakpoint, and inspect any variable.

Similarly, any programmer or process that controls the JavaScript that is loaded into a document can likewise access anything in that code. It might be hard. It might require parsing the code and mutating it before it's executed. But it can be done.

In short, it is impossible to thwart either the browser user or the programmer/process that writes/loads the code from gaining access to some deep, dark secret contained in the code. And this truism applies to any programming environment (e.g., a compiled C++ program).

On the other hand, the programmer/process that writes/loads the code can set up sandboxes that prevent different parts of the code from accessing each-others data. The references at the top explain how. Backdraft, being a library to implement user-interface components, has nothing to hide. Therefore there is no need to employ these techniques inside Backdraft. Of course it is possible that a component derived from Backdraft's Component class may need to hide data. In such cases, the subclass is free to employ whatever data hiding technique is appropriate.

Backdraft code is concerned with controlling the access to names in the classes Backdraft defines for a couple of reasons:

  • Preventing unintentional namespace collisions
  • Controlling complexity

Preventing Unintentional Namespace Collisions

Owing to JavaScript's object model, subclasses share the same instance namespace as the superclass from which they are derived (this is not true in many other programming systems). Consider...

Super.js

export default class Super {
	constructor(){
		this._private = "test1";
	}
}

MyClass.js

import Super from "path-to-/Super.js"
class MyClass extends Super {
	constructor(){
		super();
		this._private = "test2";
	}
}

let instance = new MyClass();
instance._private; // value of "test2"

In the code above, the class MyClass steps on the "private" member variable _private defined by Super. When "private" variables are defined like this, it is easy to make this mistake when deriving a class from unfamiliar code. Put another way, to safely use this method, the programmer must completely understand the internals of the superclass before deriving from it. This is unreasonable. A programmer ought not have to understand the internals of a library in order to use it safely.

Backdraft employs a technique that hides the name so that stepping on the name must be done with explicit intent. Consider modifying the example above as follows:

Super.js

const ppPrivate = Symbol();
export default class Super {
	constructor(){
		this[ppPrivate] = "test1";
	}
}

// see discussion
Super.ppPrivate = ppPrivate;

MyClass.js

import Super from "path-to-/Super.js"

const ppPrivate = Symbol();
class MyClass extends Super {
	constructor(){
		super();
		this[ppPrivate] = "test2";
	}
}

// see discussion
MyClass.ppPrivate = ppPrivate;

let instance = new MyClass();
instance[ppPrivate];                 // value of "test2"
instance[Super.ppPrivate];           // value of "test1"
Super.ppPrivate===MyClass.ppPrivate; // false

In the code above, Super defines its private variables in a manner that allows subclasses to define private variables without concern for stepping on private names defined by Super itself. The only way to access the member data located at [ppPrivate] defined by Super is to gain access to the symbol ppPrivate defined in the source file Super.js (remember, each Symbol() is unique). That is possible, for example, through something like Object.getOwnPropertySymbols(), but such actions would be intentional, and, presumably well-educated.

Since expert programmers may want to intentionally access private instance data, Backdraft always publishes private symbol names as static data on the class (Super.ppPrivate in the example above). Subclasses are free to define their own static data, but there is no concern for name clashes since static namespaces are independent as demonstrated above.

This is the technique used by the Backdraft library to define data in a way that eliminates all unintentional name collisions.

Controlling Complexity

One of the key benefits of user-defined-types (in the case of JavaScript, classes), is information hiding. The idea is that the user of a class need only concern himself with the public interface of that class and can completely ignore the internal implementation details. True enough. But what about the programmer that derives a subclass from another class. It's often true that this programmer is somewhere between the "end user" of the class, only interested in the class's public interface, and the author of the class, who is intimate with every detail of the class's implementation. Indeed, this second kind of user may need to modify some public and private methods and/or data. This is where the idea of protected data/names comes in to play

Canonically, the "protected" interface published by a base class is that part of the class that is intended to be visible to subclasses derived from that base class, but not visible to normal users who only see the "public" interface. Recall, Backdraft is not interested in hiding/protecting data, but rather names. Accordingly, Backdraft code defines protected names by convention: all protected names are preceded by an underscore. This convention gives a signal to users that the name is not intended to be used other than within subclass methods. By partitioning names like this, the size of the public interface is reduced, which, therefore, reduces complexity. At the same time, subclasses have a minimal hurdle to access the name within their own code. We think this is a good compromise. Within all of Backdraft, there are only three protected names, so this concept isn't used much within the library code itself. However, it is used in many of the examples. If you follow our conventions, you will likely use it quite a bit in your own code.