1
\$\begingroup\$

I have a Vector and 2-dimensional Vector, defined as follows:

    // A general vector
    class Vector {
        values: number[];

        constructor (...values: number[]) {
            this.values = values;
        }

        opposite(): Vector {
            return new Vector(...this.values.map((value, index) => -value));
        }
    }

    // Two-dimensional vector
    class Vector2 extends Vector {
        constructor(x: number, y: number) {
            super(x, y);
        }

        get x() {
            return this.values[0];
        }

        get y() {
            return this.values[1];
        }
    }

And I have the following code:

    let v = new Vector2(1, 2);
    let w = v.opposite();

    // this is wrong, since w is not a 2-dimensional vector
    console.log(w.x);

How could I solve this inheritance problem elegantly? For me, there are two alternatives:

  1. Override the "opposite" method. I think that this is not a good idea, since currently there is only one method, but there could be a lot of them.

    // Two-dimensional vector
    class Vector2 extends Vector {
        // ... more methods ... blah blah blah ...
        // overrides the super class method
        opposite(): Vector2 {
            let [x, y] = super.opposite().values;
            return new Vector2(x, y);
        }
    }
    
  2. Create a named constructor. I think this is a good solution, but a bit tedious:

    // Two-dimensional vector
    class Vector2 extends Vector {
        constructor(x: number, y: number) {
            super(x, y);
        }
    
        // declare a "named constructor"
        static createFromVector(v: Vector): Vector2 {
            let [x, y] = v.values;
            return new Vector2(x, y);
        }
    
        // ... more methods ... blah blah blah ...
    }
    
    let v = new Vector2(1, 2);
    // It's good, but a bit tedious writing constantly "createFromVector"
    let w = Vector2.createFromVector(v.opposite());
    

How do you address this problem?

\$\endgroup\$
5
  • \$\begingroup\$ Side note. The Vector2 is not an two-dimensional vector. It is a vector of rank equal to 2 (since it only has 2 elements). It is still using a single-dimension JS array under the hood. \$\endgroup\$ Commented May 16, 2017 at 17:39
  • \$\begingroup\$ Well, it declares also two getters: x and y. It enriches the definition of a general vectors. \$\endgroup\$
    – Cequiel
    Commented May 16, 2017 at 17:42
  • \$\begingroup\$ @Cequiel I understand that, but it is not a two-dimensional Vector. That's all I said. :) \$\endgroup\$ Commented May 16, 2017 at 17:43
  • 1
    \$\begingroup\$ The title of your question should indicate the purpose of the code, rather than what you want to gain from the review. Please read How to Ask, then edit your question accordingly. \$\endgroup\$ Commented May 16, 2017 at 17:45
  • \$\begingroup\$ As a side note: the elegant solution to this problem is to use a dependently-typed language. \$\endgroup\$
    – gardenhead
    Commented May 17, 2017 at 1:43

1 Answer 1

3
\$\begingroup\$
let v = new Vector2(1, 2);
let w = v.opposite();
// this is wrong, since w is not a 2-dimensional vector
console.log(w.x);

The fact that the .opposite() returns a result that does not fit into the expected use cases, suggests that there is a design issue. In other words, it is a "hint" that Vector2 is not really a Vector in terms of OOP. Why so? Because it does not follow the intuitive contract in which the .opposite() method returns same exact type of the vector as the one on which the method is being invoked. Things will only get worse if one will need to work against a Vector3, Vector4, etc.

There's whole bunch of sources that explain how inheritance is very often not the best way to create new types nor reuse code.

If I was to design the VectorOf2, I would make it wrap the Vector class rather than inherit from it.

class VectorOf2 {
    private _vector: Vector;

    constructor(x: number, y: number) {
        this._vector = new Vector(x, y);
    }

    get x(): number {
        return this._vector[0];
    }

    get y(): number {
        return this._vector[1];
    }

    opposite(): VectorOf2 {
        return new VectorOf2( ... );
    }
}

With this design, each method added to Vector2 is always Vector2-specific and there's no confusion about it. The regular Vector's method can still be reused by VectorOf2 when a new method is implemented.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.