Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
 [RMMZ]Using ES6 Class Inheritance/Extensions For Aliasing Without Prototypes
#1
With ES6 classes, trying to extend the parent class in plugins can lead to its children still using the old parent codes.
For instance, you can place this in a JavaScript sandbox and verify the console output yourselves:
Code:
// Default RMMZ codebase
class DefaultMZParentClass {

    method() { return "Parent method"; }

}

class DefaultMZChildClass extends DefaultMZParentClass {

    method() { return `${super.method()} Child method`; }

}
//

// Plugin codebase
const PluginAParentClass = DefaultMZParentClass;
DefaultMZParentClass = class extends DefaultMZParentClass {

    method() { return `Plugin ${super.method()}`; }

};
//

// It's "Parent method Child method" but should be "Plugin Parent method Child method"
console.info("new DefaultMZChildClass().method()", new DefaultMZChildClass().method());
//
So normally, you'll still have to directly type prototypes:
Code:
// Default RMMZ codebase
class DefaultMZParentClass {

    method() { return "Parent method"; }

}

class DefaultMZChildClass extends DefaultMZParentClass {

    method() { return `${super.method()} Child method`; }

}
//

// Plugin codebase
const parentProto = DefaultMZParentClass.prototype, pluginAMethod = parentProto.method;
parentProto.method = function() { return `Plugin ${pluginAMethod.call(this)}`; };
//

// It's "Plugin Parent method Child method" which is the intended output
console.info("new DefaultMZChildClass().method()", new DefaultMZChildClass().method());
//

But I wanted to offer an alternative for those not being familiar with ES5 or avoiding prototypes like a plague despite the fact that many RMMZ codes are written that way:
RMMZ API
And this is what I've come up with:
Code:
/*
 * Do these 2 additional things when using ES6 class inheritance aliasing
 * without directly typing prototypes:
 * 1. Add the following code right below a new class inheriting another one:
 *    - ExtendedClassAlias.inherit(Klass);
 *    Where Klass is the new class inheriting another one
 * 2. Add the following code right below extending an existing class as a way
 *    to alias its methods:
 *    - ExtendedClassAlias.updateClass(Klass);
 *    Where Klass is the existing class being extended as a way to alias its
 *    methods
 * Right now it doesn't work well with inheriting static functions in classes,
 * so those in children classes should use ParentClass.staticFunc.call(this)
 * instead of super.staticFunc()
 */

// Codes allowing ES6 class inheritance aliasing without direct prototyping
class ExtendedClassAlias {

    static inherit(Child) {
        const childProto = Child.prototype;
        const parentName = Object.getPrototypeOf(childProto).constructor.name;
        this._inherit(Child, parentName);
    }

    static updateClass(Parent) {
        const parentName = Parent.prototype.constructor.name;
        // There's no need to update anything if the passed class's no children
        if (!this._inheritances.has(parentName)) return;
        this._updateClass(this._inheritances.get(parentName), Parent);
        //
    }

    static _inherit(Child, parentName) {
        // So the parent class will know which classes are its children
        if (this._inheritances.has(parentName)) {
            const oldChildProtos = this._inheritances.get(parentName);
            const newChildProtos = oldChildProtos.concat([Child]);
            this._inheritances.set(parentName, newChildProtos);
        } else {
            this._inheritances.set(parentName, [Child]);
        }
        //
    }

    static _updateClass(children, Parent) {
        this._updateProtoMethods(children, Parent.prototype);
        this._updateStaticFuncs(children, Parent);
    }

    static _updateProtoMethods(children, parentProto) {
        // So all the children will inherit the new rather than the old parent
        children.forEach(Child => Child.prototype.__proto__ = parentProto);
        //
    }

    static _updateStaticFuncs(children, Parent) {
        // So all children will inherit all new static functions from new parent
        Object.getOwnPropertyNames(Parent).forEach(name => {
            const desc = Object.getOwnPropertyDescriptor(Parent, name);
            if (!desc || typeof desc.value !== "function") return;
            children.forEach(Child => {
                Child[name] = Child[name] || Parent[name];
            });
        });
        //
    }

}
ExtendedClassAlias._inheritances = new Map();
//
I've tested that it works for adding new instance variables and prototype methods in base classes, and extending and overriding existing prototype methods there.
You can place the linked snippet into a JavaScript sandbox and verify the console output yourselves:
linked snippet
While I failed to inherit the changes in the static functions of the base classes from plugins as well, this can be easily mitigated by using BaseClass.staticFunc.call(this) instead of super.staticFunc().

Basically, the essence of the issue when aliasing ES6 class inheritance without direct prototyping is this:
1. The original child class inherits the original base class
2. A plugin extends the original base class to alias some of its prototype methods
3. The child class still inherits the original base class
So to solve this, simply store the linkage between the child class and the base class right after creating that child class, then points the parent of the child class to the extended base class right after extending it.

As for the static functions in classes, while I tried to use the linkage to let the exiting children class use the new static functions from the extended parent class, I failed to cover the case for aliasing existing parent class static functions, because it's just impossible:
1. The super in the static functions of the child class always points to the original parent class
2. The super in the static functions of the extened parent class always points to the original parent class
3. The super in the static functions of the child class's supposed to always point to the extended parent class
Clearly, combining 1 and 2 will contradict with 3, which is the goal I've trying to achieve.

For those not being familiar with ES5 or avoiding prototypes like a plague, I hope using ExtendedClassAlias won't be too demanding for you, as all you need to do is sticking to these:
Code:
/*
 * Do these 2 additional things when using ES6 class inheritance aliasing
 * without directly typing prototypes:
 * 1. Add the following code right below a new class inheriting another one:
 *    - ExtendedClassAlias.inherit(Klass);
 *    Where Klass is the new class inheriting another one
 * 2. Add the following code right below extending an existing class as a way
 *    to alias its methods:
 *    - ExtendedClassAlias.updateClass(Klass);
 *    Where Klass is the existing class being extended as a way to alias its
 *    methods
 * Right now it doesn't work well with inheriting static functions in classes,
 * so those in children classes should use ParentClass.staticFunc.call(this)
 * instead of super.staticFunc()
 */
P.S.: I've spent almost 24 hours on this and I enjoyed the process a lot, even though this might not be practical enough to be used in MZ :)
My RMVXA/RMMV/RMMZ scripts/plugins:
http://rpgmaker.net/users/DoubleX/scripts/
Reply }


Possibly Related Threads…
Thread Author Replies Views Last Post
   Basic knowledge to the default RMMZ TPBS battle flow implementations DoubleX 0 1,371 03-05-2022, 07:33 AM
Last Post: DoubleX
   Basic knowledge to the default RMMZ turn based battle flow implementations DoubleX 0 1,411 03-01-2022, 05:11 AM
Last Post: DoubleX
   Basic knowledge on using Javascript ES5 access modifiers and inheritance DoubleX 1 5,537 02-15-2017, 03:45 PM
Last Post: DoubleX



Users browsing this thread: