Polymorphism

        Polymorphism is a fundamental concept in object-oriented programming that allows different objects to respond to the same message in different ways. In Ruby, polymorphism is achieved through method overriding and duck typing.

        Method overriding allows a subclass to override a method defined in its superclass with its own implementation. This means that when a message is sent to an object of the subclass, it will use the overridden method instead of the one defined in the superclass. This allows different objects to respond to the same message in different ways, depending on their class.

For example, let's modify our previous example of Animal and Dog to include method overriding:

    class Animal
      attr_accessor :name, :age

      def initialize(name, age)
        @name = name
        @age = age
      end

      def talk
        puts "I am an animal"
      end
    end

    class Dog < Animal
      def talk
        puts "Woof!"
      end
    end

        Now, when we call the talk method on an instance of Animal, it will print "I am an animal". But if we call it on an instance of Dog, it will print "Woof!" because the Dog class overrides the talk method defined in the Animal class.

    animal = Animal.new("Bob", 5)
    animal.talk # prints "I am an animal"

    dog = Dog.new("Fido", 3)
    dog.talk # prints "Woof!"


        Duck typing is another way of achieving polymorphism in Ruby. It is based on the idea that "if it walks like a duck and quacks like a duck, then it must be a duck". In other words, Ruby objects are not classified by their class, but by their behavior.

For example, let's define a Person class that has a method called speak:

    class Person
      def speak
        puts "Hello!"
      end
    end

        Now let's define a method called say_hello that accepts an object as an argument and calls the speak method on it:

    def say_hello(obj)
      obj.speak
    end

We can pass any object that has a speak method to this method, and it will work as expected:

    person = Person.new
    dog = Dog.new("Fido", 3)

    say_hello(person) # prints "Hello!"
    say_hello(dog) # prints "Woof!"

        Even though person and dog are of different classes and have different methods, they can both respond to the speak method, so we can treat them as the same type of object. This is the essence of duck typing and polymorphism in Ruby.