Простыми словами о полиморфизме: 5‑я часть гайда по ООП
Объясняем сложную концепцию объектно-ориентированного программирования на примере интернет-магазина.


Иллюстрация: Катя Павловская для Skillbox Media
Если вы начали изучать объектно-ориентированное программирование, то наверняка слышали о четырёх его главных концепциях: абстракции, инкапсуляции, наследовании и полиморфизме. Первые три мы подробно обсуждали в других статьях, а в этой поговорим о полиморфизме — разберёмся, что это такое, как работает и в чём его практический смысл для разработчика.
Перед началом: многие нюансы из этой статьи будет сложно освоить без понимания сути и основных принципов ООП. Если хотите освежить свои знания, почитайте наш подробный материал.
Все статьи про ООП
- Что такое классы и объекты.
- Особенности работы с объектами.
- Модификаторы доступа, инкапсуляция.
- Полиморфизм и перегрузка методов.
- Полиморфизм.
- Наследование и ещё немного полиморфизма.
- Абстрактные классы и интерфейсы.
- Практикум.
Что такое полиморфизм
Мы уже знаем, что в ООП все программы состоят из объектов. Но у разных на вид объектов может быть одинаковый интерфейс — какие-то общие методы, которые они выполняют каждый по-своему. Например, у объектов «квадрокоптер» и «самолёт» общим методом будет «летать».
Так вот, полиморфизм даёт возможность использовать одни и те же методы для объектов разных классов. Неважно, как эти объекты устроены, — в ООП можно сказать самолёту и квадрокоптеру: «Лети», и они будут делать это как умеют: квадрокоптер закрутит лопастями, а самолёт начнёт разгон по взлётно-посадочной полосе.
Грубо говоря, полиморфизм — это диспетчер в аэропорту. Ему неважно, какую топливную систему предусмотрел авиаконструктор и как работает система форсажа — он просто даёт команду: «Взлёт разрешаю». После этого на лайнере начинают происходить какие-то свои внутренние процессы, на которые диспетчер уже не влияет.
Минутка семантики. Слово «полиморфизм» переводится с греческого как «многоформенность». Смысл в том, что один и тот же метод может воплощаться по-разному — например, как полёт у дрона и самолёта.
Как полиморфизм выглядит в коде
Допустим, мы делаем онлайн-магазин с мерчем известного ютуб-канала. У нас есть три вида товаров: футболки, кружки и блокноты. Задача — сделать так, чтобы все их можно было складывать в корзину и сайт каждый раз автоматически выдавал покупателю сообщение: «Товар такой-то добавлен в корзину». Как это сделать?
Есть два варианта. Можно писать свою версию метода «добавить в корзину» на каждую категорию товара — но это долго, да и код получится неопрятный. А можно написать один полиморфный метод, а потом использовать его для каждого нового объекта — и вот это как раз наш случай. Разберём весь процесс пошагово.
Шаг 1. Создаём базовый класс «Товар»
Базовый класс описывает общие свойства объектов. Например, у нас это название товара и возможность добавлять его в корзину.
public class Good // Объявляем класс «Товар»
{
public string name; // Указываем свойство «Название»
// Создаём метод «Добавить в корзину»
// Virtual означает, что метод потом можно будет дополнить под нужды конкретного товара.
public virtual void AddToCart()
{
Console.WriteLine("Товар " + name + " добавлен в корзину");
}
}
Пока всё это выглядит очень абстрактно, но дальше — больше.
Шаг 2. Создаём три производных класса: «Футболка», «Кружка» и «Блокнот»
Производные классы расширяют функциональность основного. То есть задают какие-то дополнительные параметры — в нашем случае это характеристики товаров: размер футболки, объём кружки и количество страниц у блокнота.
public class Cup : Good // Создаём класс «Кружка»
{
public int volume; // Вводим дополнительное свойство «Объём»
public override void AddToCart()
// Override означает, что мы переопределяем метод AddToCart и добавляем ему новую функциональность
{
Console.WriteLine("Кружка " + name + " объёмом " + volume + " мл добавлена в корзину");
}
}
public class Note : Good // Создаём класс «Блокнот»
{
public int pages; // Добавляем свойство «Количество страниц»
public override void AddToCart()
{
Console.WriteLine("Блокнот " + name + " на " + pages + " страниц добавлен в корзину");
}
}
public class Shirt : Good // Создаём класс «Футболка»
{
public int size; // Добавляем свойство «Размер»
public override void AddToCart()
{
Console.WriteLine("Футболка " + name + " размером " + size + " добавлена в корзину");
}
}
Шаг 3. Вызываем метод «Добавить в корзину»
Допустим, пользователь зашёл в наш магазин и решил купить три товара: футболку «Кислота» 48 размера, кружку «Омут» на 250 мл и блокнот «Древо жизни» на 180 страниц. Поставил он галочки напротив каждого товара, нажал «Добавить в корзину» — и вот какая история начинает происходить в коде:
using System;
class Program {
static void Main() {
// Создаём объекты для каждого товара
Shirt someShirt = new Shirt();
someShirt.name = "Кислота"; // Футболка «Кислота»
someShirt.size = 48;
Cup someCup = new Cup(); // Кружка «Омут»
someCup.name = "Омут";
someCup.volume = 250;
Note someNote = new Note();
someNote.name = "Древо жизни"; // Блокнот «Древо жизни»
someNote.pages = 180;
// Создаём массив из всех трёх объектов
Good[] goods = new Good[3];
goods[0] = someShirt;
goods[1] = someCup;
goods[2] = someNote
// С помощью цикла вызываем метод «Добавить в корзину» для каждого товара
for(int i = 0; i < 3; i++)
{
goods[i].AddToCart();
}
}
}
Мы объединили товары в массив и с помощью цикла применили метод AddToCart сразу ко всем. Если бы наш магазин был реальным, пользователь получил бы примерно такие сообщения:
Футболка «Кислота размером» 48 добавлена в корзину
Кружка «Омут» объёмом 250 мл добавлена в корзину
Блокнот «Древо жизни» на 180 страниц добавлен в корзину
То есть мы достигли как раз того результата, который и планировали. Запустить код программы целиком и посмотреть, как он работает, можно по этой ссылке.
Для чего нужен полиморфизм
Смысл полиморфизма в том, что нам не надо писать для каждого товара свой метод — например, какой-нибудь AddToCartShirt для футболки или AddToCartCup для кружки. У нас просто есть один AddToCart, и мы на него полагаемся. Если в магазине появятся, например, кепки, мы просто немного допилим наш метод под особенности кепок, и дело в шляпе.
Особенно это актуально в больших коммерческих программах со сложной логикой. Представьте, если бы у нас был не магазин с аксессуарами, а крупный маркетплейс вроде «Озона». Там без полиморфизма просто не обойтись — иначе код превратится в лапшу из функций, которые делают одно и то же, а называются по-разному.
Отсюда можно выделить три главных преимущества полиморфизма:
- Читаемость. Чем меньше кода и чем лучше он упакован, тем проще работать программистам и тем быстрее идёт разработка.
- Масштабируемость. Можно добавить в магазин носки, ручки, напульсники, подвески и ещё кучу разных товаров, а за их покупку всё равно будет отвечать одна команда — AddToCart.
- Предсказуемость кода. Когда у нас есть один метод для разных объектов, мы чувствуем себя спокойно. Можно не переживать, что команда «Добавить в корзину трусы» случайно применится к носкам и вызовет какой-то сбой в программе.
Что дальше
В ООП под полиморфизмом понимают только одну его разновидность — полиморфизм подтипов. Он как раз отвечает за то, чтобы объекты разных классов можно было вызывать одним методом. Но существует ещё два вида полиморфизма: параметрический и ad-hoc. О них мы и поговорим в следующих статьях — разберёмся, чем они различаются и для чего нужны.