Getting Classy with Compose

By on May 19, 2016 8:34 am

Compose

If you’re familiar with Dojo 1, you’re probably familiar with declare. Declare provides a flexible but controlled way to handle inheritance in JavaScript. It builds on JavaScript’s prototypal inheritance with OO (Object Oriented) principles and provides single and multiple inheritance. This enables developers to create flexible components and powerful mixins.

As we started working on Dojo 2, we knew that a class system was going to be an important foundational technology.

TypeScript and ES6 enter the scene

Dojo 1.0 was released in November 2007 and declare had a major overhaul in 2009 with Dojo 1.4. In internet terms, that is ancient and the world has moved on.

With Dojo 1.6 in 2010, we started introducing composition as a pattern, first with dojo/store, and later with dgrid. The idea was ahead of its time and people mostly responded with confused looks.

ES6/ES2015 was ratified in June of 2015 and introduced some significant changes. Also during that time, we had increasingly been using TypeScript on our projects, and were seeing significant advantages.

The combination of TypeScript and ES6 not only introduced classes and a significant amount of the Object Oriented (OO) style inheritance that declare had been created to provide, but also the ability to do compile time type checking on these interfaces. TypeScript allowed access to the syntactical features of ES6 and a few other new language features, but could emit code that worked in the existing ES5 environments. Because we felt these features were a significant boon to producing maintainable code, the choice was made to develop Dojo 2 using TypeScript.

If you’re not familiar with TypeScript you can read more about it here.

The Dilemma

The choice to move to TypeScript came with many benefits, but it also presented a dilemma. ES6 and TypeScript do not support multiple inheritance or mixins as they existed in declare. These powerful patterns were used in Dojo, Dijit, dstore, and dgrid. Additionally, while OO programming is a useful and widespread paradigm, it is not without its flaws. The seemingly inevitable trend, despite the best intentions, is for class hierarchies to become more and more complex, and for stronger coupling to creep into class relationships.

So while ES6 and TypeScript provided the class system, we were frustrated that TC39 had removed the original Harmony proposal for classes which provided for mixins or traits, saying that they would revisit it at some point in the future. Currently, a year on from the ratification of ES6, there is still no formal proposal for these features.

Composition with Compose

After looking at many options, the decision was made to move away from the ES6 class system, and create our own library to meet the needs of Dojo 2. dojo/compose is the result of that effort. Compose rejects the traditional OO notion of class hierarchies and takes a more functional approach. Instead, code reuse is enabled with a small API that provides the ability to create and combine, or compose, immutable factories or ‘classes’. Compose also fully utilizes TypeScript’s static type system to help ensure the correctness of your code.

The Compose API is built around a few simple but powerful methods that can be used to create factories or classes.

create

create, also usable by simply invoking the compose function itself, is the base of the compose API and provides the ability to create a new compose factory.

import compose from 'dojo-compose/compose';

interface Foo {
    foo: Function,
    bar: string,
    qat: number
}
interface FooOptions {
	foo?: Function,
	bar?: string,
	qat?: number
}

function fooInit(instance: Foo, options?: FooOptions) {
	if (options) {
		for (let key in options) {
			instance[key] = options[key]
		}
	}
}

const fooFactory = compose({
	foo: function () {
		console.log('foo');
	},
	bar: 'bar',
	qat: 1
}, fooInit);

const foo1 = fooFactory();
const foo2 = fooFactory({
	bar: 'baz'
});

foo1.foo(); // Logs 'foo'
console.log(foo1.bar); // Logs 'bar'
console.log(foo2.bar); // Logs 'baz'

extend, overlay, static, aspect, and mixin

These functions provide the ability to take an existing compose factory or factories, and meaningfully combine them with each other, ES6 classes, or objects to produce new factories. Compose embraces immutability as a means to make it easier to reason about the state of your program, so none of the API modifies existing factories, and in fact they are frozen upon creation. The code below provides a quick demonstration of these methods in action, but more detailed examples and explanation can be found in the readme


import compose from 'dojo-compose/compose';

interface Foo {
    foo: Function
}
interface FooOptions {
	foo: Function
}

function fooInit(instance: Foo, options?: FooOptions) {
	if (options) {
		for (let key in options) {
			instance[key] = options[key]
		}
	}
}

const fooFactory = compose({
	foo: function () {
		console.log('foo');
	}
}, fooInit);

const foo1 = fooFactory();
const foo2 = fooFactory({
	foo() {
		console.log('new foo');
	}
});

foo1.foo(); // Logs 'foo'
foo2.foo(); // Logs 'new foo'

// Extending an existing factory
const fooBarFactory = fooFactory.extend({
    bar: 1
});

let foobar = fooBarFactory();

const bazFactory = compose.create({
	baz: 'baz'
}, function(instance: { baz: string }) {
	instance.baz = 'initialized';
});

// Mixin an existing factory, chaining initializer functions
const fooBarBazFactory = fooBarFactory.mixin(bazFactory);

const fooBarBaz = fooBarBazFactory();
console.log(fooBarBaz.baz); // logs 'initialized'

// Overlay additional properties onto an existing factory without changing the type
const myFooFactory = fooFactory.overlay(function (proto) {
    proto.foo = 'qat';
});

const myFoo = myFooFactory();
console.log(myFoo.foo); // logs "qat"

// Add static properties to the factory itself
const staticFoo = fooFactory.static({
	doFoo(): string {
		return 'foo';
	}
});

console.log(staticFoo.doFoo()); // logs 'foo'

A different kind of composition

The name compose comes with certain connotations, since traditionally the use of composition over inheritance refers to the pattern of delegating to objects that are properties of a class or instance rather than using inheritance to share functionality. This connotation is intentional. While the API is taking factories, or ‘classes’, and ‘extending’ them with additional functionality, or ‘mixing’ them into each other, it does so in a way that is decidedly unlike traditional inheritance. The new class has no reference to the old, and there is no class hierarchy. Rather than calling super, a class must either use traditional composition to leverage another class’ functionality, or explicitly aspect the desired functionality. While this is a bit of a departure from the type of inheritance some developers may be used to, we believe that it ultimately leads to code that is easier to write, read, and maintain.

Immutability

One of the patterns that declare allowed, but we realized was a source of errors and enabled developers to shoot themselves (and others) in the foot was the use of mutable classes. The challenge with mutable classes is allowing downstream code to make changes (sometimes unintentionally) in upstream code, leading to unpredictable behavior and a highly coupled codebase.

With Compose, we do our best to support a pattern where any mutation to a class creates a new class. This means that the upstream code that depends on that class gets what it expects, helping reduce regressions and confusion.

import compose from 'dojo-compose/compose';

const createFoo = compose({
	foo: 'bar'
});

const createFooExtended = createFoo.extend({
	bar: 1
});

console.log(createFoo === createFooExtended); // logs false

Factories

While declare used constructor functions, and ES6 classes are essentially syntactic sugar for constructor functions, we debated if that made the most sense. Eric Elliott pointed out to us that constructor functions are actually the third most common way of creating instances in JavaScript:

  1. The object literal (e.g. const foo = { foo: 'bar' };)
  2. DOM Factories (e.g. const node = document.createElement('div');)
  3. Constructor Functions (e.g. const p = new Promise();)

Compose uses initializers and a “decomposed” initialization functionality, where each initializer operates on the instance as a parameter, much like a car moving down the factory assembly line. We find factories to be a more semantically meaningful way of interacting with object instantiation. Factories also hide the details of the initializer’s implementation from the consumer. The new keyword forces a new context to be created, but a factory function allows the initializer to be bound to any context. This enables the use of patterns such as object pools, and allows more functionality to be changed without requiring corresponding changes in downstream code.

Getting started with compose today

Compose is currently in beta and should not be used in production yet, but the API is not expected to undergo significant changes at this point. The easiest way to get started with compose is to install it via npm:
$ npm install dojo-compose

Alternatively the dojo/compose repository can be cloned and compose can be built locally using Grunt. If you’re using TypeScript or ES6 modules, once you’ve obtained a built version of compose you can import it and get started:

import compose from 'dojo-compose/compose';

const createFoo = compose({
    foo: 'foo'
}, (instance, options) => {
    /* do some initialization */
});

const foo = createFoo();

Learning More

If you’re interested in compose or the advantages that developing with TypeScript and ES6 can bring and want to learn more, we provide an in depth ES6 and Typescript fundamentals workshop. This workshop is aimed to get you up to speed on the most important features of ES6 and TypeScript in a short amount of time. To register, check out our workshop schedule.

You can also contact us directly to discuss how we can help your organization learn more about ES6 and TypeScript.

Comments

  • A more functional style in Dojo is just plain awesome. λ FTW.

  • Shlomy Pery

    Here are the main issues I found with the proposed solution:
    1. No TypeScript classes at all, the same “old” key/value objects are created in a factory function. You are using some functionality of TypeScript and leaving out the main usage – which is classes.
    2. The keys declared in the “class” cannot be typed – which is what we have today with dojo 1.
    3. Inheritance is really strange and complicated + it looks like it is not type safe at all.
    4. I can understand the immutibility, but what will happen if I do want to keep the state and make this a mutable “class”.

    We are using Dojo 1 at the company I am working with and I just released a solution to use ES6 classes with Dojo, here at the company. Its not 100% solution, mainly due to the fact that I need to use the declare method and export out the result – this means interfaces only – I want to work With TypeScript classes, I dont think its bad to have a readable, extendable, maintainable, type safe code.

    Will love to hear any response :)
    Thanks for sharing this…

  • Jeff M

    If you want to build a class system that supports multiple inheritance, I’m down with that. But calling it composition is a mistake. It’s revisionism to make it seem like you’re avoiding all the problems we’ve ever had with inheritance, but it’s still just as susceptible as any class system. Fragile base is still a problem you’re vulnerable to. Diamond inheritance is still a problem you’re vulnerable to. Deep hierarchies to still a problem you’re vulnerable to. Tight coupling is still a problem you’re vulnerable to. In fact you may be more vulnerable than before because you’ve tricked yourselves into believing these problems are magically solved. They’re not. (I can provide concrete examples of every one of these if you’d like.)

    It’s also revealing that you cited Eric Elliott as a source. Elliott is a snake-oil salesman. He makes a great pitch, but most everything he’s ever claimed about inheritance and composition is flat wrong.

    * He’s claimed his (and now your) style of “composition” is immune to the fragile base problem. He was wrong. https://medium.com/@w1fj151/the-wiki-page-for-fragile-base-class-gives-a-code-example-775f67a3585c#.3r5nmijad

    * He’s claimed his (and now your) style of “composition” is immune to the diamond problem. He was wrong. https://www.reddit.com/r/javascript/comments/3oy9c3/composition_vs_eric_elliott/cw20tsl

    * He’s claimed his (and now your) style of “composition” is immune to deep hierarchies. He was wrong. https://medium.com/@jeffm712/though-be-careful-not-to-think-of-stamps-as-a-silver-bullet-5b650eaf27c6#.j6sdvm9lg

    * He’s claimed his (and now your) style of “composition” has no hierarchy at all. He was wrong.https://www.reddit.com/r/javascript/comments/3oy9c3/composition_vs_eric_elliott/cw57h6n

    Bizarrely, Elliott even claims that `class A extends B {}`, even in a language like Java or C++, is *not* classical inheritance. It’s sad that I have to explicitly rebut this, but he was wrong. https://medium.com/@xwt23v/i-m-genuinely-trying-to-figure-out-where-you-draw-the-line-between-inheritance-and-what-you-call-ab11d70f7aaa#.2uc971756

    Nothing about your proposal here is composition. There *is* still a class hierarchy. It doesn’t matter whether the new class does or doesn’t have a reference to the old — for example, C++ classes have no such reference. It doesn’t matter if you have a keyword “super” or not — for example, again C++ has no such keyword. This is not a departure from traditional inheritance. Not even a little.

    Eric Elliott sold you blue water and called is medicine.

  • Shlomy Pery

    Nicely written, thanks for sharing… :)
    What do you think about the fact Dojo is moving away from ES6 class system (looks like it anyway)?

  • Jeff M

    More details:

    > The new class has no reference to the old, and there is no class hierarchy.

    Consider this C++ code:

    class A {

    virtual void a() {}

    };

    class B : public A {

    virtual void b() {}

    };

    class C : public B {

    virtual void c() {}

    };

    C cObj;

    “C” extends “B” which extends “A”. Yet there’s no reference from any class to any other, nor does the object have a reference to any class. And the object’s functions is a flattened list of the functions “a”, “b”, and “c”.

    Important takeaways:

    1) Whether or not a class has a reference to another has no bearing on whether something is “traditional” inheritance.

    2) The terms “parent-child relationship”, “hierarchy”, and even “inheritance” are **METAPHORS**. They help us conceptualize when one type includes the behavior and structure of another type. We metaphorically say the type that gives behaviors is the parent, and we metaphorically say the type that receives behaviors is the child. When we want to visualize this behavior, we often draw metaphorical arrows from one class to another. But again, those arrows often don’t represent literal pointers or references. They’re metaphors only. Their purpose is to help us conceptualize what’s happening, not to represent a literal implementation.

    So when you do `const fooBarFactory = fooFactory.extend({`, that’s traditional inheritance, that’s a hierarchy, that’s a parent-child relationship, because each of those descriptions is simply a metaphor to help us conceptualize that one type (fooBar) includes the behavior of another (foo).

  • Bradley Maier

    When I referred to ‘traditional’ inheritance and a class hierarchy, what I was talking about specifically is the use of super calls, such as this.inherited in declare. The pattern in some forms of inheritance that we found to be troublesome was the ability to end up with a deep chain of calls to super. The lack of hierarchy that I’m referring to is the fact that when you extend one class or factory with another, you will be overwriting what’s there. This makes it more difficult to end up with a single method that travels up several levels in a class hierarchy. In that sense, this is a departure from what a lot of people are used to with traditional inheritance that they’ll encounter in Java or with ES6 classes. And of course the lack of support for mixins as they existed in declare was another factor that pushed us away from ES6 classes.

    You are of course correct that this is still a form of inheritance, that’s why the methods are called things like extend and mixin, so maybe the terminology used in the post isn’t as clear as it could be.

    I cited Eric Elliot as a source for the claim that the new keyword is not actually the most common way objects are created in JavaScript, and it’s not meant as a wholesale endorsement of his claims about stampit and inheritance in general.

    It’s our hope that compose will enable the use of single and multiple inheritance, but in a way that discourages deeply nested hierarchies, and as a result encourages the use of composition over inheritance where it makes sense.

  • Jeff M

    > When I referred to ‘traditional’ inheritance and a class hierarchy, what I was talking about specifically is the use of super calls

    Super calls are still just as easy with your proposal, and those super calls would be done in exactly the same way that they’re already done in C++… by explicitly naming the function to call. Lacking a “super” keyword is not a break from traditional inheritance, and this is still a hierarchy.

    > You are of course correct that this is still a form of inheritance, that’s why the methods are called things like extend and mixin, so maybe the terminology used in the post isn’t as clear as it could be.

    The terminology in the entire *library* isn’t as clear as it could be. You shouldn’t be calling this composition. People will be tricked (not an exaggeration) into thinking they’re following the “favor composition” rule, and that they’re inherently avoiding all the problems of inheritance, when in fact they’re not. If you (and others like Elliott) continue to hijack the word “composition”, then people will make all the same inheritance mistakes all over again due to a false sense of safety, and we’ll have to re-introduce the GoF rule all over again but rephrased as “favor containment over dojo-compose”, because of course dojo-compose is still inheritance.

  • Kitson P. Kelly

    1. I would argue that the main usage of TypeScript is not ES6 Classes. For TypeScript (and ES6), `class` is simply syntactic sugar for constructor function with automatic error throwing when calling the function without the `new` keyword. The main usage of TypeScript is really the interfaces and typing. The ES6 `class` specification has several challenges, like only allowing single inheritance. Also it doesn’t do away with the challenge of a complex prototype chain, which means if large ancestry trees, which is something we ran into with dojo.declare, you can spend huge amounts of time just looking up properties in a 27 deep chain. Also, Harmony originally specified the concept of mixins/traits, but it was dropped from Harmony due to the need to get something people could agree on. TC39 has indicated they will revisit this in the future, but there is currently no proposal at all in the TC39 pipeline on this. We championed for mixins/traits to be added to TypeScript but they declined for fear of not being forward compatible with ES specifications.
    2. Not sure what you mean by lack of typing (or that we ever got typed properties in dojo.declare). Compose was specifically written to take advantage of the TypeScript typing system. Most situations the typing can be contextually inferred, but where it is challenging, the types can be asserted.
    3. It is different, but large parts of the API were dictated to keep it working with TypeScripts type system, to make it type safe.
    4. What would be a use case of where you would want the factory, or the prototype of the factory to be mutable?

  • Kitson P. Kelly

    Thanks for all your thoughts and feedback. I am sorry you feel so strongly we mis-appropriated the composition metaphor. The crux of it seems to point to that we included `.extend()` as part of the API and that, to you, implies “traditional inheritance” versus “compostability”. Which I would agree. We included it, because we were looking at the functional use cases for Dojo 2.

    In fact that is how the whole library was developed. It wasn’t to be opinionated about “this is the right way to do classes” or to transform the world. It was done to make it work for what we need to build Dojo 2 and to avoid a lot of issues we perceived we had with dojo.declare.

    For example, we included AOP advice. We could have also have created a library that only allowed new classes to be constructed from an array of traits, therefore following a more strict view of compostability. We had several challenges with that. First it was impossible to uniquely extract the types from an array with TypeScript currently (there is no rest generic operator, though we have been one of the groups championing that with the TypeScript team), also, it seems like an artificial constraint.

    As I mentioned in my repose to Shlomy, the default answer would have been The ES6 `class` but we ran into some challenges with that that prevented it from working in a way we were happy. So we realised we needed something else that worked well with TypeScript and created classes in the way we like to create classes.

    I personally don’t think our implementation of a “.mixin()” API misappropriates the metaphor of compostability. Just because you never start a class without some sort of base that gets chained to result in the final class doesn’t negate the metaphor. Yes, “.extend()” is “traditional inheritance” and possibly we should consider dropping it. On the other hand, we don’t think we have to wrest 100% of those metaphors out of peoples hands in order to have a valid concept.

  • Jeff M

    Your `.extend()` method has nothing to do with it (I don’t recall ever even mentioning it).

    The reason your article gives for considering this (compose / compose.create) unlike traditional inheritance (and thereby worthy of being called composition) is because the new class has no reference to the old, and there is no “super”. I brought up C++ because I presume we can all agree that C++ classes and inheritance are traditional, and yet C++ classes have no reference from one to another, nor do objects have references to their classes, nor is there a “super” keyword, and a derived object’s functions is a flattened list. So what you thought was unlike traditional inheritance is actually *exactly* like traditional inheritance and not remotely composition.

    Misusing the word “composition” is going to have real consequences. The rule “favor composition over inheritance” is almost universally known. Your library does inheritance, but if the library lies to its user and claims it does composition, then there are going to be lots of poor uses of inheritance because users won’t even realize they’re using inheritance at all; they’ll believe they’re using composition when in fact they’re not.

  • W.S. Hager

    I’m relieved that this is finally settled and I’m happy with this choice. Kudos

  • W.S. Hager

    Perhaps you could explain what you mean by ‘composition’, as opposed to inheritance. Imo, if a subtype is created that only has a reference to a supertype by way of closure instead of a hierarchical chain of command, than it is composition. There’s no way to reference up the hierarchy, which may lead to more predictable behavior, but I may be mistaken of course.

  • Jeff M

    Certainly.

    First what I mean by inheritance, and this comes almost verbatim from the UML manual:

    Inheritance is the process of combining state and behavior from multiple types into a more specific type. Details such as method lookup algorithms or virtual tables or prototype chains are merely implementation details to make it work in a particular language, not part of the essential definition.

    Whether there’s a way to reference up the hierarchy is also an implementation detail and not a defining characteristic of inheritance. For example, as I’ve mentioned before, C++ has no “super” keyword.

    And second what I mean by composition:

    Composition is containing an object reference as a value in another object’s attributes.

    This is the nearly universally accepted meaning of composition, especially when we’re talking in the context of vs inheritance. On MDN, for example, composition is described as letting class instances be the values of other objects’ attributes (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript ). A description that exactly matches this Python article on composition vs inheritance (http://learnpythonthehardway.org/book/ex44.html ). Which also exactly matches this ActionScript article on composition (http://www.adobe.com/devnet/actionscript/learning/oop-concepts/composition-and-aggregation.html ). Which also exactly matches this C++ article on composition vs inheritance (http://www.artima.com/cppsource/codestandards3.html ). Which also exactly matches the Wikipedia entry for object composition (https://en.wikipedia.org/wiki/Object_composition ). Which also exactly matches the GoF Design Patterns description of object composition (“objects acquiring references to other objects”).

    This is what it means to almost everyone everywhere, except for the few folks who had the bad luck to stumble upon an Eric Elliott article and learn some very bad information.

    The process being described in this dojo article isn’t composition, it’s not even close. It’s actually plain old inheritance, the very thing we’re supposed to avoid to favor of actual composition.

  • Pingback: | Connect.Tech 2016 recap | Blog | SitePen()

  • Pingback: | Mixins and more in TypeScript 2.2 | Blog | SitePen()