[TMoS Game] Using mixins in Typescript

in Game Development2 years ago

What are mixins?

In programing a mixin is a small, partial class that can be added into another class. These components makes it possible to create reusable code that can be added to different classes that have little or nothing in common.

Personally I found the biggest benefit being that you can create complex classes, but with splitting up the code into mixins you get files with few lines of code and nicely contained. If there is an issue and you have to go bug hunting, you'll know that the issue will be in the small mixin file, not spread out in a big class file.

// Applying tons of functionality to my PixiGame class using mixins.
this.mixinInits(PixiGame, MixinEvent, MixinDict, MixinSetting, MixinGame, MixinGFX, MixinPhysics);

Mixins in TMoS

There are different ways to create mixins. The official TypeScript documentation has a page about it here. I originally decided on my method over a year a go, making me a little uncertain on exactly why I choose the option I took. My method has a bit more setup, but with code snippets it is mostly automated. I think I choose my path as it keeps everything as classes and interfaces, making all the code follow the same pattern. The rest of this post will be the code for this, in the event it could be useful for anyone.

To get started, I have an abstract mixin class that all mixins derive from. It has a method for initializing the mixin, with or without parameters, and a destruction method that cleans up when a class using a mixin is destroyed. It has two static methods, one to check if a class has a specific mixin and one to check if a class is of correct type to make use of a mixin.

abstract class Mixin {
    private mixinsApplied: Map<typeof Mixin, { classApplyingMixin: any, destroyMethod: () => void }>; // Track mixins applied to a class.
    private mixinsDestroyed: number;    // Track so that all mixins destroyed are executed.

    /**
     * Called from the constructor of a Class using mixin. Can pass only the Mixin(s) or an object with Mixin(s) + 
     * params for the Mixin(s).
     * 
     * @param classApplyingMixin 
     * @param mixinClasses 
     */
    protected mixinInits(classApplyingMixin: any, ...mixinClasses: (typeof Mixin | { mixin: typeof Mixin, args?: any[] })[]) {
        if (this.mixinsApplied == undefined) this.mixinsApplied = new Map();

        mixinClasses.forEach((mixinClass: any) => {
            if (Mixin.hasMixin(this, mixinClass.mixin ? mixinClass.mixin : mixinClass) == false) {
                (this as any)[mixinClass.mixin ? mixinClass.mixin.name : mixinClass.name](mixinClass.args ? mixinClass.args : undefined);

                let destroyMethod = mixinClass.mixin ? mixinClass.mixin.name + "Destroy" : mixinClass.name + "Destroy";
                this.mixinsApplied.set(mixinClass.mixin ? mixinClass.mixin : mixinClass, {
                    classApplyingMixin: classApplyingMixin, destroyMethod: (this as any)[destroyMethod].bind(this)
                });
            }
        });
    }

    /**
     * Called from the destroy method in a class using mixins. Execute all destroy methods of mixins applied by a certain class. This makes 
     * each class only destroy mixins it has applied, making derived classes only destroying their own mixins, leaving the other (if any) 
     * to the parents.
     * 
     * @param classApplyingMixin 
     */
    protected mixinDestroys(classApplyingMixin?: any) {
        if (this.mixinsApplied) {
            let tempArray = Array.from(this.mixinsApplied).filter(value => value[1].classApplyingMixin == classApplyingMixin).reverse();

            if (this.mixinsDestroyed == undefined) this.mixinsDestroyed = 0;

            tempArray.forEach(mixin => {
                mixin[1].destroyMethod();
                this.mixinsDestroyed++;
            });

            if (this.mixinsApplied.size == this.mixinsDestroyed) {
                this.mixinsApplied.clear();

                delete this.mixinsApplied;
                delete this.mixinsDestroyed;
            }
        }
    }

    /**
     * Check if an instance of a class can apply a certain mixin.
     * 
     * @param instanceApplyingMixin 
     * @param mixinClass 
     * @param instanceRequiredClass 
     * @returns 
     */
    static notApplicable(instanceApplyingMixin: any, mixinClass: any, instanceRequiredClass: any) {
        if ((instanceApplyingMixin instanceof instanceRequiredClass) == false && Mixin.hasMixin(instanceApplyingMixin, instanceRequiredClass) == false) {
            console.error(
                instanceApplyingMixin.constructor.name + " is not an instance of " + instanceRequiredClass.name +
                " and can not use mixin " + mixinClass.name + ". Correct this fault as it can lead to unpredictable behaviour."
            );

            return true;
        }

        return false;
    }

    /**
     * Check if an instance of a class has a specific mixin.
     * 
     * @param instanceApplyingMixin 
     * @param mixin 
     * @returns 
     */
    static hasMixin(instanceApplyingMixin: Mixin, mixin: typeof Mixin) {
        if (instanceApplyingMixin.mixinsApplied == undefined) return false;
        return instanceApplyingMixin.mixinsApplied.has(mixin);
    }
}



A Mixin derived from the abstract class looks like this. The method with the same name
as the class is used like a constructor.

class MixinTest extends Mixin {
    private MixinTest(args?: any[]) {
        //Executed by mixinInits in the constructor of ClassUsingMixin
    }

    private MixinTestDestroy() {
        //Executed by mixinDestroys in the destroy of the ClassUsingMixin
    }
}


Here we have a class that uses the mixin MixinTest.

class ClassUsingMixin {
    constructor() {
        this.mixinInits(ClassUsingMixin, MixinTest);
    }

    destroy() {
        this.mixinDestroys(ClassUsingMixin);
    }
}
interface ClassUsingMixin extends MixinTest { }
applyMixins(ClassUsingMixin, MixinTest);


The final bit of code is the applyMixins function that takes care of mashing the classes together.


function applyMixins(classGettingMixins: any, ...mixinClasses: any[]) {
    const constructorSave = classGettingMixins.prototype.constructor;

    mixinClasses.push(Mixin);

    mixinClasses.forEach(classToMixin => {
        Object.getOwnPropertyNames(classToMixin.prototype).forEach(name => {
            Object.defineProperty(classGettingMixins.prototype, name, Object.getOwnPropertyDescriptor(classToMixin.prototype, name));
        });
    });

    classGettingMixins.prototype.constructor = constructorSave;
}



That is all the coded needed to make use of the mixin pattern. It is not the least lines of code option there is, but so far in TMoS is has behaved perfect. You can create parent classes, derive child classes, create parent mixin classes and derive mixin child classes, even mixins that use other mixins work. The code intellisense works and, most important, the code seems to run as intended!

Here is also a couple of code snippets to make it easier to start on a new mixin or class using mixins.

{
    "New mixin": {
        "scope": "javascript,typescript",
        "prefix": "Mixin",
        "body": [
            "export class Mixin${1:Name} extends Mixin {",
            "\tprivate Mixin${1:Name}(args?: any[]) {\n\t\t$3\n\t}\n",
            "\tprivate Mixin${1:Name}Destroy() {\n\t\t$4\n\t}",
            "}"
        ]
    },
    "New limited mixin": {
        "scope": "javascript,typescript",
        "prefix": "MixinLimited",
        "body": [
            "export class Mixin${1:Name} extends Mixin {",
            "\tprivate Mixin${1:Name}(args?: any[]) {\n\t\tif (Mixin.notApplicable(this, Mixin${1:Name}, ${2:ApplicableToClass})) return;\n\t\t$3\n\t}\n",
            "\tprivate Mixin${1:Name}Destroy() {\n\t\t$4\n\t}",
            "}",
            "export interface Mixin${1:Name} extends ${2:ApplicableToClass} { }"
        ]
    },
    "New class using Mixin": {
        "scope": "javascript,typescript",
        "prefix": "ClassWithMixin",
        "body": [
            "export class ${1:ClassName} {",
            "\tconstructor() {\n\t\tthis.mixinInits(${1:ClassName}, ${2:Mixins});\n\t}\n",
            "\tdestroy() {\t\t\n\t\tthis.mixinDestroys(${1:ClassName});\n\t\tsuper.destroy();\n\t}",
            "}",
            "export interface ${1:ClassName} extends ${2:Mixins} {}",
            "applyMixins(${1:ClassName}, ${2:Mixins});"
        ]
    }
}

Cheers!


Spelmakare is game development using web technologies.

Spelmakare.se
Discord
GitHub
Play Hive P v. S

Sort:  

Congratulations @smjn! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s):

You distributed more than 63000 upvotes.
Your next target is to reach 64000 upvotes.

You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

To support your work, I also upvoted your post!

Support the HiveBuzz project. Vote for our proposal!