Inheritance in OOP

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.

javascript
	// 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
javascript
	// 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.

javascript
	// 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.

javascript
	// 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}.`);
	  }
	}
javascript
	// 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}`);
	  }
javascript
	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.

javascript
	// 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}`);
	  }
javascript
	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

  1. Create a class called Employee that extends the Person class.

  2. Add private fields jobTitle and salary to the Employee class.

  3. Add a method getJobDetails that logs the job title and salary.

  4. Override the introduce method in the Employee class to include the job title in the greeting.

  5. Create instances of both Person and Employee, and use invoke the introduce method on both objects to demonstrate polymorphism.

Additional resources

MDN: Classes

MDN: Inheritance

Javascript.info: Class