Inheritance in Details
- JavaScript remains a prototype based, although the
class
keyword was introduced inES2015
but is a syntactical sugar - Inheritance for JavaScript has only 1 construct
objects
- each object has a private property which holds a link to another object called
prototype
- that prototype object has a prototype of its own and so on until the object is reached with null as its prototype
- by definition
null
has no prototype and acts as the final link in the prototype chain
- by definition
- each object has a private property which holds a link to another object called
- nearly all objects in JavaScript are instances of
Object
which sits on the top of a prototype chain - the confusion is JavaScript's weakness
- however, the prototypical inheritance model itself is more powerful than the classic model
- e.g.: it is trivial to build a classic model on top of a prototypal model
- however, the prototypical inheritance model itself is more powerful than the classic model
- inheritance mechanisms play a key role in the object approach in terms of extensibility and reuse, model the relationship (
IS-A relationship
)- and exploit the relationship between the base class and its descendant
- Experienced developers can tell you that overuse of inheritance leads to code that is difficult to understand and maintain
- This is primarily because the
IS-A relationship
is much stronger than the relationship that appears during composition - Therefore, when making changes, need to be very careful and see if any methods have been overridden, what is the contract of the parent class, at the level of coupling
- This is primarily because the
- Inheritance is essentially an automatic message delegation mechanism
- Inheritance creates a relationship in which if one object cannot respond to a received message, it passes that message to another
- this transfer happens automatically
Recognizing Where to Use Inheritance
-
problem
class Bicycle {
constructor(options) {
// previous options
this.style = options.style;
this.frontShock = options.frontShock;
}
// becomes strange when a new style is added
spares() {
if (this.style === "road") {
return {
chain: "11-speed",
tireSize: "28",
tapeColor: this.tapeColor,
};
}
return {
chain: "11-speed",
tireSize: "29",
frontShock: this.frontShock,
};
}
}
const crossCountryBike = new Bicycle({
style: "XC",
size: "M",
frontShock: "mountain",
});
const roadBike = new Bicycle({style: "road", size: "M", tapeColor: "red"});
Creating an Abstract Superclass with Shared Abstract Behavior
- abstract class
- can store both abstract and concrete methods
- abstract methods must be implemented to use, concrete methods can be used without overriding them
- all the abstract methods must be implemented in every non-abstract subclass
- You always need to implement all the abstract methods and there should be no unused behavior in any subclass, if such thing happens this may be a sign of incorrect inheritance structure
- stores the behavior which is common to all subclasses
- subclasses inherited an abstract class should fully use its functionality, otherwise you need to review the inheritance structure
- can store both abstract and concrete methods
-
solution: use Abstract class and inherit
abstract class Bicycle {
// keep only common parts
}
class RoadBike extends Bicycle {
constructor(options) {
super(options);
this.tapeColor = options.tapeColor;
}
spares() {
return {
...super.spares(),
tapeColor: this.tapeColor,
};
}
}
class MountainBike extends Bicycle {
constructor(options) {
super(options);
this.frontShock = options.frontShock;
}
spares() {
return {
...super.spares(),
frontShock: this.frontShock,
};
}
} -
Abstract classes exist in order to inherit from them
- They provide a common repository that stores the behavior common to all subclasses
- each of them is a specialization of an abstract class
- It almost never makes sense to create an abstract superclass with a single subclass
Template Method Pattern: Default Implementation
-
This gives subclasses the ability to inject specialization by overriding the default values set in the parent class
-
This technique of describing the basic structure/algorithm in a superclass and redefining parts of this structure/algorithm to those that are already specific for a particular class is called the template method
-
allows you to define the base algorithm in the superclass and control its lifecycle and then override only needed parts in the subclass
-
solution: use Template method
abstract class Bicycle {
protected readonly defaultChain = "11-speed";
constructor(opts) {
// ...
this.chain = opts.chain || this.defaultChain;
this.tireSize = opts.tireSize || this.defaultTireSize;
}
}
class RoadBike extends Bicycle {
protected readonly defaultTireSize = "28";
}
class MountainBike extends Bicycle {
protected readonly defaultTireSize = "29";
} -
now there are new problems:
- Mountain bike and road bike classes depend on their abstract class
- Abstract class depends on children
- If you forget to call super methods – the result might not contain all data required
- Users of road and mountain bike depend on the abstract class, even if they don't know anything about it
Using Hook Messages: Decoupling Subclasses
-
This strategy removes the knowledge of the algorithm from the subclass and returns control to the superclass
- Which was done by adding the postInitialize method
-
solution: Using Hook Messages
abstract class Bicycle {
constructor(opts) {
this.size = opts.size;
this.chain = opts.chain;
this.tireSize = opts.tireSize;
this.postInitialize(opts);
}
protected postInitialize() {}
spares() {
return {
tireSize: this.tireSize,
chain: this.chain,
...this.localSpares(),
};
}
}
class RoadBike extends Bicycle {
protected postInitialize(opts) {
this.tapeColor = opts.tapeColor;
}
protected localSpares() {
return {tapeColor: this.tapeColor};
}
}
class MountainBike extends Bicycle {
protected postInitialize(opts) {
this.frontShock = opts.frontShock;
}
protected localSpares() {
return {frontShock: this.frontShock};
}
} -
RoadBike and MountainBike no longer control the initialization process
- but instead bring specialization to a more abstract algorithm
- This algorithm is defined in the abstract superclass Bicycle, which in turn is responsible for sending postInitialize
- To achieve this result Bicycle constructor should always be called, this will happen automatically if derived classes will have no constructor
-
This same technique can be used to remove the dispatch of super in the spares method
Summary
- Inheritance solves the problem of related types that share a great deal of common behavior but differ across some dimension
- The best way to create an abstract superclass is by pushing code up from concrete subclasses
- Abstract superclasses use the template method pattern to invite inheritors to supply specializations
- they use hook methods to allow these inheritors to contribute these specializations without being forced to send super
- Well-designed inheritance hierarchies are easy to extend with new subclasses, even for programmers who know very little about the application
- what is the disadvantage of multiple inheritance
- ambiguity can arise
- this will allow you to decouple parent classes from each other and use only the functionality you need
- with this inheritance scheme, ambiguity can arise
- ambiguity can arise