terminal

codeando_simple

terminal

menu

terminal

search_module

guest@codeandosimple: ~/system/search $ grep -r "" .

Press [ENTER] to execute search

Status

Engine: Ready

Database: Online

Index: V2.1.0_LATEST

bash -- cat oop-polymorphism.md
guest@codeandosimple: ~/blog/oop $ cat polymorphism.md

Polymorphism_

// "It's not that I'm so smart, it's just that I stay with problems longer" - Albert Einstein

Polymorphism is a key principle in object-oriented programming; it allows objects to be treated as instances of their parent class.

In this chapter, we will see how polymorphism improves flexibility and extensibility, allowing objects of different classes to be treated uniformly.

# What is Polymorphism?

The term polymorphism comes from Greek and means "many forms".

In the context of object-oriented programming, we call polymorphism the capacity for the same message to invoke different methods (same method name, different implementation, redefined in subclasses).

Polymorphism delays the decision about the type of object until the moment the method is going to be used (late binding or runtime binding); at that point, it decides which object will respond, using its own implementation.

The important thing is that objects of different classes (derived from the same ancestor) can be treated in the same way and respond to the same message.

Essentially, polymorphism allows the same message to be responded to by different objects, which are viewed in the same way.

# Without Polymorphism

Suppose we have a series of shapes: Circle, Rectangle, Triangle. Each one is drawn in a certain way, so each one has its own drawing method.

// Without polymorphism: 
//    we should find out what type of shape it is and invoke its drawing method
public void drawAll() {
    for (int i = 0; i < array.length; i++) {
        if (array[i] instanceof Circle) {
            array[i].drawCircle();
        } else if (array[i] instanceof Rectangle) {
            array[i].drawRectangle();
        } else {
            array[i].drawTriangle();
        }
    }
}

If we have an array with all the shapes, we must find out the type of shape it is and invoke its drawing method.

This causes the code to be coupled; if we create another type of shape, we must add it to the condition as well.

# With Polymorphism

Thanks to polymorphism, we can have an array of shapes and draw them all regardless of their concrete type.

polymorphism

Each one redefines the 'draw' method of the parent class.

// With polymorphism: shape type doesn't matter
public void drawAll() {
    for (int i = 0; i < array.length; i++) {
        array[i].draw();
    }
}

We are not interested in what type of shape it is; we simply invoke the 'draw' method, and through polymorphism, it will invoke the corresponding one. 

This makes our code simpler, and importantly, we can add more types of shapes without having to modify the points where they are used, which makes it easier to maintain and extend; that's why polymorphism is so important.

# Method Overriding (Redefinition)

Method overriding or redefinition occurs when a subclass declares a method that has the same signature as a method in its superclass.

This allows the subclass to provide a specific implementation of that method, replacing that of its ancestor.

# Execution Time (Runtime)

Polymorphism works at runtime through method overriding.

In this case, a derived class has a method with the same signature as a method in its base class, but with a different implementation.

At runtime, the implementation of the actual object type is used.

# Practical Example: Notification System

We have different types of notifications: email, SMS, push notifications.

This will demonstrate how polymorphism allows treating different types of notifications in a uniform way.

public class Notification {
    public void sendMessage(String message) {
        System.out.println("Base class message: " + message);
    }
}

Concrete classes

public class EmailNotification extends Notification {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}
216: 
217: public class SMSNotification extends Notification {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}
223: 
224: public class PushNotification extends Notification {
    @Override
    public void sendMessage(String message) {
        System.out.println("Push notification: " + message);
    }
}

Service

public class NotificationService {
    public void sendNotification(Notification notification, String message) {
        notification.sendMessage(message);
    }
}

Client or test class

// In the main method or any other test method
public static void main(String[] args) {
    NotificationService service = new NotificationService();
244: 
    Notification email = new EmailNotification();
    Notification sms = new SMSNotification();
    Notification push = new PushNotification();
248: 
    service.sendNotification(email, "This is an email.");
    service.sendNotification(sms, "This is an SMS.");
    service.sendNotification(push, "This is a push notification.");
}

In this example, NotificationService can send any type of notification without needing to know exactly what type of notification it is.

At runtime, Java will determine the object type and call the corresponding sendMessage method. This is polymorphism in action.

# Conclusions

Polymorphism is for many the most important concept in object-oriented programming, because it allows designing extensible and easily maintainable systems where components can be exchanged or updated without altering the core of the system.

Using it alongside other principles of object-oriented programming, we can write cleaner, more understandable, and more adaptable code that will be ready to grow and evolve according to needs.

Polymorphism gives rise to abstract classes and interfaces, fundamental topics that we will see in the next chapters.