For starters, a quick example of why people don't want you using class
.
I don't necessarily hate class
, but 90% of the reason to use class
is to get inheritance, and while it's occasionally helpful, it can frequently be very painful.
class Person { }
class ArmedPerson extends Person {
constructor (details) {
super(details);
const gun = new Gun();
this.equipment = { gun };
}
attack (target) {
this.equipment.gun.fireAt(target);
}
}
class Civilian extends Person { }
class ArmedCivilian extends ArmedPerson {
/* ... shouldn't it be extending Civilian?
Maybe all Civilians should be armed?
Is this why most games take place in the US?
*/
}
class Soldier extends ArmedPerson {
constructor (personDetails) {
super(personDetails);
}
}
class UnarmedSoldier extends Soldier {
/* HOW DO I TAKE HIS GUN AWAY? */
constructor (personDetails) {
super(personDetails);
}
attack () {
/* I know he has a gun, and anybody hacking the game can use it, but what do I do here? */
}
}
class
inheritance has shown itself to be one of those things that people have misused terribly, for the past 30+ years (just like every other useful tool out there).
Rather than inheritance, we can look at composition (via Dependency Inversion).
class Soldier {
constructor (personDetails, gun) {
/*...setup...*/
this.equipment = { gun };
this.selectedWeapon = gun;
}
attack (target) {
this.selectedWeapon.fireAt(target);
}
}
const soldier = new Soldier({ /*...details... */ }, new Gun());
Not a lot has changed, in terms of the end-result we wanted... we've been able to simplify really a lot, and now we can even give him a method to swap guns if we want, all because rather than bolting the gun into something that he inherits from, we're handing him a gun when we first meet him.
It could be any type of gun we want, as long as it can still be fired in a similar fashion.
The question arises:
are there better ways of making things reusable, then, if inheritance is completely off the table?
To that I say: inheritance shouldn't be completely off the table... ...it should just be so far off to the side that it should be an "aha" moment, when you discover that it really is the best and cleanest way to accomplish something (rather than attempting to inherit from .......something, anything, right now!).
Various languages have a concept referred to as Traits or Mix-Ins.
In something like Java, a close-ish approximation is Interfaces.
I'm not a huge fan of Interfaces (the structure, not the concept - love the concept).
In Java, Interfaces make you do more work, because they have you define the function, what the function takes, what it returns...
...but you can't give it any default behaviour (traditionally), so if you have 14 objects which implement the same interface, that's the same method you write out 14 times (plus the signature for the interface). Sometimes, those methods are going to be completely different in the specifics of the implementation; that's fine... ...sometimes, they'll be the exact same as what you intended when you wrote the interface to begin with.
That's less okay. Queue Traits; these are things which you define the interface of, define the behaviour for, and then copy onto your object.
In JS, we can even have some closure safety around them, by injecting context that they get to work from, rather than letting them assume they get to mess around with the entirety of this
.
const Movable = (pos) => ({
up (distance) { pos.y += distance; },
down (distance) { pos.y -= distance; },
left (distance) { pos.x -= distance; },
right (distance) { pos.x += distance; }
});
class Point {
constructor (x, y) {
Object.assign(this, { x, y });
}
}
class Person {
constructor (position) {
Object.assign(this, { position }, Movable(position));
}
}
const person = new Person( new Point(0, 0) );
person.up( 20 );
person.position.y; // 20
If you'll note, Movable is returning a new instance of an object, with methods which change values on position
. That object is having its methods copied onto the instance of person.
I can now create as many of these Traits as I'd like, and copy them onto as many objects as I'd like, and get reuse that way.