1

I need to define a function that can be used by multiple classes, but as far as I understand, inheriting from a superclass doesn't work for me. Essentially, what I would like to achieve is the ability to extend multiple interfaces for each class.

For example, if I have defined classes Apple, Orange, Banana, I want all of them to have an identical isFresh() function. I also like to let Apple, Orange, and Earth to have a getRadius() method. This is somewhat similar to Apple interface Fruit, SphericalObject {...} I also want to be able to override the functions if I want to. However, inheritance doesn't work for me because I would like to inherit from multiple superclasses.

What is the best way to achieve this?

I am aware of this similar post, I understand from that JavaScript is dynamically typed and does not have interfaces, and the suggested Duck Type doesn't seem to solve my problem. I don't really care to check if the method in interface exist in child classes.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
  • 2
    [`Object.assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) might be what you're looking for. – jsejcksn May 11 '23 at 08:08
  • Thanks! From the first glance of the resource you linked, it seems to be exactly what I'm looking for, but I'll need to take a closer look! I will definitely give it a try. – user21877241 May 11 '23 at 09:28
  • It depends on how one does implement all the distinct classes in terms of how one e.g. wants them all the additional functionality get applied to, which again depends on how one wants to grant property visibility / protection / access. – Peter Seliger May 16 '23 at 19:30
  • @user21877241 ... The OP might check on a late provided answer which nevertheless meets any of the OP's specifications. – Peter Seliger Jul 06 '23 at 13:25

5 Answers5

1

Looks like you're looking for "mixins". They are not built-in in javascript, but are quite easy to implement in userland, for example:

function augment(cls, ...mixins) {
    return class extends cls {
        constructor(...args) {
            super(...args)

            for (let c of mixins)
                for (let p of Object.getOwnPropertyNames(c.prototype))
                    if (p !== 'constructor')
                        this[p] = c.prototype[p]
        }
    }
}

//

class Apple {}

class PhysicalObject {
    isFresh() {
        return 'hey'
    }
}

let AppleWithObject = augment(Apple, PhysicalObject)
let x = new AppleWithObject()
console.log(x.isFresh())
gog
  • 10,367
  • 2
  • 24
  • 38
  • It's a nice workaround and I can now achieve the functionality, but I have concerns on styles. Suppose I have `PhysicalObject` and `ChemicalObject`, do you think it is a good style to write: `let AppleWithObject = augment(augment(Apple, PhysicalObject),ChemicalObject)`? But it's ugly. – user21877241 May 11 '23 at 09:26
  • 2
    this function accepts a list of arguments, so it's fine to write `augment(Apple, PhysicalObject,ChemicalObject)` – gog May 11 '23 at 11:51
  • @user21877241 ... The above approach is **not mixin**-based but depends on even **class-based inheritance**. Moreover since it of cause can not implement true multiple inheritance, it is forced to copy over (assign) prototype properties to an instance's own properties. And since this approach is so fragile the example stays vague and would not even touch the matter of protecting/accessing certain properties. Maybe one of the worst examples I've seen so far at the battlefield of mixins and inheritance. – Peter Seliger May 16 '23 at 19:21
0

You only needs a single extends to achieve your result.

class PhysicalObject {
   constructor(x,y) {this.x=x;this.y=y;}
   getPosition() {return {x:this.x,y:this.y}}
   displayPosition() {console.log(this.getPosition().x+', '+this.getPosition().y)}
   }
   
Earth=new PhysicalObject(0,0);
Earth.displayPosition();

class Fruit extends PhysicalObject {
  constructor(x,y,a) {super(x,y);this.age=a;}
  isFresh() {return this.age<7}
  }
  
Apple=new Fruit(1,1,6);
Apple.displayPosition();
console.log(Apple.isFresh());
JMP
  • 4,417
  • 17
  • 30
  • 41
  • Thanks for your answer. I apologize for my bad example. Essentially, I don't want the superclasses to have logical connection to each other, but my old example happened to be in the case that Fruit is a type of Physcial Object. Now I updated the question with better clarity. – user21877241 May 11 '23 at 09:00
0

On the risk of being misprised extensively: inspired by Douglas Crockford I stopped using classes or prototypes (well, classes I never used in ES, never had any use for it).

Instead I create factory functions. Here's an examplary Fruit factory.

To play with the idea I created a small Stackblitz project, with a more generic approach.

const FruitStore = FruitFactory();

FruitStore.apple = { 
  mustPeal: false, color: `red`, fresh: "Nope", origin: `Guatamala`, 
  inheritsFrom: {
    ...PhysicalObjectFactory(true), 
    ...ChemicalObjectFactory(true, null, true) },
};
FruitStore.orange = { inheritsFrom: {
  origin: `Spain`, fresh: false, color: `orange` } 
};
FruitStore.pineapple = { color: `yellow`, spherical: false, qty: `200Kg` };
console.log(FruitStore.all);
FruitStore.orange.fresh = `UNKNOWN`;
console.log(FruitStore.orange);

function PhysicalObjectFactory(spherical) {
  return { isPhysical: true, isSpherical: spherical };
}

function ChemicalObjectFactory(
  carbonBased = null, boilingPoint = null, solid = null) {
  return { carbonBased, boilingPoint, solid };
}

function FruitFactory() {
  let allFruits = {};
  // all fruits 'inherit' these properties
  // you can override any of them on
  // creating a fruit instance
  const fruitDefaults = {
    mustPeel: true,
    fresh: true,
    qty: `unset`,
  };
  const instance = { get all() { return allFruits; }, };
  // the proxy ensures you're working on the `allFruits` instance
  const proxy = { 
    get (obj, key) { return allFruits[key] ?? obj[key]; },
    set(_, key, props) { 
      allFruits[key] = createFruitInstance(key, props); 
      return true; 
    },
  };
  
  return new Proxy(instance, proxy);
  
  function createFruitInstance(name, props = {}) {
    const fruit = { name };
    let inherits = {};
    let inheritsFrom = { ...props.inheritsFrom };
    delete props.inheritsFrom;
    Object.entries({...fruitDefaults, ...props, ...inheritsFrom})
    .forEach( ([key, value]) => 
      value || key in fruitDefaults ? fruit[key] = value : false 
    );
    return fruit;
  }
}
.as-console-wrapper {
    max-height: 100% !important;
}
KooiInc
  • 119,216
  • 31
  • 141
  • 177
  • Thanks! Very interesting. I'll give it a try. But I don't see how to create an instance that 'inherit' properties from multiple 'factories' – user21877241 May 11 '23 at 09:45
  • 1
    Hi @user21877241, see edits for an ideas on inheritance for this pattern – KooiInc May 11 '23 at 10:47
0

Inspired by @gog's answer, I would like to share a modified version that works better for me. This solution

  1. avoids defining a temporary class that is ultimately not used. (e.g. the empty Apple class in gog's answer),
  2. is a clean way to initialize variables in 'superclasses' (with gog's original code, I wasn't able to find a clean way to define and inherit variables in the 'superclasses', which makes it 'unsafe' if I want to define functions in the 'childclass' that uses these variables.

    function augment(ins, ...mixins) {
                for (let c of mixins)
                    for (let p of Object.getOwnPropertyNames(c.prototype))
                        if (p !== 'constructor')
                            ins[p] = c.prototype[p]
    }
    
    class Alice {
      initAlice() {
        this.name = 'Alice';
      }
    }
    
    class Teacher {
      initTeacher() {
        this.occupation = 'Teacher';
      }
    }
    
    class RealAlice {
      constructor() {
        augment(this,Alice,Teacher);
        this.initAlice();
        this.initTeacher();
      }
    }
    
    const alice = new RealAlice(30);
    console.log(alice.name); // logs 'Alice'
    console.log(alice.occupation); // logs 'Teacher'
Philip Zhu
  • 119
  • 4
0

From my above comment on the OP's question ...

"It depends on how one does implement all the distinct classes in terms of how one e.g. wants them all the additional functionality get applied to, which again depends on how one wants to grant property visibility / protection / access."

The next provided example covers the OP's specifications entirely ... mainly based on two function-based mixin implementations, each targeting a specific trait/behavior via an approach based on shared private state, ... whereas the provided solitary class implementations of Earth, Apple, Orange and Banana reflect the OP's rather unusual design approach, but also do apply each the necessary mixin/s according to the OP's specifications.

// - function-based mixin implementations
//   each targeting a specific trait/behavior
//   via an approach based on shared private state.

function withFreshnessIndication(sharedPrivateState) {
  this.isFresh = () => sharedPrivateState.isFresh;
}
function asSphericalObject(sharedPrivateState) {
  Object.defineProperty(this, 'radius', {
    get: () => sharedPrivateState.radius,
  });
}

// - Earth applies the trait of an spherical object
//   which is the only thing it will have in common
//   with Apple and Orange.
class Earth {
  constructor() {
    // radius in meter.
    const state = { radius: 6_371_000 };

    // code-reuse via mixin application.
    asSphericalObject.call(this, state);
  }
  // - prototypal access of the locally encapsulated
  //   `state` object is not anymore possible.
}

// - Apple applies both traits, the one of an
//   spherical object and the one of indicating
//   its freshness which it does have in common
//   with Orange.
class Apple {
  #state;

  constructor({ isFresh = true, radius = 0.05 } = {}) {
    // radius in meter.
    this.#state = { isFresh: Boolean(isFresh), radius };

    // code-reuse via mixin application.
    withFreshnessIndication.call(this, this.#state);
    asSphericalObject.call(this, this.#state);
  }
  // - prototypal access of the privatly declared
  //   `#state` object is still possible.
}

// - A lot of code duplication (no code-reuse)
//   due to the OP's base type/object design.
class Orange {
  #state;

  constructor({ isFresh = true, radius = 0.08 } = {}) {
    // radius in meter.
    this.#state = { isFresh: Boolean(isFresh), radius };

    // code-reuse via mixin application.
    withFreshnessIndication.call(this, this.#state);
    asSphericalObject.call(this, this.#state);
  }
}

// - Banana comes without the trait of an spherical object.
//   (again some code-duplication due to the OP's design.)
class Banana {
  #state;

  constructor({ isFresh = true } = {}) {
    this.#state = { isFresh: Boolean(isFresh) };

    // code-reuse via mixin application.
    withFreshnessIndication.call(this, this.#state);
  }
}

const earth = new Earth;
const apple = new Apple({ radius: .04 });
const orange = new Orange;
const banana = new Banana({ isFresh: false,  radius: 42 });

console.log('earth ...', {
  isFresh: earth.isFresh?.(),
  radius: earth.radius,
});
console.log('apple ...', {
  isFresh: apple.isFresh(),
  radius: apple.radius,
});
console.log('orange ...', {
  isFresh: orange.isFresh(),
  radius: orange.radius,
});
console.log('banana ...', {
  isFresh: banana.isFresh(),
  radius: banana.radius,
});
.as-console-wrapper { min-height: 100%!important; top: 0; }
Peter Seliger
  • 11,747
  • 3
  • 28
  • 37