Can I emulate Multiple Class Inheritance with Decorators?

103 views Asked by At

a class overwrites the properties of the class it is extending :

class ClassA {
  msg = 'ClassA'
}

export default class ClassB extends ClassA {
  msg = 'ClassB'
  onClick() {
    console.log(this.msg) // will log "ClassB"
  }
}

a decorator overwrites the properties of the class it is decorating :

function ClassA(BaseClass) {
  return class extends BaseClass {
    msg = 'ClassA'

  }
}

@ClassA
export default class ClassB {
  msg = 'ClassB'
  onClick() {
    console.log(this.msg) // will log "ClassA"
  }
}

extending multiple classes is not allowed - you cannot do (pseudocode):

export default class ClassC extends [ClassB, ClassA] {}

but you can do:

   @ClassA
   @ClassB
   export default class ClassC {}

The problem with the above is that the "extension" is reversed. The decorators will overwrite any properties that ClassC has defined. But I don't want that.

In other words, I want to use decorators to emulate multiple class extension but I don't want my decorators to overwrite any properties that the decorated class has already defined.

I could do the following:

function ClassA(BaseClass) {
  return class extends BaseClass {
    constructor() {
      super(...arguments)
      if (!this.hasOwnProperty('msg')) {
        this.msg = 'ClassA'
      }
    }
  }
}

@ClassA
export default class ClassB {
  msg = 'ClassB'
  onClick() {
    console.log(this.msg) // will log "ClassB"
  }
}

but this is too verbose.

Imagine a class with many properties, some of which are even decorated themselves. Having to implement the above solution below seems unsustainable:

function ClassA(BaseClass) {
  return class extends BaseClass {
    msg = 'ClassA' // this should "register" for ClassB, not for ClassC

    @service('x') aa // this should "register" for ClassC, not for ClassB

    @computed('aa')
    get bb() { // this should "register" for ClassC, not for ClassB
      return this.aa + 5000
    } 
  }
}

@ClassA
export default class ClassB {
  @service('y') aa
  
  @computed('aa')
  get bb() {
    return this.aa + 3
  }
}

@ClassA
export default class ClassC {
  msg = 'Class C'
}

Is there a easier way of having a decorator not overwrite the properties of the class it is decorating if they already exist?

Or is what I'm trying to achieve impossible?

Note 1: I do not want to use class extension because I want to extend multiple classes which is impossible so the idea is to use multiple decorators as a workaround

Note 2: Please do not suggest multiple class extension examples. I've seen them all and they are all pretty hacky and only work with very basic simple classes with primitive properties, not with classes which themselves contain decorated properties, complicated methods, other class instances etc..

EDIT: I just thought of this solution but have yet to test it. It's meant to emulate multiple class extension (ClassA, ClassB) using decorators, while at the same not allowing the decorators to overwrite properties that ClassC has already defined:

@ClassA
@ClassB
class MyDecorators { /* empty on purpose */ }

export default class ClassC extends MyDecorators {}
2

There are 2 answers

0
Kawd On BEST ANSWER

I found a solution to my problem so I'm posting it here in case it helps.

function decoratorB(BaseClass) { // Class B
  return class extends BaseClass {
    unchangedClassBproperty = 'unchanged Class B property'
    originallyDefinedByClassB = 'originally Defined By Class B'
    originallyDefinedByClassC = 'overwritten by Class B'
  }
}

function decoratorC(BaseClass) { // Class C
  return class extends BaseClass {
    unchangedClassCproperty = 'unchanged Class C property'
    originallyDefinedByClassC = 'originally Defined By Class C'
    originallyDefinedByClassD = 'overwritten by Class C'
  }
}

function decoratorD(BaseClass) { // Class D
  return class extends BaseClass {
    unchangedClassDproperty = 'unchanged Class D property'
    originallyDefinedByClassD = 'originally Defined By Class D'
  
    showInheritance() {
      console.log('unchangedClassAproperty = ', this.unchangedClassAproperty)
      console.log('unchangedClassBproperty = ', this.unchangedClassBproperty)
      console.log('unchangedClassCproperty = ', this.unchangedClassCproperty)
      console.log('unchangedClassDproperty = ', this.unchangedClassDproperty)
      console.log('originallyDefinedByClassB = ', this.originallyDefinedByClassB)
      console.log('originallyDefinedByClassC = ', this.originallyDefinedByClassC)
      console.log('originallyDefinedByClassD = ', this.originallyDefinedByClassD)
    }
  }
}

@decoratorB
@decoratorC
@decoratorD
class ClassesBCD {}

class A extends ClassesBCD {
  unchangedClassAproperty = 'unchanged Class A property'
  originallyDefinedByClassB = 'overwritten by Class A'
}

const a = new A()

a.showInheritance()

will log:

  unchangedClassAproperty = unchanged Class A property
  unchangedClassBproperty = unchanged Class B property
  unchangedClassCproperty = unchanged Class C property
  unchangedClassDproperty = unchanged Class D property
originallyDefinedByClassB = overwritten by Class A
originallyDefinedByClassC = overwritten by Class B
originallyDefinedByClassD = overwritten by Class C

See jsFiddle

1
Alexander Nenashev On

You can create a chain of you class instances and access them through a proxy and inside the traps you can implement any logic you want:

class ClassA {
  msg = 'ClassA'
}

class ClassB {
  msg = 'ClassB'
  logMessage() {
    console.log(this.msg) // will log "ClassB"
  }
}

class ClassC {
  msg = 'ClassC';
}

class ClassD {
  logMessage(){
    console.log('CLASS D LOGGING MSG:', this.msg);
  }
}

function multi(...classChain){ // add support of arguments if needed
  return new Proxy(classChain.map(x => new x), {
    get(target, prop){
      for(const child of target){
        if(prop in child){
          return child[prop];
        }
      }
    },
    set(target, prop, value){
      for(child of target){
        if(Object.hasOwn(child, prop)){
          child[prop] = value
          return true;
        }
      }
    }
  });
}

const obj = multi(ClassC, ClassB, ClassC);

obj.logMessage();

const obj2 = multi(ClassD, ClassB, ClassC);

obj2.logMessage();