The Composable class can provide your JavaScript framework or application with a flexible mixin architecture. Composable takes the form of a general-purpose factory for composing classes and objects from mixins. You can configure its behavior through composition rules: functions that run at class creation time to resolve conflicts in property or method names. This architecture lets you achieve a good separation of concerns, and address scenarios which are difficult to solve with classical inheritance.
Features:
- Focuses exclusively on composing behavior, independent of any particular framework. Composable can be used in a wide variety of situations.
- Leverages the JavaScript prototype chain as a unifying data structure. This provides a solid basis for explaining how mixins are composed together, and takes advantage of native language features.
- Supports both implicit and and explicit means of resolving conflicts in property and method names. Implicit resolutions provide good default behavior, and explicit resolutions let you accommodate novel situations.
- Designed with forthcoming JavaScript features in mind, including the
class
keyword in ES6 and decorators in ES7. All features are also available in plain ES5.
The Composable class can be installed from this repository via npm. Composable
can then be imported as an ES6 module from /src/Composable.js
. An ES5-ready
version can be included from /dist/Composable.js
.
Many JavaScript frameworks include a mixin facility in their class factory, but the behavior of the resulting composed class varies from framework to framework. A critical question is how the framework resolves conflicts in the names of properties or methods supplied by mixins. Consider how you might create a class with mixins in a typical framework:
let mixin1 = {
foo() {
// Do mixin 1 work here...
return "mixin1";
}
};
let mixin2 = {
foo() {
// Do mixin2 work here...
return "mixin2";
}
};
let MyClass = TypicalFramework.createClass({
foo() {
// Do base class work here...
return "MyClass";
}
mixins: [mixin1, mixin2]
});
let instance = new MyClass();
instance.foo(); // Returns... what? And what work got performed?
The base class and the two mixins all define a method called foo()
, but we
don't know what will happen when we invoke that method:
-
Depending on the framework, the invocation of
instance.foo()
might invoke all thefoo
implementations, or perhaps just one. If it just invokes one, that might bemixin2.foo()
, because that was the "last writer", or it might beMyClass.foo()
, because that's the base class and perhaps more important. -
We have to wonder not only about what work will be performed, but also about what result will be returned. The answer will vary from framework to framework.
-
The answer may also depend on the specific name of the method in conflict. Some frameworks apply one conflict-resolution strategy to methods with special names (for example, component lifecycle methods), and a different strategy to other methods.
-
We also have to wonder about what other side effects this framework's
createClass()
factory performs. Most frameworks will do more work than just applying mixins. That black-box approach makes class creation feel magic, hampers understanding, and may complicate debugging.
As a quick review, every object in JavaScript has a prototype chain: a linked list of prototypes that define the properties and methods available on that object. The following code defines a simple class hierarchy:
class BaseClass {
foo() {
return "BaseClass";
}
}
class Subclass extends BaseClass {
foo() {
return "Subclass";
}
}
let instance = new Subclass();
instance.foo(); // Returns "Subclass"
This code creates the following prototype chain:
instance → Subclass → BaseClass → Object
To execute the foo()
method on the object instance
, the JavaScript engine
starts at the head of this chain (instance
) and walking down the chain (here,
to the right) until it finds an object that has a foo
method. In this case,
the first foo
implementation belongs to the prototype for Subclass.
Therefore, when we execute instance.foo()
, it will be Subclass' implementation
that is invoked. Although BaseClass also has an implementation of foo
, it's
effectively obscured or overridden by the presence of Subclass' foo()
method.
That behavior is inherent in the JavaScript prototype chain, and cannot be
changed. When working with mixins, however, such behavior is often undesirable.
A mixin may want to augment the behavior of a base class. In the context of
the example above, we may want a way to run both the foo()
implementations
of Subclass and BaseClass.
One standard way to solve this problem in ES6 is to use the super
keyword,
which lets a class invoke a method implementation further up the prototype chain.
However, the use of super
in a mixin is tricky because mixins don't know
ahead of time what base class they will be extending.
- An ES6-to-ES5 transpiler, like Babel for example, will
attempt to translate a
super
call into a hard-coded reference to a base class. That won't work if the mixin doesn't have a base class at compile time. - A mixin written for native ES6 still needs to know whether it should invoke
super
or not. The mixin may have been applied to a base class that doesn't include the property or method defined by the mixin, in which case invokingsuper
will throw an exception. - Property getters and setters are particularly tricky to implement in a mixin. If a mixin supplies a getter but fails to define a setter, for example, the JavaScript engine will conclude there is no setter available further along the prototype chain.
The Composable class attempts to provide a firmer foundation for working with mixins. A very basic example:
import Composable from 'path/to/Composable.js';
class MyClass {
foo() {
console.log("Hello");
return "MyClass";
}
}
let mixin = {
foo() {
console.log("World");
return "mixin";
}
};
let MyClassWithMixin = Composable.compose.call(MyClass, mixin);
let instance = new MyClassWithMixin();
// By default, composed methods are invoked base first, then mixin.
instance.foo(); // Writes "Hello" then "World". Returns "mixin".
The Composable.compose
method does nothing but compose behavior — it has no
side effects, and it does not destructively modify any classes. It just returns
a new class with the desired behavior. It has no relation to any specific
framework, and in fact this approach could be made to work with many frameworks.
The compose
method works by extending a JavaScript prototype chain with
a copy of the given mixin(s). The original base MyClass and the original mixin
are left untouched, but a new class is created that extends the chain:
instance → MyClassWithMixin → MyClass → Object
The prototype chain is still working the way it always does in JavaScript: when
asked to invoke foo()
, the language engine will start at the head of the chain
and walk down the chain until it finds a foo
implementation. All that's
happened here is that Composable has composed the two foo
implementations
here to create a single function that invokes both. Composable then adds that
composed function to the prototype for MyClassWithMixin.
So invoking instance.foo()
has the effect of invoking MyClass.foo()
, then
mixin.foo()
, and getting back the result of the latter. That composition is
just the default behavior of Composable, but as we'll see shortly, other
composition behaviors are easy to arrange for.
Before diving into the details of how Composable works, it's worth pointing out
that Composable works with any prototype chain. The starting point you give
compose
can be either a base class or a plain object. Similarly, the mixin(s)
you give compose
can be classes or objects.
So the above example could also have been written:
class MyClass {
...
}
class Mixin {
...
}
let MyClassWithMixin = Composable.compose.call(MyClass, Mixin);
If you ask compose
to compose a base class with a mixin class, it will not
only extend the prototype chain as described above, but it will also copy any
static methods from the mixin class to the new subclass.
The remainder of this documentation will define mixins as classes. Whether you
decide to do that is a matter of taste. One advantage of defining mixins as
classes is that it makes the mixin a first-class object. You could, for example,
use new Mixin()
to instantiate the mixin class and get a new object. The
class
syntax also conveys an intention to package behavior.
In the above example, Composable resolved a conflict between a mixin and base
class method that both had the name foo
. To do this, Composable applied a
default composition rule for resolving such a conflict. Let's look at how this
rule was applied, and how you can configure Composable's behavior for different
situations.
The Composable class does its critical work at the moment it adds a new mixin
to a prototype chain. In the example above, at the point a copy of mixin
is
added to the chain, there are two foo()
methods along the chain:
MyClassWithMixin → MyClass → Object
Mixin.foo() MyClass.foo()
We can generalize this by just thinking about the mixin being composed in as the head of a prototype chain, and the set of base classes along the existing chain as constituting the tail. In the abstract:
New prototype at Existing prototype chain becomes
head of chain: the tail of the new chain:
Mixin → Base1 → Base2 → Base3 → ... → Object
foo() foo()
When adding a new head to the chain, Composable inspects all the properties on
the new head (that is, on the mixin being composed in). For each property it
finds, like foo
, Composable looks at the chain's tail to see if there's a
identically-named foo
property anywhere along it. The exact point along the
tail where the property exists is immaterial. All that matters is whether the
mixin's implementation at the head of the prototype chain will obscure that base
implementation. If it will, then a composition rule can be used to reconcile the
two.
Rather than applying a hard-coded conflict resolution, Composable applies
developer-configured and default composition rules to resolve the conflict. If
no custom composition rules are in effect, the default rule will be to compose
the two foo()
methods together using a rule called baseMethodFirst
:
MyClassWithMixin → MyClass → Object
composed foo() MyClass.foo()
calls MyClass.foo()
then Mixin.foo()
Calling foo()
on an instance of MyClassWithMixin will invoke the composed
foo()
function. As its name implies, the baseMethodFirst
rule will invoke
the base MyClass.foo()
implementation first, then the mixin.foo()
implementation, and finally return the result of the latter.
The baseMethodFirst
rule is Composable's default composition rule for
resolving method name conflicts, but other rules are available, and new ones can
be created.
When Composable applies a composition rule, the rule only ever effects the prototype of the new class or object being created. The rule never affects anything along the tail of the prototype chain. So it is, in principle, safe to use Composable with base classes created by manually or with other frameworks, and still get predictable results.
The standard composition rules are:
-
baseMethodFirst
: Invokes the base method first, then invokes the mixin method, and return the mixin's result as the overall result. This is the default rule for methods discussed above. -
baseSetterFirst
: This is the default rule for composing getter/setter properties. For getters, the mixin's getter will override the base getter (as usual in JavaScript). For setters, the base setter will be invoked first, then the mixin's setter. -
chainPrototypes
. This is used to compose object-valued properties. This rule sets the prototype of the object value of the mixin property to point to the object value of the base property. The effect of this is to perform a shallow merge of the two objects, without copying over values. Compare withshallowMerge
, which does copy over values. -
override
: The mixin's implementation overrides the base implementation. This is the standard JavaScript behavior for all object members. For classes created with Composable, this is the default behavior only for scalar- or plain object-valued properties. -
preferBaseGetter
: Invokes the base getter first. If that returns a truthy value, that is returned immediately. Otherwise, the result of the mixin getter is returned. Setters are invoked base first, then mixin. -
preferBaseResult
: This rule invokes the base method first. If that returns a truthy value, that is returned immediately. Otherwise, the mixin method is invoked, and its result is returned. -
preferMixinGetter
: Invokes the mixin getter first. If that returns a truthy value, that is returned immediately. Otherwise, the result of the base getter is returned. Setters are invoked base first, then mixin. -
preferMixinResult
: The converse of the above rule. This rule invokes the mixin method first. If that returns a truthy value, that is returned immediately. Otherwise, the base method is invoked, and its result is returned. -
shallowMerge
: Compose object-valued properties by copying the members of the mixin value over the members of base value. Compare withchainPrototypes
, which has the same effect, but chains prototypes rather than copying values.
You can define other rules for your application or framework, as discussed further on.
One concise way to describe and think about specifying composition rules is with ES7 decorators. Language support for decorators is still being designed, but the core idea is that annotations on object methods and properties can cause functions called decorators to run. These are able to retroactively edit the definition for the property or method in question.
While decorators are not yet a standard JavaScript feature, they nevertheless provide a good way of thinking about how you can specify a composition rule. Using decorators, you can override Composable's default composition rules.
For example, if you have a mixin that wants to defer to a base implementation if one exists, you could write:
class MyClass {
get foo() { return this._foo; }
set foo(value) { this._foo = value; }
}
class Mixin {
// Provide a default value for the foo property.
@Composable.rule(Composable.rules.preferBaseGetter)
foo() { return "Mixin"; }
}
let MyClassWithMixin = Composable.compose.call(MyClass, Mixin);
let instance = new MyClassWithMixin();
instance.foo // Returns "Mixin", since base value is initially undefined
instance.foo = "Hello";
instance.foo // Returns "Hello", since base value is now defined
Because the preferBaseGetter
rule was specified with the @Composable.rule
decorator, the Composable class resolved the name conflict using that rule.
Why is that rule useful? Here, the base MyClass defines a property foo that may
or may not be set. The mixin wants to provide a default value for that property
that is only used if the property value is currently undefined.
Such rules are particularly valuable when composing mixins on top of a wide variety of base classes. Some of those base classes may implement a method in the mixin, while others may not. This level of flexility goes beyond what's possible in classical inheritance, where a given subclass can only extend the behavior of a single inheritance chain. In contrast, mixins defined using Composable can be applied to multiple base classes.
Until the @decorator
syntax becomes widely available, you can also use an
helper function called Composable.decorate()
to manually apply decorators in
ES6/5:
class Mixin {
foo() { return "Mixin"; }
}
Composable.decorate.call(Mixin.prototype, {
foo: Composable.rule(Composable.rules.preferBaseGetter)
});
This has the same effect as the @Composable.rule
syntax shown above. Note
that you can use the Composable.decorate()
helper function to apply any
function with the decorator signature, not just composition rules.
To create your own composition rule, define a function that has the decorator signature:
function decorator(target, key, descriptor) {}
where
target
is the head of the prototype chainkey
is the name of the method both base and mixin sharedescriptor
is the property descriptor for the method
For example, the implementation of preferBaseResult
(like the earlier
preferBaseGetter
, but for methods) is:
// Standard rule: return base's result if truthy, otherwise mixin result.
Composable.rules.preferBaseResult = (target, key, descriptor) => {
let mixinImplementation = descriptor.value;
// Use a helper to find the base implementation up the chain.
let baseDescriptor = Composable.rules.getBaseDescriptor(target, key);
let baseImplementation = baseDescriptor.value;
// Compose a function to run base first, then mixin.
// Use a plain function() because we don't want to bind to current 'this'.
descriptor.value = function() {
return baseImplementation.apply(this, arguments)
|| mixinImplementation.apply(this, arguments);
}
};
Note that your composition rule will only be invoked if a name conflict actually exists. If someone applies your rule to a property, but no naming conflict exists, the rule will not be invoked.
Because Composable works directly on JavaScript prototype chains, you immediately get a great deal of flexibility in how you can use it. This section presents a sampling of ideas to explore.
If you anticipate people wanting to extend your classes with mixins, you can do that in several ways. The simplest is to derive your base class from Composable:
class MyBaseClass extends Composable {
foo() { return "MyBaseClass"; }
}
class Mixin {
foo() { return "Mixin"; }
}
let MyClass = MyBaseClass.compose(Mixin);
let instance = new MyClass();
instance.foo(); // returns "Mixin"
The prototype chain looks like this:
instance → MyClass → MyBaseClass → Composable → Object
This means that MyBaseClass gains Composable's compose
method. This allows
the more concise MyBaseClass.compose(...)
syntax instead of the more verbose
syntax, Composable.compose.call(MyBaseClass, ...)
.
Composable will happily mix anything into a prototype chain — including itself. This is useful if you're working with a base class defined elsewhere and can't, or don't want to, make that particular base class composable. Suppose you are working with a Thing class defined elsewhere, and you want to create a composable variant of Thing that supports mixins:
import Thing from 'some/external/dependency.js';
// Use Composable to add a copy of *itself* to a prototype chain.
let ComposableThing = Composable.compose.call(Thing, Composable);
// ComposableThing now embodies composable Thing objects.
// You can now use that to compose new classes using mixins.
class Mixin { ... }
class ThingSubclass extends ComposableThing.compose(Mixin) {}
The resulting prototype chain looks like:
ThingSubclass → Mixin → ComposableThing → Thing → Object
In most of the above examples, the output of compose
is a new class. For
example, a class MyClass is composed with Mixin to create MyClassWithMixin.
class MyClass extends Composable {
foo() { return "MyClass"; }
}
class Mixin = {
foo() { return "Mixin"; }
}
let MyClassWithMixin = MyClass.compose(Mixin);
But maybe you don't want to expose both MyClass and MyClassWithMixin. It's easy enough to redefine a class to include a mixin:
class MyClass extends Composable {
foo() { return "MyClass"; }
}
class Mixin = {
foo() { return "Mixin"; }
}
MyClass = MyClass.compose(Mixin); // Redefine MyClass to include Mixin
let instance = new MyClass();
instance.foo(); // Returns "Mixin"
This creates the prototype chain:
instance → MyClass (includes Mixin) → (original MyClass) → Object
The ES6 class
syntax allows you to provide an expression as base class
for extends
. Depending on what result you want to achieve, this can be
a concise way to compose mixins:
class Mixin = {
foo() { return "Mixin"; }
}
class MyClass extends Composable.compose(Mixin) { // Base is an expression
foo() { return "MyClass"; }
}
let instance = new MyClass();
instance.foo(); // Returns "MyClass", since MyClass is at head of the chain
Because the base class for extends
is Composable.compose(Mixin)
, we get
the following prototype chain:
instance → MyClass → (copy of Mixin) → Composable → Object
Compared to the previous example, the positions of MyClass and Mixin are reversed. This is because here Mixin was composed into the starting point for MyClass, rather than composed into MyClass afterwards.
Composable's compose
method can take any number of arguments. These arguments
will be composed onto the head of the prototype chain in the order the arguments
are supplied.
class Mixin1 {}
class Mixin2 {}
class Mixin3 {}
class MyClass extends Composable {}
let MyClassWithMixins = MyClass.compose(Mixin1, Mixin2, Mixin3);
This creates the prototype chain:
MyClassWithMixins (Mixin3) → Mixin2 → Mixin1 → Object
Most of the examples in this document use ES6's class
syntax, but you can also
use Composable as a general-purpose class factory in ES5:
var mixin = {
foo() { return "mixin"; }
}
var MyClass = Composable.compose.call(
Object,
{
foo() { return "MyClass" }
},
Mixin
);
var instance = new MyClass();
instance.foo() // Returns "mixin"
The compose
call above says: use Object
as the base class, add in a foo()
method, and then apply a mixin that also has the same method.
As one example of how you could use Composable to compose complex behaviors from mixins, let's take a quick look at creating a custom HTML element using Composable and the Custom Elements API.
// Create a general-purpose element base class that supports composition.
let ComposableElement = Composable.compose.call(HTMLElement, Composable);
// A mixin that sets an element's text content.
class HelloMixin {
createdCallback() {
this.textContent = "Hello, world!";
}
}
// A sample element class that uses the above mixin.
let HelloElement = ComposableElement.compose(HelloMixin);
// Register the sample element class with the browser.
document.registerElement('hello-element', HelloElement);
// Create an instance of our new element class.
let element = document.createElement('hello-element');
document.body.appendChild(element); // "Hello, world!"
When we're done, we've constructed the following prototype chain:
element → HelloElement → HelloMixin → ComposableElement → HTMLElement → Object
So element
is an instance of HelloElement composed of behaviors from
HTMLElement (so it can render), a ComposableElement (so mixins can be applied),
HelloMixin (to set its text content).
As discussed earlier, the critical moment of composition comes when a new mixin is being added to an existing prototype chain. Both sides of that operation can influence composition. The examples of composition rules above have shown rules being defined on the mixin. This is the most common case, but Composable also allows composition rules to be defined on the prototype chain. Composable allows a base class to define inheritable composition rules that are consulted whenever that prototype chain is extended.
This is an advanced use of Composable that may be appropriate for application
frameworks. Suppose we have a framework where many classes inherit from a class
called Base. All classes that inherit from Base can define a set of string
messages as a plain object-valued property called messages
. Normally such a
property would be overridden in a subclass, but the Base class wants to define
the default composition behavior is to merge the messages
property.
Rather than using @decorators for this purpose, a base class can define
a compositionRules
dictionary that defines default composition rules.
class Base extends Composable {}
// Default messages
Base.prototype.messages = {
greeting: "Hello",
parting: "Goodbye"
};
// Define a default composition rule for the messages property.
Base.prototype.compositionRules = {
messages: Composition.rules.shallowMerge
};
let mixin = {
messages: {
greeting: "Have a nice day"
}
};
let Subclass = Base.compose(mixin);
let instance = new Subclass();
instance.messages // { greeting: "Have a nice day", parting: "Goodbye" }
The Base class here defines default composition rules that are inherited down
the prototype chain. When Base.compose()
is invoked, these default rules are
consulted, so the Mixin class doesn't completely override the Base messages
.
Instead the Mixin's messages
and the Base messages
are merged.
-
The
compositionRules
dictionary is itself composed along the prototype chain, so that different classes/mixins can contribute default composition rules. -
When composing a mixin onto a prototype chain, any composition rules defined on the mixin supersede default composition rules defined along the prototype chain.
The ability to define default composition rules lets a framework adopt Composable as a mixin architecture, while preserving any special rules the framework would like to impose regarding mixin composition.
The presence of a class
keyword or class A extends B
syntax in these
examples might be taken as an indication of classical single inheritance. That
might be problem for people who believe that composition of behavior (such as
mixins) is superior to classical inheritance. They may feel that inheritance
leads to complex, brittle systems. Whether or not that claim is true, it's
important to note that what's going on in the examples above is not classical
single inheritance.
People from backgrounds in traditional class-oriented languages think of JavaScript's prototype chain as synonymous with inheritance, but that's just one way to conceptualize JavaScript. The prototype chain is just a dynamic linked list — how you want to use it is up to you.
With Composable, we can compose behaviors in arbitrary combinations and orders not possible with classical single inheritance. Among other things, this means we can apply the same mixin at different points in the "class hierarchy" to create results that cannot be achieved with strict inheritance.
Suppose we have two classes, Base1 and Base2, which share no common ancestor (aside from Object). We can extend both of those classes with the same mixin:
class Base1 {}
class Base2 {}
class Mixin {
foo() {
return "foo";
}
}
let ExtendedBase1 = Composable.compose.call(Base1, Mixin);
let ExtendedBase2 = Composable.compose.call(Base2, Mixin);
let obj1 = new ExtendedBase1();
let obj2 = new ExtendedBase2();
We now have a copy of the Mixin behavior along two different prototype chains:
obj1 → ExtendedBase1 (copy of Mixin) → Base1 → Object
obj2 → ExtendedBase2 (copy of Mixin) → Base2 → Object
Such a structure is not possible in classical single inheritance. Even though obj1 and obj2 share no ancestor but Object, we can apply the same method to both:
obj1.foo(); // "foo"
obj2.foo(); // "foo" also
This form of composition allows an enormous degree of flexibility in factoring code. Each distinct behavior can be packaged as a mixin object or class, and then combined in many ways to create a desired set of instantiable classes. This permits a good separation of concerns. At the same time, capitalizing on the native nature of the JavaScript prototype chain allows your codebase to minimize the number of new concepts that must mastered by future maintainers.