Lesson 3
Polymorphism and Backward Compatibility in TypeScript
Introduction

Hello, learner! In today's exciting chapter, we will unravel Polymorphism, a prominent feature of Object-Oriented Programming (OOP). Specifically, we will study its role in maintaining backward compatibility while introducing new features. Think of it as a software update that introduces new functions without breaking the older functionality — ingenious, isn't it?

Understanding Polymorphism

Polymorphism, a principle that derives from the Greek words 'poly' (many) and 'morphism' (forms), enables a variable or method to assume multiple roles — to embody various behaviors or functions determined by its data type or class.

Consider a class Bird with a method canFly(). If we create subclasses like Sparrow, Penguin, and Ostrich, we can override the canFly() method for certain subclasses. This demonstrates polymorphism in action.

TypeScript
1class Bird { // Superclass 2 canFly(): string { 3 return "Unknown"; 4 } 5} 6 7class Sparrow extends Bird { // Subclass 8 canFly(): string { 9 return "Yes, I can fly!"; 10 } 11} 12 13class Penguin extends Bird { // Subclass 14 canFly(): string { 15 return "No, I prefer swimming."; 16 } 17} 18 19const sparrow = new Sparrow(); 20const penguin = new Penguin(); 21console.log("Sparrow says:", sparrow.canFly()); // Output: "Yes, I can fly!" 22console.log("Penguin says:", penguin.canFly()); // Output: "No, I prefer swimming."

The Bird example demonstrates runtime polymorphism, where the canFly method behaves differently depending on the object type (Sparrow or Penguin). The superclass Bird provides a default implementation of canFly as "Unknown". This acts as a generic behavior for birds when no specific information is available. Subclasses override this method to provide species-specific behaviors. For example, the Sparrow class overrides canFly to return "Yes, I can fly!", while the Penguin class returns "No, I prefer swimming.". This ability to modify behavior while maintaining a shared interface (canFly) is the essence of polymorphism.

Polymorphism for Backward Compatibility

When adding new features, which introduce new behaviors to some components, polymorphism ensures that the existing parts function as before, thereby retaining backward compatibility. In complex cases, we maintain an older version of the method in the superclass for legacy support while offering newer functionalities in subclasses.

Take, for instance, a MathOperations class with a multiply() method that accepts two parameters. To support the multiplication of three numbers, we design a subclass, ExtendedMathOperations, and include a new multiply() method in it, ensuring backward compatibility.

TypeScript
1class MathOperations { // Superclass 2 multiply(a: number, b: number): number { 3 return a * b; 4 } 5} 6 7class ExtendedMathOperations extends MathOperations { // Subclass 8 multiply(a: number, b: number, c: number = 1): number { 9 return a * b * c; 10 } 11} 12 13const mathOps = new MathOperations(); 14const extendedMathOps = new ExtendedMathOperations(); 15console.log(mathOps.multiply(2, 3)); // Output: 6 16console.log(extendedMathOps.multiply(2, 3)); // Output: 6, keeping backward compatibility 17console.log(extendedMathOps.multiply(2, 3, 4)); // Output: 24
Real-Life Examples

Consider a Document class that prints a text document and a subclass PhotoDocument, which supports color prints, as an example. This design allows the implementation without changing Document.printDoc().

TypeScript
1class Document { 2 text: string; 3 4 constructor(text: string) { 5 this.text = text; 6 } 7 8 printDoc(): void { 9 console.log("Printing document:", this.text); 10 } 11} 12 13class PhotoDocument extends Document { 14 printDoc(isColourPrint: boolean = false): void { 15 const printType = isColourPrint ? "Colour " : ""; 16 console.log(`${printType}Printing document: ${this.text}`); 17 } 18} 19 20const doc = new Document("Hello"); 21doc.printDoc(); // Output: Printing document: Hello 22 23const photoDoc = new PhotoDocument("Beautiful Sunset!"); 24photoDoc.printDoc(); // Output: Printing document: Beautiful Sunset! 25photoDoc.printDoc(true); // Output: Colour Printing document: Beautiful Sunset!
Pros and Cons of Using Polymorphism for Backward Compatibility

As with any programming approach, using polymorphism to maintain backward compatibility comes with its own set of advantages and disadvantages. Here, we will explore two key pros and two cons to give you a more balanced understanding.

Pros

  1. Flexibility in Feature Expansion: Polymorphism allows for easy extension and addition of new features without altering the existing system's functionality. This means developers can introduce new subclasses that provide enhanced features while the original classes remain unaffected and continue to support legacy systems.

  2. Seamless Integration with Existing Codebase: Since polymorphism enables new features to coexist with older ones through subclassing, integrating these features into the existing codebase does not disrupt the functionality of legacy systems. This approach minimizes the risk of breaking changes and ensures a smoother transition for systems adopting new features.

Cons

  1. Increased Complexity: While polymorphism promotes flexibility and integration, it can also lead to a more complex codebase when there are many subclasses and overridden methods. Developers must understand the entire hierarchy and relationships between classes to effectively implement and maintain the system. This complexity might slow down development and increase the likelihood of errors. For instance, adding a new subclass may inadvertently affect existing logic if not implemented carefully.

  2. Potential Overhead in Performance: In systems with deep class hierarchies or frequent method overrides, determining the correct method to execute at runtime can introduce slight performance overhead. This is because polymorphism relies on dynamic dispatch, which requires the system to resolve the method implementation during execution. In performance-critical applications, such as real-time systems, this overhead might become a concern.

Understanding these pros and cons is essential for making informed decisions when considering polymorphism as a strategy for adding new features while keeping backward compatibility.

Lesson Summary and Practice

You have aced understanding polymorphism and its role in maintaining backward compatibility. Now, gear up for insightful exercises to reinforce today's learning. Regular application is the key to mastering these concepts. Are you ready to put your skills to the test? Let's go!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.