Skip to content
/ mics Public

Multiple Inheritance Class System: Intuitive mixins for ES6 classes

License

Notifications You must be signed in to change notification settings

Download/mics

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mics 0.6.1

Multiple Inheritance Class System

Intuitive mixins for ES6 classes

npm license travis greenkeeper mind BLOWN

Multiple Inheritance is like a parachute. You don't often need it, but when you do, you really need it. Grady Booch

What is it

mics (pronounce: mix) is a library that makes multiple inheritance in Javascript a breeze. Inspired by the excellent blog post "Real" Mixins with Javascript Classes by Justin Fagnani, mics tries to build a minimal library around the concept of using class expressions (factories) as mixins. mics extends the concepts presented in the blog post by making the mixins first-class citizens that can be directly used to instantiate objects and can be mixed in with other mixins instead of just with classes.

Install with NPM

npm install --save mics

Direct download

  • mics.umd.js (universal module works in browser and node)
  • mics.min.js (minified version of universal module file)

Include in your app

import

import { mix, is, like } from 'mics'

require

var mix = require('mics').mix
var is = require('mics').is
var like = require('mics').like

AMD

define(['mics'], function(mics){
  var mix = mics.mix
  var is = mics.is
  var like = mics.like
});

Script tag

<script src="https://cdn.rawgit.com/download/mics/0.6.1/dist/mics.min.js"></script>
<script>
  var mix = mics.mix
  var is = mics.is
  var like = mics.like
</script>

Usage

Creating a mixin

Mixins are like classes on steroids. They look and feel a lot like ES6 classes, but they have some additional capabilities that ES6 classes do not have:

  • They can 'extend' from multiple other mixins including (at most one) ES6 class
  • They have an explicit interface which can be inspected and tested at runtime
  • They have an ES6 class that is used to create instances
  • They have a mixin function that mixes in their class body into another type.
  • They can be invoked without new to create new instances

mixin: An ES5 constructor function that has properties mixin, class and interface.

You create mixins with the mix function.

mix([superclass] [, ...mixins] [, factory])

mix accepts an optional superclass as the first argument, then a bunch of mixins and an optional class factory as the last argument and returns a mixin.

Mostly, you will be using mix with a factory to create mixins, like this:

import { mix, is, like } from 'mics'

var Looker = mix(superclass => class Looker extends superclass {
  constructor() {
    super()
    console.info('A looker is born!')
  }
  look() {
    console.info('Looking good!')
  }
})

typeof Looker                 // 'function'
typeof Looker.mixin           // 'function'
typeof Looker.class           // 'function'
typeof Looker.interface       // 'object'

Notice that the argument to mix is an arrow function that accepts a superclass and returns a class that extends the given superclass. The body of the mixin is defined in the returned class. We call this a class factory.

Class factory: An arrow function that accepts a superclass and returns a class extends superclass.

The mix function creates a mixing function based on the given mixins and the class factory and invokes it with the given superclass to create the ES6 class backing the mixin. It then creates an ES5 constructor function that uses the ES6 class to create and return new instances of the mixin. Finally it constructs the mixin's interface from the class prototype and attaches the mixin function, the class and the interface to the ES5 constructor function, creating what in the context of mics we call a mixin.

Creating instances of mixins

We can directly use the created mixin to create instances, because it is just a constructor function:

var looker = new Looker()     // > A looker is born!
looker.look()                 // > Looking good!
looker instanceof Looker      // true

And because it's an ES5 constructor function, we are allowed to invoke it without new:

var looker = Looker()         // > A looker is born!
looker.look()                 // > Looking good!

ES6 made newless invocation of constructors throw an error for ES6 classes, because in ES5 it was often a cause for bugs when programmers forgot new with constructors that assumed new was used. However I (with many others) believe that not using new is actually better for writing maintainable code. So mics makes sure that it's constructors work whether you use new on them or not, because the backing ES6 class is always invoked with new as it should be. Whether you want to write new or not in your code is up to you.

Mixing multiple mixins into a new mixin

Let us define mixins Walker and Talker to supplement our Looker:

var Walker = mix(superclass => class Walker extends superclass {
  walk() {
    console.info('Step, step, step')
  }
})

var Talker = mix(superclass => class Talker extends superclass{
  talk(){
    console.info('Blah, blah, blah')
  }
})

Now that we have a bunch of mixins, we can start to use them to achieve multiple inheritance:

var Duck = mix(Looker, Walker, Talker, superclass => class Duck extends superclass {
  talk() {
    var org = super.talk()
    console.info('Quack, quack, quack (Duckian for "' + org + '")')
  }
})

var donald = Duck()
donald.talk()                 // > Quack, quack, quack (Duckian for "Blah, blah, blah")

As you can see, we can override methods and use super to call the superclass method, just like we can with normal ES6 classes.

Testing if an object is (like) a mixin or class

instanceof works for mixin instances like it does for ES6 classes. But, like ES6 classes, it does not support multiple inheritance. In the example above, Looker is effectively the superclass for Duck. Walker and Talker are mixed into Duck by dynamically creating new classes and injecting them into the inheritance chain between Looker and Duck. Because these are new classes, instances of them are not recognized by instanceof as instances of Walker and Talker.

Fortunately, mics gives us an is function, which does understand multiple inheritance.

is(subject, type)

Tests whether subject is-a type or extends from type. The first parameter to is defines the subject to test. This can be an instance or a type. The second parameter is either a type (constructor function, ES6 class or mixin) or a type string.

duck instanceof Duck          // true
duck instanceof Looker        // true, but:
duck instanceof Walker        // false! mix created a *new class* based on the factory

// `is` to the rescue!
is(duck, Walker)              // true
// we can also test the type
is(Duck, Walker)              // true
is(Talker, Walker)            // false

like(subject, type)

Often, we don't really care whether the object is a certain type, we just want to know whether we can treat it like a certain type. Use like(subject, type) to test whether a subject adheres to the same interface as is defined by type:

var viewer = {                // create an object with the
  look(){}                    // same interface as Looker
}
is(viewer, Looker)            // false, but
like(viewer, Looker)          // true

A good example of how this might be useful can be found in the new ES6 feature Promises. Here we have the concept of a 'thenable'. This is any object that has a then method on it. Methods in the Promise API often accept thenables instead of promise instances. Have a look at Promise.resolve for example:

Promise.resolve(value) Returns a Promise object that is resolved with the given value. If the value is a thenable (i.e. has a then method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value. mdn

Using mix to define an interface and like to test for it, we can very naturally express the concept of a thenable from the Promise spec in code:

/** Defines a Thenable */
var Thenable = mix(superclass => class Thenable extends superclass {
  then() {}
})
/** Some mixin which can be treated as a Thenable */
var MyPromise = mix(superclass => class MyPromise extends superclass {
  then(resolve, reject) {
    resolve('Hello, World!')
  }
}
// We can check whether the class is thenable using like
like(MyPromise, Thenable)     // true
// we can also check instances
var promise = new MyPromise()
like(promise, Thenable)       // true
// Ok, that means we can use Promise.resolve!
Promise.resolve(promise).then((result) => {
  console.info(result)        // > 'Hello, World!'
})

Using a custom ES5 constructor

The default constructor returned from mix is a one-liner that invokes the ES6 class with new. But there could be reasons to use a different function instead. mix allows you to supply a custom constructor to be used instead. You do this by providing a static constructor in the class body:

var Custom = mix(superclass => class Custom extends superclass{
  static constructor(...args){
    console.info('Custom constructor called!')
    return new this(...args)
  }
})

var test = Custom()           // > 'Custom constructor called!'
is(test, Custom)              // true

Bonus

As a bonus, you can use is() to do some simple type tests by passing a string for the type:

class X {}
var factory = superclass => class Y extends superclass {}
var Y = mix(factory)
var Z = mix(X, Y)

is(X, 'function')             // true
is(X, 'class')                // true
is(X, 'mixin')                // false
is(X, 'factory')              // false

is(factory, 'function')       // true
is(factory, 'class')          // false
is(factory, 'mixin')          // false
is(factory, 'factory')        // true

is(Y, 'function')             // true
is(Y, 'class')                // false
is(Y, 'mixin')                // true
is(Y, 'factory')              // false

is(Z, 'function')             // true
is(Z, 'class')                // false
is(Z, 'mixin')                // true
is(Z, 'factory')              // false

Supported type strings: "class", "mixin", "factory", and any type strings that can be passed to typeof.

  • class: x is a (possibly Babel-transpiled) ES6 class
  • mixin: x is a mixin that is the result of calling mix
  • factory: x is a class factory function

Issues

Add an issue in this project's issue tracker to let me know of any problems you find, or questions you may have.

Credits

Credits go to Justin Fagnani for his excellent blog post "Real" Mixins with JavaScript Classes and the accompanying library mixwith.js.

Contributors

Many thanks to Marco Alka for his contributions to this project.

Copyright

Copyright 2017 by Stijn de Witt and contributors. Some rights reserved.

License

Licensed under the Creative Commons Attribution 4.0 International (CC-BY-4.0) Open Source license.

About

Multiple Inheritance Class System: Intuitive mixins for ES6 classes

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •