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

Полиморфизм

TODO: отвалидировать – текст собран автоматически из материалов курса на Google Drive, проверьте формулировки и код.

Введение

Инкапсуляция отвечала на вопрос «что спрятать», наследование – «как переиспользовать». Полиморфизм отвечает на третий: как написать код, который работает с объектами, ничего не зная об их конкретном типе.

Из трёх базовых концепций эта самая непростая. Инкапсуляцию видно в коде сразу – вот закрытое поле, вот метод. Наследование тоже: вот двоеточие после имени класса. А полиморфизм в исходнике не виден вовсе – он проявляется в момент выполнения, когда за одним и тем же вызовом оказывается разный код. Отсюда и репутация: механизм простой, а поймать его на глаз тяжело.

Проще всего увидеть его на бытовых примерах:

  • Кнопка «Пуск». У стиральной машины, микроволновки и кофеварки она называется одинаково и нажимается одинаково, а происходит после нажатия каждый раз своё. Вы не задумываетесь, что там внутри, – вы знаете, что кнопка работает.
  • Розетка. В неё включают чайник, ноутбук и пылесос. Розетка не знает, что именно в неё воткнули, и знать не должна: договор описан вилкой, а не устройством.
  • Педаль тормоза. В легковушке она давит на диски, в электричке замыкает контакт, в грузовике управляет пневматикой. Водитель во всех трёх случаях делает одно движение.

Общее у примеров одно: есть договор – кнопка, вилка, педаль, – и есть разные исполнители этого договора. Тот, кто пользуется договором, не выбирает исполнителя и не знает о нём. В коде так же: вызывающий код опирается на договор, а какой объект придёт, решается уже во время работы программы.

Определение: полиморфизм – способность одного и того же обращения приводить к разному поведению в зависимости от того, с каким объектом или типом мы работаем.

  • Пульт от телевизора включает телевизор.
  • Пульт от кондиционера включает кондиционер.

Как именно язык связывает обращение с реализацией – вопрос отдельный, и способов несколько: интерфейсы, наследование с виртуальными методами, перегрузка, обобщения, утиная типизация. Каждому из них посвящён отдельный урок, а здесь разберём сквозной пример на интерфейсах.

Сквозной пример: одно обращение – разное поведение

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";
    
    // Реализация интерфейса
    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(); // Работает для всех животных
        }
    }
}
#include <iostream>
#include <memory>
#include <vector>

// Абстрактный класс с чисто виртуальным методом – аналог интерфейса
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:
    // Реализация интерфейса
    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(); // Работает для всех животных
    }

    return 0;
}
package main

import "fmt"

// Интерфейс в Go реализуется неявно: Dog и Cat нигде не пишут,
// что они Animal – достаточно совпадения метода.
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() // Работает для всех животных
	}
}
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";

    // Реализация интерфейса
    say(): void {
        console.log(`Meow im ${this.name}`);
    }
}

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

for (const animal of animals) {
    animal.say(); // Работает для всех животных
}
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";

    // Реализация интерфейса
    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(); // Работает для всех животных
        }
    }
}
interface Animal {
    fun say()
}

class Dog : Animal {
    val name = "Bob"

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

class Cat : Animal {
    val name = "Murzik"

    // Реализация интерфейса
    override fun say() = println("Meow im $name")
}

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

    animals.forEach { it.say() } // Работает для всех животных
}