Search code examples
javascriptclassecmascript-6mixinscomposition

Accessors Composition in ES6 Classes


Let's say I have a Thing class which I want to be both Hideable and Openable.

Using a similar approach to Douglas Crockford's object creation through composition, I have been able to "inherit" from multiple classes.

This approach does not work with accessors (getter/setters).

I need to use classes as it's a requirement. I'm also finding that I am duplicating functionality from class to class, but I don't want these to inherit from a base class.

Any ideas?

The progress I have made so far is in the below snippet:

class Openable {

  constructor(isOpen = false) {
    this._isOpen = isOpen;
  }

  get isOpen() {
    return this._isOpen + ' is stupid.';
  }

  set isOpen(value) {
    this._isOpen = value;
  }

}


class Hideable {

  constructor(isHidden = false) {
    this._isHidden = isHidden;
  }

  get isHidden() {
    return this._isHidden + ' is stupid.';
  }

  set isHidden(value) {
    this._isHidden = value;
  }

}


class Thing {

  constructor(config) {
    let { isOpen, isHidden } = config;

    let openable = new Openable(isOpen);
    this.isOpen = openable.isOpen;

    let hideable = new Hideable(isHidden);
    this.isHidden = openable.isHidden;
  }

}


let thing = new Thing({
  isOpen: true,
  isHidden: false
});


Solution

  • Because isOpen and isHidden are accessors, you can't just grab a copy of them, you have to access them when you want them.

    Still, you can create your own isOpen, isHidden which use the underlying ones:

    let openable = new Openable(isOpen);
    Object.defineProperty(this, "isOpen", {
        get: () => openable.isOpen,
        set: value => {
            openable.isOpen = value;
        }
    });
    
    let hideable = new Hideable(isHidden);
    Object.defineProperty(this, "isHidden", {
        get: () => hideable.isHidden,
        set: value => {
            hideable.isHidden = value;
        }
    });
    

    Live example on Babel's REPL

    Naturally, if you do this a lot, you'd want to have a worker function to set that up rather than retyping it all the time:

    function wrapProperty(dest, src, name) {
        Object.defineProperty(dest, name, {
            get: () => src[name],
            set: value => { src[name] = value; }
        });
    }
    

    (or do it by grabbing the property descriptor and updating it)

    then:

    wrapProperty(this, openable, "isOpen");
    wrapProperty(this, hideable, "isHidden");
    

    I'd question the requirement that you must use class for Openable and Hideable. They look much more like mixins to me.