Search code examples
javascriptmultiple-inheritance

Composition and Mixins with JavaScript


I am learning about compositions in Javascript. So I want to ask if this is correct way of doing things.

I made some exercises that look like this:

    class Animal {
    // constructor() {
    // }
    eat = () => {
        console.log("this creature is eating");
    }
}

const AnimalsWithWings = superclass => class extends superclass {
    constructor(Feathers, ...args) {
        super(...args);
        Object.assign(this, { Feathers });
    }
}

const CanDive = superclass => class extends superclass {
    // constructor( ...args) {
    //     super(...args);
    // }
    dive = () => {
        console.log("Can Dive");
    }
}

class Duck extends AnimalsWithWings(CanDive(Animal)) {
    constructor(eats, ...args) {
        super(...args);
        Object.assign(this, { eats });
    }
}

const duffy = new Duck("watermelon", true);
console.log(duffy);

duffy.dive();
duffy.eat()

I am still in learning process so I just need some pointers.


Solution

  • Did it do more or less what you expected? Then, sure, it's a correct way to do it, whatever "correct" means here.

    It looks to me like it does what it was meant to do when I pop it into the console. I can't really say much more on your code specifically because I'm not sure what concrete domain it's trying to model, aside from maybe breaking down Ducks into atomic pieces.

    If you're going to do it this way, though, I'd personally prefer to use a params object instead of just changing the constructor signature like that with AnimalsWithWings. That way, the order of extra parametrizations doesn't depend on the order in which the mixins were applied, which I would consider a Surprise. Surprises are bad.

    const AnimalsWithWings = superclass => class extends superclass {
        // Everyone receives the same `params` object.
        // They only take what they know about, and ignore the rest.
        constructor(params) {
            super(params);
            Object.assign(this, { Feathers: params.Feathers });
        }
    }
    

    Even more personal opiniony, I'd name them WithDiving and WithWings instead, just to keep a somewhat consistent naming scheme, and to better imply that these are modifiers, not "real" base classes.

    Your code does saddle every Duck with a prototype chain 4 prototypes long, but eh, whatever. If it somehow becomes a performance problem then you can create a utility function to optimize the mixin process or something. Flatten the prototypes, maybe.

    Your code does also let you call super.method() in methods, though it's debatable whether you should ever use that in a mixin at all. I'd say you shouldn't, unless you want your mixins to implicitly depend on each other, which is a Surprise.

    There are plenty of other ways of doing mixins, too.

    • You could create a utility function to flatten all the prototypes into a single new one and return a base class from that which you extend. (Just be sure to iterate property descriptors rather than just using Object.assign() when doing that flattening, if you want to properly handle things like get/set accessors, etc.)
    • You could eschew Classes and just directly create prototype objects and use Object.create() to create instances. (same thing about iterating property descriptors.)
    • You could create a Duck prototype using a bunch of iterative calls to Object.create() instead of iteratively extending base classes.
    • You could control the additional behaviors with helper Controller Classes instead of composing behavior directly into the base.
    • You could deal just in plain objects with data, and pass the objects to functions that expect the object to have certain properties on it in order to do things. (Amusingly, called "duck typing") I'll grant that's not really mixins, just calling functions, but if it accomplishes the same thing in effect...
    • Probably a bunch others I can't really think about at the moment. It's all sticking sets of behaviors onto some base thing.