javascriptclassextends

How to avoid repetitive code when different classes use the same methods but have different constructors?


I've been working on a code for a couple of months and it works fine, but the more features I add, the more I would like to reduce the amount of code somehow, since I use the same functions in various classes.

The code consists of one of original class called "Shape" here where various functions are created. Then there are various new classes called "Rectangle" and "Hexagon" here. In the constructor of those, the class "Shape" is called up with the lines "this.rec = new Shape {...}" and "this.hex = new Shape {...}". Otherwise the classes "Rectangle", "Hexagon" and so on use the same functions (like for example "plotShape") with the only difference being the reference of arrays and such being "this.rec.xCoordinates" in the class "Rectangle" and "this.hex.xCoordinates" in the class "Hexagon".

Later in the code those classes are used with the lines "recShape = new Rectangle {...}" and "hexShape = new Hexagon {...}".

So my question is, what is a good way to reduce the repetition of code?

First of, a few snippets of code to better understand the what I am talking about.

Here is the constructor of the original class plus a function included in the class:

export class Shape {
    constructor (options) {
        this.size = options.size
        this.type = options.type
        switch (this.type) {
            ...
            case 4:
                this.gridsizex = options.gridsizex
                this.gridsizey = options.gridsizey
                break
            case 6:
                this.gridsize = options.gridsize
                break
        }
        this.xCoordinates = new Float64Array()
        this.yCoordinates = new Float64Array()
    }

    createShape() {
        this.xCoordinates = new Float64Array(this.size * this.size)
        this.yCoordinates = new Float64Array(this.size * this.size)
        switch (this.type) {
            ...
            case 4:
                for (let i = 0; i < this.size; i++) {
                    for (let j = 0; j < this.size; j++) {
                        let pos = i * this.size + j
                        this.yCoordinates[pos] = i * this.gridsizey
                        this.xCoordinates[pos] = j * this.gridsizex
                    }
                }
                break
            case 6:
                let distXh = this.gridsize
                let distYh = distXh * ...
                for (let i = 0; i < this.size; i++) {
                    for (let j = 0; j < this.size; j++) {
                        let pos = i*this.size+j
                        this.yCoordinates[pos] = i * distYh
                        this.xCoordinates[pos] = (j * distXh + i * (distXh / 2))
                        while ((this.xCoordinates[pos] - (this.size * distXh)) >= 0) {
                            this.xCoordinates[pos] -= (this.size * distXh)
                        }
                    }
                }
                break
        }
    }
}

And here are the classes created from that OG class as well as the plotShape function. In the full code, there are more functions (using functions and arrays from the Shape class) in each class, but just like the plotShape function they are basically identical.

class Rectangle {
    constructor (options) {
        this.size = options.size
        this.gridsizex = options.gridsizex
        this.gridsizey = options.gridsizey
        this.rec = new Shape ({
            size: this.size,
            type: 4,
            gridsizex: this.gridsizex
            grdisizey: this.gridsizey
        })
        this.rec.createShape()
    }
    
    plotShape (plot) {
        plot.data = {
            xAxis: this.rec.xCoordinates,
            yAxis: this.rec.yCoordinates,
        }
    }
}


class Hexagon {
    constructor (options) {
        this.size = options.size
        this.gridsize = options.gridsize
        this.hex = new Shape ({
            size: this.size,
            type: 6,
            gridsize: this.gridsize
        })
        this.hex.createShape()
    }
    
    plotShape (plot) {
        plot.data = {
            xAxis: this.hex.xCoordinates,
            yAxis: this.hex.yCoordinates,
        }
    }
}


recShape = new Retangle({
    size: 6,
    gridsizex: 3,
    gridsizey: 2,
})

hexShape = new Hexagon ({
    size: 6,
    gridsize: 1,
})

function depiction(){
    recShape.plotShape(shapePlot)
    hexShape.plotShape(shapePlot)
}

I've been attempting to create a new class called "Forms" where I would create all the functions and then extend the classes "Rectangle", "Hexagon" and so on to the class "Forms". Those subclasses could then use all those functions together and I would save a lot of lines of code. But because I call the class "Shape" in all classes differently, I am not quite sure how to do it, because I always arrive at a point where I realize this won't work. Another way might be the use of static methods, however I still haven't really understood how they work.

That being said, I have no code to show of previous attempts because I always realized at one point or another that it won't work.


Solution

  • You are using the aggregation pattern, while here plain inheritance is the better option. It's the difference between saying that a hexagon has a shape, and that a hexagon is a shape. The latter is inheritance.

    Related to that, you shouldn't need a type property. This follows from the prototype of the object you create, and you can test hexShape instanceof Hexagon instead of hexShape.type == 6.

    The Shape constructor thus only needs a size, and I would therefor not pass that as an option object -- it seems overkill unless you have other options that apply to all shapes.

    Your code seems to represent bits and pieces of a large code base, so I'll just revamp the same bits and pieces into that pattern:

    class Shape {
        constructor(size) {
            this.size = size;
        }
    
        createGrid() {
            this.xCoordinates = new Float64Array(this.size * this.size);
            this.yCoordinates = new Float64Array(this.size * this.size);
        }
    
        plotGrid(plot) {
            plot.data = {
                xAxis: this.xCoordinates,
                yAxis: this.yCoordinates,
            }
            //...
        }
    }
    
    class Rectangle extends Shape {
        constructor (options) {
            super(options.size);
            this.gridsizex = options.gridsizex;
            this.gridsizey = options.gridsizey;
            this.createGrid();
        }
        createGrid() {
            super.createGrid();
            for (let i = 0; i < this.size; i++) {
                for (let j = 0; j < this.size; j++) {
                    let pos = i * this.size + j
                    this.yCoordinates[pos] = i * this.gridsizey
                    this.xCoordinates[pos] = j * this.gridsizex
                }
            }
        }
    }
    
    class Hexagon extends Shape {
        constructor(options) {
            super(options.size);
            this.gridsize = options.gridsize;
            this.createGrid();
        }
        createGrid() {
            super.createGrid();
            let distXh = this.gridsize;
            let distYh = distXh /* ... */
            for (let i = 0; i < this.size; i++) {
                for (let j = 0; j < this.size; j++) {
                    let pos = i*this.size+j;
                    this.yCoordinates[pos] = i * distYh;
                    this.xCoordinates[pos] = (j * distXh + i * (distXh / 2));
                    while ((this.xCoordinates[pos] - (this.size * distXh)) >= 0) {
                        this.xCoordinates[pos] -= (this.size * distXh);
                    }
                }
            }
        }
    }
    
    const recShape = new Rectangle({
        size: 6,
        gridsizex: 3,
        gridsizey: 2,
    });
    
    const hexShape = new Hexagon({
        size: 6,
        gridsize: 1,
    });
    
    function depiction(shapePlot) {
        recShape.plotGrid(shapePlot);
        hexShape.plotGrid(shapePlot);
    }