Наследование и полиморфизм
Предпосылки: Объектно-ориентированное программирование (класс, объект, методы, инкапсуляция).
← Объектно-ориентированное программирование | Функциональное программирование →
Один класс удобен, пока все объекты ведут себя одинаково. Но это заканчивается быстро. Например, в магазине появляются разные способы доставки: самовывоз, обычный курьер, экспресс-доставка. У каждого свой срок и своя цена.
Когда ветвление расползается
Первый рабочий вариант обычно выглядит так: в коде хранят признак kind, а дальше в нескольких функциях пишут ветвление по этому признаку. Одно и то же различие уже повторяется три раза:
delivery_labelрешает, как назвать способ доставки;delivery_priceрешает, сколько он стоит;delivery_etaрешает, какой срок показать.
Пока способов доставки мало, это ещё можно читать. Но как только появляется новый locker delivery, правку нужно внести сразу во все три функции. Потом рядом обычно появляются ещё delivery_refundable?, delivery_tracking_label, delivery_warning_text, и то же ветвление течёт дальше по файлу.
Проблема не в самом if. Проблема в том, что знание о различиях между видами доставки больше не живёт в одном месте. Оно уже размазано по нескольким функциям, и при каждой новой правке нужно вспоминать, все ли такие места остались согласованными.
Вот пример — здесь неважно перечислить все ветки, важно увидеть, как одно различие по kind начинает течь сразу в подпись, цену и срок.
Что нужно изменить, чтобы добавить новый способ доставки
Даже для одного нового вида доставки придётся лезть в
delivery_label,delivery_priceиdelivery_eta. Во всех них повторяется одно и то же различие междуpickup,courierиexpress.
Наследование
Наследование позволяет вынести общую часть в базовый класс, а различия оставить в дочерних:
class DeliveryMethod
def summary
label + ": " + price.to_s + ", " + eta
end
end
class Pickup < DeliveryMethod
def label
"Самовывоз"
end
def price
0
end
def eta
"сегодня"
end
end
class Courier < DeliveryMethod
def label
"Курьер"
end
def price
300
end
def eta
"2 дня"
end
endPickup < DeliveryMethod — знак < здесь означает «наследует от»: Pickup получает всё, что описано в DeliveryMethod, и добавляет своё. Здесь общий метод summary живёт один раз в базовом классе, а дочерние классы задают различия через label, price и eta.
Наследование полезно тогда, когда у сущностей действительно есть общий каркас и общая форма поведения.
Полиморфизм
После этого внешний код может работать с разными объектами через один и тот же набор методов:
deliveries = [Pickup.new, Courier.new]
deliveries.each do |delivery|
puts delivery.summary
end.each do |delivery| ... end — конструкция, уже знакомая по коллекциям объектов: .each обходит массив, вызывая блок do ... end для каждого элемента; |delivery| — имя, которое блок даёт текущему элементу. .to_s внутри summary превращает число в строку, чтобы его можно было соединить с текстом.
Цикл не ветвится по типу. Он просто вызывает summary у каждого объекта. Для Pickup и Courier результат будет разный, но форма вызова одна и та же.
Это и есть полиморфизм (от греч. poly — «много», morphe — «форма»): один и тот же внешний вызов, разное поведение в зависимости от конкретного объекта.
Важна именно эта идея, а не конкретно наследование в Ruby. В Java и C# её часто строят на классах и интерфейсах; в Go ту же задачу обычно решают через интерфейс без общего базового класса. Общая цель одна: внешний код работает с общим контрактом, а не с ветвлением по каждому виду объекта.
Польза здесь опять не для процессора, а для чтения и развития программы. Внешний код знает меньше подробностей о разновидностях сущностей. При добавлении нового вида доставки чаще всего достаточно добавить новый класс, а не переписывать старую функцию-распределитель.
Когда это уместно
Наследование и полиморфизм не нужны автоматически всякий раз, когда объектов больше одного.
Они окупаются, когда:
- у объектов есть общий контракт;
- внешний код должен обрабатывать их одинаковым образом;
- ветвление по виду объекта уже повторяется в нескольких местах.
Если общего контракта нет, искусственная иерархия только усложнит код.
Иерархия классов убирает разбросанное ветвление. Но у объектов осталось свойство, знакомое с заметки о памяти: их можно менять по ходу работы. Если баланс продавца прошёл через deposit, withdraw, charge_fee и refund, по итоговому числу уже не видно, какой шаг привёл к ошибке. Для отладки таких проблем помогает функциональное программирование.
Sources
- Liskov, B. & Zilles, S., 1974, Programming with Abstract Data Types. ACM SIGPLAN Notices.
- Gamma, E. et al., 1994, Design Patterns. Addison-Wesley.
- Thomas, D. et al., 2023, Programming Ruby 3.3. Pragmatic Bookshelf.
← Объектно-ориентированное программирование | Функциональное программирование →