0

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
});
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
evolutionxbox
  • 3,932
  • 6
  • 34
  • 51
  • How can a thing be both hidden and open? – Bergi Jan 28 '16 at 15:01
  • It can't. Sorry, it's a quick demo – evolutionxbox Jan 28 '16 at 15:02
  • 1
    You shouldn't use classes for this. Period. If your spec doesn't allow to use mixins, throw it away. The closest thing that could help you: http://stackoverflow.com/questions/29879267/es6-class-multiple-inheritance – Microfed Jan 28 '16 at 15:05
  • It's almost like classes are not fit for purpose in JS? – evolutionxbox Jan 28 '16 at 15:07
  • Well, I can't even think of an instance of the Openable *class*. What does it mean? It's just a variable and two methods: setter & getter. It doesn't mean anything without some object, which could be either open or not open. So, it is just an interface or a mixin. It's just a set of variables and methods which represent a state of an external object. For this purpose we should use mixins. In C++ you'd use multiple inheritance with *abstract* classes, or *interfaces*. So it's not just a Javascript, it's the whole object-oriented logic. :) – Microfed Jan 28 '16 at 15:18
  • Personally I prefer class free OOP. Like, where functionality and behaviour are inherited. So openable is neither a mixin nor a interface. Remember that JavaScript is not C++, is not a classical OOP language and doesn't need to follow those conventions. – evolutionxbox Jan 28 '16 at 15:21
  • 1
    "mixin" and "interface" are just a buzzwords. There is nothing like this in ECMA-262. But it's a convenient way of dealing with patterns while having a conversation. – Microfed Jan 28 '16 at 15:25
  • @evolutionxbox: The issue is that JavaScript only allows inheriting from a single prototype chain. So unless `Openable` and `Hideable` are related (an `Openable` is a `Hideable` or vice-versa), you can't inherit from both of them. This is a large part of *why* we have mixins. – T.J. Crowder Jan 28 '16 at 15:25
  • @T.J.Crowder I guess in a way I'm trying to use mixins with classes. – evolutionxbox Jan 28 '16 at 15:31

2 Answers2

6

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.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I've heard that mixins are falling by the wayside. Also, the class requirement comes from using ESDoc, and it's far too late in the project to change. – evolutionxbox Jan 28 '16 at 14:57
  • Changing to `openable.isOpen.bind(openable)` gives me `openable.isOpen.bind is not a function`. I didn't think you could bind to a accessor. – evolutionxbox Jan 28 '16 at 14:59
  • @evolutionxbox: Mixins aren't going anywhere. Re ESDoc, it can't handle something simple like mixins? JSDoc can. – T.J. Crowder Jan 28 '16 at 15:06
  • ESDoc basically requires classes and there's nothing that I can see where it supports mixins. I'd have to write my own tags for that. – evolutionxbox Jan 28 '16 at 15:09
  • @evolutionxbox: I don't know ESDoc, but it would be *really* shocking for a JavaScript documentation generator not to handle mixins. I notice [it has `@implements`](https://esdoc.org/tags.html#-implements), which sounds related at least. May be worth a question specifically about ESDoc in hopes there's some trick. In any case, the above addresses the composition of those properties. Good luck! :-) – T.J. Crowder Jan 28 '16 at 15:11
  • 1
    Thanks @T.J.Crowder you've been a big help. – evolutionxbox Jan 28 '16 at 15:22
1

Besides that the OP's accessor approach via "pseudo private property" notation and prototypal getters/setters for Openable/Hideable classes already is questionable, traits would come closest to the also doubtable requirement of using classes as mixin surrogates just for the sake of meeting documentation requirements.

As long as JavaScript does not provide traits natively, one has to stick to either more advanced class based mixin patterns ore one remembers Angus Croll's Flight Mixins.

A mixin's function body one has to write is close enough to the constructor body of a class. Nevertheless function based mixins never will be instantiated but always have to be applied to an object/type via either call or apply.

A possible solution, featuring this kind of mixin approach, that already reliably fulfills the OP's requirements then might look like the next provided example code ...

let
  Openable = (function openableMixinFactory () {
    let
      defineProperty = Object.defineProperty,
      isBoolean = (type => (typeof type == 'boolean'));

    return function openableMixinApplicator (isOpen = false) {
      let
        openableCompositeType = this,

        getIsOpen = (() => isOpen),
        setIsOpen = (value => ((isBoolean(value) && (isOpen = value)) || (void 0)));

      defineProperty(openableCompositeType, 'isOpen', {
        get: getIsOpen,
        set: setIsOpen,
        enumerable: true
      });
      return openableCompositeType;
    };
  }()),

  Hideable = (function hideableMixinFactory () {
    let
      defineProperty = Object.defineProperty,
      isBoolean = (type => (typeof type == 'boolean'));

    return function hideableMixinApplicator (isHidden = false) {
      let
        hideableCompositeType = this,

      //getIsHidden = (() => isHidden),
        getIsHidden = (() => [isHidden, 'is stupid.'].join(' ')),
        setIsHidden = (value => ((isBoolean(value) && (isHidden = value)) || (void 0)));

      defineProperty(hideableCompositeType, 'isHidden', {
        get: getIsHidden,
        set: setIsHidden,
        enumerable: true
      });
      return hideableCompositeType
    };
  }());


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

    Openable.call(this, isOpen);
    Hideable.call(this, isHidden);
  }
}


var
  thing = new Thing({ isOpen: true/*, isHidden: false*/ });

console.log('thing : ', thing);
console.log('thing.isOpen : ', thing.isOpen);
console.log('thing.isHidden : ', thing.isHidden);
console.log('(thing.isOpen = "xyz") : ', (thing.isOpen = "abc"));
console.log('(thing.isHidden = "xyz") : ', (thing.isHidden = "xyz"));
console.log('thing.isOpen : ', thing.isOpen);
console.log('thing.isHidden : ', thing.isHidden);
console.log('(thing.isOpen = false) : ', (thing.isOpen = false));
console.log('(thing.isHidden = true) : ', (thing.isHidden = true));
console.log('thing.isOpen : ', thing.isOpen);
console.log('thing.isHidden : ', thing.isHidden);
.as-console-wrapper { max-height: 100%!important; top: 0; }

Other answers of mine at SO, that provide similar solutions to related questions, featuring the same approach are ...

Community
  • 1
  • 1
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37