Polymorphism

ধরো, তুমি একটি রিমোট কন্ট্রোল ব্যবহার করছো। এই একটি রিমোট দিয়ে তুমি টিভি, এসি এবং ফ্যান; সবকিছুই অপারেট করতে পারো। তুমি যখন রিমোটের "Power" বাটন চাপো, তখন টিভির ক্ষেত্রে সেটি স্ক্রিন চালু করে, এসির ক্ষেত্রে কম্প্রেসর চালু করে এবং ফ্যানের ক্ষেত্রে মোটর ঘোরায়।

লক্ষ্য করো, বাটন বা নির্দেশ (Interface) একটাই, কিন্তু প্রতিটি ডিভাইস তার নিজস্ব লজিক অনুযায়ী ভিন্ন ভিন্ন কাজ করছে।

সহজ কথায়: Polymorphism (পলিমরফিজম) হলো অবজেক্ট-ওরিয়েন্টেড প্রোগ্রামিংয়ের এমন একটি নীতি, যেখানে একই মেথড বা ইন্টারফেস ব্যবহার করে ভিন্ন ভিন্ন অবজেক্টকে তাদের নিজস্ব বৈশিষ্ট্য অনুযায়ী হ্যান্ডেল করা যায়।

শব্দটির উৎপত্তি গ্রিক থেকে: Poly মানে অনেক (Many) এবং Morph মানে রূপ (Forms)। অর্থাৎ, একই আচরণের একাধিক রূপ (Many forms of the same behavior)।

Polymorphism এর ধরন (Types of Polymorphism)

পলিমরফিজম মূলত দুটি প্রধান ভাগে বিভক্ত:

১. Compile-time Polymorphism (Static Polymorphism বা Early Binding): এটি প্রোগ্রাম কম্পাইল হওয়ার সময়ই নির্ধারিত হয়ে যায় কোন মেথডটি কল হবে।

  • Method Overloading: একই ক্লাসের ভেতর একই নামের একাধিক মেথড থাকে, কিন্তু তাদের প্যারামিটারের সংখ্যা বা টাইপ ভিন্ন হয়।

  • Operator Overloading: একই অপারেটর (যেমন +) ভিন্ন ভিন্ন টাইপের ডেটার জন্য ভিন্নভাবে কাজ করে (যেমন পাইথনে সংখ্যার ক্ষেত্রে যোগ করা এবং স্ট্রিংয়ের ক্ষেত্রে জোড়া লাগানো)।

২. Run-time Polymorphism (Dynamic Polymorphism বা Late Binding): প্রোগ্রাম যখন রান করে, তখন অবজেক্টের প্রকৃত টাইপ অনুযায়ী মেথড কল হয়।

  • Method Overriding: চাইল্ড ক্লাস যখন প্যারেন্ট ক্লাসের কোনো মেথডকে নিজের মতো করে নতুনভাবে সংজ্ঞায়িত (Override) করে। প্যারেন্ট ক্লাসের রেফারেন্স দিয়ে চাইল্ড অবজেক্টকে কল করলেও চাইল্ডের মেথড কাজ করে।

Run-time Polymorphism (Method Overriding)

এখানে আমরা দেখব কীভাবে একটি কমন প্যারেন্ট রেফারেন্স ব্যবহার করে ভিন্ন ভিন্ন চাইল্ড অবজেক্টের মেথড কল করা যায়।

Java Implementation:

// Parent Class
class Animal {
    public void makeSound() {
        System.out.println("Some generic animal sound");
    }
}

// Child Class 1
class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Dog says: Bark Bark");
    }
}

// Child Class 2
class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Cat says: Meow Meow");
    }
}

public class RuntimePolymorphismDemo {
    public static void main(String[] args) {
        // Parent reference pointing to different Child objects
        Animal[] animals = { new Animal(), new Dog(), new Cat() };

        // Same method call, different behaviors dynamically
        for (Animal animal : animals) {
            animal.makeSound(); 
        }
    }
}

সুবিধা: একটিমাত্র লুপ বা ফাংশন ব্যবহার করে তুমি হাজারটা ভিন্ন প্রাণীর সাউন্ড হ্যান্ডেল করতে পারবে, সিস্টেমকে মেইনটেইন করা অনেক সহজ হয়ে যায়।

Polymorphism via Interface

পলিমরফিজমের সবচেয়ে শক্তিশালী প্রয়োগ দেখা যায় ইন্টারফেস ব্যবহারের মাধ্যমে। এর মাধ্যমে লুজ কাপলিং (Loose coupling) অর্জন করা যায় এবং বিদ্যমান কোড পরিবর্তন না করেই নতুন ফিচার (যেমন নতুন পেমেন্ট মেথড) যুক্ত করা যায়।

Python Implementation:

from abc import ABC, abstractmethod

# Common Interface
class PaymentGateway(ABC):
    @abstractmethod
    def pay(self, amount: float) -> None:
        pass

# Implementation 1
class PayPal(PaymentGateway):
    def pay(self, amount: float) -> None:
        print(f"Paid ${amount} via PayPal ecosystem.")

# Implementation 2
class Stripe(PaymentGateway):
    def pay(self, amount: float) -> None:
        print(f"Paid ${amount} via Stripe API.")

# A polymorphic function accepting any PaymentGateway
def process_payment(gateway: PaymentGateway, amount: float) -> None:
    # It doesn't care whether it's PayPal or Stripe
    gateway.pay(amount)

if __name__ == "__main__":
    paypal = PayPal()
    stripe = Stripe()

    process_payment(paypal, 1000.0)
    process_payment(stripe, 500.0)

সুবিধা: কাল যদি সিস্টেমে BkashPayment যোগ করতে হয়, তবে শুধু নতুন ক্লাস বানাতে হবে, process_payment ফাংশনটির ভেতরে হাত দেওয়ার কোনো প্রয়োজন পড়বে না।

Compile-time Polymorphism (Method Overloading)

জাভাতে ডিরেক্ট মেথড ওভারলোডিং করা যায়, তবে পাইথনে একই নামে একাধিক মেথড লেখা যায় না। পাইথনে ডিফল্ট প্যারামিটার বা ভ্যারিয়াডিক আর্গুমেন্ট (*args) দিয়ে ওভারলোডিংয়ের ফ্লেভার আনা হয়। নিচে জাভার ডিরেক্ট ওভারলোডিং দেখানো হলো।

Java Implementation:

class Calculator {
    // Method with 2 parameters
    public int add(int a, int b) {
        return a + b;
    }

    // Overloaded method with 3 parameters
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    // Overloaded method with different parameter types
    public double add(double a, double b) {
        return a + b;
    }
}

public class CompileTimeDemo {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        
        System.out.println(calc.add(5, 10));        // Calls 2-param int method
        System.out.println(calc.add(5, 10, 20));    // Calls 3-param int method
        System.out.println(calc.add(5.5, 10.5));    // Calls 2-param double method
    }
}

সুবিধা: একই নামের ফাংশন ভিন্ন প্যারামিটার কাউন্ট বা টাইপের জন্য ভিন্নভাবে রেসপন্স করে, ফলে ডেভেলপারকে addTwoNumbers(), addThreeNumbers() এমন ভিন্ন ভিন্ন নাম মনে রাখতে হয় না।

এক নজরে Polymorphism এর সুবিধা

  • Flexibility & Reusability: কোড অনেক বেশি নমনীয় হয়। প্যারেন্ট ক্লাসের রেফারেন্স দিয়ে যেকোনো চাইল্ড অবজেক্টকে পয়েন্ট করা যায়।

  • Maintainability: ভবিষ্যতে নতুন কোড যোগ করলে (যেমন নতুন ক্লাস বা ইন্টারফেস ইমপ্লিমেন্টেশন) পুরনো কোড ব্রেক হওয়ার সম্ভাবনা কমে যায়।

  • Simplicity: ডেভেলপারদের জন্য একই অ্যাকশন (যেমন draw(), pay(), makeSound()) এর জন্য একাধিক ফাংশনের নাম মনে রাখার দরকার পড়ে না।