Skip to content

Commit

Permalink
0.5.0: Mixins are ES5 classes
Browse files Browse the repository at this point in the history
  + Make instanceof work for the single inheritance scenario
  + Allow custom static constructor
  + Hoist static class properties
  + Allow for newless invocation
  + Improved README
  + Improved test coverage
  • Loading branch information
Download committed Apr 18, 2017
1 parent b57bb21 commit ed5c1b6
Show file tree
Hide file tree
Showing 14 changed files with 3,482 additions and 981 deletions.
152 changes: 122 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# mics
# mics <sup><sub>0.5.0</sub></sup>
### Multiple Inheritance Class System
**Intuitive mixins for ES6 classes**

Expand All @@ -15,7 +15,7 @@
**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](http://justinfagnani.com/2015/12/21/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 (literally!) citizens
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.


Expand All @@ -25,8 +25,8 @@ npm install --save mics
```

## Direct download
* [mics.umd.js](https://cdn.rawgit.com/download/mics/0.4.0/dist/mics.umd.js) (universal module works in browser and node)
* [mics.min.js](https://cdn.rawgit.com/download/mics/0.4.0/dist/mics.min.js) (minified version of universal module file)
* [mics.umd.js](https://cdn.rawgit.com/download/mics/0.5.0/dist/mics.umd.js) (universal module works in browser and node)
* [mics.min.js](https://cdn.rawgit.com/download/mics/0.5.0/dist/mics.min.js) (minified version of universal module file)


## Include in your app
Expand All @@ -44,30 +44,43 @@ var is = require('mics').is

### AMD
```js
define(['mics'], function(mix){
var is = mix.is
define(['mics'], function(mics){
var mix = mics.mix
var is = mics.is
});
```

### Script tag
```html
<script src="https://cdn.rawgit.com/download/mics/0.4.0/dist/mics.min.js"></script>
<script src="https://cdn.rawgit.com/download/mics/0.5.0/dist/mics.min.js"></script>
<script>
var mix = mics.mix
var is = mics.is
</script>
```

## Usage
### Creating a mixin
Mixins are normal ES6 classes that have two class properties: `mixin` and `interface`.
You create them with the `mix` function.
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 or ES5 constructor function
* 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
factory as the last argument. If no `factory` is given, a regular class is created, otherwise a
mixin class. In both cases, the result of the call will be an ES6 class.
`mix` accepts an optional *superclass* as the first argument, then a bunch of *mixin*s 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:

```js
import { mix } from 'mics'
import { mix, is } from 'mics'

var Looker = mix(superclass => class Looker extends superclass {
constructor() {
Expand All @@ -82,10 +95,17 @@ var Looker = mix(superclass => class Looker extends superclass {

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*. The `mix` function creates a mixing function based on the given mixins and the class
factory and uses it to create the resulting class. It then attaches the mixing function to the resulting
class (under the key `mixin`), creating what in the context of **mics** we call a mixin. We can directly
use that mixin to create instances, because it is just a class:
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:

```js
var looker = new Looker() // > A looker is born!
Expand All @@ -94,28 +114,79 @@ looker instanceof Looker // true
typeof looker.mixin // function
```

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

```js
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`:

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

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

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

```js
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` does not work for mixin instances. Instead use **mics**' `is` function, which works on
mixins (type as well as instance) and on classes (again, type as well as instance).
`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])
The first parameter to `is` is required and defines the subject to test. The second parameter is optional.
If specified, it calls `a` (see below) and returns a boolean. If not specified, it returns an object that
has submethods `a`/`an` and `as`.
The first parameter to `is` is required and defines the subject to test. This can be an instance or a type.
The second parameter is optional. If specified, `is` calls `a` (see below) and returns a boolean. If not specified, `is` returns an object that has submethods `a`/`an` and `as`.

#### a(type)
Tests whether the subject is-a `type`.
Tests whether the subject is-a `type`, or, when the subject is itself a type, whether the subject extends from `type`.

```js
import { is } from 'mix'

looker instanceof Looker // true, but:
var GoodLooker = mix(Looker, superclass => class GoodLooker extends superclass {})
var hottie = new GoodLooker()
hottie instanceof Looker // false! mix created a *new class* based on the factory
duck instanceof Duck // true
duck instanceof Looker // true, but:
duck instanceof Walker // false! mix created a *new class* based on the factory

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

#### an(type)
Expand Down Expand Up @@ -174,6 +245,23 @@ Promise.resolve(promise).then((result) => {
})
```
### 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:

```js
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).a(Custom) // true
```

### Bonus
As a bonus, you can use `is(..).a(..)` to do some simple type tests by passing a
string for the type:
Expand Down Expand Up @@ -201,6 +289,10 @@ Supported type strings: `"mix"`, `"mixin"`, `"factory"`, and any type strings th
Add an issue in this project's [issue tracker](https://github.com/download/mics/issues)
to let me know of any problems you find, or questions you may have.
## Credits
Credits go to [Justin Fagnani](http://justinfagnani.com) for his excellent blog post ["Real" Mixins with JavaScript Classes](http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/) and the accompanying library
[mixwith.js](https://github.com/justinfagnani/mixwith.js).
## Copyright
Copyright 2017 by [Stijn de Witt](https://StijnDeWitt.com). Some rights reserved.
Expand Down
36 changes: 23 additions & 13 deletions dist/mics.cjs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ed5c1b6

Please sign in to comment.