Main menu

Pages

Creating Class Hierarchies with Inheritance and Prototypal Chains in JavaScript

1. Introduction

Inheritance and prototypal chains form the cornerstone of object-oriented programming in JavaScript, enabling developers to craft well-organized and efficient class hierarchies. Central to this concept is the capacity to fashion novel classes that inherit properties and behaviors from pre-existing classes, thereby streamlining code development and augmenting code reusability.

Within this guide, we shall delve into the foundational principles that underpin inheritance, explore its implementation through prototypes, and navigate the intricate network of relationships that materialize within class hierarchies. Throughout this guide, we shall equip ourselves with the knowledge and tools essential for constructing robust and scalable applications. Harnessing the potency of inheritance, we shall artfully structure our codebase and foster modular design.

2. Creating Constructors and Prototype Objects

2.1 Using Constructor Functions to Create Objects

In JavaScript, constructor functions are used to create objects with a specific blueprint or template. They act as a blueprint for creating multiple similar objects with shared properties and methods. In the case of a digital product e-commerce application, let's consider creating a constructor for the "Product" object.


// Constructor function for creating Product objects
function Product(name, price, category) {
  this.name = name;
  this.price = price;
  this.category = category;
}

// Creating a new Product object using the constructor
const product1 = new Product('Smartphone', 599, 'Electronics');
console.log(product1); // Output: Product { name: 'Smartphone', price: 599, category: 'Electronics' }

Explanation:
In this example, we created a constructor function Product that takes in three parameters: name, price, and category. Inside the constructor, we use the this keyword to set the properties of the newly created object. The new keyword is then used to instantiate a new Product object, and we store it in the variable product1.

2.2 The Prototype Property and Its Significance

In JavaScript, every function has a special property called prototype, which allows us to add properties and methods that will be shared among all instances created from that constructor. This is particularly useful for methods that are not meant to be duplicated in each object, saving memory and improving performance.


// Adding a shared method to the Product constructor using the prototype
Product.prototype.displayInfo = function() {
  return `${this.name} - $${this.price} - Category: ${this.category}`;
};

// Creating a new Product object and calling the shared method
const product2 = new Product('Laptop', 899, 'Electronics');
console.log(product2.displayInfo()); // Output: Laptop - $899 - Category: Electronics

Explanation:
In this example, we added a method displayInfo to the Product constructor's prototype. This method can be accessed by any instance of Product, and it allows us to display relevant information about the product.

2.3 Linking Objects Through the Prototype Chain

In JavaScript, objects created from a constructor inherit properties and methods from the constructor's prototype. This forms a prototype chain, allowing objects to access properties and methods of their parent constructors. If a property or method is not found in the object itself, JavaScript looks up the prototype chain until it finds it or reaches the end of the chain.


// Parent constructor
function DigitalProduct(name, price, category, format) {
  Product.call(this, name, price, category);
  this.format = format;
}

// Linking DigitalProduct prototype to Product prototype (Inheritance)
DigitalProduct.prototype = Object.create(Product.prototype);

// Adding a method specific to DigitalProduct
DigitalProduct.prototype.displayFormat = function() {
  return `Format: ${this.format}`;
};

// Creating a new DigitalProduct object and calling shared and specific methods
const digitalProduct1 = new DigitalProduct('eBook', 19.99, 'Books', 'PDF');
console.log(digitalProduct1.displayInfo()); // Output: eBook - $19.99 - Category: Books
console.log(digitalProduct1.displayFormat()); // Output: Format: PDF

Explanation:
In this example, we created a new constructor DigitalProduct, which represents digital products like eBooks. We linked its prototype to the Product prototype using Object.create(Product.prototype). Now, DigitalProduct instances have access to both the shared methods from Product and their specific methods defined within DigitalProduct.

Note:

The moment we utilize constructors and prototypes, we can efficiently create and manage a digital product e-commerce application, organizing and sharing code effectively among different product types. The prototype chain ensures that objects have access to the properties and methods they need, making our code cleaner and more maintainable.

3. Implementing Single Inheritance

3.1. Creating a Simple Parent-Child Class Relationship

Single inheritance involves creating a relationship between a parent class and a child class. In the context of our digital product e-commerce application, let's consider creating a new class called PhysicalProduct, which will inherit from the Product class we defined earlier. PhysicalProduct will represent physical items such as books or electronics.


// Child class PhysicalProduct inheriting from the parent class Product
function PhysicalProduct(name, price, category, weight) {
  Product.call(this, name, price, category);
  this.weight = weight;
}

// Creating a new PhysicalProduct object
const physicalProduct1 = new PhysicalProduct('Smartphone', 599, 'Electronics', '150g');
console.log(physicalProduct1); // Output: PhysicalProduct { name: 'Smartphone', price: 599, category: 'Electronics', weight: '150g' }

Explanation:
In this example, we created a new constructor PhysicalProduct, representing physical products with an additional property called weight. We used Product.call(this, name, price, category) to invoke the Product constructor within PhysicalProduct, ensuring the properties of the parent class are properly initialized.

3.2. Accessing Properties and Methods from the Parent Class

One of the key benefits of inheritance is the ability for child classes to access properties and methods defined in the parent class. Child instances inherit these properties and methods, allowing us to reuse existing code and avoid duplication.


// Extending the prototype of PhysicalProduct from the prototype of Product
PhysicalProduct.prototype = Object.create(Product.prototype);

// Adding a method specific to PhysicalProduct
PhysicalProduct.prototype.displayWeight = function() {
  return `Weight: ${this.weight}`;
};

// Creating a new PhysicalProduct object and calling methods from both parent and child classes
const physicalProduct2 = new PhysicalProduct('Book', 24.99, 'Books', '500g');
console.log(physicalProduct2.displayInfo()); // Output: Book - $24.99 - Category: Books
console.log(physicalProduct2.displayWeight()); // Output: Weight: 500g

Explanation:
In this example, we extended the prototype of PhysicalProduct from the prototype of Product using Object.create(Product.prototype). As a result, PhysicalProduct instances can now access and use the shared displayInfo() method from the parent class Product.

3.3. Overriding Methods in the Child Class

Sometimes, the behavior of a method in the child class may need to be different from the parent class. Inheritance allows us to override methods in the child class, providing a specific implementation that differs from the parent's behavior.


// Overriding the displayInfo method in the PhysicalProduct class
PhysicalProduct.prototype.displayInfo = function() {
  return `Product Name: ${this.name}, Price: $${this.price}, Weight: ${this.weight}, Category: ${this.category}`;
};

// Creating a new PhysicalProduct object and calling the overridden method
const physicalProduct3 = new PhysicalProduct('DVD', 9.99, 'Movies', '100g');
console.log(physicalProduct3.displayInfo()); // Output: Product Name: DVD, Price: $9.99, Weight: 100g, Category: Movies

Explanation:
In this example, we have overridden the displayInfo() method in the PhysicalProduct class. Now, when we call displayInfo() on a PhysicalProduct instance, it provides specific information including the product's weight in addition to the default information inherited from Product.

4. Building Multilevel Inheritance

4.1. Extending Classes in Multiple Levels

Multilevel inheritance involves creating a series of classes that inherit from one another, forming a chain of inheritance. In our digital product e-commerce application, we can expand the inheritance hierarchy to represent more specialized product types. Let's consider adding a new class ElectronicProduct that inherits from PhysicalProduct and, in turn, PhysicalProduct inherits from Product.


// Child class ElectronicProduct inheriting from PhysicalProduct
function ElectronicProduct(name, price, category, weight, brand) {
  PhysicalProduct.call(this, name, price, category, weight);
  this.brand = brand;
}

// Extending the prototype of ElectronicProduct from the prototype of PhysicalProduct
ElectronicProduct.prototype = Object.create(PhysicalProduct.prototype);

// Adding a method specific to ElectronicProduct
ElectronicProduct.prototype.displayBrand = function() {
  return `Brand: ${this.brand}`;
};

// Creating a new ElectronicProduct object and calling methods from all levels of inheritance
const electronicProduct1 = new ElectronicProduct('Smartwatch', 199, 'Wearables', '50g', 'TechCo');
console.log(electronicProduct1.displayInfo()); // Output: Product Name: Smartwatch, Price: $199, Weight: 50g, Category: Wearables
console.log(electronicProduct1.displayWeight()); // Output: Weight: 50g
console.log(electronicProduct1.displayBrand()); // Output: Brand: TechCo

Explanation:
In this example, we created the ElectronicProduct class, which inherits from PhysicalProduct, and PhysicalProduct inherits from Product. The prototype chain now allows ElectronicProduct instances to access properties and methods from all levels of the inheritance hierarchy.

4.2. Traversing the Prototype Chain for Method Resolution

When calling a method on an object, JavaScript looks for the method within the object itself. If the method is not found, it traverses the prototype chain until the method is located or until it reaches the end of the chain.


// Overriding the displayInfo method in the ElectronicProduct class
ElectronicProduct.prototype.displayInfo = function() {
  return `Product Name: ${this.name}, Price: $${this.price}, Weight: ${this.weight}, Category: ${this.category}, Brand: ${this.brand}`;
};

// Creating a new ElectronicProduct object and calling the overridden method
const electronicProduct2 = new ElectronicProduct('Wireless Earbuds', 89, 'Audio', '30g', 'AudioTech');
console.log(electronicProduct2.displayInfo()); // Output: Product Name: Wireless Earbuds, Price: $89, Weight: 30g, Category: Audio, Brand: AudioTech
console.log(electronicProduct2.displayWeight()); // Output: Weight: 30g
console.log(electronicProduct2.displayBrand()); // Output: Brand: AudioTech

Explanation:
In this example, we have overridden the displayInfo() method in the ElectronicProduct class. When calling displayInfo() on an ElectronicProduct instance, JavaScript first looks for the method in the ElectronicProduct class. If it doesn't find it, it then traverses the prototype chain to the PhysicalProduct class and finally to the Product class.

4.3. Potential Pitfalls and Best Practices

4.3.1. Avoid Deep Inheritance Chains

While inheritance is a powerful concept, deep inheritance chains can lead to complex code and decreased performance. Aim for a balanced and concise inheritance hierarchy.

4.3.2. Favor Composition over Inheritance

Sometimes, using composition (combining multiple objects to create more complex objects) can be a better approach than inheritance, especially when dealing with unrelated functionalities.

4.3.3. Super Calls

When overriding methods in child classes, consider using the super keyword to call the parent's implementation if needed. This helps maintain the behavior from the parent class while extending it.

4.3.4. Keep It Simple

Inheritance can make code easier to maintain when used judiciously. Stick to a clear and logical class hierarchy, keeping the relationships between classes straightforward.

Note:

The moment we build multilevel inheritance, we can model complex relationships between product types in our digital product e-commerce application. Understanding the prototype chain ensures smooth method resolution and allows us to create robust and flexible class hierarchies. However, it's essential to be mindful of potential pitfalls and adhere to best practices to maintain clean and efficient code.

5. Implementing Hierarchical Inheritance

5.1. Creating Class Hierarchies with Multiple Branches

Hierarchical inheritance involves creating a structure where multiple classes inherit from a single parent class. This allows for the organization of related classes into a tree-like structure. In our digital product e-commerce application, we can extend our hierarchy to include different product categories, such as "Electronics," "Books," and "Clothing."


// Parent class Product
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// Child classes inheriting from Product
function ElectronicsProduct(name, price, brand) {
  Product.call(this, name, price);
  this.brand = brand;
}

function BooksProduct(name, price, genre) {
  Product.call(this, name, price);
  this.genre = genre;
}

// Creating objects from child classes
const electronicProduct = new ElectronicsProduct('Smartphone', 599, 'TechCo');
const bookProduct = new BooksProduct('Mystery Novel', 19.99, 'Mystery');

Explanation:
In this example, we have Product as the parent class, and ElectronicsProduct and BooksProduct as child classes. Each child class inherits properties from the parent class while adding its specific properties. This hierarchical structure enables us to classify products more precisely.

5.2. Sharing Common Functionality Between Related Classes

Hierarchical inheritance is particularly useful when related classes share common attributes or behaviors. You can place shared methods and properties in the parent class, ensuring that all child classes inherit and can access them.


// Adding a shared method to the Product parent class
Product.prototype.displayInfo = function() {
  return `${this.name} - $${this.price}`;
};

// Displaying product information using the shared method
console.log(electronicProduct.displayInfo()); // Output: Smartphone - $599
console.log(bookProduct.displayInfo()); // Output: Mystery Novel - $19.99

Explanation:
In this example, we added a shared displayInfo method to the Product parent class. Both ElectronicsProduct and BooksProduct instances can now access and use this method, promoting code reusability and maintainability.

5.3. Advantages and Disadvantages of Hierarchical Inheritance

5.3.1. Advantages

  • Code Reusability: Hierarchical inheritance allows for shared code and behaviors among related classes, reducing duplication.
  • Structured Organization: Classes can be organized in a logical and hierarchical manner, making the codebase more manageable.
  • Easy Updates: Changes made to the parent class propagate to all child classes, making updates and enhancements easier to implement.

5.3.2. Disadvantages

  • Rigidity: Hierarchical inheritance can lead to a rigid structure that's difficult to modify, especially if changes impact multiple classes.
  • Limited Flexibility: Inheriting from a single parent class can limit the flexibility of combining behaviors from different sources.
  • Complexity: As the hierarchy grows, the complexity of the codebase may increase, potentially making it harder to understand and maintain.
Note:

The moment we implement hierarchical inheritance, we can create a structured and organized hierarchy of related classes in our digital product e-commerce application. This approach enhances code reusability and provides a foundation for sharing common functionality across multiple classes, ultimately contributing to a more efficient and maintainable codebase.

6. The Role of Prototype Object in Inheritance

6.1. Understanding the Prototype Object's Role in Inheritance

In JavaScript, the prototype object plays a fundamental role in inheritance. It serves as a blueprint for creating new objects and defines shared properties and methods. When an object is created from a constructor, it inherits properties and methods from the constructor's prototype.

6.2. How Objects Inherit Properties and Methods from Prototypes

Let's revisit our Product constructor and explore how objects inherit properties and methods from its prototype:


// Parent class Product
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// Adding a shared method to the Product prototype
Product.prototype.displayInfo = function() {
  return `${this.name} - $${this.price}`;
};

// Creating a new product object
const product = new Product('Headphones', 99);

// Accessing the inherited method
console.log(product.displayInfo()); // Output: Headphones - $99

Explanation:
In this example, the displayInfo method is added to the Product prototype. When we create a new Product object (product), it inherits the displayInfo method from the prototype. This inheritance mechanism allows us to access shared functionality across multiple instances.

6.3. Modifying Prototype Objects Dynamically

One of the advantages of prototypes is the ability to modify them dynamically. Changes made to the prototype are immediately reflected in all objects that inherit from it. This dynamic nature allows us to add or modify properties and methods, even after objects have been created.


// Modifying the displayInfo method dynamically
Product.prototype.displayInfo = function() {
  return `Product: ${this.name}, Price: $${this.price}`;
};

// Modifying the prototype affects all objects created from the constructor
console.log(product.displayInfo()); // Output: Product: Headphones, Price: $99

Explanation:
In this example, we dynamically modified the displayInfo method on the Product prototype. As a result, the change is reflected in the previously created product object. This feature is particularly useful when you want to make global updates to shared methods or properties.

7. The Role of Prototype Object in Inheritance

7.1. Understanding the Prototype Object's Role in Inheritance

In JavaScript, the prototype object plays a fundamental role in inheritance. It serves as a blueprint for creating new objects and defines shared properties and methods. When an object is created from a constructor, it inherits properties and methods from the constructor's prototype.

7.2. How Objects Inherit Properties and Methods from Prototypes

Let's revisit our Product constructor and explore how objects inherit properties and methods from its prototype:


// Parent class Product
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// Adding a shared method to the Product prototype
Product.prototype.displayInfo = function() {
  return `${this.name} - $${this.price}`;
};

// Creating a new product object
const product = new Product('Headphones', 99);

// Accessing the inherited method
console.log(product.displayInfo()); // Output: Headphones - $99

Explanation:
In this example, the displayInfo method is added to the Product prototype. When we create a new Product object (product), it inherits the displayInfo method from the prototype. This inheritance mechanism allows us to access shared functionality across multiple instances.

7.3. Modifying Prototype Objects Dynamically

One of the advantages of prototypes is the ability to modify them dynamically. Changes made to the prototype are immediately reflected in all objects that inherit from it. This dynamic nature allows us to add or modify properties and methods, even after objects have been created.


// Modifying the displayInfo method dynamically
Product.prototype.displayInfo = function() {
  return `Product: ${this.name}, Price: $${this.price}`;
};

// Modifying the prototype affects all objects created from the constructor
console.log(product.displayInfo()); // Output: Product: Headphones, Price: $99

Explanation:
In this example, we dynamically modified the displayInfo method on the Product prototype. As a result, the change is reflected in the previously created product object. This feature is particularly useful when you want to make global updates to shared methods or properties.

8. Working with the `instanceof` Operator

8.1. Understanding the `instanceof` Operator Usage

The instanceof operator in JavaScript is used to determine whether an object is an instance of a particular class or constructor function. It helps us identify the relationship between objects and classes, which is valuable in scenarios like our digital product e-commerce application, where we deal with various product types.


// Parent class Product
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// Child class ElectronicsProduct inheriting from Product
function ElectronicsProduct(name, price, brand) {
  Product.call(this, name, price);
  this.brand = brand;
}

// Creating objects from different classes
const product = new Product('Book', 19.99);
const electronicProduct = new ElectronicsProduct('Smartphone', 599, 'TechCo');

// Using the instanceof operator to check object relationships
console.log(product instanceof Product); // Output: true
console.log(electronicProduct instanceof ElectronicsProduct); // Output: true
console.log(electronicProduct instanceof Product); // Output: true

Explanation:
In this example, we use the instanceof operator to determine whether an object is an instance of a particular class. It returns true if the object is an instance of the specified class or any of its parent classes, and false otherwise.

8.2. Determining Object Relationships with instanceof

The instanceof operator is particularly useful when you want to validate object relationships and ensure that you're working with the expected types.


// Function to process a product and its details
function processProduct(product) {
  if (product instanceof Product) {
    console.log(`Processing ${product.name} - Price: $${product.price}`);
    if (product instanceof ElectronicsProduct) {
      console.log(`Brand: ${product.brand}`);
    }
  } else {
    console.log('Invalid product.');
  }
}

// Using the processProduct function with different objects
processProduct(product); // Output: Processing Book - Price: $19.99
processProduct(electronicProduct); // Output: Processing Smartphone - Price: $599
// Brand: TechCo

Explanation:
In this example, the processProduct function processes different product objects. By using the instanceof operator, we ensure that we're working with valid objects and access relevant properties based on their relationships with different classes.

8.3. Potential Issues and Alternatives

While the instanceof operator is useful, it's important to note that it only checks the immediate prototype chain. If you're dealing with more complex inheritance hierarchies or interfaces, you might need to consider other techniques, such as duck typing or explicit property checks.

Additionally, be cautious when using instanceof across different contexts or if your codebase undergoes significant changes. It's a good practice to combine instanceof with other checks or design patterns to ensure robust type checking.

Note:

The moment you master the instanceof operator, you can confidently validate object relationships in your digital product e-commerce application, ensuring that you're working with the correct types and avoiding potential issues.

9. Applying Mixins for Composition

9.1. Introduction to Mixins as an Alternative to Classical Inheritance

Mixins offer an alternative approach to classical inheritance by allowing you to compose functionalities from multiple sources into a single class. This is particularly useful when you want to add specific behaviors to classes without creating deep and rigid inheritance chains.

In our digital product e-commerce application, we can use mixins to add common shipping-related functionality to different product types.

9.2. Implementing Mixins in JavaScript

To implement mixins, we can create standalone objects with the desired methods and properties. Then, we use these mixins to augment our classes. Let's consider adding a ShippingMixin to our product classes:


// Mixin for shipping-related functionality
const ShippingMixin = {
  calculateShippingCost() {
    // Add logic to calculate shipping cost based on weight, distance, etc.
    return `Shipping cost: $${(this.weight * 0.1).toFixed(2)}`;
  }
};

// Parent class Product
function Product(name, price) {
  this.name = name;
  this.price = price;
}

// Child class ElectronicsProduct inheriting from Product
function ElectronicsProduct(name, price, brand, weight) {
  Product.call(this, name, price);
  this.brand = brand;
  this.weight = weight;
}

// Applying the ShippingMixin to the ElectronicsProduct class
Object.assign(ElectronicsProduct.prototype, ShippingMixin);

// Creating an ElectronicsProduct object and using the added shipping method
const electronicProduct = new ElectronicsProduct('Smartphone', 599, 'TechCo', 1500);
console.log(electronicProduct.calculateShippingCost()); // Output: Shipping cost: $150.00

Explanation:
In this example, we created the ShippingMixin, which includes the calculateShippingCost method. We then applied this mixin to the ElectronicsProduct class using Object.assign. Now, instances of ElectronicsProduct have access to the shipping-related method.

9.3. Advantages and Trade-offs of Mixins

9.3.1. Advantages

Flexibility: Mixins allow for more flexible composition of behaviors. You can add and remove mixins as needed, avoiding deep inheritance chains.

Code Reuse: Mixins encourage code reuse by applying common functionality to multiple classes.

Avoiding Diamond Problem: Mixins help prevent issues like the diamond problem that can occur with complex inheritance hierarchies.

9.3.2. Trade-offs

Name Collisions: Mixins may lead to name collisions if multiple mixins or classes define methods or properties with the same name.

Maintainability: As the number of mixins grows, managing their interactions and dependencies can become complex.

Composition Over Inheritance: While mixins provide a more flexible approach, understanding their composition and interactions might require a learning curve.

Note:

The moment you apply mixins, you can efficiently compose functionalities in your digital product e-commerce application, avoiding the potential downsides of deep inheritance while promoting code reusability and maintainability.

10. ES6 Class Syntax and Inheritance

10.1. Introduction to ES6 Classes as Syntactic Sugar for Prototypes

ES6 introduced a class syntax that provides a more intuitive and organized way to define and work with constructors and prototypes. While classes in JavaScript are often referred to as syntactic sugar over prototypes, they offer a cleaner and more readable way to achieve the same underlying prototype-based inheritance.

In the context of our digital product e-commerce application, let's explore how to use ES6 classes to define product types.

10.2. Extending Classes Using the extends Keyword

ES6 classes make inheritance straightforward through the extends keyword. Child classes can extend the functionality of parent classes, inheriting both properties and methods. This improves the readability of our code and simplifies the process of creating class hierarchies.


// Parent class Product defined using ES6 class syntax
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  displayInfo() {
    return `${this.name} - $${this.price}`;
  }
}

// Child class ElectronicsProduct extending Product
class ElectronicsProduct extends Product {
  constructor(name, price, brand) {
    super(name, price);
    this.brand = brand;
  }

  displayBrand() {
    return `Brand: ${this.brand}`;
  }
}

// Creating an ElectronicsProduct object and using methods from both classes
const electronicProduct = new ElectronicsProduct('Smartwatch', 199, 'TechCo');
console.log(electronicProduct.displayInfo()); // Output: Smartwatch - $199
console.log(electronicProduct.displayBrand()); // Output: Brand: TechCo

Explanation:
In this example, we defined the Product class using ES6 class syntax and extended it with the ElectronicsProduct class. The extends keyword simplifies the process of inheriting properties and methods from the parent class.

10.3. Underlying Prototype Behavior in ES6 Classes

Behind the scenes, ES6 classes still utilize prototypes for inheritance. Each class defined using ES6 syntax has a prototype object that holds its methods. The extends keyword establishes the prototype chain, linking child and parent classes.


// Checking the prototype behavior of ES6 classes
console.log(ElectronicsProduct.prototype instanceof Product); // Output: true

Explanation:
In this example, we use the instanceof operator to demonstrate the underlying prototype behavior of ES6 classes. It confirms that the prototype of ElectronicsProduct is an instance of Product, indicating the inheritance relationship.

Note:

ES6 classes provide a cleaner and more structured way to work with prototypes and inheritance, making your digital product e-commerce application's codebase more organized and easier to understand. The extends keyword simplifies the process of building class hierarchies while maintaining the powerful prototype-based inheritance mechanism.

11. Common Design Patterns Utilizing Inheritance

11.1. The Factory Pattern with Inheritance: Creating Objects with a Common Interface

The Factory Pattern is a creational design pattern that utilizes inheritance to create objects while providing a unified interface. In our digital product e-commerce application, we can use the Factory Pattern to create different types of products with a common interface.


// ProductFactory using inheritance and the Factory Pattern
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }

  displayInfo() {
    return `${this.name} - $${this.price}`;
  }
}

class ElectronicsProduct extends Product {
  constructor(name, price, brand) {
    super(name, price);
    this.brand = brand;
  }

  displayBrand() {
    return `Brand: ${this.brand}`;
  }
}

class BookProduct extends Product {
  constructor(name, price, genre) {
    super(name, price);
    this.genre = genre;
  }

  displayGenre() {
    return `Genre: ${this.genre}`;
  }
}

// Creating different types of products using the Factory Pattern
function createProduct(type, ...args) {
  switch (type) {
    case 'electronics':
      return new ElectronicsProduct(...args);
    case 'book':
      return new BookProduct(...args);
    default:
      throw new Error('Invalid product type');
  }
}

const electronicProduct = createProduct('electronics', 'Smartphone', 599, 'TechCo');
const bookProduct = createProduct('book', 'Mystery Novel', 19.99, 'Mystery');

Explanation:
In this example, we defined the Product class and its child classes using ES6 class syntax. The createProduct function serves as a factory, creating different types of products based on the specified type while adhering to the common Product interface.

11.2. The Singleton Pattern with Inheritance: Creating a Single Instance of a Class

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance. This can be helpful in scenarios like managing a shopping cart for our digital product e-commerce application.


// Singleton Cart using inheritance and the Singleton Pattern
class Cart {
  constructor() {
    if (!Cart.instance) {
      this.products = [];
      Cart.instance = this;
    }
    return Cart.instance;
  }

  addProduct(product) {
    this.products.push(product);
  }

  getTotalPrice() {
    return this.products.reduce((total, product) => total + product.price, 0);
  }
}

const cart1 = new Cart();
const cart2 = new Cart();

console.log(cart1 === cart2); // Output: true

// Adding products to the singleton cart
cart1.addProduct(electronicProduct);
cart1.addProduct(bookProduct);

console.log(cart1.getTotalPrice()); // Output: Total price: $618.99

Explanation:
In this example, the Cart class uses the Singleton Pattern to ensure that only one instance of the class is created. Any attempt to create a new instance returns the existing instance. This helps maintain a single shopping cart throughout the application.

11.3. Other Design Patterns Using Inheritance

In addition to the Factory Pattern and Singleton Pattern, there are many other design patterns that leverage inheritance to solve specific problems. For instance:

  • The Decorator Pattern allows adding behavior to objects dynamically.
  • The Strategy Pattern defines a family of algorithms and makes them interchangeable.
  • The Template Method Pattern defines the structure of an algorithm and lets subclasses redefine specific steps.
Note:

Design patterns provide proven solutions to common software design challenges. The moment you utilize inheritance within these patterns, you can enhance the structure and maintainability of your digital product e-commerce application's codebase.

12 . Encouragement

Thank you for investing your time in reading this guide! I trust that you have found it informative and beneficial in enhancing your comprehension in " Creating Class Hierarchies with Inheritance and Prototypal Chains in JavaScript" Ultimately, I strongly urge you to continue your learning journey by delving into the next guide [Deep Dive into Simplified Object-Oriented Programming using ES6 Classes in JavaScript for Beginners] Thank you once more, and I look forward to meeting you in the next guide

Comments