- 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
-
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"});
- 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
-
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
-
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
- 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