tc39 / proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
https://arai-a.github.io/ecma262-compare/?pr=1668
1.72k stars 113 forks source link

Class composition #319

Open rowaasr13 opened 4 years ago

rowaasr13 commented 4 years ago

Composition, mix-ins, etc is a natural feature of JS as dynamic language. How can prviate fields from this proposal used with dynamically composing a class?

Say, I have a class that have several utility fields and some mix-ins with functions that want to read private data - i.e. some generic logging and some mix-ins with functions that want to modify data - some generic normalization. Classes are not homogenic enough to throw everything into single base class and extend from there: I want different set of mixins in each class.

With read-only functions this can be somewhat "solved" with wrapper for each "external" function that would pass hidden field as argument, but this doesn't look very dynamic with lots of extra boilerplate in "host" class. Wrappers for R/W functions would be even more ugly with first passing everything that could possibly be needed in and then reading returns and writing them as necessary to hidden fields.

Is there a prettier and more natural solution for me as author of both "host" and "mixin" code?

rdking commented 4 years ago

@rowaasr13

Composition, mix-ins, etc is a natural feature of JS as dynamic language. How can prviate fields from this proposal used with dynamically composing a class?

It might be possible if you're willing to heavily abuse the super override trick....

class Host {
   ...
}

function Mixer(inst, proto) {
   //Put the functions you want from the mixin on a new prototype object and inherit the prototype of Host.
   let newProto = Object.assign({}, proto);
   Object.setPrototypeOf(newProto, Object.getPrototypeOf(inst));
   Object.setPrototypeOf(inst, newProto);
   return inst;
}

class MixinA extends Mixer {
   ...
   constructor(obj) {
      super(obj, new.target.prototype);
      ...
   }
}

class MixinB extends Mixer {
   ...
   constructor(obj) {
      super(obj, new.target.prototype);
      ...
   }
}

class MixinC extends Mixer {
   ...
   constructor(obj) {
      super(obj, new.target.prototype);
      ...
   }
}

let instA = new MixinA(new MixinB(new Host));
let instB = new MixinC(new MixinA(new Host));

Like this, the mixin classes can extend the host class in any combination and order. If you're dealing with mixins that themselves have complicated heirarchies (why?), then you would just have to handle that in the Mixer function.

ljharb commented 4 years ago

https://raganwald.com/2016/07/20/prefer-composition-to-inheritance.html and https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750 may be helpful reads; you can ignore the React-specific parts.

trusktr commented 4 years ago

The type of composition @ljharb mentions is great. But so is inheritance. They both have their place. Anywho, I do not think that is what the OP is asking about.

I believe what @rowaasr13 wants is something like "module protected" or "package protected" or "friend" features of other classes.

Private fields do not currently allow that. Only the code inside the body of a class can access private fields, and yes, just like what I think you described @rowaasr13 (let me know if I misunderstood), you'd need to read values from the private fields, pass them into a function, get the return values, then set them back onto the private fields.

Unless I missed it, your example @rdking doesn't solve that problem.

I'm not entirely sure as I haven't looked into it too much, but I believe there was talk that the decorator proposal would provide a way to make private fields shareable outside of a class. @ljharb Can you expand on that?


Side topic: I haven't tried mixing classes that have #private fields with my multiple() inheritance tool yet. I should add that to the unit tests. I know the Proxies are gonna break because as far as I recall I haven't implemented the workaround needed for private fields yet.

But basically it works like the following:

import {EventEmitter} from 'events' // Node.js module
import {multiple} from 'lowclass' // npm install lowclass

class WebComponent extends HTMLElement { ... }
class One { ... }
class Two extends One { ... }

class MyElement extends multiple(WebComponent, Two, EventEmitter) { ... }

customElements.define('my-element', MyElement)

I'll post back once I update it for private fields (but still doesn't solve the OP). It also has some edge cases that aren't solved yet (or need some formal convention on how to be handled, one of them is the diamond problem, extending from two builtins, and others).