samedi 23 mai 2020

Design pattern: Don't store direct references (memory leaks & code maintenance)

With a large project with many connected systems and moving parts (a video game, where a lot of state needs to be kept and used in different areas at different times), I am having a growing code maintenance problem where any references I assign to a created class instance object need to be cleaned up on destruction of said instance when it is no longer needed. Finding where all of these references might be is becoming a nuisance.

For this I was thinking about simply not storing references to actual objects directly in other areas of the code, but to instead just store them in a list where they can be accessed by some ID, and just store that ID on the interested parties, so that for deletion, all I have to worry about is removing the object from it's containing list and I don't have to worry about references to it hiding somewhere that prevents it from being garbage collected and persisting for longer than I want it to.

This way objects are no longer responsible for removing their own references to other objects. They should only be able to get temporary references to other objects, that they should not keep.

So something like:

const Spaceships = {};
class Spaceship {
    constructor(id, captain) {
        // Only actual reference to the object stored.
        Spaceships[id] = this;
        this.id = id;
        if (captain) {
            this.captain = captain;
        }
    }
    destroy() {
        // Can safely remove reference here and be confident it will be GCed.
        delete Spaceships[this.id];
    }
    // ES5 accessor syntax.
    get captain() {
        // If the object no longer exists it won't be returned.
        return Captains[this._captainID];
    }
    set captain(captain) {
        if (Captains[captain.id]) {
            this._captainID = captain.id;
            // Prevent setter call loop.
            if (captain.spaceship !== this) {
                captain.spaceship = this;
            }
        }
    }
    warp(location) {
        console.log(`${this.id}: Warping to ${location}`);
    }
}

const Captains = {};
class Captain {
    constructor(id, spaceship) {
        Captains[id] = this;
        this.id = id;
        if (spaceship) {
            this.spaceship = spaceship;
        }
    }
    destroy() {
        delete Captains[this.id];
    }
    get spaceship() {
        return Spaceships[this._spaceshipID];
    }
    set spaceship(spaceship) {
        if (Spaceships[spaceship.id]) {
            this._spaceshipID = spaceship.id;
            if (spaceship.captain !== this) {
                spaceship.captain = this;
            }
        }
    }
    speak(text) {
        console.log(`${this.id}: ${text}`);
    }
}

const kirk = new Captain("J. T. Kirk");
const enterprise = new Spaceship("NCC-1701", kirk);
enterprise.warp("Betazed");
// Destroy the object, which will make any IDs used to access it no longer work.
enterprise.destroy();
// The object would now still be usable for the scope of the variable it is
// assigned to, and no longer.
enterprise.warp("Vulcan");
// This new object would only receive an ID to another object that is no longer
// accessible. 
const spock = new Captain("Spock", enterprise);
// Calling this now would error as the object cannot be found. Would need to
// wrap each call in a check for object existence to handle errors.
// `if (kirk.spaceship) ...`
kirk.spaceship.warp("Andor");

Is there a name for this pattern?

Aucun commentaire:

Enregistrer un commentaire