This is a case of:
- Methods that do way too much
- A lack of calling out to methods of
super
- Deeply-nested hierarchies being a painful way of writing maintainable systems; multiplied by 4000x in JS
First:
// assume
class A extends Array {
doX (x) {
console.log(`A.doX(${x})`);
return new A();
}
}
class B extends A {
doX (x, y) {
super.doX(x);
console.log(`B.doX(${x}, ${y})`);
return new B();
}
}
class C extends B {
doX (x, y, z) {
super.doX(x, y);
console.log(`C.doX(${x}, ${y}, ${z})`);
return new C();
}
}
const c = new C();
c.doX(1, 2, 3);
// "A.doX(1)"
// "B.doX(1, 2)"
// "C.doX(1, 2, 3)"
Now, I should expect to have no problems whatsoever running this code.
I will have a hell of a time actually making use of it, though.
Why? Because I made the decision to overload the method to worry about construction and logic at the same time.
The A class will always return an A, the B class will always return a B, despite the fact that the super call actually returns a completely different object (a new A).
The C returns a new C, despite the fact that it's already created an A and a B (but has no reference to A).
So what do you do?
A couple of thoughts:
- Move the construction of the object way the heck out of the object, and pass it in, instead
- Separate your data object from your library
So let's try construction separation
class Person {
static make () { return new Person(); }
setDetails (name, age) {
this.name = name;
this.age = age;
}
clone (cfg = this) {
const person = Person.make();
person.setDetails(cfg.name, cfg.age);
return person;
}
}
class Employee extends Person {
static make () { return new Employee(); }
setDetails (name, age, id) {
super.setDetails(name, age);
this.id = id;
}
clone (cfg = this) {
const employee = Employee.make();
employee.setDetails(cfg.name, cfg.age, cfg.id);
return employee;
}
}
You'll note that the clone
methods are not inherited. They are focused on instantiation. They are basically factories (which is really the realm of a separate object, or a static method, like make|from|of|empty|unit
).
Then they are calling setDetails
which is an instance method, which is basically doing what the constructor should have done, or what a factory should do, and does have inherited behaviour.
Speaking of DRY, inheritance is sort of a terrible way of staying that way. Just from what I've written, how many lines have been dedicated either to overriding constructors (clone
, make
), or calling out to parents (super
) or even just worrying about extension, just because?
This brings me to a different pattern: libraries, pure functions/methods, and decoration.
If you don't care about the actual "type" (and in raw JS, while helpful in the console, you ought not, because it's useless elsewhere), then you can make happily data-centric objects.
Structs, just like you would see in C, or Go, or Python, or the like.
Then you would be free to write all of the reusable calculations that you could possibly desire on libraries/services, which you use upon those structs (or ideally copies thereof).
class Vector {
static make2D (x = 0, y = 0) {
return { x, y };
}
static make3D (x = 0, y = 0, z = 0) {
return { ...Vector.make2D(x, y), z };
}
static add2D (v1, v2) {
return Vector.make2D(v1.x + v2.x, v1.y + v2.y);
}
}
const vectors = [
{ x: 0, y: 1 },
{ x: 32, y: 8 },
{ x: 10, y: 12 },
{ x: 0, y: 0 },
];
const vectorSum = vectors.reduce(Vector.add2D, Vector.make2D());
vectorSum; // { x: 42, y: 21 }
If you really needed them to be typed, then you could do something like:
class Vector {
add2D (
{x: x1, y: y1},
{x: x2, y: y2}
) {
return Vector2D.of(x1 + x2, y1 + y2);
}
}
class Vector2D {
constructor (x, y) {
return Object.assign(this, { x, y });
}
static of (x, y) { return new Vector2D(x, y); }
static from ({ x, y }) { return Vector2D.of(x, y); }
static empty () { return Vector2D.of(0, 0); }
}
const vector = vectors
.map(Vector2D.from)
.reduce(Vector.add2D, Vector2D.empty());
// Vector2D { x: 42, y: 21 }
You'd have a hard time complaining about your code not being DRY at that point. You could even put 5D vectors into the entry data, and you would get correct 2D vectors out...