Imitating Classes

In this post, we are going to explore how you might imitate classes in a language that doesn’t have them, to get a better feel for how various object-oriented language features actually work. We will use JavaScript, but ban ourselves from using

  • the class keyword (obviously),
  • the new keyword (we will construct all of our own objects),
  • the this keyword,
  • prototypal inheritance (we will build our own inheritance mechanism).

The main language features we will be using are functions (including higher-order functions), closures, and object literals (including some of the nice new syntactic sugar for constructing objects).

To start off with, let’s create a really basic collection class.

// Create a new collection, initially with the given items.
function collection(items = []) {
  const _items = [...items];

  // Clear the collection, removing all items.
  function clear() {
    _items.splice(0);
  };

  // Determine whether the given item is in the collection.
  function contains(item) {
    return _items.includes(item);
  };

  // Add the given item to the collection.
  function add(item) {
    _items.push(item);
  };

  // Remove the given item from the collection.
  function remove(item) {
    const index = _items.indexOf(item);
    if (index >= 0) _items.splice(index, 1);
  };

  return {
    clear,
    contains,
    add,
    remove
  };
};

Each time we call the collection function, we get a new collection object with its own copy of the four methods, clear, contains, add, and remove, that each close over the private array of items. The _items array is private because it isn’t included as a member of the returned object.

It’s also worth mentioning that although we haven’t made use of this, the fact that methods are defined as local functions means that they can all refer to each other.

Next, we’ll create a class which extends collection. Let’s add some methods for adding and removing items transactionally.

// Create a new collection, initially with the given items.
function transactionalCollection(...args) {
  const _added = [];
  const _removed = [];

  const base = collection(...args);

  // Add an item with the current transaction.
  function addOnSubmit(item) {
    _added.push(item);
  };

  // Remove an item with the current transaction.
  function removeOnSubmit(item) {
    _removed.push(item);
  };

  // Submit the current transaction
  function submit() {
    _added.forEach(base.add);
    _removed.forEach(base.remove);
    _added.splice(0);
    _removed.splice(0);
  };

  // Abandon the current transaction.
  function abandon() {
    _added.splice(0);
    _removed.splice(0);
  };

  return {
    ...base,
    addOnSubmit,
    removeOnSubmit,
    submit,
    abandon
  };
};

Here we have added four additional methods—addOnSubmit, removeOnSubmit, submit, and abandon, and the additional methods are able to reference the exising methods. It’s also worth noting that aside from assuming that collection has an add and a remove method, transactionalCollection makes no other assumptions about collection, it will continue to work if the constructor is changed or other methods are added, and importantly it will not remove such methods either.

Next, let’s consider method overriding. We’ll create another class, localStorageCollection, which also persists the collection of items to local storage so that the collection can be used across sessions. Unfortunately we’re going to want access to the private _items member of collection, so what we really want is to be able to make it protected. More unfortunately still, since public members are just those added to the returned object and private members are those which aren’t, there’s no mechanism left for us to define protected members.

We will instead use convention to define protected members, as those whose name starts with an underscore (_). If we want to enforce this, we can define a helper function which removes protected members from a class, as below.

// Create a new collection, initially with the given items.
function collectionBase(items = []) {
  // Array of items in the collection.
  const _items = [...items];

  // Clear the collection, removing all items.
  function clear() {
    _items.splice(0);
  };

  // Determine whether the given item is in the collection.
  function contains(item) {
    return _items.includes(item);
  };

  // Add the given item to the collection.
  function add(item) {
    _items.push(item);
  };

  // Remove the given item from the collection.
  function remove(item) {
    const index = _items.indexOf(item);
    if (index >= 0) _items.splice(index, 1);
  };

  return {
    _items,
    clear,
    contains,
    add,
    remove
  };
};

// Return a copy of the given object with only its public members.
function publics(obj) {
  const newObj = { ...obj };
  Object.keys(newObj)
    .filter(m => m.startsWith('_'))
    .forEach(m => delete base[m]);
  return newObj;
};

// Seal a class by removing protected members.
function seal(cls) {
  return function (...args) {
    return publics(cls(...args));
  }
};

const collection = seal(collectionBase);

The defintion of transactionalCollection from above is fully compatible with this new defintion.

We can now define localStorageCollection using collectionBase.

// Create a new collection which persists to local storage, initially with the
// given items.
function localStorageCollection(keyName, items) {
  const storage = window.localStorage;
  items = items || JSON.parse(storage.getItem(keyName) || '[]');

  const base = collectionBase(items);

  function _save() {
    storage.setItem(keyName, JSON.stringify(base._items));
  }

  // Clear the collection, removing all items.
  function clear() {
    base.clear();
    _save();
  };

  // Add the given item to the collection.
  function add(item) {
    base.add(item);
    _save();
  };

  // Remove the given item from the collection.
  function remove(item) {
    base.remove(item);
    _save();
  }

  return {
    ...publics(base),
    clear,
    add,
    remove
  };
}

The obvious question now is, can we make a transactional collection that also persists to local storage—or to put it another way, do we have multiple inheritance? If you look back at our definitions for transactionalCollection and localStorageCollection, you’ll notice that they “know” what they inherit from—that is, the name of the class that they inherit from appears (in the call to the base constructor) inside the definition of the class. Could we instead rewrite them such that they don’t “know” what they inherit from? That is, can we write them in such a way that they are parameterised over their base class? The answer is yes, easily!

// Create a new class which adds transactions to a base collection class.
function makeTransactionalCollection(collection) {
  return function (...args) {
    const _added = [];
    const _removed = [];

    const base = collection(...args);

    // Add an item with the current transaction.
    function addOnSubmit(item) {
        _added.push(item);
    };

    // Remove an item with the current transaction.
    function removeOnSubmit(item) {
        _removed.push(item);
    };

    // Submit the current transaction.
    function submit() {
        _added.forEach(base.add);
        _removed.forEach(base.remove);
        _added.splice(0);
        _removed.splice(0);
    };

    // Abandon the current transaction.
    function abandon() {
        _added.splice(0);
        _removed.splice(0);
    };

    return {
        ...base,
        addOnSubmit,
        removeOnSubmit,
        submit,
        abandon
    };
  };
};

const transactionalCollection = makeTransactionalCollection(collection);
const transactionalLocalStorageCollection = makeTransactionalCollection(
  localStorageCollection);

Notice that the core of the body of makeTransactionalCollection is identical to that of transactionalCollection from above. The only difference is that now the variable collection no longer refers to the existing collection class defined in the global scope, but to the collection parameter which refers to whatever argument we choose to supply. This makes it a far more powerful construct.