Drani Academy – Interview Question, Search Job, Tuitorials, Cheat Sheet, Project, eBook

Object-Oriented Programming

Tutorials – Object-Oriented Programming (OOPs)

 
Chapter 5: Polymorphism

 

Polymorphism is one of the core concepts of Object-Oriented Programming (OOP) and is often considered one of the pillars of OOP, along with encapsulation, inheritance, and abstraction. Polymorphism is a concept that allows objects of different classes to be treated as objects of a common superclass. It enables you to write code that can work with objects of various classes in a uniform way, leading to more flexible and extensible software. In this chapter, we will explore the concept of polymorphism, its types, and how it’s implemented in various programming languages.

5.1. Understanding Polymorphism

Polymorphism comes from the Greek words “poly” (many) and “morph” (form), which together mean “having many forms.” In the context of OOP, polymorphism refers to the ability of different objects to respond to the same message or method call in a way that is specific to their individual classes. In other words, objects of different classes can exhibit different behaviors while sharing a common interface.

Polymorphism allows you to write code that operates on objects of a superclass but can work with objects of any subclass of that superclass. This makes your code more flexible and less dependent on the specific classes of objects it interacts with, promoting the reuse of code and simplifying the design of software systems.

5.2. The “Is-A” and “Can-Do” Relationships

To understand polymorphism, it’s crucial to distinguish between two relationships:

  • The “Is-A” Relationship: This relationship is established through inheritance. When a class inherits from another class, it is considered to be a “kind of” that class. For example, a Car is a kind of Vehicle. This relationship is the foundation for polymorphism.

  • The “Can-Do” Relationship: This relationship is established through interfaces or common method signatures. When multiple classes can respond to the same method call, they are said to have a “can-do” relationship. For example, both a Car and a Bicycle can respond to the start and stop methods. This relationship enables polymorphism.

Polymorphism is based on the “can-do” relationship, allowing objects of different classes to respond to the same method call in their specific way. It goes beyond the “is-a” relationship, which is about inheritance.

5.3. Types of Polymorphism

Polymorphism can be categorized into two main types: compile-time (static) polymorphism and runtime (dynamic) polymorphism.

5.3.1. Compile-Time (Static) Polymorphism

Compile-time polymorphism, also known as static polymorphism or method overloading, occurs when the decision about which method to call is made at compile time, based on the method signature and the number or types of arguments. The appropriate method is selected by the compiler, and it’s bound to the method call at compile time.

Method Overloading

Method overloading is a common form of compile-time polymorphism. In method overloading, multiple methods in the same class have the same name but different parameters. The appropriate method to call is determined by the number or types of arguments provided at compile time.

Here’s an example in Java:

class Calculator {
int add(int a, int b) {
return a + b; }
double add(double a, double b) {
return a + b; } }


In this example, the Calculator class has two add methods with different parameter types (int and double). The compiler selects the appropriate method based on the argument types when the method is called.

5.3.2. Runtime (Dynamic) Polymorphism

Runtime polymorphism, also known as dynamic polymorphism or method overriding, occurs when the decision about which method to call is made at runtime, based on the actual object that the method is called on. This type of polymorphism is closely related to inheritance.

Method Overriding

Method overriding is a key feature of runtime polymorphism. In method overriding, a subclass provides a specific implementation of a method that is already defined in its superclass. The overridden method in the subclass should have the same method signature as the one in the superclass.

Here’s an example in Python:

class Shape:
def area(self):
pass
class Circle(Shape):
def __init__(self, radius): self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width): self.length = length self.width = width
def area(self):
return self.length * self.width

 

In this example, the Shape class defines a method called area. Both the Circle and Rectangle classes inherit from Shape and provide their own specific implementations of the area method.

When you call the area method on a Circle or Rectangle object, the decision about which implementation to use is made at runtime based on the actual type of the object.

5.4. Polymorphism in Action

Polymorphism enables you to write code that works with objects of different classes in a uniform way. Let’s see polymorphism in action by using a simple example involving shapes.

5.4.1. Base Class: Shape

class Shape:
def area(self):
pass

The Shape class defines a method called area that is meant to be overridden by subclasses. This method is an example of a common interface shared by different shapes.

5.4.2. Derived Classes: Circle and Rectangle

class Circle(Shape):
def __init__(self, radius): self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
class Rectangle(Shape):
def __init__(self, length, width): self.length = length self.width = width
def area(self):
return self.length * self.width

 

The Circle and Rectangle classes inherit from the Shape class and provide their own specific implementations of the area method.

5.4.3. Using Polymorphism

Now, let’s create objects of the Circle and Rectangle classes and use polymorphism to calculate their areas:

shapes = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
printf(". Area: {shape.area()}")

The output of this code will be:

Area: 78.5
Area: 24

In this example, we created a list of shapes, which includes objects of both the Circle and Rectangle classes. We then iterated through the list and called the area method on each shape. The decision about which area method to call was made at runtime based on the actual type of the object.

This demonstrates the power of polymorphism: you can write code that operates on objects of different classes in a uniform way, without needing to know the specific class of each object.

5.5. The Role of Interfaces

Interfaces play a significant role in achieving polymorphism. An interface defines a contract of methods that a class must implement. In many programming languages, a class can implement multiple interfaces, allowing it to respond to multiple common method signatures.

For example, in Java:

interface Drawable {
void draw(); }
class Circle implements Drawable {
// Implementing the draw method from the Drawable interface
public void draw() { System.out.println("Drawing a circle"); } }
class Square implements Drawable {
// Implementing the draw method from the Drawable interface
public void draw() { System.out.println("Drawing a square"); } }

 

In this example, the Drawable interface defines a draw method. Both the Circle and Square classes implement the Drawable interface by providing their own implementations of the draw method.

This allows you to create a collection of objects of different classes that implement the same interface and invoke the draw method on each object without knowing their specific types.

5.6. The “instanceof” Operator

In some programming languages, you can use the instanceof operator to determine the actual class of an object at runtime. This operator allows you to check if an object is an instance of a specific class or its subclasses.

For example, in Java:

Shape shape = new Circle();
if (shape instanceof Circle) { System.out.println("It's a Circle"); } else if (shape instanceof Rectangle) { System.out.println("It's a Rectangle"); }

In this example, the shape object is an instance of the Circle class, and the instanceof operator is used to identify its actual class.

While the instanceof operator can be helpful in certain situations, it’s generally considered a code smell in object-oriented design. It’s better to rely on polymorphism and a common interface when designing your classes to avoid the need for type checks.

5.7. Benefits of Polymorphism

Polymorphism offers several significant benefits in software design:

5.7.1. Code Reusability

Polymorphism allows you to write code that works with objects of different classes through a common interface. This promotes code reusability, as you can create new classes that adhere to the same interface and seamlessly integrate them into existing code.

5.7.2. Extensibility

Polymorphism makes your code more extensible. You can add new classes that implement the same interface without modifying existing code. This is particularly useful for accommodating future requirements and changes.

5.7.3. Flexibility

Polymorphism provides flexibility in handling objects of different classes. It allows you to write generic code that can work with a wide range of objects, reducing the need for complex conditional logic based on object types.

5.7.4. Simplified Design

Polymorphism simplifies the design of software systems. It promotes the use of common interfaces, reducing the need for explicit type checks and conditional branches. This leads to cleaner and more maintainable code.

5.7.5. Encapsulation

Polymorphism is closely related to encapsulation. By providing a common interface and hiding the implementation details, you can achieve a higher level of abstraction and encapsulation in your classes.

5.8. Challenges and Considerations

While polymorphism offers numerous benefits, it’s essential to be aware of some challenges and considerations:

5.8.1. Performance

Polymorphism can introduce a performance overhead, especially in dynamic dispatch scenarios where the method to call is determined at runtime. This overhead is typically minimal but may be a concern in performance-critical applications.

5.8.2. Abstraction Complexity

Excessive use of polymorphism can lead to complex class hierarchies and interfaces, making the code harder to understand. It’s important to strike a balance between abstraction and simplicity.

5.8.3. Interface Design

Designing effective interfaces is crucial for successful polymorphism. Careful consideration of method signatures and their meanings is necessary to create a meaningful and flexible interface.

5.8.4. Potential for Mistakes

Polymorphism can lead to unexpected behavior if not used carefully. It’s important to ensure that the common interface is well-defined and adhered to by implementing classes.

5.8.5. Overuse of Type Checks

While type checks (using the instanceof operator) can be useful in certain situations, overusing them can indicate a design issue. It’s generally better to rely on polymorphism and well-designed interfaces to achieve flexibility and extensibility.

5.9. Polymorphism in Other Languages

Polymorphism is a fundamental concept in OOP and is supported by various programming languages, each with its syntax and mechanisms for implementing polymorphism. Here are examples of polymorphism in a few popular languages:

5.9.1. Java

In Java, polymorphism is achieved through method overriding, and it relies on the use of the @Override annotation to indicate that a method in a subclass is intended to override a method in the superclass.

class Animal {
void makeSound() { System.out.println("Animal makes a sound"); } }
class Dog extends Animal {
@Override
void makeSound() { System.out.println("Dog barks"); } }
class Cat extends Animal {
@Override
void makeSound() { System.out.println("Cat meows"); } }

 

In this example, both Dog and Cat are subclasses of Animal and override the makeSound method.

5.9.2. C++

Polymorphism in C++ is achieved using virtual functions. A function declared as virtual in the base class can be overridden by derived classes, and the correct implementation is determined at runtime.

class Animal {
public:
virtual void makeSound() { cout << "Animal makes a sound" << endl; } };
class Dog : public Animal {
public:
void makeSound() override { cout << "Dog barks" << endl; } };
class Cat : public Animal {
public:
void makeSound() override { cout << "Cat meows" << endl; } };

 

In C++, the virtual keyword is used to declare a method as virtual. This enables dynamic binding, which allows the method to be determined at runtime.

5.9.3. C#

In C#, polymorphism is achieved through method overriding, similar to Java.

class Animal { public virtual void MakeSound() { Console.WriteLine(“Animal makes a sound”); } }

class Dog : Animal { public override void MakeSound() { Console.WriteLine(“Dog barks”); } }

class Cat : Animal { public override void MakeSound() { Console.WriteLine(“Cat meows”); } }

C# uses the `virtual` and `override` keywords to indicate which methods can be overridden and how they are overridden in derived classes.

### 5.9.4. Python

Polymorphism in Python is natural due to its dynamic typing and dynamic dispatch. You can define methods in base and derived classes, and the appropriate method is determined at runtime.

```python
class Animal:
def make_sound(self):
print("Animal makes a sound")
class Dog(Animal):
def make_sound(self):
print("Dog barks")
class Cat(Animal):
def make_sound(self):
print("Cat meows")

In Python, you can simply define methods with the same name in different classes, and the appropriate method is invoked based on the actual object type at runtime.

5.10. Conclusion

Polymorphism is a fundamental concept in Object-Oriented Programming that allows objects of different classes to respond to the same method calls through a common interface. It promotes code reusability, extensibility, flexibility, and simplified design.

By designing classes with well-defined interfaces and using inheritance and method overriding, you can harness the power of polymorphism to create more versatile and maintainable software systems. While it comes with some challenges and considerations, a thoughtful approach to polymorphism can lead to cleaner and more flexible code that is easier to maintain and extend.

Scroll to Top