Javascript Concatenative Inheritance

A better way to build reusable interfaces.

One of the most commonplace things that developers do is designing interfaces, and it is also one of the easiest places to start introducing unneeded complexity into an application. It is pretty easy to build a feature that oversteps the bounds of what it should be capable of doing, and introducing unwanted side-effects to these features can be a huge cause of technical debt. We as developers want to build interfaces that can handle all the interactions needed to complete a feature, simultaneously express how and why these interactions are related, and make it so our modules are flexible enough to be called in other parts of our app and, possibly more importantly, easily tested and maintained.

One common pattern to achieve this is classes. Most Object Oriented languages implement classes, and with ES6 Javascript added support for classes as well. Classes are a great tool since they help us achieve exactly what we described above; they encapsulate different functionality, expressly describe what variables and methods are available and their relationships, and are simple to extend and build on top of for added functionality. They check off a lot of boxes for what we want, so naturally they are a go to tool for a lot of developers.

I do think classes are a useful tool, however I find myself becoming less and less of a fan of them in recent months. The following reasons are things I don't like about them these days.

Extendable, but not flexible

One problem that I have with classes is that they are rigid. When we initialize an instance of a class that's exactly what we get; but if there are ever times we want to change the inner machinations of a class then things get tricky. Sure, we can overwrite methods by extending the class or mucking around with the object prototype, but that's a lot of fuss for an action that should be relatively simple. Classes make it easy to add new functionality, but there is a lot more effort if you need to change any existing functionality.

The 'this' keyword

This is simultaneously one of Javascript's most important features and one of its most confusing. This can mean a few of different things depending on the context it is used in and several different javascript features have different functionality around how this gets bound (arrow functions vs. standard functions). I don't think there is anything wrong with the this keyword, but I find myself avoiding it more often than not simply due to the complexity it introduces.

The 'new' keyword

New is a fine keyword, but I do think we can get along without it. New does a lot of stuff under the hood for us and makes classes possible in JS. I would be willing to bet that a fair amount of developers don't know (and don't need to know) what it actually is doing though, and all most people need to be told is 'you use them with classes'.

That's a lot of words to say "Classes aren't my jam" so if you made it this far thank you for entertaining me. To summarize, we don't want to use classes because they have a few problems, but we still want to be able to build interfaces that encapsulate functionality and express the relationships between variables and methods. We want these to be extensible and more flexible than classes without getting lost in implementation details.

This is where concatenative inheritance comes in. Concatenative inheritance is 'the process of combining properties of one or more source objects into a new destination object.(source)' Sometimes these objects are referred to as mix-ins, but if that definition sounds decently familiar then great! Concatenative inheritance is pretty common in javascript, and you've probably done it before. If you've ever written code that looks like this:

return {
  ...objectOne,
  ...objectTwo
}

or

Object.assign({}, objectOne, objectTwo);

then you've done this kind of thing before!

Essentially, this is a pattern for sharing properties across objects without explicitly reassigning those properties. It's definitely a type of inheritance, but it's not inheritance in the way that people usually think of it. Developers tend to get stuck in the definition of class inheritance or prototypal inheritance, but there are plenty of ways to share properties across different objects. We constantly extend objects dynamically in Javascript, and a lot of dynamic extension is done through this pattern!

Honestly I've yammered on enough so let's just get to some examples of what I mean. What we are going to do is use Javascript's ability to quickly and dynamically combine objects to create interfaces that can be easily altered if we need them to be, while still retaining the functionality that we want.

One of the most important ways we are going to achieve this is with factory functions. If you're not familiar with what that means, a factory function is any function that returns an object. Any time we need to repeatedly produce an object with similar properties we can use a factory function. That sounds about right for what we are looking for, so let's go with that!

const instanceFactory = ({ name, propsOptions, methodsOptions }) => {
  return {
    ...propsInstanceFactory(name, propsOptions),
    ...methodsInstanceFactory(methodsOptions)
  }
}

const propsInstanceFactory = (name, options) => {
  return {
    name,
    ...options
  }
}

const methodsInstanceFactory = (options) => {
  return {
    hello() {
      console.log(`Hi there! My name is ${this.name}`);
    },
    ...options
  }
}

const instance = instanceFactory({
  name: 'bobby', 
  propsOptions,
  methodsOptions,
});

So here, we have three factory functions. Our top level one is instanceFactory, which is the function that we will return our interface from. This function accepts whatever parameters you need, but note that it accepts two objects; propsOptions and methodsOptions. These objects are then passed into more factory functions propsInstanceFactory and methodsInstanceFactory. These objects are going to be spread into the return objects and then returned, mixed in together to become a single interface, which is what we will then act upon.

instance.hello(); // Hi there! My name is bobby

All we had to do was call our function and we got back our interface. We didn't have to use the new keyword or worry about constructors. Just a plain function call returning a plain object. On top of that, we get a lot of ability to modify what we return. Since we are spreading our options objects out, we can pass in details that we want to overwrite with new data or functionality.

propsOptions = { name: 'sophia' };
methodsOptions = { hello() { console.log(`This is a new hello from ${this.name}!`)} };

const secondInstance = instanceFactory({
  name: 'bobby', 
  propsOptions,
  methodsOptions,
});

secondInstance.hello(); // This is a new hello from sophia!

So we were able to overwrite our name parameter by passing it through the options, and were able to do the same with our hello function. This is what I mean when I say this is a more flexible pattern. With classes, we would have a much more difficult time overwriting the internals of our class at instantiation. This way, we are able to dictate with more control what our interfaces look like and how they behave. We can even extend our functionality simply by declaring another function, we don't have to extend a class prototype and call a whole new class.

const instance = instanceFactory({
  name: 'bobby', 
  propsOptions: {},
  methodsOptions: {},
});

const propsOptions = { name: 'sophia' };
const methodsOptions = { 
  newMethod() { console.log(`A whole new method!`)} 
};

const secondInstance = instanceFactory({
  name: 'bobby', 
  propsOptions,
  methodsOptions,
});

secondInstance.newMethod(); // A whole new method!
instance.newMethod(); // throws an error since this instance does not have this method

This easily extensible model is one of my favorite parts about this style of coding. With classes, we would have a lot more work of declaring a new class that extends our previous one. Now we can extend functionality in a much easier manner.

Now, there is one more benefit to this pattern that I will go into, and I personally see it as a big one. One problem with classes is they don't support asynchronous actions at instantiation. Consider:

class UserNotifications {
  async constructor(userName) {
    this.user = await getUser(userName);
  }
}

The above is not valid code, and neither is this:

class UserNotifications {
  constructor(userName) {
    this.user = getUser(userName).then(user => user);
  }
}

We cannot turn constructors into asynchronous generators, or conduct any asynchronous activity in them. A constructors only job is to build and return objects. Because of this, we must declare an init method that we call after class instantiation that can be used to conduct any async work we need done.

class UserNotifications {
  constructor(userName) {
    this.userName = userName;
    this.user = null;
  }
  async init() {
    this.user = await getUser(this.userName);
  }
}

const notify = new UserNotifications(myUser);
await notify.init();

Since we are not allowed to do asynchronous work when we initialize our object, we define a method called init to run our async work, which we call directly after object instantiation. However, what if we didn't have to have the extra step? Realistically our object initialization and the asynchronous work are closely related, so it is a bit of a pain to have separate actions to do them.

Well our functions we used earlier are just plain functions, which do support async actions since they are just functions. We could rewrite the above like this:

const notificationsFactory = async ({ userName, options }) => {
  const user = await getUser(userName);
  return {
    user,
    ...options
  }
}

const notify = await notificationsFactory({ userName, options: {});

Of course our function supports async operations, it's just a normal function. Ultimately that is exactly what I love about this pattern; we are implementing normal functions returning normal objects to build the same type of interfaces we would get from classes. We have a lot more predictability with our code since we are not relying on the syntactic sugar of classes, and we end up with interfaces that are even more flexible and extensible. Overall, I have found it a super clean and easy to read way to write my code and I will continue to build structures in this way!

Tags