When should you prefer inheritance?
Like S.Lotti said above when it has an clear and unambigous IS-A relationship with its parent. It's just that the explanation he gave you lacks an example:
Why model it as an IS-A relationship?
Because it implies strong behavioral subtyping which means you can treat all it's subtypes as substitutable, therefore you can safely assume you can treat them the same, i.e:
// - assume Cat, Dog & Bird
// are subtypes of Animal.
// - they just override `speak` without changing call signature
const animals = [cat, dog, bird]
animals.forEach(animal => animal.speak())
I could have any of those 3 instances be the animal in forEach and it wouldn't produce an error or cause them to speak something incorrectly.
Because I made sure to model the relationships in a way that's safe.
This is Liskov Substitution. It's applicable to both static and dynamic languages because behavioral subtyping cannot be checked by a compiler.
So if it semantically makes sense, you need all the methods of the parent and also satisfies Liskov then it is a good candidate for strong behavioral subtyping, which can be expressed via an IS-A relationship
The concepts of subclassing via inheritance and subtyping are not one and the same AFAIK; but the intent to express such a relationship is via inheritance.
Inheritance is the strongest coupling relationship. Often times too strong, inflexibly so.
What you're expressing is that the child class must have an is-a relationship with the parent and what you're implying is that type: child is a subset of type: parent, therefore the parent can be substituted with the child without the potential for crashes or incorrect behaviour.
When an is-a relationship is established, it's expected that you should be able to treat all subtypes uniformly, through their polymorphic interface without the potential for issues; otherwise an is-a relationship is incorrect.
The classroom example of a Liskov Violation is the Circle/Ellipse problem:
Example:
- I have an
Ellipse class
- I assume a
Circle is an Ellipse with a fixed rx/ry so I model it as: Circle extends Ellipse
class Ellipse {
constructor(rx, ry) {
this.rx = rx
this.ry = ry
}
resize(rx, ry) {
this.rx = rx
this.ry = ry
return this
}
}
class Circle extends Ellipse {
constructor(radius) {
super(radius, radius)
}
resize(radius) {
this.resizeWidth(radius)
this.resizeHeight(radius)
return this
}
}
// not really relevant,
// assume they are implemented
class Rectangle {}
class Triangle {}
Now suppose I have this list of shapes:
const shapes = [
new Ellipse(20, 30),
new Rectangle(20, 10),
new Triangle(10, 5, 10),
new Circle(10),
new Ellipse(20, 20)
]
I decide to resize all Circles:
shapes
.filter(shape => shape instanceof Circle)
.map(circle => circle.resize(50))
// Circle rx: 50, ry: 50
All good, no issues here.
But if I were to attempt a method on the parent:
shapes
.filter(shape => shape instanceof Ellipse)
.map(ellipse => ellipse.resizeWidth(50))
// Ellipse rx: 50, ry: 30
// Circle rx: 50, ry: 10 <-- oops
// Ellipse rx: 50, ry: 20
ok, so it didn't throw an Error.
... but the Circle was stretched horizontally;
therefore it's no longer a Circle.
It's invariant was violated and the program is now incorrect.
All I've done is a run-of-the-mill call of a method assumed to be polymorphic. So an is-a relationship is not correct, even though it looks very appropriate. A different & more complex hierarchy needs to be built if you need to squeeze as much code reuse & as much polymorphism as possible.
Mixins allow attaching behaviour without creating this is-a relationship.
They allow breaking up the parent type into multiple, smaller & more specific parent types from which you can selectively subtype, so the above principle isn't violated.
For example:
Note that you can make sure the Liskov principle isn't violated by simply avoid subtyping altogether.
But that means you lost the ability of treating all subsets of a type in a uniform method. So you have to find the right design that maximizes code reuse and correct & safe type relations.
I think mixins are just a type of than composition, usually found in languages that don't support multiple inheritance and need to emulate it.