3

I'm trying to wrap my mind around Object Linking Other Objects to write a Node module. This is what I have so far (inspired by this answer):

'use strict'

// Composable prototype object
var parent = {
  publicVar: 1,
  doSomething() {
    return externalMethod(this.publicVar) + 10
  }
}

// Composable prototype object
var child = {
  doSomethingChild() {
    return this.publicVar + 20
  } 
}

// an external method
function externalMethod(arg) {
  return arg
}

// the parent factory
function Parent() {
  let privateVar = 2

  return Object.assign({
    getPrivate() {
      return privateVar
    }
  }, parent)
}

// the child factory
function Child() {
  let privateVar = 4
  let parent = Parent() // call to the Parent factory
  return Object.assign(parent, child, {
    getPrivateChild() {
        return privateVar
    }
  })
}

// Node export
module.exports = {
  Parent: Parent(),
  Child: Child()
}

Later, I will require the module like this:

Parent = require('./my-module').Parent
Child = require('./my-module').Child
Parent.getPrivate() // 2
Parent.doSomething() // 11
Child.getPrivateChild() // 4
Child.doSomethingChild() // 21

I'm afraid there might be a more elegant way of doing this using OLOO. My main concern is that I think I should be doing let parent = Object.create(Parent) in the Child factory, but if I do that it doesn't work.

So, 1) am I missing something and 2) can this be refactored?

Community
  • 1
  • 1
nachocab
  • 13,328
  • 21
  • 91
  • 149
  • 1
    I'm not sure that the linked answer is a good example that should be followed. It claims that it does 'composition over inheritance', but in fact it is just mix-in pattern. Which looks crippled in ES6, considering that we have classes. Do you have problems with classes and prototypal ineritance that you try to solve this way? Since the question has `ecmascript-6` tag, the obvious answer is 'don't do that in ES6'. – Estus Flask Mar 05 '17 at 18:18
  • @estus Are you recommending the use of ES6 classes? Several people make a good case against them: [Kyle Simpson](https://github.com/getify/You-Dont-Know-JS/blob/master/this%20%26%20object%20prototypes/ch6.md#es6-class-sugar), [Eric Elliott](https://medium.com/javascript-scene/common-misconceptions-about-inheritance-in-javascript-d5d9bab29b0a#.3uteqhyf7). How would you refactor this? – nachocab Mar 05 '17 at 22:00
  • 1
    For this case, absolutely. Two classes, two instances, there's nothing to think about. Once you mastered both ways and know how they affect design and performance, you can choose one or another - but most times you will choose classes, because they are effective and do the job. I find YDKJS series quite enlightening but also unreasonably opinionated. The said chapter bashes prototypes, while they are idiomatic to JS and may [perform better](http://stackoverflow.com/questions/36338289/object-descriptor-getter-setter-performance-in-recent-chrome-v8-versions) IRL. – Estus Flask Mar 05 '17 at 22:13
  • Interesting. Thanks for your input! I'm interested to see what other people think. I don't yet have a feel for how "crippled" the code above is – nachocab Mar 05 '17 at 23:21
  • It's just not expressive and harder to read for no good reason. Looks like antipattern to me in this context. And as it was mentioned above, it will have performance problems in V8 with property accessors (and will have troubles with descriptor inheritance, too). – Estus Flask Mar 05 '17 at 23:59
  • 1
    You *really* should not export `Parent()` and `Child()` instances but the `Parent` and `Child` functions themselves. – Bergi Mar 06 '17 at 01:46
  • If you wanted to use prototypical inheritance, you'd need to use `Object.create(parent)` inside of the `Parent` factory function, not `Object.create(Parent)` – Bergi Mar 06 '17 at 01:50
  • @Bergi Thanks. What's wrong with exporting the instances? Regarding your second comment, do you mean `Object.create(parent)` inside the `Child` factory? I guess doing `let parent2 = Object.create(parent)` would work. – nachocab Mar 06 '17 at 02:17
  • 1
    @nachocab If you export instances, they become singletons, and all your complicated factory stuff could be replaced by a trivial object literal. – Bergi Mar 06 '17 at 11:34
  • @nachocab No, I meant the `Parent` factory. To use `Object.create` in the `Child` factory, you'd probably use `Object.create(child)` and have `child` inherit from `Parent` itself or something, but you need to get an idea of what your linear inheritance chain should look like and how to get all the initialisers (with their private vars and instance-specific methods) to run. – Bergi Mar 06 '17 at 11:37

4 Answers4

9

You absolutely should prefer composition (including mixins) over single-ancestor class inheritance, so you're on the right track. That said, JavaScript doesn't have private properties as you might know them from other languages. We use closures for data privacy in JS.

For composable prototypes with real data privacy (via closure), what you're looking for is functional mixins, which are functions that take an object and return an object with new capabilities added.

However, in my opinion, it's usually better practice to do your functional inheritance using composable factories (such as stamps). AFAIK, Stampit is the most widely-used implementation of composable factories.

A stamp is a composable factory function that returns object instances based on its descriptor. Stamps have a method called .compose(). When called the .compose() method creates new stamp using the current stamp as a base, composed with a list of composables passed as arguments:

const combinedStamp = baseStamp.compose(composable1, composable2, composable3);

A composable is a stamp or a POJO (Plain Old JavaScript Object) stamp descriptor.

The .compose() method doubles as the stamp’s descriptor. In other words, descriptor properties are attached to the stamp .compose() method, e.g. stamp.compose.methods.

Composable descriptor (or just descriptor) is a meta data object which contains the information necessary to create an object instance. A descriptor contains:

  • methods — A set of methods that will be added to the object’s delegate prototype.
  • properties — A set of properties that will be added to new object instances by assignment.
  • initializers — An array of functions that will run in sequence. Stamp details and arguments get passed to initializers.
  • staticProperties — A set of static properties that will be copied by assignment to the stamp.

Basic questions like “how do I inherit privileged methods and private data?” and “what are some good alternatives to inheritance hierarchies?” are stumpers for many JavaScript users.

Let’s answer both of these questions at the same time using init() and compose() from the stamp-utils library.

  • compose(…composables: [...Composable]) => Stamp takes any number of composables and returns a new stamp.
  • init(…functions: [...Function]) => Stamp takes any number of initializer functions and returns a new stamp.

First, we’ll use a closure to create data privacy:

const a = init(function () {
  const a = 'a';

  Object.assign(this, {
    getA () {
      return a;
    }
  });
});

console.log(typeof a()); // 'object'
console.log(a().getA()); // 'a'

It uses function scope to encapsulate private data. Note that the getter must be defined inside the function in order to access the closure variables.

Here’s another:

const b = init(function () {
  const a = 'b';

  Object.assign(this, {
    getB () {
      return a;
    }
  });
});

Those a’s are not typos. The point is to demonstrate that a and b’s private variables won’t clash.

But here’s the real treat:

const c = compose(a, b);

const foo = c();
console.log(foo.getA()); // 'a'
console.log(foo.getB()); // 'b'

WAT? Yeah. You just inherited privileged methods and private data from two sources at the same time.

There are some rules of thumb you should observe when working with composable objects:

  1. Composition is not class inheritance. Don't try to model is-a relationships or think of things in terms of parent/child relationships. Instead, use feature-based thinking. myNewObject needs featureA, featureB and featureC, so: myNewFactory = compose(featureA, featureB, featureC); myNewObject = myNewFactory(). Notice that myNewObject is not an instance of featureA, featureB, etc... instead, it implements, uses, or contains those features.
  2. Stamps & mixins should not know about each other. (No implicit dependencies).
  3. Stamps & mixins should be small. Introduce as few new properties as possible.
  4. When composing, you can and should selectively inherit only the props you need, and rename props to avoid collisions.
  5. Prefer modules for code reuse whenever you can (which should be most of the time).
  6. Prefer functional programming for domain models and state management. Avoid shared mutable state.
  7. Prefer higher order functions and higher order components over inheritance of any kind (including mixins or stamps).

If you stick to those guidelines, your stamps & mixins will be immune to common inheritance problems such as the fragile base class problem, the gorilla/banana problem, the duplication by necessity problem, etc...

Eric Elliott
  • 4,711
  • 1
  • 25
  • 33
  • 1
    This is an outstanding run-through of stamps. thank you! I've been exploring this approach and - though very different from the classical approach I'm used to in other oop languages - I can see that it's a very powerful way of working! – Jay Edwards Nov 25 '17 at 00:52
1

With ES6 classes, it is as simple as

class Parent {
  constructor() {
    this.publicVar = 1;
    this._privateVar = 2;
  }

  getPrivate() {
    return this._privateVar;
  }

  doSomething() {
    return externalMethod(this.publicVar) + 10
  }
}

class Child extends Parent {
  constructor() {
    super();
    this._privateVar = 4;
  }

  doSomethingChild() {
    return this.publicVar + 20
  } 
}

module.exports = {
  parent: new Parent(),
  child: new Child()
}

Depending on the roles of publicVar and _privateVar, they may be static properties.

The use of _privateVar property isn't accidental. Usually _ naming convention (and possibly non-enumberable descriptor) is enough to designate the member as private/protected.

Object.assign is ineffective as main inheritance technique in ES6, but can be used additionally to implement polymorphic inheritance.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 1
    The trouble here is that `._privateVar` isn't really private. – Eric Elliott Mar 05 '17 at 23:37
  • 2
    @EricElliott Is it really a trouble? I've tried to use local variables and symbols for private members before, and encapsulation was the only pro - and an end in itself. While the lack of testability and extensibility was a major con. Since encapsulation is never a matter of security in JS, usually _ naming convention (and possibly non-enumberable descriptor) is enough. Of course, the code above can have scoped `_privateVar` variables for each class instead of property, the result is that child class should define its own getPrivate method, as shown in the OP. – Estus Flask Mar 06 '17 at 00:14
  • @EricElliott if you are concerned about that, you could put `var privateVar = 2; this.getPrivate = () => privateVar;` in the constructor. But as estus explains, it's rarely worth it. – Bergi Mar 06 '17 at 01:48
  • 3
    Encapsulation is one of the primary reasons OOP was invented in the first place. Encapsulation helps create objects that are more self-documenting by *only exposing the public API*. Likewise, if you're depending on private props in your tests or extensions, you're depending on implementation rather than interface. "Program to an interface, not an implementation" is key advice from the Gang of Four "Design Patters" for good reason. Implementation details may change and break things that depend on them. – Eric Elliott Mar 06 '17 at 01:54
  • @Bergi You can't just put a private prop in the constructor if you're trying to compose multiple mixins, each with their own private props, because for methods to have access to those private props, those methods must be created inside the same function scope as the private props, which is why functional mixins were invented. – Eric Elliott Mar 06 '17 at 01:56
  • @EricElliott Luckily, we have tests for changed implementation details! Hungarian notation efficiently solves the problem with self-documentation, consider _ protected when it is accessed from the inside, consider accessing it a reflection from the outside. Speaking of which, in other lingos a private member can be reflected, while JS local var cannot, being a half-hearted substitution. – Estus Flask Mar 06 '17 at 10:45
  • @EricElliott It is nice to speak about encapsulation in vitro. But in vivo I had to fork third-party libraries or copy-paste their parts too often, just because the devs were too anal in concealing methods they considered internal. It's just encapsulation for encapsulation sake. – Estus Flask Mar 06 '17 at 10:45
  • @EricElliott Yes, a constructor is not a mixin, but you'd use the same principle of declaring private vars and exposing public methods in both. – Bergi Mar 06 '17 at 11:31
  • 2
    @Bergi See my comment for a much more detailed explanation of using closures for composable mixins. @estus Careful not to blame encapsulation for some lib-author's poor design choices. One of the main reasons `_` does not work is that newbies don't know what it means, and experienced devs think, "I know what I'm doing, this privacy doesn't apply to me." When nobody respects privacy, things break a lot more. – Eric Elliott Mar 06 '17 at 19:10
1

After having read carefully all the posts and trying to understand the OP's problem, I think the OP's approach already was pretty close to a solid solution. After all, reliable encapsulation in JS mostly falls back to some closure based techniques. The pure object and factory based approach is lean too.

In order to fully understand the provided example, I did refactor it with being more explicit about naming the different composable parts, especially the "behavior" section. I also did not feel comfortable with the child-parent relationship and with how to export the factories. But since such examples are mostly boiled down code, one has to guess as often the OP's real world problem.

This is what a running refactored code, that does preserve the OP's approach, could look like ...

// an external method
function externalPassThroughMethod(value) {
  return value;
}

// some composable object based behavior
const withGetPublicValueIncrementedByTen = {
  getPublicValueIncrementedByTen() {

    return (externalPassThroughMethod(this.publicValue) + 10);
  }
};

// another composable object based behavior
const withGetPublicValueIncrementedByTwenty = {
  getPublicValueIncrementedByTwenty() {

    return (externalPassThroughMethod(this.publicValue) + 20);
  }
};

// the parent factory
function createParent(publicOptions = {}) {
  var localValue = 2;

  // `publicValue` via `publicOptions`
  return Object.assign({}, publicOptions, withGetPublicValueIncrementedByTen, {
    getLocalValue() {

      return localValue;
    }
  });
}

// the child factory
function createChild(parent) {
  var localValue = 4;

  // `publicValue` via `parent`
  return Object.assign({}, parent, withGetPublicValueIncrementedByTwenty, {
    getLocalValue() {

      return localValue;
    },
    getLocalValueOfParent() { // object linking other object ...

      return parent.getLocalValue(); // ... by forwarding.
    }
  });
}


// // Node export
// module.exports = {
//   createParent: createParent,
//   createChild : createChild
// }


// some (initial) key value pair
const initalPublicValue = { publicValue: 1 };

const parent  = createParent(initalPublicValue);
const child   = createChild(parent);

console.log('parent.getLocalValue()', parent.getLocalValue());                                      // 2
console.log('parent.getPublicValueIncrementedByTen()', parent.getPublicValueIncrementedByTen());    // 11
console.log('parent.getPublicValueIncrementedByTwenty', parent.getPublicValueIncrementedByTwenty);  // [UndefinedValue]

console.log('child.getLocalValue()', child.getLocalValue());                                        // 4
console.log('child.getLocalValueOfParent()', child.getLocalValueOfParent());                        // 2
console.log('child.getPublicValueIncrementedByTen()', child.getPublicValueIncrementedByTen());      // 11
console.log('child.getPublicValueIncrementedByTwenty', child.getPublicValueIncrementedByTwenty());  // 21
.as-console-wrapper { max-height: 100%!important; top: 0; }

The next given example code takes the just provided refactored example but uses function based instead of object based mixins and factories that do create class based types instead of plain object (literal) based ones. Yet, both examples have in common one and the same approach about how to handle encapsulation and composition ...

// an external method
function externalPassThroughMethod(value) {
  return value;
}

// some composable function based behavior
const withGetPublicValueIncrementedByTen = (function () {
  function getPublicValueIncrementedByTen() {
    // implemented once ...
    return (externalPassThroughMethod(this.publicValue) + 10);
  }
  return function () {
    // ... shared (same implementation) code.
    this.getPublicValueIncrementedByTen = getPublicValueIncrementedByTen;
  };
}());

// another composable function based behavior
const withGetPublicValueIncrementedByTwenty = (function () {
  function getPublicValueIncrementedByTwenty() {
    // implemented once ...
    return (externalPassThroughMethod(this.publicValue) + 20);
  }
  return function () {
    // ... shared (same implementation) code.
    this.getPublicValueIncrementedByTwenty = getPublicValueIncrementedByTwenty;
  };
}());

class Parent {
  constructor(publicOptions = {}) {

    function getLocalValue() {
      return localValue;
    }
    var localValue = 2;

    // `publicValue` via `publicOptions`
    Object.assign(this, publicOptions);

    withGetPublicValueIncrementedByTen.call(this);

    this.getLocalValue = getLocalValue;
  }
}

class Child {
  constructor(parent) {

    function getLocalValue() {
      return localValue;
    }
    function getLocalValueOfParent() {  // object linking other object ...
      return parent.getLocalValue();    // ... by forwarding.
    }
    var localValue = 4;

    // `publicValue` via `parent`
    Object.assign(this, parent);

    withGetPublicValueIncrementedByTwenty.call(this);

    this.getLocalValue          = getLocalValue;
    this.getLocalValueOfParent  = getLocalValueOfParent;
  }
}

function createParent(publicOptions = {}) {
  return (new Parent(publicOptions));
}
function createChild(parent) {
  return (new Child(parent));
}


// // Node export
// module.exports = {
//   createParent: createParent,
//   createChild : createChild
// }


// some (initial) key value pair
const initalPublicValue = { publicValue: 1 };

const parent  = createParent(initalPublicValue);
const child   = createChild(parent);

console.log('parent.getLocalValue()', parent.getLocalValue());                                      // 2
console.log('parent.getPublicValueIncrementedByTen()', parent.getPublicValueIncrementedByTen());    // 11
console.log('parent.getPublicValueIncrementedByTwenty', parent.getPublicValueIncrementedByTwenty);  // [UndefinedValue]

console.log('child.getLocalValue()', child.getLocalValue());                                        // 4
console.log('child.getLocalValueOfParent()', child.getLocalValueOfParent());                        // 2
console.log('child.getPublicValueIncrementedByTen()', child.getPublicValueIncrementedByTen());      // 11
console.log('child.getPublicValueIncrementedByTwenty', child.getPublicValueIncrementedByTwenty());  // 21
.as-console-wrapper { max-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37
-1

For completion's sake, using Eric Elliott's answer turns out to be pretty simple:

var stampit = require('stampit')

function externalMethod(myVar) {
  return myVar
}

parent = stampit().init(function({value}){ 
    this.privateVar = 2
  }).props({
    publicVar: 1
  }).methods({
    doSomething() {
      return externalMethod(this.publicVar) + 10
    },
    getPrivate() {
      return this.privateVar
    }
  })

child = parent.init(function({value}){
    this.privateVar = 4
  }).methods({
  doSomethingChild() {
    return this.publicVar + 20
  }
})

parent().getPrivate() // 2
parent().doSomething() // 11
child().getPrivate() // 4
child().doSomething() // 11
child().doSomethingChild() // 21
Community
  • 1
  • 1
nachocab
  • 13,328
  • 21
  • 91
  • 149