Inheritance
Inheritance is a fundamental concept in OOP. It allows you to create a new class (called a subclass or child class) based on an existing class (called a superclass or parent class). The child class inherits properties and methods from the parent class. Inheritance allows you to establish a relationship between classes by creating a common set of properties and methods in a parent class and then create more specialized child classes that inherit these features.
Using our previous examples, let’s say we wanted to include different type of specific order, say coffee order and wine orders. All orders have an orderId
and customerName
. So we put those in the Order
class.
// Parent class
class Order {
// Common fields for all orders
#orderId;
#customerName;
constructor(orderId, customerName) {
this.#orderId = orderId;
this.#customerName = customerName;
}
// Getter and Setters ... omitted for brevity
// Common method for all orders
printOrderDetails() {
console.log(`Order Number: ${this.orderId}, Customer Name: ${this.customerName}`);
}
// Common method for all orders
processOrder() {
console.log(`Processing order #${this.orderId} for ${this.customerName}.`);
}
}
Extends keyword
For the coffee and wine order classes we only include fields and methods unique to that type of order. The extends keyword indicates that the class is inheriting from another class, in this case Order. Creating an *Is-A relationship. As in, a coffee order is an order and therefore should have all the fields and methods of an order too. These are automatically available to the inheriting child class. No need to repeat them.
NOTE
// CoffeeOrder child class
class CoffeeOrder extends Order {
#qty;
#type;
constructor(orderId, customerName, qty, type) {
super(orderId, customerName); // Calls the constructor of the parent class (Order)
this.#qty = qty;
this.#type = type;
}
// Getter and Setters ... omitted for brevity
// Method unique to CoffeeOrder
printCoffeeDetails() {
console.log(`Coffee Order: ${this.qty} x ${this.type}`);
}
}
// WineOrder child class
class WineOrder extends Order {
#bottleCount;
#wineType;
constructor(orderId, customerName, bottleCount, wineType) {
super(orderId, customerName); // Calls the constructor of the parent class (Order)
this.#bottleCount = bottleCount;
this.#wineType = wineType;
}
// Getter and Setters ... omitted for brevity
// Method unique to WineOrder
printWineDetails() {
console.log(`Wine Order: ${this.bottleCount} bottles of ${this.wineType}`);
}
}
// Example usage
const generalOrder = new Order(1, 'Alice');
generalOrder.printOrderDetails(); // Order Number: 1, Customer Name: Alice
generalOrder.processOrder(); // Processing order #1 for Alice.
const coffeeOrder = new CoffeeOrder(2, 'Bob', 3, 'Latte');
coffeeOrder.printOrderDetails(); // Order Number: 2, Customer Name: Bob (inherited method)
coffeeOrder.processOrder(); // Processing order #2 for Bob. (inherited method)
coffeeOrder.printCoffeeDetails(); // Coffee Order: 3 x Latte (unique method)
const wineOrder = new WineOrder(3, 'Charlie', 5, 'Merlot');
wineOrder.printOrderDetails(); // Order Number: 3, Customer Name: Charlie (inherited method)
wineOrder.processOrder(); // Processing order #3 for Charlie. (inherited method)
wineOrder.printWineDetails(); // Wine Order: 5 bottles of Merlot (unique method)
// Using getters and setters for CoffeeOrder
coffeeOrder.qty = 4;
coffeeOrder.type = 'Espresso';
console.log(`Updated Coffee Order: ${coffeeOrder.qty} x ${coffeeOrder.type}`); // Updated Coffee Order: 4 x Espresso
// Using getters and setters for WineOrder
wineOrder.bottleCount = 6;
wineOrder.wineType = 'Chardonnay';
console.log(`Updated Wine Order: ${wineOrder.bottleCount} bottles of ${wineOrder.wineType}`); // Updated Wine Order: 6 bottles of Chardonnay
The CoffeeOrder
and WineORder
objects inherit the properties and methods of Order
objects. In this example, a CoffeeOrder
is an Order
and should therefore contain these shared properties and methods that all Orders
have. In this way, there is no need to redefine them in the CoffeeOrder
class, as they can be passed down through inheritance. If other types are required, for example, a DinnerOrder
, the Order
can be used again to pass on shared properties and methods. Only properties and methods that are unique to the child are included in it.
A general rule of thumb in OOP is that whenever properties or methods appear to be common or shared among objects they should be placed in a parent class and inherited.
Polymorphism
Polymorphism is an approach that can be used if a child class is inheriting from a parent class. It allows a child class to decide how to execute a method.
Let’s first look at a simple example. All bears need to find food. But different types of bears do this differently.
// Parent class
class Bear {
constructor(name, age) {
this.name = name;
this.age = age;
}
// Common method for all bears
hibernate() {
console.log(`${this.name} is hibernating.`);
}
// Method to be overridden by child classes
findFood() {
console.log(`${this.name} is looking for food.`);
}
}
// GrizzlyBear child class
class GrizzlyBear extends Bear {
constructor(name, age) {
super(name, age); // Calls the constructor of the parent class (Bear)
}
// Overriding the method
findFood() {
console.log(`${this.name} the grizzly bear is catching fish in the river.`);
}
}
// PolarBear child class
class PolarBear extends Bear {
constructor(name, age) {
super(name, age); // Calls the constructor of the parent class (Bear)
}
// Overriding the method
findFood() {
console.log(`${this.name} the polar bear is hunting seals on the ice.`);
}
}
// Example usage
const grizzlyBear = new GrizzlyBear('Grizzly', 10);
const polarBear = new PolarBear('Polar', 5);
grizzlyBear.findFood(); // Grizzly the grizzly bear is catching fish in the river.
polarBear.findFood(); // Polar the polar bear is hunting seals on the ice.
With polymorphism set up, we simply ask any bear object to findFood and it will execute it differently according to the type of bear it is.
To get a better understanding of how this works and why me might want to use polymorphism let’s consider the previous example using orders. The Order parent class has a processOrder method but wine orders and coffee orders are processed differently. It may seem logical to simply include a new method in each child class, shown below.
// Parent class
class Order {
// Common fields for all orders
#orderId;
#customerName;
constructor(orderId, customerName) {
this.#orderId = orderId;
this.#customerName = customerName;
}
// Getter and Setters ... omitted for brevity
// Common method for all orders
printOrderDetails() {
console.log(`Order Number: ${this.orderId}, Customer Name: ${this.customerName}`);
}
// Common method for all orders
processOrder() {
console.log(`Processing order #${this.orderId} for ${this.customerName}.`);
}
}
// snippet from Wine class
processWineOrder() {
console.log(
`Processing wine order #${this.orderId} for ${this.customerName}.`
);
console.log(`Wine Order: ${this.#bottleCount} x ${this.#wineType}`);
let total = 0;
switch (this.#wineType) {
case "Pino":
total = this.#bottleCount * 200;
break;
case "Merlot":
total = this.#bottleCount * 100;
break;
case "Champagne":
total = this.#bottleCount * 400;
break;
default:
break;
}
console.log(`Total: ${total}`);
}
// snippet from Coffee class
processCoffeeOrder() {
console.log(
`Processing coffee order #${this.orderId} for ${this.customerName}.`
);
console.log(`Coffee Order: ${this.qty} x ${this.type}`);
let total = 0;
switch (this.type) {
case "Latte":
total = this.#qty * 20;
break;
case "Americano":
total = this.#qty * 10;
break;
case "Cappuccino":
total = this.#qty * 15;
break;
default:
break;
}
console.log(`Total: ${total}`);
}
const coffeeOrder = new CoffeeOrder(2, "Bob", 3, "Latte");
const wineOrder = new WineOrder(3, "Charlie", 5, "Merlot");
coffeeOrder.processCoffeeOrder();
wineOrder.processWineOrder();
This works but it results in some issues. For example, it may be confusing when working with these objects. Which method should be used to process the order, processOrder
or processCoffeeOrder
? and each new child class of order (dinnerOrder) would need to provide a new method name for their processOrder
method.
Instead, the child class can override the processOrder method. This way, we can still call processOrder on an object but it will use it’s own version of it. To override a method in JS we provide a method with the exact name as the method we want to override. If no overridden method is provided in a child class, the one in the parent will be used by default. Below is the refactored version of the above example.
// snippet from Wine class
// note this method has the same name as in the Order class
processOrder() {
console.log(
`Processing wine order #${this.orderId} for ${this.customerName}.`
);
console.log(`Wine Order: ${this.#bottleCount} x ${this.#wineType}`);
let total = 0;
switch (this.#wineType) {
case "Pino":
total = this.#bottleCount * 200;
break;
case "Merlot":
total = this.#bottleCount * 100;
break;
case "Champagne":
total = this.#bottleCount * 400;
break;
default:
break;
}
console.log(`Total: ${total}`);
}
// snippet from Coffee class
// note this method has the same name as in the Order class
processOrder() {
console.log(
`Processing coffee order #${this.orderId} for ${this.customerName}.`
);
console.log(`Coffee Order: ${this.qty} x ${this.type}`);
let total = 0;
switch (this.type) {
case "Latte":
total = this.#qty * 20;
break;
case "Americano":
total = this.#qty * 10;
break;
case "Cappuccino":
total = this.#qty * 15;
break;
default:
break;
}
console.log(`Total: ${total}`);
}
const coffeeOrder = new CoffeeOrder(2, "Bob", 3, "Latte");
const wineOrder = new WineOrder(3, "Charlie", 5, "Merlot");
coffeeOrder.processOrder();
wineOrder.processOrder();
//output
// Processing coffee order #2 for Bob.
// Coffee Order: 3 x Latte
// Total: 60
// Processing wine order #3 for Charlie.
// Wine Order: 5 x Merlot
// Total: 500
Now, when we call processOrder()
on these objects, the processOrder
method in the child classes is invoked instead of the one in the parent class. The method names are still the same but the result is different depending on the class being used. This is the essence of polymorphism, the child classes can do some common methods differently based on it’s type. It provides consistency in method interaction but automatic specificity in execution. The word ‘Polymorphism’ in Greek means “having many forms”, i.e. many forms of the same behaviour for example finding food or processing an order.
NOTE
Lesson task
Goal
To be able to implement inheritance and polymorphism on an inheriting class.
Brief
We are going to modify our previous lessons task’s class based on what you have learnt so far. Extend the Person class to create an Employee class, demonstrating inheritance and polymorphism.
Level 1 process
Create a class called Employee that extends the Person class.
Add private fields jobTitle and salary to the Employee class.
Add a method getJobDetails that logs the job title and salary.
Override the introduce method in the Employee class to include the job title in the greeting.
Create instances of both Person and Employee, and use invoke the introduce method on both objects to demonstrate polymorphism.