Course program← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF
7Методические материалы

Polymorphism

This lesson was translated automatically – it may contain errors.

Introduction

Encapsulation answered the question «what to hide», inheritance answered «how to reuse». Polymorphism answers a third one: how do you write code that works with objects while knowing nothing about their concrete type.

Of the three basic concepts this is the trickiest. Encapsulation is visible in the code straight away – here is a private field, here is a method. So is inheritance: here is the colon after the class name. Polymorphism is not visible in the source at all – it shows up at the moment of execution, when the same call turns out to run different code. Hence its reputation: a simple mechanism that is hard to catch by eye.

The easiest way to see it is through everyday examples:

  • The start button. On a washing machine, a microwave and a coffee maker it is called the same and pressed the same, and what happens afterwards is different every time. You do not think about what is inside – you know the button works.
  • The wall socket. A kettle, a laptop and a vacuum cleaner all go into it. The socket does not know what was plugged in, and does not need to: the contract is described by the plug, not by the device.
  • The brake pedal. In a car it presses the discs, in a train it closes a contact, in a lorry it drives the pneumatics. The driver makes the same movement in all three.

The examples share one thing: there is a contract – the button, the plug, the pedal – and there are different performers of that contract. Whoever uses the contract does not choose the performer and knows nothing about it. Code works the same way: the calling side relies on the contract, and which object arrives is decided while the program runs.

Definition: polymorphism is the ability of one and the same call to lead to different behaviour depending on which object or type we are working with.

  • A TV remote turns on the TV.
  • An air conditioner remote turns on the air conditioner.

Exactly how a language ties a call to an implementation is a separate question, and there are several ways: interfaces, inheritance with virtual methods, overloading, generics, duck typing. They get a lesson of their own; here we go through one example built on interfaces.

One call, different behaviour: a worked example

using System;
using System.Collections.Generic;

public interface IAnimal
{
    void Say();
}

public class Dog : IAnimal
{
    public string Name { get; set; } = "Bob";
    
    public void Say()
    {
        Console.WriteLine($"Woof im {Name}");
    }
}

public class Cat : IAnimal
{
    public string Name { get; set; } = "Murzik";
    
    // Implementing the interface
    public void Say()
    {
        Console.WriteLine($"Meow im {Name}");
    }
}

class Program
{
    static void Main()
    {
        List<IAnimal> animals = new List<IAnimal> { new Cat(), new Dog() };
        
        foreach (var animal in animals)
        {
            animal.Say(); // Works for every animal
        }
    }
}
#include <iostream>
#include <memory>
#include <vector>

// An abstract class with a pure virtual method – the C++ counterpart of an interface
class Animal {
public:
    virtual void Say() const = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void Say() const override { std::cout << "Woof im " << name_ << std::endl; }

private:
    std::string name_ = "Bob";
};

class Cat : public Animal {
public:
    // Implementing the interface
    void Say() const override { std::cout << "Meow im " << name_ << std::endl; }

private:
    std::string name_ = "Murzik";
};

int main() {
    std::vector<std::unique_ptr<Animal>> animals;
    animals.push_back(std::make_unique<Cat>());
    animals.push_back(std::make_unique<Dog>());

    for (const auto& animal : animals) {
        animal->Say(); // Works for every animal
    }

    return 0;
}
package main

import "fmt"

// A Go interface is implemented implicitly: Dog and Cat never state
// that they are Animal – a matching method is enough.
type Animal interface {
	Say()
}

type Dog struct {
	Name string
}

func (d Dog) Say() { fmt.Printf("Woof im %s\n", d.Name) }

type Cat struct {
	Name string
}

func (c Cat) Say() { fmt.Printf("Meow im %s\n", c.Name) }

func main() {
	animals := []Animal{Cat{Name: "Murzik"}, Dog{Name: "Bob"}}

	for _, animal := range animals {
		animal.Say() // Works for every animal
	}
}
import random

class Dog:
    def __init__(self):
        self.name = "Bob"

    def say(self):
        print("Woof im %s" % self.name)

class Cat:
    def __init__(self):
        self.name = "Murzik"

    def say(self):
        print("Meow im %s" % self.name)

if __name__ == "__main__":
    animals = [Cat(), Dog()]
    for i in range(0, 5):
        random.choice(animals).say()
interface Animal {
    say(): void;
}

class Dog implements Animal {
    name = "Bob";

    say(): void {
        console.log(`Woof im ${this.name}`);
    }
}

class Cat implements Animal {
    name = "Murzik";

    // Implementing the interface
    say(): void {
        console.log(`Meow im ${this.name}`);
    }
}

const animals: Animal[] = [new Cat(), new Dog()];

for (const animal of animals) {
    animal.say(); // Works for every animal
}
import java.util.List;

public interface Animal {
    void say();
}

public class Dog implements Animal {
    private String name = "Bob";

    public void say() {
        System.out.println("Woof im " + name);
    }
}

public class Cat implements Animal {
    private String name = "Murzik";

    // Implementing the interface
    public void say() {
        System.out.println("Meow im " + name);
    }
}

public class Program {
    public static void main(String[] args) {
        List<Animal> animals = List.of(new Cat(), new Dog());

        for (Animal animal : animals) {
            animal.say(); // Works for every animal
        }
    }
}
interface Animal {
    fun say()
}

class Dog : Animal {
    val name = "Bob"

    override fun say() = println("Woof im $name")
}

class Cat : Animal {
    val name = "Murzik"

    // Implementing the interface
    override fun say() = println("Meow im $name")
}

fun main() {
    val animals: List<Animal> = listOf(Cat(), Dog())

    animals.forEach { it.say() } // Works for every animal
}