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:
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); } }
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?
1 Answer 1
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.
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\$