Among the chief problems regarding software maintainability are the ones related to code duplication. It makes senses that there are quite the number of language features, design patterns and software principles to give the developers means to keep their projects DRY. By now we’ve learned lots of good and bad ways to do it and this is about one of these good ways that we can use in the modern JavaScript based projects.
I think before going further we can take a look at how did the javascript community handled this problem until now.
Then and Now
Even before ES6 classes took the world by storm -despite numerous people’s grievances- we had lots of options. JavaScript at a language level is prototypical, which solves the problem of behaviour re-use in itself. But framework owners had provided their solutions by creating their own object model that gives their users a simpler form of inheritance (Backbone, Ember, React…) that can be easily understood by developers who came from the back-end platforms. Most of these inheritance-based object models also came with mixin support to give developers a tool to let them use a more composition based approach.
When ES6 classes became the de facto way for javascript developers to achieve inheritance, a big part of JavaScript’s flexibility has been lost. This is one of the biggest reasons javascript decorators gain such wide adoption even when the feature was still in its infancy in stage-1. They provided that much needed flexibility.
Higher Order Functions
One other way to achieve behaviour re-use is higher order functions. They are a big part of the functional programming goodness. But what if the thing we want to decorate with a behaviour is not a function but a class? Can we talk about higher order classes? Is that even a thing? With javascript magic it kinda is. Technically, the one receiving and returning a class, is not another class but a function, but I think that’s ok since we are calling these things higher order components now.
I’d say solution that I’m going to show here is better than the current ones in use, because;
- It’s achieved by lots of ES2017 sugar, so it’s tasty.
- It works without including a dependency, so there is no fat.
- And most importantly in a clean and readable manner that what we do is transparent to the people reading it, so it looks good.
It makes use of the fact that classes are expressions in javascript and how that fact plays so nicely with the way decorators work in javascript.
Higher Order Classes
This thing is going to make much more sense if we knew couple of facts about JavaScript.
First, how do classes work in javascript? We know that they are just syntactic sugar above functions. So in turn, classes behave very similar to functions. You can have a class with no name, you can’t access the class itself inside the definition, you can assign classes to a variable through an expression etc. In JavaScript, class declarations are are expressions.
And second, how do decorators work in JavaScript? Their behaviour kinda gets demystified when when you understand this about them; @ is an operator that feeds the thing that its decorating to the function returned from expression by its right side, and replaces it with definition returned from that said function.
We’re gonna try to make use of these facts in this approach. Here’s how.
Lets say we have flying behaviour, defined like this;
export default target => class Flying extends target { constructor(){ super(…arguments); console.log("constructing flying…"); } fly(){ console.log("I flied!"); }
}
and then we have a tweeting behaviour.
export default target => class Tweeting extends target {
constructor(){ super(…arguments); console.log("constructing tweeting…"); }
tweet(){ console.log("I tweeted…"); }
}
These modules are exporting a lambda function with an implicit return as default. Function they export receives a class as its first parameter, than returns a new class that extends it.
This enables us to do stuff like;
import Flying from './flying'; import Tweeting from './tweeting';
@Flying @Tweeting class Bird {
constructor(){ console.log("constructing bird…"); } doBirdyStuff(){ this.fly(); this.tweet(); }
}
Let’s explain what happened here step by step;
- @ operator above the Bird class evaluated the expression by it’s side (Tweeting), which resulted in a function.
- It then passed the Bird class to that function, which returned another class that extends the Bird and adds tweeting capability.
- @ operator besides ‘Flying’ took that class, and extended again with a flying capability.
Here is how it behaves when we execute it:
const awesomeBird = new Bird(); // constructing bird… // constructing tweeting… // constructing flying… awesomeBird.doBirdyStuff(); // I flied // I tweeted
Notice that constructors are invoked starting from the class that we are decorating and then upwards. Beauty here is that you can even share initialisation behaviour of your capabilities easily through this approach, through the decorator constructors.
Behavioural Difference with Mixins and Inheritance
There are some behavioural differences with mixins that we need to be aware of.
Because of the order of execution;
- We can not not override new behaviours added by the decorators
- We won’t be able to call this.fly in the bird’s constructor
Methods you declared in the Bird class can not override the ones in flying and tweeting decorators, because they do not exist at that point. Calling this.fly in another method of Bird class is OK because in the moment of their execution there will be a fly method but they will not be there at the moment of declaration.
This might look like a big limitation but there is a way to get around it. If we want that kind of composition, this is how we achieve it;
import Flying from './flying'; import Tweeting from './tweeting';
@Flying @Tweeting class Birdiness {}
class Bat extends Birdiness { tweet(){ console.log("[squeaking ultrasonically]"); } }
In this case, we were able to override tweet method because in the moment of Bat class’ declaration since it already exists under the extended Birdinessclass.
How Do We Type Check?
Another problem arises when we try to type check classes that returned from this decorator. We might want to know if a class has the capability to do something before executing relevant code.
What I prefer using in this case is a named export in the same module we defined the decorator, beside the default export. One that provides a class that we can use with instanceof operator. This class will be the most basic form of a class decorated with our function because it uses the native Object class as its first parameter.
const decorator = target => class Tweeting extends target { ... }
export default decorator; export const ATweeting = decorator(Object);
While this provides a basic Tweeting class that we can use with instance of checks rather than a function, those instanceof checks will always return false. It’s because each time we decorate a class with this approach we are creating a new class and those classes will be different from the ATweetingclass that we exported. Luckily JavaScript lets us override the default instanceof behaviour by overriding has.InstanceOf symbol.
const typeTag = Symbol('type');
const decorator = target => class Tweeting extends target {
static __isTweetingInstance__ = typeTag; static [Symbol.hasInstance](instance) { return instance.constructor.__isTweetingInstance__ === typeTag; }
...
}
We created a new unique JavaScript symbol named typeTag that is private to the decorator module. We then defined a private property in the extended class with initial value of typeTag. Lastly in the in the method we checked if the constructor of the checked instance has this private value to see if this decorator has been used on its class.
Here is an example usage;
import { ATweeting } from './tweeting'
...
const awesomeBird = new Bird(); console.log(awesomeBird instanceof ATweeting) // true
Have fun with it.